在Python的面向对象编程里,实例属性和方法是最基础也最容易用错的部分。很多看似能跑的代码,其实埋着共享状态污染、方法绑定混乱的隐患。理解它们背后的数据模型和绑定机制,才能真正写出可维护的类。

实例属性应该定义在哪儿
初学者常把属性直接声明在类体中,以为和在__init__里赋值是一回事。实际上,写在类体中的变量属于类属性,存放在类的__dict__里,被所有实例共享;而写在__init__中用self.x = ...赋值的才是实例属性,存放在每个实例自己的__dict__中。
当访问obj.attr时,Python的解释器会先查找实例的__dict__,找不到再去类的__dict__里找,最后到父类的__dict__。这种查找链意味着:如果类属性是个可变对象(如列表),一个实例修改它,其他实例看到的也会变。下面这段代码就暴露了问题:
class BadBox:
items = [] # 类属性,所有实例共享
def add(self, x):
self.items.append(x)
b1 = BadBox()
b1.add(1)
b2 = BadBox()
print(b2.items) # 输出 [1],b2并未添加却有了数据
正确的做法是将可变状态放到实例属性中,保证每个对象独立:
class GoodBox:
def __init__(self):
self.items = [] # 实例属性
def add(self, x):
self.items.append(x)
b1 = GoodBox()
b1.add(1)
b2 = GoodBox()
print(b2.items) # 输出 [],互不干扰
从内存和语义上看,实例属性让对象之间边界清晰,类属性只适合放真正的常量或方法。如果强行用类属性存状态,在多线程或长期运行的服务里会引发极难排查的bug。
实例方法如何绑定到对象
实例方法的第一个参数通常叫self,它并不是关键字,而是约定俗成的名字。当你写obj.method()时,Python会把obj作为第一个参数自动传进去。这背后是描述符协议:函数对象实现了__get__,在通过实例访问时返回绑定方法(bound method)。
看一个简单例子,注意方法内部如何操作实例属性:
class User:
def __init__(self, name):
self.name = name
def greet(self):
return 'hello ' + self.name
u = User('tom')
print(u.greet()) # hello tom
如果直接拿类调用方法,比如User.greet(u),则需要手动传实例,此时它只是一个普通函数。这种机制说明:方法本质上是属于类的函数,但通过实例调用时会自动绑定self。滥用静态逻辑到实例方法里,或把本该是实例方法写成静态方法却依赖状态,都会破坏封装。
用property优化属性访问
有时我们希望在获取或设置属性时做校验,又不想让调用方改成方法调用。Python的@property装饰器可以把方法变成只读或可读写属性,对外看起来像普通字段,内部却有逻辑保护。
class Score:
def __init__(self, value):
self._value = value
@property
def value(self):
return self._value
@value.setter
def value(self, v):
if v < 0 or v > 100:
raise ValueError('分数必须在0到100之间')
self._value = v
s = Score(90)
s.value = 95
# s.value = 200 会抛异常
这样实例属性的使用就兼顾了易用性和安全性。相比直接暴露self.value,property让我们在不改变接口的前提下加上约束。需要注意,property本身是定义在类上的描述符,实例并没有一份拷贝,因此它是轻量的。
常见误区与写法建议
一个典型误区是在__init__之外给实例动态添加属性,虽然Python允许,但会让对象结构不可控。建议所有实例属性都在__init__中声明,哪怕先赋None。另一个误区是方法里直接改类属性,例如self.__class__.count += 1,这在继承场景下容易误改父类状态。
总结下来,实例属性用self在初始化时定义,方法通过self访问和修改这些属性,需要控制访问就用property。把类属性留给方法、常量和配置,就能避开大多数面向对象设计的坑。