## 一、Python 动态属性赋值

### 1.1 核心概念

Python 是**动态类型语言**，对象的属性可以在**运行时**动态添加、修改、删除，不必在 `__init__` 中全部定义。

### 1.2 三种动态赋值方式

#### 方式 1：直接点号赋值

python

class Person:
    def __init__(self, name):
        self.name = name
p = Person("Alice")
p.age = 25            # 动态添加属性
p.city = "Beijing"    # 再添加一个
print(p.age, p.city)  # 输出: 25 Beijing

#### 方式 2：`setattr()` 函数

python

class DataProcessor:
    def load(self, dataset_name, feats):
        # 属性名根据参数动态生成
        setattr(self, dataset_name + '_feats', feats)
        setattr(self, dataset_name + '_loaded', True)
dp = DataProcessor()
dp.load('iris', [1, 2, 3])
print(dp.iris_feats)    # 输出: [1, 2, 3]
print(dp.iris_loaded)   # 输出: True

#### 方式 3：操作 `__dict__`

python

class Bag:
    pass
b = Bag()
b.__dict__['size'] = 10
print(b.size)  # 输出: 10

### 1.3 实例属性 vs 类属性

这是**非常关键**的区别！

python

class A(object):
    bar = 1   # 类属性
a = A()
print(a.bar)   # 1（从类属性读）
print(A.bar)   # 1
# 修改实例的 bar
a.bar = 5      # 创建实例属性，不覆盖类属性
print(a.bar)   # 5（实例属性）
print(A.bar)   # 1（类属性没变）
# 查看两者的 __dict__
print(a.__dict__)   # {'bar': 5}
print(A.__dict__)   # {'bar': 1, ...}
# 删除实例属性
del a.bar
print(a.bar)   # 1（恢复访问类属性）

**属性查找顺序**（MRO）：

text

a.bar  →  实例 __dict__  →  类 __dict__  →  父类 __dict__  →  AttributeError

### 1.4 `setattr` vs 直接赋值的等价性

python

setattr(a, 'bar', 5)   ≡   a.bar = 5
getattr(a, 'bar')      ≡   a.bar
delattr(a, 'bar')      ≡   del a.bar

### 1.5 使用场景

- **数据处理**：根据数据集名动态存储特征
    
- **配置管理**：从 JSON 加载到对象属性
    
- **元编程**：框架内部动态构造对象
    

### 1.6 注意事项

- ❌ 拼写错误难发现：`self.feat` 和 `self.feats` 可能是两个不同属性
    
- ❌ 使用 `__slots__` 的类无法动态添加属性：
    
    python
    
    class Limited:
        __slots__ = ('x',)
    obj = Limited()
    obj.y = 1   # AttributeError!
    

---

## 二、C++ 对比：能动态加属性吗？

**不能。** C++ 是静态类型语言，类的成员必须在编译期确定。

### 替代方案：`std::unordered_map<std::string, std::any>`

cpp

#include <iostream>
#include <string>
#include <unordered_map>
#include <any>
class DynamicObject {
    std::unordered_map<std::string, std::any> attrs;
public:
    void set(const std::string& k, std::any v) { attrs[k] = v; }
    std::any get(const std::string& k) { return attrs.at(k); }
};
int main() {
    DynamicObject obj;
    obj.set("age", 25);
    obj.set("name", std::string("Alice"));
    std::cout << std::any_cast<int>(obj.get("age")) << std::endl;        // 25
    std::cout << std::any_cast<std::string>(obj.get("name")) << std::endl; // Alice
}

**对比总结**：

|特性|Python|C++|
|---|---|---|
|动态添加属性|✔️ 原生支持|❌ 只能模拟|
|类型安全|❌ 运行时|✔️ 编译期（但 `any` 例外）|
|性能|慢（哈希查找）|快（直接偏移）|

---

## 三、PyTorch 模型调用

### 3.1 为什么 `graphSage(x)` 自动调用 `forward`？

因为 `nn.Module` 实现了 **`__call__`** 方法：

python

# PyTorch 简化源码
class Module:
    def __call__(self, *args, **kwargs):
        # 1. 执行 forward 前钩子
        # 2. 调用 forward
        return self._call_impl(*args, **kwargs)
    
    def _call_impl(self, *args, **kwargs):
        return self.forward(*args, **kwargs)

**验证**：

python

import torch.nn as nn
class MyModel(nn.Module):
    def forward(self, x):
        print("forward 被调用")
        return x * 2
m = MyModel()
m(3)            # 输出: forward 被调用
# m.forward(3)  # 也能跑，但不推荐！

### 3.2 为什么不能直接调 `forward`？

python

model(x)          # ✅ 正确：触发 __call__，走完整流程
model.forward(x)  # ❌ 错误：跳过钩子、缓存、梯度检查等

`__call__` 相比 `forward` 额外做了：

- **Forward hooks**：`register_forward_hook` 生效
    
- **Backward hooks**：`register_backward_hook` 生效
    
- **梯度缓存**：保证自动求导正常
    
- **混合精度**：AMP 上下文正确处理
    

### 3.3 例子：hook 只在 `model(x)` 时触发

python

def hook_fn(module, input, output):
    print("Hook triggered!")
model = MyModel()
model.register_forward_hook(hook_fn)
model(5)           # 输出: Hook triggered! forward 被调用
model.forward(5)   # 输出: forward 被调用 (hook 不触发!)

---

## 四、`nn.Linear` 的参数

### 4.1 默认包含两个参数

python

linear = nn.Linear(in_features=128, out_features=7)  # bias 默认为 True

|参数|形状|维度|
|---|---|---|
|`weight`|`(7, 128)`|2 维|
|`bias`|`(7,)`|1 维|

### 4.2 验证

python

linear = nn.Linear(128, 7)
for name, param in linear.named_parameters():
    print(name, param.shape)
# 输出:
# weight torch.Size([7, 128])
# bias torch.Size([7])

### 4.3 如果设置 `bias=False`

python

linear = nn.Linear(128, 7, bias=False)
# 只有 weight，没有 bias

### 4.4 初始化逻辑分析

python

def init_params(self):
    for param in self.parameters():
        if len(param.size()) == 2:      # 只判断二维 → 只匹配 weight
            nn.init.xavier_uniform_(param)

- `weight` 是 2 维 → 被 Xavier 初始化
    
- `bias` 是 1 维 → 跳过，保持 PyTorch 默认初始化
    

如果想要初始化 bias：

python

def init_params(self):
    for name, param in self.named_parameters():
        if 'weight' in name:
            nn.init.xavier_uniform_(param)
        elif 'bias' in name:
            nn.init.zeros_(param)

---

## 五、为什么 `len(params) == 4`？

### 5.1 代码回顾

python

models = [graphSage, classification]
params = []
for model in models:
    for param in model.parameters():
        if param.requires_grad:
            params.append(param)

### 5.2 表面层级 vs 实际参数

假设模型结构：

python

GraphSage(
  (sage_layer 1): SageLayer()   # 假设含 weight + bias
  (sage_layer 2): SageLayer()   # 假设含 weight + bias
)
Classification(
  (layer): Sequential(
    (0): Linear(128, 7)        # 含 weight + bias，可能被冻结
  )
)

### 5.3 情形分析

|情形|`GraphSage`|`Classification`|总计|
|---|---|---|---|
|**全部可训练**|4（2 层 × 2）|2|6|
|**Classification 冻结**|4|0|**4** ✅|
|**两个 SageLayer 共享参数**|2|2|4|
|**SageLayer 无 bias**|2|2|4|

### 5.4 如何定位原因？

**方法 1：打印所有参数信息**

python

for model in models:
    for name, param in model.named_parameters():
        print(f"{name:30 s} shape={tuple(param.shape)} requires_grad={param.requires_grad}")

输出示例（对应"Classification 冻结"的情形）：

text

sage_layer 1.weight    shape=(64, 128)  requires_grad=True
sage_layer 1.bias      shape=(64,)      requires_grad=True
sage_layer 2.weight    shape=(64, 128)  requires_grad=True
sage_layer 2.bias      shape=(64,)      requires_grad=True
layer.0.weight        shape=(7, 128)   requires_grad=False   ← 冻结!
layer.0.bias          shape=(7,)       requires_grad=False   ← 冻结!

**方法 2：检查参数共享**

python

print(graphSage.sage_layer 1.weight is graphSage.sage_layer 2.weight)  # True 表示共享

**方法 3：查看 `__dict__` 中的子模块**

python

print(graphSage)          # 打印结构
print(classification)     # 打印结构

---

## 六、核心知识串联图

text

┌─────────────────────────────────────────────────────────┐
│                    Python 动态特性                        │
│  ┌───────────────────────────────────────────────────┐  │
│  │ setattr(obj, 'name', val)  →  动态创建属性         │  │
│  │ 实例属性 vs 类属性  →  __dict__ 查找顺序           │  │
│  │ C++ 无法原生支持  →  用 map<string, any> 模拟      │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────┐
│                    PyTorch 模型机制                       │
│  ┌───────────────────────────────────────────────────┐  │
│  │ model(x) → __call__ → forward                     │  │
│  │ （触发 hooks、梯度缓存、AMP 等）                    │  │
│  │                                                    │  │
│  │ nn.Linear → weight (2D) + bias (1D)               │  │
│  │                                                    │  │
│  │ parameters() → 遍历所有子模块的可训练参数          │  │
│  │ requires_grad → 决定是否纳入优化器                 │  │
│  └───────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────┘
                          ↓
              ┌───────────────────────┐
              │ 调试技巧               │
              │ named_parameters()     │
              │ 检查 requires_grad     │
              │ 检查参数共享            │
              └───────────────────────┘

---

## 七、实践建议

### ✅ 推荐做法

1. **Python 属性**：核心属性在 `__init__` 中显式声明为 `None`，提高可读性。
    
2. **动态属性**：用于配置、特征等场景，可用 `setattr` + 命名规范。
    
3. **PyTorch 调用**：始终用 `model(x)`，不要直接调 `forward`。
    
4. **参数统计**：用 `named_parameters()` 打印，别猜。
    
5. **调试冻结**：检查 `requires_grad` 状态。
    

### ❌ 常见坑

1. 拼错属性名（Python 不报错，静默创建新属性）。
    
2. `len(params)` 和"层数"混淆（一个是参数张量数，一个是结构）。
    
3. `SageLayer` 内部结构不透明，想当然估算层数。
    
4. 直接调 `forward`，导致 hook 失效。