前言类里有什么、对象里有什么是理解 Python 面向对象的第一道门槛。很多人以为类和对象共享同一份数据或者以为对象创建时会把类里的属性复制一遍。这两种理解都不对。准确的说法是类是对象class 本身也是type的实例对象实例是一份独立的命名空间。类属性活在类的命名空间里实例属性活在实例的命名空间里两者通过属性查找规则关联但从不复制。本文从对象里面到底装了什么这个角度切入命名空间__dict__、身份identity与相等equality、动态属性操作以及__slots__对对象结构的改变。一、类对象与实例对象在 Python 里几乎一切都是对象。类本身也是对象它的类型是元类metaclass默认是type。# 适用于 Python 3.8class Dog:species Canis familiarisd Dog()print(type(Dog)) # class typeprint(type(d)) # class __main__.Dogprint(Dog.__class__) # class typeprint(d.__class__) # class __main__.Dogprint(d.__class__ is Dog) # Truetype(obj)返回对象所属的类型obj.__class__通常与之相同但代理proxy对象的__class__可以指向被代理的类因此类型判断优先用isinstance()。表达式含义例子结果type(d)对象所属的类型Dogd.__class__同上但可被代理改写Dogid(d)对象身份生命周期内不变一个整数isinstance(d, Dog)类型检查含子类Trued is e身份比较看是否同一对象二、实例的命名空间__dict__与vars()普通实例携带一个字典用来存自己的属性。这个字典就是实例的命名空间。# 适用于 Python 3.8class Dog:species Canis familiarisdef __init__(self, name):self.name named Dog(Rex)print(vars(d)) # {name: Rex}print(d.__dict__) # {name: Rex}print(vars(d) is d.__dict__) # Trueprint(species in d.__dict__) # False —— 类属性不在实例字典里关键结论species是类属性不会出现在实例字典里。d.species之所以能读到是因为属性查找在实例字典里没找到后继续去类型里找。vars()与__dict__的关系对普通实例vars(obj)就是obj.__dict__对类vars(SomeClass)返回类的命名空间一个mappingproxy。Vars不适合没有__dict__的对象。# 适用于 Python 3.8print(type(vars(Dog))) # class mappingproxyprint(Dog.__dict__[species]) # Canis familiaris三、身份、相等与可变性is比较身份是不是同一个对象比较相等内容是否相同由__eq__决定。对用户定义的类默认的等价于is。# 适用于 Python 3.8a Dog(Rex)b Dog(Rex)c aprint(a is b) # False两个不同实例print(a is c) # True同一个对象print(a b) # False默认按身份比较print(a c) # True对不可变的内置类型小整数缓存CPython 缓存 -5 到 256和字符串驻留会让小值对象共享这是实现细节不要依赖# 适用于 Python 3.8x 256y 256print(x is y) # CPython 里 True但这是实现细节p 257q 257print(p is q) # 不保证CPython 交互式下可能 True这段代码在交互式解释器里和脚本里的结果可能不同所以判断值相等请一律用。四、动态属性操作对象属性可以在运行时增删查改。即使__init__里没定义也可以事后加# 适用于 Python 3.8d Dog(Rex)d.age 3 # 新增实例属性print(hasattr(d, age)) # Trueprint(getattr(d, age)) # 3print(getattr(d, color, unknown)) # unknown带默认值setattr(d, color, brown)print(d.color) # browndelattr(d, age)print(hasattr(d, age)) # Falsedel d.colorprint(hasattr(d, color)) # False函数作用找不到时的行为hasattr(o, name)是否有该属性返回Falsegetattr(o, name)取属性抛AttributeErrorgetattr(o, name, default)取属性带默认值返回默认值setattr(o, name, value)设属性无返回值delattr(o, name)删属性抛AttributeError因为可以随手加属性拼错属性名不会立刻报错——这是动态语言里很常见的坑。五、__slots__换掉实例字典如果一个类会创建海量实例且属性固定可以用__slots__声明允许的属性名从而不再为每个实例创建__dict__和__weakref__。# 适用于 Python 3.8class Point:__slots__ (x, y)def __init__(self, x, y):self.x xself.y yp Point(1, 2)print(p.x, p.y) # 1 2try:print(p.__dict__)except AttributeError as e:print(AttributeError:, e) # 没有 __dict__try:p.z 3except AttributeError as e:print(AttributeError:, e) # z 不在 slots 里注意几个官方文档明确列出的限制如果父类没有__slots__子类实例仍会带上__dict__不声明__weakref__就不能对该实例做弱引用__slots__通过描述符实现因此不能用类属性给它设默认值那样会覆盖描述符从int、bytes、tuple这类变长内置类型派生时定义非空__slots__会抛TypeError。六、一个完整的小例子# 适用于 Python 3.8class Account:kind savings # 类属性所有实例共享def __init__(self, owner, balance0):self.owner owner # 实例属性self.balance balancedef deposit(self, amount):if amount 0:raise ValueError(金额必须为正)self.balance amountreturn self.balancedef __repr__(self):return fAccount({self.owner!r}, {self.balance})acc Account(Ada, 100)acc.deposit(50)print(acc) # Account(Ada, 150)print(acc.__dict__) # {owner: Ada, balance: 150}print(Account.kind) # savings这里能清楚看到__dict__只装了owner和balance两个实例属性kind留在类上。常见坑点以为实例化会复制类属性❌python # 适用于 Python 3.8 class C: data []c C() c.data.append(1) # 改的是类属性 print(C.data) # [1] ✅python # 适用于 Python 3.8 class C: def __init__(self): self.data []c C() c.data.append(1) print(C.data if hasattr(C, data) else 类上没有 data) 用判断是不是同一个对象❌python # 适用于 Python 3.8 a [1, 2] b [1, 2] print(a b and a is b) # False✅python # 适用于 Python 3.8 print(a is b) # False要判身份就用 is依赖小整数缓存❌python # 适用于 Python 3.8 n 1000 print(n is 1000) # 结果不保证不要这样判等✅python # 适用于 Python 3.8 print(n 1000) # 比较值一律用 del obj.attr后继续访问❌python # 适用于 Python 3.8 d Dog(Rex) del d.name print(d.name) # AttributeError✅python # 适用于 Python 3.8 d Dog(Rex) if hasattr(d, name): del d.name给__slots__的字段设类属性默认值❌python # 适用于 Python 3.8 class P: __slots__ (x,) x 0 # 覆盖了槽描述符赋值会出问题✅python # 适用于 Python 3.8 class P: __slots__ (x,)def __init__(self): self.x 0 在__slots__类上做弱引用❌python # 适用于 Python 3.8 import weakrefclass P: __slots__ (x,)weakref.ref(P(1)) # TypeError ✅python # 适用于 Python 3.8 class P: __slots__ (x, __weakref__)getattr忘了第三个参数导致崩溃❌python # 适用于 Python 3.8 getattr(d, color) # AttributeError✅python # 适用于 Python 3.8 getattr(d, color, None)总结概念结论类对象也是对象类型是元类默认type实例命名空间obj.__dict__即vars(obj)类属性存在类命名空间不复制到实例身份与相等is判身份判内容动态属性getattr/setattr/hasattr/delattr__slots__去掉实例字典属性受限类和对象的关系是查找而不是复制实例只装自己的属性读不到时再沿类型上溯。抓住__dict__、is/和__slots__这三块内容对象里装了什么就一目了然了。拼错的属性名不会自动报错这一点要格外小心。
阅读完成 · 觉得有帮助?