前言
很多初学者把 Python 的类(class)理解成"把函数和变量装进一个盒子",于是写出了能跑但语义混乱的代码:类属性当实例属性用、self忘了写、继承关系理不清。真正理解类,需要知道三件事——类本身也是一个对象、属性查找有一套明确的优先级、类体(class body)在定义时就被执行了一次。
另一个常见误解是"类只是组织代码的语法糖"。事实并非如此:class语句运行时会新建一个命名空间(namespace),执行类体里的语句,然后用type构造出一个类对象。换句话说,类定义是运行时行为,不是编译期标记。
本文从数据模型(data model)的角度拆解类的构造过程、命名空间、属性查找顺序与绑定方法(bound method),并给出可以直接复制运行的示例。
一、class 语句到底做了什么
class语句的执行过程大致是三步:
- 解析类体的字节码,准备一个新的命名空间(实际就是一个字典);
- 在这个命名空间里按顺序执行类体内的所有语句(赋值、函数定义、
if、循环等都会真的执行); - 调用元类(默认是
type)来创建类对象,类名绑定到当前作用域。
因为第 2 步会真的执行,所以类体里的表达式会在导入或定义的那一刻求值一次,而不是每次实例化时求值:
# 适用于 Python 3.8+
import time
def make_stamp():
print("类体被执行了一次")
return time.time()
class Config:
created_at = make_stamp() # 这一行在定义类时就跑了这里的make_stamp()只会被调用一次。类对象创建后,Config.created_at是一个固定值。如果误以为它每次实例化都刷新,就会写出过期的配置。
类创建好之后,类的命名空间可以从__dict__看到,它是一个映射代理(mappingproxy):
# 适用于 Python 3.8+
class Point:
"""一个二维点。"""
dim = 2
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
print(Point.__dict__["dim"]) # 2
print("__dict__" in Point.__dict__) # False
print(vars(p)) # {'x': 1, 'y': 2}注意Point.__dict__里没有__dict__这个键,但实例p有__dict__(除非用了__slots__)。vars(p)等价于p.__dict__。
二、属性查找的优先级
实例属性访问obj.name时,查找顺序是:
- 先查类型(type)的 MRO,找数据描述符(data descriptor,定义了
__set__或__delete__)。找到就直接用; - 再查实例的
__dict__; - 最后查类型 MRO,找非数据描述符或普通类属性。
这个顺序解释了两个经典现象:@property能挡住实例属性赋值(数据描述符优先),而实例属性可以覆盖普通类属性(实例字典优先于类的普通属性)。
# 适用于 Python 3.8+
class Base:
name = "base"
class Child(Base):
pass
c = Child()
print(c.name) # base(沿 MRO 找到 Base.name)
c.name = "child"
print(c.name) # child(进入实例字典)
print(Child.name) # base(类属性没变)
del c.name
print(c.name) # base再看看方法绑定。从类上取方法得到的是函数,从实例上取方法得到的是绑定方法(bound method),它把self记住了:
# 适用于 Python 3.8+
class Greeter:
def hello(self):
return "hi"
g = Greeter()
print(Greeter.hello) # <function ...>
print(g.hello) # <bound method ...>
print(g.hello()) # hi
print(g.hello.__self__) # <__main__.Greeter object ...>| 访问方式 | 得到的东西 | 是否需要手动传 self |
|---|
Greeter.hello | 函数(function) | 需要,Greeter.hello(g) |
g.hello | 绑定方法(bound method) | 不需要 |
Greeter.__dict__["hello"] | 函数 | 需要 |
三、类属性、实例属性与可变对象
最容易踩的坑是可变的类属性被所有实例共享。因为类属性只存在一份,放在类上:
# 适用于 Python 3.8+
class Bad:
items = [] # 类属性,全类共享
def add(self, x):
self.items.append(x) # 原地修改的是类属性,不是新建实例属性
a = Bad()
b = Bad()
a.add(1)
print(b.items) # [1] —— b 也"被改了"
print(a.items is b.items) # Trueself.items.append(...)只做了属性读取再原地修改,没有给self.items赋值,所以不会写入实例字典。正确做法是在__init__里给每个实例建自己的列表:
# 适用于 Python 3.8+
class Good:
def __init__(self):
self.items = [] # 每个实例一份
def add(self, x):
self.items.append(x)
a = Good()
b = Good()
a.add(1)
print(b.items) # []
print(a.items) # [1]四、类的相等性与可哈希性
默认情况下,用户定义的类用身份(identity)比较:两个不同实例永远不相等。如果想按内容比较,就定义__eq__。但要小心一个联动规则:定义__eq__而没有定义__hash__,__hash__会被自动设为None,实例变成不可哈希(unhashable)。
# 适用于 Python 3.8+
class WithEq:
def __init__(self, v):
self.v = v
def __eq__(self, other):
return isinstance(other, WithEq) and self.v == other.v
w = WithEq(1)
print(w == WithEq(1)) # True
try:
hash(w)
except TypeError as e:
print("TypeError:", e) # unhashable type: 'WithEq'如果这个类的实例需要放进set或当字典键,就必须同时显式定义__hash__,并且保证相等的对象哈希值相同:
# 适用于 Python 3.8+
class Key:
def __init__(self, v):
self.v = v
def __eq__(self, other):
return isinstance(other, Key) and self.v == other.v
def __hash__(self):
return hash(self.v)
print(hash(Key(1)) == hash(Key(1))) # True
print(len({Key(1), Key(1)})) # 1常见坑点
- 可变类属性当实例属性用
❌
``python # 适用于 Python 3.8+ class Box: data = {}``
✅
``python # 适用于 Python 3.8+ class Box: def __init__(self): self.data = {}``
- 忘了写
self形参
❌
``python # 适用于 Python 3.8+ class A: def f(): # 调用时参数数量不匹配 return 1``
✅
``python # 适用于 Python 3.8+ class A: def f(self): return 1``
__init__写了return却返回非None
❌
``python # 适用于 Python 3.8+ class A: def __init__(self): return 5 # TypeError: __init__() should return None``
✅
``python # 适用于 Python 3.8+ class A: def __init__(self): self.value = 5``
- 用
is比较值相等的对象
❌
``python # 适用于 Python 3.8+ a = WithEq(1) b = WithEq(1) print(a is b) # False,身份不同``
✅
``python # 适用于 Python 3.8+ print(a == b) # True,走 __eq__``
- 定义了
__eq__还指望能放进set
❌
``python # 适用于 Python 3.8+ class C: def __eq__(self, other): return True # hash(C()) 会 TypeError``
✅
```python # 适用于 Python 3.8+ class C: def __eq__(self, other): return isinstance(other, C)
def __hash__(self): return 0 ```
- 在类体里引用同类的前一个类属性
❌
``python # 适用于 Python 3.8+ class C: base = 10 derived = base * 2 # NameError:类体不是闭包作用域``
✅
```python # 适用于 Python 3.8+ class C: base = 10
C.derived = C.base * 2 ```
- 用
type(x) == T判类型,漏掉子类
❌
``python # 适用于 Python 3.8+ print(type(True) == int) # False,bool 是 int 的子类``
✅
``python # 适用于 Python 3.8+ print(isinstance(True, int)) # True``
总结
| 概念 | 关键结论 |
|---|
| class 语句 | 运行时执行类体,用元类构造类对象 |
| 属性查找 | 数据描述符 → 实例字典 → 类 MRO |
| 类属性 vs 实例属性 | 类上只有一份;可变类属性会被所有实例共享 |
| 方法绑定 | 函数 vs 绑定方法,绑定方法自带self |
__eq__与__hash__ | 定义__eq__会把__hash__置为None |
| 实例命名空间 | vars(obj)即obj.__dict__ |
类不是语法糖,而是一套可推理的运行时机制:类体只执行一次,属性查找有固定优先级,相等性与哈希性互相牵制。把这三条记牢,绝大多数"奇怪的行为"都能自己解释清楚。写继承和比较逻辑时,优先问一句"这个属性到底存在类上还是实例上"。