实例变量与类变量
Python 中不存在"成员变量声明"的概念。类体中直接赋值的名称成为类变量(class variable),而在方法内部通过 self.attr 赋值的成为实例变量(instance variable)。两者的存储位置、生命周期和共享方式截然不同,混淆它们是新手最常见的错误之一。
实例变量:每个对象独立持有
实例变量绑定在实例对象自身的 __dict__ 中,不同实例之间完全隔离。它们不需要声明,首次赋值时即产生,也可以在运行时动态删除。
class Dog:
def __init__(self, name):
self.name = name # 实例变量
self.tricks = [] # 每个实例获得独立的列表
def add_trick(self, trick):
self.tricks.append(trick)
d = Dog('Fido')
e = Dog('Buddy')
d.add_trick('roll over')
e.add_trick('play dead')
print(d.tricks) # ['roll over']
print(e.tricks) # ['play dead']
print(d.name) # Fido
实例变量通常在 __init__ 中初始化,但也可以在其他方法或外部代码中随时创建。如果访问一个尚未赋值的实例属性,会抛出 AttributeError:
class Cat:
pass
c = Cat()
# print(c.name) # AttributeError: 'Cat' object has no attribute 'name'
c.name = 'Whiskers' # 动态创建
print(c.name) # Whiskers
类变量:所有实例共享
类变量存储在类对象的 __dict__ 中,被该类的所有实例共享。它适合存放常量、默认值或跨实例统计信息。
class Dog:
kind = 'canine' # 类变量:所有 Dog 共享
count = 0 # 类变量:用于统计实例数
def __init__(self, name):
self.name = name # 实例变量
Dog.count += 1 # 修改类变量
d = Dog('Fido')
e = Dog('Buddy')
print(d.kind) # canine
print(e.kind) # canine
print(Dog.count) # 2
通过类名访问类变量(如 Dog.count)语义清晰,直接表明操作的是共享数据。虽然通过实例也能读取类变量,但修改时行为差异极大,需要格外小心。
可变类变量的陷阱
当类变量指向可变对象(列表、字典、集合等)时,所有实例操作的是同一个对象。这是 Python 面向对象中最隐蔽的陷阱之一。
class Dog:
tricks = [] # 危险:所有实例共享同一个列表
def __init__(self, name):
self.name = name
def add_trick(self, trick):
self.tricks.append(trick) # 修改的是共享列表!
d = Dog('Fido')
e = Dog('Buddy')
d.add_trick('roll over')
e.add_trick('play dead')
print(d.tricks) # ['roll over', 'play dead'] —— 非预期结果!
print(e.tricks) # ['roll over', 'play dead']
print(d.tricks is e.tricks) # True,确实是同一个对象
正确的做法是在 __init__ 中为每个实例创建独立的可变对象:
class Dog:
def __init__(self, name):
self.name = name
self.tricks = [] # 每个实例获得新列表
def add_trick(self, trick):
self.tricks.append(trick)
d = Dog('Fido')
e = Dog('Buddy')
d.add_trick('roll over')
e.add_trick('play dead')
print(d.tricks) # ['roll over']
print(e.tricks) # ['play dead']
属性查找顺序
当通过实例访问属性时,Python 按照以下顺序查找:
- 首先在实例的
__dict__中查找(数据属性) - 如果未找到,在类的
__dict__中查找 - 如果类中未找到,沿继承链向上查找(见继承章节)
这一规则意味着实例属性会遮蔽类属性。当实例和类拥有同名属性时,实例优先。
class Warehouse:
purpose = 'storage'
region = 'west'
w1 = Warehouse()
w2 = Warehouse()
print(w1.region) # west —— 读取类变量
w2.region = 'east' # 在 w2 的 __dict__ 中创建实例变量
print(w2.region) # east —— 实例变量遮蔽类变量
print(w1.region) # west —— w1 不受影响
# 查看命名空间
print(w2.__dict__) # {'region': 'east'}
print(Warehouse.__dict__['region']) # west
删除实例属性后,对该属性的访问会重新回落到类属性:
del w2.region
print(w2.region) # west —— 重新暴露类属性
通过实例修改类变量的误区
初学者常犯的错误是通过实例对类变量赋值,期望修改所有实例共享的值:
class Counter:
total = 0
c1 = Counter()
c2 = Counter()
c1.total = 100 # 这不是修改类变量!
print(c1.total) # 100 —— 实例变量
print(c2.total) # 0 —— 仍是类变量
print(Counter.total) # 0 —— 类变量未被修改
c1.total = 100 在 c1 的 __dict__ 中新建了键 total,完全遮蔽了类变量 Counter.total。要真正修改类变量,必须通过类名赋值:
Counter.total = 200
print(c2.total) # 200
print(c1.total) # 100 —— c1 的实例变量仍遮蔽类变量
类变量作为默认值
类变量有时被用作实例属性的默认值。当实例没有显式设置该属性时,自动回退到类变量。
class Config:
timeout = 30 # 类级别的默认超时
def __init__(self):
self.host = 'localhost'
def get_timeout(self):
# 优先返回实例设置,否则使用类默认值
return getattr(self, 'timeout', Config.timeout)
c = Config()
print(c.get_timeout()) # 30
c.timeout = 60 # 为特定实例覆盖
print(c.get_timeout()) # 60
这种设计模式在框架和库中很常见,但需要注意可变默认值的陷阱。如果默认值是列表或字典,务必在 __init__ 中创建副本或新对象。