首页 / 资讯中心 / 文章详情

深入浅析Python的类

深入浅析Python的类 ★ FEATURED ARTICLE
前言很多初学者把 Python 的类class理解成把函数和变量装进一个盒子于是写出了能跑但语义混乱的代码类属性当实例属性用、self忘了写、继承关系理不清。真正理解类需要知道三件事——类本身也是一个对象、属性查找有一套明确的优先级、类体class body在定义时就被执行了一次。另一个常见误解是类只是组织代码的语法糖。事实并非如此class语句运行时会新建一个命名空间namespace执行类体里的语句然后用type构造出一个类对象。换句话说类定义是运行时行为不是编译期标记。本文从数据模型data model的角度拆解类的构造过程、命名空间、属性查找顺序与绑定方法bound method并给出可以直接复制运行的示例。一、class 语句到底做了什么class语句的执行过程大致是三步解析类体的字节码准备一个新的命名空间实际就是一个字典在这个命名空间里按顺序执行类体内的所有语句赋值、函数定义、if、循环等都会真的执行调用元类默认是type来创建类对象类名绑定到当前作用域。因为第 2 步会真的执行所以类体里的表达式会在导入或定义的那一刻求值一次而不是每次实例化时求值# 适用于 Python 3.8import timedef make_stamp():print(类体被执行了一次)return time.time()class Config:created_at make_stamp() # 这一行在定义类时就跑了这里的make_stamp()只会被调用一次。类对象创建后Config.created_at是一个固定值。如果误以为它每次实例化都刷新就会写出过期的配置。类创建好之后类的命名空间可以从__dict__看到它是一个映射代理mappingproxy# 适用于 Python 3.8class Point:一个二维点。dim 2def __init__(self, x, y):self.x xself.y yp Point(1, 2)print(Point.__dict__[dim]) # 2print(__dict__ in Point.__dict__) # Falseprint(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.8class Base:name baseclass Child(Base):passc Child()print(c.name) # base沿 MRO 找到 Base.namec.name childprint(c.name) # child进入实例字典print(Child.name) # base类属性没变del c.nameprint(c.name) # base再看看方法绑定。从类上取方法得到的是函数从实例上取方法得到的是绑定方法bound method它把self记住了# 适用于 Python 3.8class Greeter:def hello(self):return hig Greeter()print(Greeter.hello) # function ...print(g.hello) # bound method ...print(g.hello()) # hiprint(g.hello.__self__) # __main__.Greeter object ...访问方式得到的东西是否需要手动传 selfGreeter.hello函数function需要Greeter.hello(g)g.hello绑定方法bound method不需要Greeter.__dict__[hello]函数需要三、类属性、实例属性与可变对象最容易踩的坑是可变的类属性被所有实例共享。因为类属性只存在一份放在类上# 适用于 Python 3.8class 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.8class 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.8class WithEq:def __init__(self, v):self.v vdef __eq__(self, other):return isinstance(other, WithEq) and self.v other.vw WithEq(1)print(w WithEq(1)) # Truetry:hash(w)except TypeError as e:print(TypeError:, e) # unhashable type: WithEq如果这个类的实例需要放进set或当字典键就必须同时显式定义__hash__并且保证相等的对象哈希值相同# 适用于 Python 3.8class Key:def __init__(self, v):self.v vdef __eq__(self, other):return isinstance(other, Key) and self.v other.vdef __hash__(self):return hash(self.v)print(hash(Key(1)) hash(Key(1))) # Trueprint(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 10C.derived C.base * 2 用type(x) T判类型漏掉子类❌python # 适用于 Python 3.8 print(type(True) int) # Falsebool 是 int 的子类✅python # 适用于 Python 3.8 print(isinstance(True, int)) # True总结概念关键结论class 语句运行时执行类体用元类构造类对象属性查找数据描述符 → 实例字典 → 类 MRO类属性 vs 实例属性类上只有一份可变类属性会被所有实例共享方法绑定函数 vs 绑定方法绑定方法自带self__eq__与__hash__定义__eq__会把__hash__置为None实例命名空间vars(obj)即obj.__dict__类不是语法糖而是一套可推理的运行时机制类体只执行一次属性查找有固定优先级相等性与哈希性互相牵制。把这三条记牢绝大多数奇怪的行为都能自己解释清楚。写继承和比较逻辑时优先问一句这个属性到底存在类上还是实例上。
阅读完成 · 觉得有帮助?
咨询建站