040、类与实例:init、new、实例属性与类属性的混淆点
上周帮团队排查一个诡异的bug:某个配置类,明明在初始化时给实例赋了默认值,但多个实例之间竟然互相“串数据”。调试器一打,发现所有实例共享同一个列表对象。这种坑,我见过不下十次——根源就是对__init__、__new__以及类属性与实例属性的混淆。
先看一个“翻车”现场
class Config:
defaults = [] # 类属性,这里踩过坑
def __init__(self, name):
self.name = name
self.defaults.append(name) # 别这样写!你以为在操作实例属性?
两个实例一创建,Config('a').defaults和Config('b').defaults都变成了['a', 'b']。为什么?因为self.defaults在实例上没有找到,直接去类上找了那个共享的列表。你以为是实例属性,实际上操作的是类属性。
new__和__init:谁先谁后?谁管谁?
很多人以为__init__是“构造函数”,其实它只是初始化器。真正的构造器是__new__,它负责创建实例并返回。__init__拿到__new__返回的对象,再往里塞属性。
class Demo:
def __new__(cls, *args, **kwargs):
print("__new__被调用了,cls是", cls)
instance = super().__new__(cls) # 必须调用父类的__new__,否则拿不到实例
return instance # 必须返回实例,否则__init__不会执行
def __init__(self, value):
print("__init__被调用了,self是", self)
self.value = value
一个冷知识:如果__new__返回的不是当前类的实例,__init__压根不会执行。这个特性可以用来实现单例模式——在__new__里控制只返回同一个实例。
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self, value):
# 注意:每次实例化都会执行__init__,但实例是同一个
self.value = value
这里有个坑:单例模式下__init__每次都会被调用,如果你在里面重置了属性,之前的状态就丢了。解决方案是加个标志位,只在第一次初始化。
实例属性 vs 类属性:Python的查找链
Python的属性查找顺序是:实例属性 -> 类属性 -> 父类属性。这个链很容易让人产生错觉。
class Parent:
x = 10
class Child(Parent):
pass
obj = Child()
print(obj.x) # 10,从Parent继承来的
obj.x = 20 # 这里创建了实例属性,覆盖了类属性
print(Child.x) # 还是10,类属性没变
print(obj.x) # 20,实例自己的
这个机制本身没问题,但当你操作可变对象时就会翻车。比如类属性是个列表,你通过实例去append,实际上修改的是类属性本身。
class Team:
members = [] # 类属性,所有实例共享
def add_member(self, name):
self.members.append(name) # 这里self.members指向类属性
正确的做法是在__init__里创建实例属性:
class Team:
def __init__(self):
self.members = [] # 每个实例都有自己的列表
def add_member(self, name):
self.members.append(name)
那些年我踩过的坑
坑1:在__init__里用默认参数传递可变对象
class Bad:
def __init__(self, items=[]): # 别这样写!默认参数在定义时只计算一次
self.items = items
这个坑和类属性无关,但同样致命。默认参数[]在函数定义时就被创建了,所有不传items的实例共享同一个列表。
坑2:混淆了__init__和__new__的职责
__new__适合做:控制实例创建(单例、缓存)、返回不同类型的对象。__init__适合做:初始化实例属性、执行验证逻辑。别在__new__里做初始化,也别在__init__里控制实例的创建。
坑3:类属性被意外修改
class Counter:
count = 0
def increment(self):
self.count += 1 # 这里创建了实例属性,不是修改类属性
self.count += 1实际上是self.count = self.count + 1,先读取类属性,然后赋值给实例属性。类属性count根本没变。如果想修改类属性,要用type(self).count += 1或者Counter.count += 1。
我的经验性建议
-
所有可变对象都在__init__里初始化,别放在类定义里。类属性只放不可变常量或者共享的配置(比如数据库连接池)。
-
写代码时多问自己一句:这个属性是每个实例独享的,还是所有实例共享的?如果是独享的,必须放在
__init__里。 -
用
__slots__来约束实例属性,虽然会损失一些灵活性,但能避免拼写错误导致的属性意外创建,也能节省内存。 -
调试时用
vars(obj)或者obj.__dict__,一眼就能看出哪些是实例属性,哪些是从类上继承来的。 -
别滥用
__new__,90%的场景用__init__就够了。只有当你需要控制实例创建过程时(比如单例、缓存、元编程),才去碰__new__。
最后说一句:Python的灵活是双刃剑。类属性和实例属性的混淆,本质上是“共享”和“独有”的边界没划清楚。每次写类的时候,先想清楚这个边界在哪里,很多bug就能提前规避。

477

被折叠的 条评论
为什么被折叠?



