前言
「特殊属性」和「特殊方法」这两个词经常被混着用,其实它们在语言参考里是两套不同的东西。特殊方法(也叫魔术方法)是那些会被语法隐式调用的、可调用的下划线方法,比如__init__、__len__;特殊属性是那些由解释器自动维护、通常不是可调用的数据成员,比如__dict__、__class__、__module__、__qualname__。前者你写它是为了改变对象的行为,后者你读它是为了反射(introspection)——看看这个对象到底是什么、从哪来、长什么样。
初学者最常见的误解有三个。第一,以为__doc__、__name__是普通属性,随便就能改——本对象上有些能改有些不能,类上的__dict__甚至是只读的映射代理。第二,以为属性能不能拿到只看「类里有没有」,忽略了实例字典优先这条查找顺序。第三,以为__dict__一定存在——用了__slots__的实例就可能没有。
本文按「属性住在哪里」这条线索把它们梳理清楚,并配一张对照表区分属性和方法。
一、先分清:属性和方法的区别
| 类别 | 长什么样 | 谁在维护 | 典型代表 | 你拿它干什么 |
|---|
| 特殊属性 | 数据成员(多为只读或半只读) | 解释器 | __dict__、__class__、__doc__、__module__、__qualname__ | 反射、调试、序列化 |
| 特殊方法 | 可调用的方法 | 你定义、解释器调用 | __init__、__repr__、__eq__、__enter__ | 改变对象在语法下的行为 |
| 普通属性 | 普通数据成员 | 你 | self.name | 存业务数据 |
判断标准很简单:能被语法自动触发的下划线成员是方法;描述对象自身信息的是属性。当然边界不是绝对的——__init__也是属性(它在类字典里),只是它的值是个函数对象,可以调用而已。理解反射的视角是:类本身也是一个对象,也有自己的__dict__。
二、对象上的特殊属性
# 适用于 Python 3.8+
class Config:
"""配置对象的文档字符串。"""
timeout = 3
def __init__(self, host):
self.host = host
cfg = Config("localhost")
print(cfg.__class__) # <class '__main__.Config'>
print(cfg.__dict__) # {'host': 'localhost'}——注意只有实例属性
print(Config.__dict__["timeout"]) # 3,类属性在类字典里
print(Config.__doc__) # 配置对象的文档字符串。几个必须记牢的点:
obj.__class__是这个实例所属的类;type(obj)也能拿到,但对象可以伪造__class__,type()不会。obj.__dict__是实例的命名空间,只装实例自己的属性,看不到类属性。Config上的timeout不在实例字典里。Config.__dict__是映射代理(mapping proxy),提供类命名空间的只读视图,你改不动它。__doc__来自类(或函数/模块)的第一条字符串字面量;没写 docstring 时是None。
三、实例属性与类属性的查找顺序
语言参考把这条链路和描述符协议合起来看才完整。取obj.attr时:
1. 先在类型(沿 MRO 向上)里找同名成员;
若找到且它是数据描述符,直接用它的结果,实例字典被忽略
2. 否则查实例命名空间 obj.__dict__
3. 实例字典里也没有,就用类型上找到的那个成员
(普通类属性或非数据描述符;函数会被变成绑定到实例的方法对象)
4. 以上都找不到,才调用类的 __getattr__(如果定义了)反直觉的地方在第 1 步:数据描述符(带__set__/__delete__的描述符,比如property)的优先级高于实例字典,而普通属性(包括函数,属于非数据描述符)会被实例字典盖住。所以「实例属性覆盖类属性」这条经验法则,对数据描述符是无效的。
# 适用于 Python 3.8+
class Demo:
class_attr = "我是类属性"
@property
def computed(self):
return "我是数据描述符"
def method(self):
return "我是方法"
d = Demo()
d.class_attr = "我盖住了类属性" # 赋值只会写进实例字典,不会改类字典
print(d.class_attr) # 我盖住了类属性
print(Demo.class_attr) # 我是类属性——类字典没变
print(d.computed) # 我是数据描述符——property 优先级更高最后一行尤其重要:即使你给d硬塞一个computed到实例字典里,property作为数据描述符依然优先,会抛AttributeError——这就是「实例属性覆盖类属性」这条经验法则的例外。
四、类、函数、模块上的特殊属性
同一套__name__/__qualname__/__module__/__doc__会出现在好几类对象上,含义略有差异:
| 属性 | 类(type) | 函数 | 模块 |
|---|
__name__ | 类的名字 | 函数名 | 模块名 |
__qualname__ | 带外层作用域的限定名 | 带外层作用域的限定名 | 无(模块用__name__) |
__module__ | 定义该类的模块名 | 定义该函数的模块名 | —— |
__doc__ | 类 docstring | 函数 docstring | 模块 docstring |
__dict__ | 映射代理(只读) | 函数的命名空间 | 模块的命名空间(可写) |
__qualname__(qualified name,限定名)是 3.3 起引入的:它带上了定义处的嵌套路径,所以嵌在类或函数里的东西,__name__只给最后一段,__qualname__给完整路径。
# 适用于 Python 3.8+
class Outer:
class Inner:
def method(self):
pass
print(Outer.Inner.__name__) # Inner
print(Outer.Inner.__qualname__) # Outer.Inner
print(Outer.Inner.method.__qualname__) # Outer.Inner.method__mro__是类上的另一个常用特殊属性:它是方法解析顺序(method resolution order)的元组,C.__mro__能让你一眼看出多重继承时到底先找谁。
五、注解与 __slots__:两个特殊的「声明式」属性
__annotations__存的是对象上的注解(通常是类型提示)。语言参考明确提醒它是惰性求值的——访问这个属性本身可能执行任意代码、也可能抛异常;从 3.14 起注解改为默认延迟求值。类、函数、模块都能有__annotations__,但对没有注解的对象访问它,行为与版本相关,具体以官方文档为准。
__slots__是一个纯声明式的类属性:值可以是字符串、可迭代对象或字符串序列,作用是显式声明实例会有哪些数据成员,并阻止自动创建__dict__和__weakref__。它带来两个实际后果:一是没有__dict__的实例不能挂新属性,赋值会抛AttributeError;二是数据成员固定下来之后,内存占用和属性查找都可能更省。要注意的几个陷阱:从没有__slots__的父类继承时,子类实例照样有__dict__;__slots__用描述符实现,所以不能靠类属性给 slot 变量设默认值;给int、bytes、tuple这类变长内置类型定义非空__slots__会直接抛TypeError。
__match_args__是 3.10 起配合结构化模式匹配用的类属性,值是一个字符串元组,让case MyClass(x, y)这种位置写法能映射到关键字。它的缺省等价于空元组。
六、把反射用起来:一个小例子
# 适用于 Python 3.8+
class Invoice:
"""一张发票。"""
currency = "CNY"
def __init__(self, amount):
self.amount = amount
def describe(obj):
cls = obj.__class__
print("类名:", cls.__name__)
print("限定名:", cls.__qualname__)
print("定义于模块:", cls.__module__)
print("文档:", cls.__doc__)
print("实例属性:", obj.__dict__)
print("类属性键:", sorted(k for k in cls.__dict__ if not k.startswith("__")))
describe(Invoice(99))cls.__dict__里既能看到currency,也能看到__init__——因为类的方法本质上就是存在类字典里的属性。这正是「属性和方法在底层是同一回事」的佐证:区分它们的不是存储位置,而是能不能被语法隐式调用。
常见坑点
- ❌ 以为
obj.__dict__里能看到类属性。
✅ 实例字典只装实例自己的属性;类属性在type(obj).__dict__里,要反射得查两处。
- ❌ 给实例赋一个和数据描述符(如
property)同名的属性,指望能盖住它。
✅ 数据描述符的优先级高于实例字典,「实例属性覆盖类属性」在这类成员上不成立。
- ❌ 认为所有实例都有
__dict__。
✅ 用了__slots__的类,实例可能没有__dict__,反射代码要先hasattr再访问。
- ❌ 把
type(obj).__dict__当普通字典去改。
✅ 类的__dict__是只读的映射代理,改它会报错;要改类型属性用setattr并清楚自己在做什么。
- ❌ 用
__name__当对象的唯一标识。
✅ 嵌套定义时__name__只有最后一段,会重名;限定名要用__qualname__(3.3+)。
- ❌ 给子类只写
__slots__就以为省掉了__dict__。
✅ 父类没有__slots__时,子类实例依然有__dict__,省内存的目标落空。
- ❌ 直接用类属性给
__slots__里的变量设默认值。
✅__slots__是描述符实现的,类属性会覆盖描述符赋值,默认值拿不到;默认值应写在__init__里。
- ❌ 以为
__annotations__一定是个现成的字典,随手就读。
✅ 它是惰性求值的,访问可能触发代码执行甚至抛异常;3.14 起注解默认延迟求值。
总结
| 属性 | 住在哪 | 关键性质 |
|---|
__class__ | 实例 | 实例所属的类 |
__dict__ | 实例 / 类 / 函数 / 模块 | 实例的可见写;类的只读映射代理 |
__name__ | 类 / 函数 / 模块 | 名字最后一段 |
__qualname__ | 类 / 函数 | 带外层路径的限定名(3.3+) |
__module__ | 类 / 函数 / 方法 | 定义处的模块名 |
__doc__ | 类 / 函数 / 模块 | 文档字符串,没有时为 None |
__mro__ | 类 | 方法解析顺序 |
__slots__ | 类 | 声明式;阻止创建__dict__ |
__annotations__ | 类 / 函数 / 模块 | 惰性求值(3.14 起默认延迟) |
__match_args__ | 类 | 配合模式匹配的位置参数(3.10+) |
一句话区分:特殊方法改变行为,特殊属性暴露身份。写类时你关心前者,做反射、调试、序列化时你关心后者。而把两者串起来的那条主线,就是本节反复强调的查找顺序——实例字典先于非数据描述符的类属性,却被数据描述符反超。