040、类与实例:__init__、__new__、实例属性与类属性的混淆点

040、类与实例:initnew、实例属性与类属性的混淆点

上周帮团队排查一个诡异的bug:某个配置类,明明在初始化时给实例赋了默认值,但多个实例之间竟然互相“串数据”。调试器一打,发现所有实例共享同一个列表对象。这种坑,我见过不下十次——根源就是对__init____new__以及类属性与实例属性的混淆。

先看一个“翻车”现场

class Config:
    defaults = []  # 类属性,这里踩过坑
    def __init__(self, name):
        self.name = name
        self.defaults.append(name)  # 别这样写!你以为在操作实例属性?

两个实例一创建,Config('a').defaultsConfig('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

我的经验性建议

  1. 所有可变对象都在__init__里初始化,别放在类定义里。类属性只放不可变常量或者共享的配置(比如数据库连接池)。

  2. 写代码时多问自己一句:这个属性是每个实例独享的,还是所有实例共享的?如果是独享的,必须放在__init__里。

  3. __slots__来约束实例属性,虽然会损失一些灵活性,但能避免拼写错误导致的属性意外创建,也能节省内存。

  4. 调试时用vars(obj)或者obj.__dict__,一眼就能看出哪些是实例属性,哪些是从类上继承来的。

  5. 别滥用__new__,90%的场景用__init__就够了。只有当你需要控制实例创建过程时(比如单例、缓存、元编程),才去碰__new__

最后说一句:Python的灵活是双刃剑。类属性和实例属性的混淆,本质上是“共享”和“独有”的边界没划清楚。每次写类的时候,先想清楚这个边界在哪里,很多bug就能提前规避。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值