类属性属于类本身、全体实例共享,实例属性属于单个实例;__dict__ 是二者的命名空间载体,__slots__ 则用固定槽位替代字典,换取内存与结构约束。

类属性与实例属性

  • 类属性:直接定义在类体中(不在任何方法内),属于类对象本身,所有实例共享同一份
  • 实例属性:定义在 __init__ 等方法中,通过 self.xxx 绑定,属于单个实例对象,每个实例独立一份
class A:
    class_attr = "类属性"       # 类属性:类定义加载时创建
 
    def __init__(self, val):
        self.instance_attr = val  # 实例属性:实例化时创建
维度类属性实例属性
归属类对象单个实例
创建时机类定义加载时实例化(__init__ 执行)时
内存占用全类仅一份每个实例各存一份
修改影响通过类修改同步所有实例;实例赋值则生成同名实例属性覆盖仅影响当前实例

经典坑:可变类属性

「共享可变对象」的机制根源见 引用与可变性。

类属性为列表、字典等可变对象时,实例原地修改会影响所有实例:

class B:
    lst = []  # 可变类属性
 
b1 = B()
b2 = B()
b1.lst.append(1)  # 原地修改共享的列表
print(b2.lst)     # [1],所有实例受影响

若执行 b1.lst = [2],则是给 b1 新建同名实例属性,从此遮蔽类属性、不再共享。

属性查找顺序

访问 obj.attr 遵循先实例后类:

  1. 实例自身的命名空间
  2. 类的命名空间
  3. 沿继承链(MRO)向上查找父类
  4. 全部失败则触发 __getattr__ 或抛出 AttributeError

完整七步流程见属性访问与查找。

__dict__ 命名空间

__dict__ 是对象的命名空间字典,存储其全部属性(变量、方法),但不同对象的类型与特性完全不同。

类的 __dict__:只读代理

print(type(A.__dict__))  # <class 'mappingproxy'>
  • 类型为 types.MappingProxyType,是只读映射代理,不允许直接增删改键,保护类的命名空间
  • 存放类属性、所有方法、特殊方法等
  • 底层真实字典仍可通过类的 __setattr__ 等机制修改,代理仅提供只读视图

实例的 __dict__:可写字典

a = A("测试")
print(type(a.__dict__))  # <class 'dict'>
  • 原生 dict,可读写,动态存储实例的全部自定义属性
  • 仅用户自定义类的实例默认拥有 __dict__

没有 __dict__ 的对象

  1. object() 实例:object 是最顶层基类,实例极简,为节省内存不附带 __dict__,无法动态新增属性:
o = object()
o.abc = 1  # AttributeError: 'object' object has no attribute 'abc'
  1. 定义了 __slots__ 的类实例:默认禁用实例 __dict__,改用固定槽位存储属性。

__slots__ 机制

原理与作用

默认情况下,Python 为每个实例创建 __dict__ 哈希表存储属性,开销显著。__slots__ 声明实例允许拥有的固定属性名列表,CPython 改用紧凑的结构体数组存储属性,替代字典。

核心作用:

  1. 大幅降低内存占用:大量实例场景下节省可达数倍,是数据类、ORM 实体的核心优化手段
  2. 禁止动态新增属性:实例只能使用声明的属性,约束对象结构,避免随意赋值
  3. 属性访问速度小幅提升:数组索引访问快于字典哈希查找

基础用法

class Person:
    __slots__ = ["name", "age"]  # 声明实例仅允许 name、age 两个属性
 
p = Person()
p.name = "张三"
p.age = 20
p.gender = "男"    # AttributeError,无法新增未声明属性
print(p.__dict__)  # AttributeError,默认无实例字典

四大坑点

  1. 不自动继承:父类定义 __slots__ 后,子类默认仍会生成 __dict__、可动态新增属性;子类也需限制时必须重新定义,最终有效属性是父子 slots 的并集
  2. 保留 __dict__:在列表中显式加入 '__dict__',可在使用 slots 的同时保留动态属性能力,但内存节省效果大打折扣
class C:
    __slots__ = ["name", "__dict__"]
  1. 弱引用:slots 实例默认不支持弱引用,需要时须将 '__weakref__' 加入列表
  2. 不影响类属性:__slots__ 仅约束实例属性,类本身仍可自由新增类属性