第 8 章 类与对象
8.1 修改实例的字符串表示
# 修改实例的字符串表示 即修改打印实例所产生的输出 让输出更有意义
# 即通过修改__repr__() 和 __str__()方法
class Pair:
def __init__(self, x, y):
self.x = x
self.y = y
# 返回对象的“代码表示”字符串
# 即可以通过 eval(repr(obj)) 重新构造出原对象
# 如果没有!r 系统会默认使用!s 导致无法使用eval()重建对象
def __repr__(self):
return 'Pair({0.x!r}, {0.y!r})'.format(self)
# 返回对象的“可读字符串”表示
def __str__(self):
return '({0.x!s}, {0.y!s})'.format(self)
p = Pair(3, 4)
print(p) # 输出: (3, 4),调用 __str__
print(repr(p)) # 输出: Pair(3, 4),调用 __repr__
# 使用 {0!r} 进行占位时,默认会调用对应对象的 __repr__() 方法来返回字符串
print('p is {0!r}'.format(p)) # 输出: p is Pair(3, 4)
# 使用 {0} 进行占位时,默认会调用对应对象的 __str__() 方法来返回字符串
print('p is {0}'.format(p)) # 输出: p is (3, 4)
# 如果没有为一个类定义 __str__() 方法,Python 会使用 __repr__() 的输出作为替代
占位符的使用
def __repr__(self):
return 'Pair({0.x!r}, {0.y!r})'.format(self)
# {0} 是 .format() 中的位置参数索引,指的是.format() 括号中传入的第一个参数(索引从0开始)
# {0.x} 就是访问 self.x 属性
# !r 是一个 转换标志,表示对这个值使用 repr() 函数进行转换。
# !s 表示使用 str() 转换对应的值,即调用对象的 __str__() 方法
# 在 .format() 中,不加转换标志时,默认是 !s
# 旧式操作符
# %r 在字符串格式化中就等价于 !r 的作用
def __repr__(self):
return 'Pair(%r, %r)' % (self.x, self.y)
8.2 自定义字符串的输出格式
# 通过在类中定义_format__()实现自定义字符串的输出形式
_formats = {
'ymd' : '{d.year}-{d.month}-{d.day}',
'mdy' : '{d.month}/{d.day}/{d.year}',
'dmy' : '{d.day}/{d.month}/{d.year}'
}
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
# 自定义格式化输出方法
# 使用format(obj) 或使用 '{}:{}'.format(d) 时,就会调用这个方法
def __format__(self, code):
if code == '':
code = 'ymd'
fmt = _formats[code]
return fmt.format(d=self)
d = Date(2012, 12, 21)
print(format(d)) # 输出: 2012-12-21
print(format(d, 'mdy')) # 输出: 12/21/2012
print('The date is {:ymd}'.format(d)) # 输出: The date is 2012-12-21
print('The date is {:mdy}'.format(d)) # 输出: The date is 12/21/2012
# :mdy 是传给 __format__() 方法的格式码 code
from datetime import date
# 对格式化代码的解释完全取决于类本身
d = date(2012, 12, 21)
print(format(d)) # 输出: '2012-12-21'
print(format(d, '%A, %B %d, %Y')) # 输出: 'Friday, December 21, 2012'
print('The end is {:%d %b %Y}. Goodbye'.format(d))
# 输出: 'The end is 21 Dec 2012. Goodbye'
8.3 让对象支持上下文管理协议
from socket import socket, AF_INET, SOCK_STREAM
# socket:用于创建网络连接的类
# AF_INET:表示 IPv4 地址族
# SOCK_STREAM:表示 TCP 协议(流式套接字)
from functools import partial
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
# 要连接的目标地址
self.address = address
# 地址族
self.family = family
# 套接字类型
self.type = type
# 当前的连接
self.sock = None
# 实现上下文管理协议的__enter__方法
# __enter__() 方法会在进入 with 块之前被自动调用,返回值会被赋值给as后面的变量
def __enter__(self):
# 已经存在连接 就抛出异常 避免重复连接
if self.sock is not None:
raise RuntimeError('Already connected')
# 创建一个新的socket连接
self.sock = socket(self.family, self.type)
self.sock.connect(self.address)
# 返回socket供socket块使用
return self.sock
# 实现离开方法 将在退出with块的时候调用
# 三个参数分别为
# exc_type 异常类型 如果没有异常发生,则为None
# exc_val 异常实例(对象) 包含错误信息等属性
# exc_tb traceback对象 指向异常发生时的调用栈信息
def __exit__(self, exc_ty, exc_val, tb):
self.sock.close()
# 释放资源
self.sock = None
# 可以在__exit__方法中进行异常处理 通过在本方法返回 True 可以抑制异常传播
if __name__ == '__main__':
conn = LazyConnection(('www.python.org', 80))
try:
# 通过with语句来建立和关闭网络链接
#
with conn as s:
# 发送 HTTP GET 请求
s.send(b'GET /index.html HTTP/1.0\r\n')
s.send(b'Host: www.python.org\r\n')
s.send(b'\r\n')
# 接收响应数据
resp = b''.join(iter(partial(s.recv, 8192), b''))
# 打印响应内容长度及前 200 字节
print(f"Response length: {len(resp)}")
print("First 200 bytes:")
print(resp[:200].decode('utf-8', errors='ignore'))
except Exception as e:
print("Error occurred:", e)
嵌套连接的实现
# 原先的代码只允许建立一条单独的socket链接 重复使用with语句会导致异常
# 下面代码可以实现嵌套连接
from socket import socket, AF_INET, SOCK_STREAM
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = AF_INET
self.type = SOCK_STREAM
self.connections = [] # 用列表保存多个 socket
def __enter__(self):
sock = socket(self.family, self.type) # 创建新 socket
sock.connect(self.address) # 建立连接
self.connections.append(sock) # 添加到列表中
return sock # 返回给 with as 使用
def __exit__(self, exc_ty, exc_val, tb):
self.connections.pop().close() # 弹出最后一个 socket 并关闭
from functools import partial
conn = LazyConnection(('www.python.org', 80))
with conn as s1:
print("s1 connected")
with conn as s2:
print("s2 connected")
# s1 和 s2 是两个独立的 socket 连接
# s2 离开后自动关闭
# s1 离开后也自动关闭
# 使用上下文管理器能避免死锁 因为可以在exit方法中执行清理方法
8.4 当创建大量实例时如何节省内存
class Date:
__slots__ = ['year', 'month', 'day']
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
# 定义 __slots__ 后,Python 会为类实例使用一种基于数组的紧凑内存布局,替代原本的 __dict__,从而节省内存并提高访问速度。但这也带来了无法动态添加属性的限制,除非你手动保留 __dict__
# 但是尽量在必要时再使用这个属性,在多重继承时其存在行为限制,我们只应该在被当作数据结构频繁使用的类上才哟给这个技巧
# 同时其不应该被用于阻止用户添加新的属性 而是应该将其作为优化工具使用
8.5 基于名称约定控制权限
使用_表示内部实现
# python本身并不提供强制性的访问控制机制(private、protected等)
# 使用命名约定来表达封装性 而不是强制访问控制
class A:
def __init__(self):
self._internal = 0 # An internal attribute
self.public = 1 # A public attribute
def public_method(self):
# 实现某些操作
# 以单下划线(\_)开头的名字应该总是被认为只属于内部实现
# 在外部代码中 不应该直接访问这写内部代码
def _internal_method(self):
# 实现某些操作
使用__避免继承冲突
class B:
def __init__(self):
self.__private = 0 # 私有属性
def __private_method(self):
... # 私有方法
def public_method(self):
... # 公共方法
self.__private_method() # 调用私有方法
class C(B):
def __init__(self):
super().__init__()
# 这样子不会覆盖B中的私有变量
# 这个变量会被重命名为_C__private
self.__private = 1
# 这个方法会被重命名为 _C__private_method
def __private_method(self):
...
# 使用双下划线 __ 开头会触发 Python 的名称重整(Name Mangling)机制,以避免子类与父类之间的属性/方法名冲突。
总结
| 表示内部使用的属性或方法 | _name | _helper(),_cache |
|---|---|---|
| 防止子类覆盖父类的属性/方法 | __name | __balance,__secret() |
| 变量名与关键字冲突 | name_ | lambda_= 2.0,class_= "A" |
8.6 创建可管理的属性
使用装饰器
# 在实例属性的获取和设定上 增加一些额外的处理过程
class Person:
def __init__(self, first_name):
# 构造函数中使用 self.first_name = first_name 是为了让初始化也受到验证逻辑的控制
# 实际调用的是 setter 方法,会进行类型检查
self.first_name = first_name
@property
# @property装饰器将方法伪装成属性 允许你像访问普通属性一样对其进行访问 也能加入逻辑控制
# 返回内部存储的名字
def first_name(self):
return self._first_name
# 要先将first设置为@property才能设置@first_name.setter
@first_name.setter
def first_name(self, value):
# 设置姓名时进行内存检查
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value
@first_name.deleter
def first_name(self):
# 禁止删除该属性
raise AttributeError("Can't delete attribute")
if __name__ == '__main__':
# 创建实例
# 像访问属性一样访问方法
try:
p = Person("Guido")
print("Name:", p.first_name) # 输出 Guido
# 修改名字
p.first_name = "Python"
print("Updated name:", p.first_name) # 输出 Python
# 尝试设置非法类型
try:
p.first_name = 42
except TypeError as e:
print("Error (expected):", e)
# 尝试删除属性
try:
del p.first_name
except AttributeError as e:
print("Error (expected):", e)
except Exception as e:
print("Unexpected error:", e)
# first_name 是 property,是对外的接口;
# _first_name 是内部变量,是真正的数据存储位置;
使用property( )
# 将get和set方法也定义为property
class Person:
def __init__(self, first_name):
self.set_first_name(first_name)
# Getter function
def get_first_name(self):
return self._first_name
# Setter function
def set_first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value
# Deleter function (optional)
def del_first_name(self):
raise AttributeError("Can't delete attribute")
# 将方法绑定为属性
name = property(get_first_name, set_first_name, del_first_name)
property 的使用哲学
# property 本质上是一个描述符对象(descriptor object) ,它将一组方法(getter、setter、deleter)绑定在一起,并对外呈现出一个“看起来像属性”的接口
# 在类中访问 Person.first_name 时,实际上得到的是一个 roperty 对象
# 这几个属性分别指向已定义的方法
Person.first_name.fget
<function Person.first_name at 0x1006a60e0>
Person.first_name.fset
<function Person.first_name at 0x1006a6170>
Person.first_name.fdel
<function Person.first_name at 0x1006a62e0>
# fget、fset 等方法是 property 内部机制的一部分,通常我们不会手动调用它们;
# 它们会在访问或修改属性时自动触发;
# property 应该只在需要对属性访问进行额外处理时才使用
# 不要像 Java 那样“为了封装而封装”
# java风格的python代码
class Person:
def __init__(self, name):
self.name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
# 如果 @property 并不会带来任何额外处理逻辑(如验证、封装、懒加载等),就不应该为了“封装而封装”
使用property 实现计算
# 使用 @property 可以让类中的某些计算属性看起来就像普通属性一样,不需要调用括号,提高了接口的一致性和可读性。
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return math.pi * self.radius ** 2
@property
def perimeter(self):
return 2 * math.pi * self.radius
c = Circle(4.0)
c.radius
# 4.0
c.area # 不需要使用()
# 50.26548245743669
错误使用案例
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value
@property
def last_name(self):
return self._last_name
@last_name.setter
def last_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._last_name = value
# first_name和last_name的验证逻辑完全一样,但写了两遍
# 如果将来要修改验证逻辑,必须改两个地方
8.7 调用父类中的方法
# 使用super方法调用父类中的方法
class A:
def spam(self):
print('A.spam')
class B(A):
def spam(self):
print('B.spam')
super().spam() # 调用父类的 spam 方法
调用父类中的初始化方法
class A:
def __init__(self):
self.x = 0
class B(A):
def __init__(self):
super().__init__()
self.y = 1
代理类的使用
#定义一个代理类 用于控制另一个对象的访问方式
class Proxy:
# 保存传进来的对象
def __init__(self, obj):
self._obj = obj
# 使用魔术方法处理未知属性的访问
# 当访问一个不存在于当前对象的属性或方法时,就会调用这个方法
# 假设不存在这个属性 用户访问proxy.x 实际访问的是proxy._obj.x
def __getattr__(self, name):
return getattr(self._obj, name)
# 将属性赋值委托给内部对象
def __setattr__(self, name, value):
# 以下划线开头的是内部属性 我们希望proxy自己进行处理
if name.startswith('_'):
# 使用父类的setattr可以避免无限递归
super().__setattr__(name, value) # 调用原始的 setattr 方法
else:
setattr(self._obj, name, value)
class Spam:
def __init__(self):
self.x = 10
# 使用代理类进行封装
s = Spam()
p = Proxy(s)
p.x = 20 # 修改的是 s.x
print(p.x) # 输出 20
p._secret = "abc" # 设置的是 p 自己的属性
print(p._secret) # 输出 abc
# 每当给对象的属性赋值时(比如 obj.attr = value),Python 都会自动调用 __setattr__() 方法
# 使用是应该注意
# 当你在 __setattr__ 方法中使用 self.name = value 或 self[name] = value 这种方式设置属性时,Python 又会再次调用 __setattr__ 方法,从而导致无限递归
# 错误的代码示范
class Proxy:
def __init__(self, obj):
self._obj = obj
def __setattr__(self, name, value):
if name == '_obj':
self._obj = value
else:
setattr(self._obj, name, value)
# 执行 self._obj = obj → 触发 __setattr__('self._obj', obj)
# 进入 __setattr__ 方法中判断是否是 _obj
# 执行 self._obj = value → 又一次触发 __setattr__
# 然后进入无限循环
多重继承
# 经常被存在缺陷的代码
# 无法支持多重继承
class Base:
def __init__(self):
print('Base.__init__')
def __init__(self):
Base.__init__(self)
print('A.__init__')
# 存在缺陷的多重继承
class Base:
def __init__(self):
print('Base.__init__')
class A(Base):
def __init__(self):
Base.__init__(self)
print('A.__init__')
class B(Base):
def __init__(self):
Base.__init__(self)
print('B.__init__')
class C(A, B):
def __init__(self):
A.__init__(self)
B.__init__(self)
print('C.__init__')
# c = C()
# Base.__init__
# A.__init__
# Base.__init__
# B.__init__
# C.__init__
# Base.__init__()方法被调用了两次
C.__init__ 被调用
│
├── A.__init__ 被显式调用
│ │
│ ├── Base.__init__ 被显式调用
│ │ └── 输出: Base.__init__
│ │
│ └── 输出: A.__init__
│
├── B.__init__ 被显式调用
│ │
│ ├── Base.__init__ 被显式调用(再次)
│ │ └── 输出: Base.__init__
│ │
│ └── 输出: B.__init__
│
└── 输出: C.__init__
# 修正后版本
class Base:
def __init__(self):
print('Base.__init__')
class A(Base):
def __init__(self):
# 注意这里使用的是super() 所以会先对B的初始化进行调用
super().__init__()
print('A.__init__')
class B(Base):
def __init__(self):
super().__init__()
print('B.__init__')
class C(A, B):
def __init__(self):
super().__init__() # 只调用一次 super()
print('C.__init__')
# Base.__init__
# B.__init__
# A.__init__
# C.__init__
# C.__init__ 被调用;
# super().__init__() → 调用 A.__init__;
# A.__init__ 中的 super().__init__() → 调用 B.__init__;
# B.__init__ 中的 super().__init__() → 调用 Base.__init__;
# 返回并依次打印:B.__init__ → A.__init__ → C.__init__
MRO
# MRO 就是 Python 在查找类的方法时所遵循的顺序。
# 当你调用一个对象的方法时(比如 obj.method()),如果这个方法在当前类中没有定义,Python 会按照 MRO 列表中的顺序 向上查找,直到找到该方法为止。
print(C.__mro__)
# 输出:
# (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,
# <class '__main__.Base'>, <class 'object'>)
Python 使用一种叫做 C3 线性化(C3 Linearization) 的算法来生成 MRO。它的目标是保证:
- 子类出现在父类之前;
- 保持基类的声明顺序;
- 在整个类层次结构中保持一致的顺序。
使用 super() 的本质是:协作式继承 。只要每个类都在适当的时候调用一次 super(),Python 就会按照 MRO 自动完成方法链的调用,确保每个方法都被调用一次,不多不少。
super( )可能调用非父类方法
# 正常的使用是会报错的
class A:
def spam(self):
print('A.spam')
super().spam()
a = A()
a.spam()
# A.spam
# Traceback (most recent call last):
# File "<stdin>", line 1, in <module>
# File "<stdin>", line 4, in spam
# AttributeError: 'super' object has no attribute 'spam'
# A 没有继承任何类,即它默认继承自 object
# A 的 MRO A → object
# super().spam()会调用object中的spam方法 但是object中并没有进行此定义 所以报错
# 不会报错 调用了无关的B中的方法
class B:
def spam(self):
print('B.spam')
class A:
def spam(self):
print('A.spam')
super().spam()
class C(A, B):
pass
c = C()
c.spam()
# 输出 A.spam B.spam
# 因为 C.__mro__ = (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)
- 确保在继承体系中所有同名的方法都有可兼容的调用签名(即参数数量相同,参数名称也相同)
- 确保最顶层的类实现了这个方法。这样沿着 MRO 列表展开的查询链会因为最终找到了实际的方法而终止。
8.8 在子类中扩展属性
# 定义一个父类 尝试在子类中对其进行拓展
class Person:
def __init__(self, name):
# __init__ 中的 self.name = name 实际上会触发 name.setter
# 构造函数中的赋值也会收到类型检查的限制
self.name = name
# Getter function
@property
def name(self):
return self._name
# Setter function
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._name = value
# Deleter function
@name.deleter
def name(self):
raise AttributeError("Can't delete attribute")
类级别上父类的调用
class SubPerson(Person):
@property
def name(self):
print('Getting name')
return super().name
# 这个注解实现了set的绑定
@name.setter
def name(self, value):
print('Setting name to', value)
# super(SubPerson, SubPerson).name在类级别上调用父类的实现
# 第一个 SubPerson:告诉 super() 从哪个类开始查找继承链
# 第二个 SubPerson:表明你要从类层面进行查找(不是从实例)
# 获取Person.name的描述符
# 描述符内部实现了__get__()、__set__()、__delete__()方法
# 这个代码实际上会调用的是Person.name.__set__ 因为传入了slef参数 所以是当前子类的-name被赋值
super(SubPerson, SubPerson).name.__set__(self, value)
# 使用super().__setattr__('name', value)也可
# → 调用 Person.__setattr__ (即 property 描述符的 __set__)
# → 设置 self._name = value
# → 不会再次进入 SubPerson.name.setter 方法
@name.deleter
def name(self):
print('Deleting name')
super(SubPerson, SubPerson).name.__delete__(self)
s = SubPerson('Guido')
# Setting name to Guido
s.name
# Getting name 'Guido'
s.name = 'Larry'
# Setting name to Larry
s.name = 42
# Traceback (most recent call last): File "<stdin>", line 1, in <module> File "example.py", line 16, in name raise TypeError('Expected a string') TypeError: Expected a string
描述符的访问方式
# 通过实例访问属性
print(s.name) # 触发 getter
s.name = 'Larry' # 触发 setter
del s.name # 触发 deleter
# 通过类访问属性
print(Person.name) # 获取的是 property 对象本身
print(SubPerson.name) # 同样获取的是 property 对象
# 描述符的行为取决于你是从实例访问还是从类访问,这决定了你是否触发了 getter/setter/deleter
# @property.setter 和 @property.deleter 中,我们要手动调用父类的实现,因此必须通过“类访问”的方式获取到原始的 property 描述符对象,然后手动调用它的 __set__ 或 __delete__ 方法。否则就会导致递归调用或错误的行为。
# 上面类定义的方法中 使用super(SubPerson, SubPerson)可以获取Person.name这个描述符对象
# 错误的调用方式如super().name = value
# super() 是一个代理对象,它帮你自动在父类中查找方法或属性,而不需要你手动写父类名
# super() 在找到方法后,会自动将该方法绑定到当前实例(即 self)。
# 因此,super().name = value 实际上被解释为 self.name = value,
# 这会再次触发当前类的 name.setter 方法,从而导致无限递归。
安全继承property
在子类中扩展 @property 时,要清楚是要重写整个属性,还是只扩展其中一个方法(getter/setter/deleter)。
当需要调用父类的描述符方法时,必须通过 super(当前类, 当前类) 的方式访问描述符对象,然后手动调用其 __set__ __delete__ 方法。
# 错误示范 没有重写整个属性
class SubPerson(Person):
@property # ❌ 只重写了 getter,丢失了 setter
def name(self):
print('Getting name')
return super().name
s = SubPerson('Guido')
# Traceback (most recent call last): File "<stdin>", line 1, in <module> File "example.py", line 5, in __init__ self.name = name AttributeError: can't set attribute
# 正确的修改方式
class SubPerson(Person):
@Person.name.getter # ✅ 只替换 getter 方法
def name(self):
print('Getting name')
return super().name
8.9 创建一种新形式的类属性或实例属性
# 以描述符类的形式定义其功能 创建一种新形式的实例属性
# 自定义描述符类 Integer,用于限制属性必须为整数
# 述符就是以特殊方法__get__()、__set__()和__delete__()的形式实现了三个核心属性访问操作的类
class Integer:
def __init__(self, name):
# 保存属性名称,如 'x' 或 'y'
# self.name 属性会保存字典的键,通过这些键可以找到存储在实例字典中的实际数据
self.name = name
def __get__(self, instance, cls):
# 如果是通过类访问(如 Point.x),返回描述符自身
if instance is None:
return self
else:
# 如果是通过实例访问(如 p.x),返回实际存储在 __dict__ 中的值
return instance.__dict__[self.name]
def __set__(self, instance, value):
# 检查赋值是否为整数类型
if not isinstance(value, int):
raise TypeError('期望一个整数(int)')
# 将值存入实例的 __dict__ 中,以 self.name 为键
instance.__dict__[self.name] = value
def __delete__(self, instance):
# 删除实例中对应的属性
del instance.__dict__[self.name]
# 关于上述中instance的使用
# instance 表示操作的对象实例(如 Point 的实例 p),由 Python 自动传入
# 使用描述符的类
class Point:
x = Integer('x') # 使用描述符管理 x 属性
y = Integer('y') # 使用描述符管理 y 属性
def __init__(self, x, y):
self.x = x # 实际调用 Integer.__set__
self.y = y # 实际调用 Integer.__set__
# 这些方法通过接受类实例作为输入 来工作。之后,底层的实例字典会根据需要适当地进行调整
# 创建实例
if __name__ == '__main__':
try:
p = Point(2, 3)
print("p.x =", p.x) # 输出: p.x = 2
p.y = 5
print("p.y =", p.y) # 输出: p.y = 5
p.x = 2.3 # 触发类型错误
except TypeError as e:
print("捕获异常:", e) # 输出: 捕获异常: Expected an int
__dict__属性
在 Python 中,每个类的实例(对象)都有一个 __dict__ 属性 ,它是一个字典(dict),用于存储该对象的所有实例属性 。
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
p = Person("Tom", 20)
print(p.__dict__)
# {'name': 'Tom', 'age': 20}
#
p = Point(2,3)
>>> p.x # Calls Point.x.get(p, Point)
2
>>> Point.x # Calls Point.x.get(None, Point)
<__main__.Integer object at 0x100671890>
>>>
描述符的补充
# 描述符只能在类层次定义 不能根据实例来产生
# 错误示范
class Point:
def __init__(self, x, y):
self.x = Integer('x')
self.y = Integer('y') # y = Integer('y')
self.x = x
self.y = y
# 同时在实现get方法上会比较复杂
# 如上文的return instance.__dict__[self.name]
# 实例调用与类调用
# 实例调用
p = Point(2,3)
p.x # 输出2 调用Point.x.get(p, Point)
# 类调用
Point.x # 调用 Point.x.get(None, Point)
# 输出<__main__.Integer object at 0x100671890>
# 描述符本身不保存数据,它只是控制如何获取或设置数据
# 真实的数据是保存在实例的 __dict__ 中的。所以当你是“实例访问”时,要从它的字典里取;而当你是“类访问”,则不应该去实例里取。
更高级的基于描述符的代码
# 描述符类:用于类型检查的属性
class Typed:
def __init__(self, name, expected_type):
self.name = name # 属性名
self.expected_type = expected_type # 期望的类型
def __get__(self, instance, cls):
if instance is None:
return self
else:
return instance.__dict__[self.name] # 获取实际值
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError('Expected ' + str(self.expected_type))
instance.__dict__[self.name] = value # 存储经过类型检查的值
def __delete__(self, instance):
del instance.__dict__[self.name]
# 类装饰器:为指定属性添加类型检查功能
def typeassert(**kwargs):
def decorate(cls):
for name, expected_type in kwargs.items():
# 给类动态添加 Typed 类型检查描述符作为类属性
setattr(cls, name, Typed(name, expected_type))
return cls
return decorate
# 示例使用
@typeassert(name=str, shares=int, price=float)
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
s = Stock('Apple', 100, 536.7)
print(s.name) # 正常输出: Apple
s.shares = 50 # 正常赋值
s.shares = '100' # 报错:TypeError: Expected <class 'int'>
如果只是针对一个类的一个属性做定制处理,优先使用 property 。
如果你想写出可复用、通用的属性行为(如类型检查器、缓存机制等),才考虑使用描述符 。
8.10 让属性具有惰性求值的能力
# 将一个定义为property 只有在访问时才参与计算 同时计算后要把这个值缓存起来
# 定义一个描述符类 lazyproperty,用于实现惰性求值的属性
# Python 中描述符是一种类,它实现了 __get__()、__set__() 或 __delete__() 方法,可以控制属性访问的行为
class lazyproperty:
def __init__(self, func):
self.func = func
# instance指的是调用该属性的类实例 当通过类访问时 instance为None 此时会直接返回描述符本身
# self是当前的描述符对象 如area或perimeter属性
def __get__(self, instance, cls):
if instance is None:
return self
else:
# 第一次访问时调用函数计算值
value = self.func(instance)
# 将计算结果设置为实例属性,覆盖描述符
setattr(instance, self.func.__name__, value)
return value
import math
# Circle 类定义半径,并使用 lazyproperty 实现 area 和 perimeter 的延迟计算
class Circle:
def __init__(self, radius):
self.radius = radius
# 将其包装成描述符
@lazyproperty
def area(self):
print('Computing area')
return math.pi * self.radius ** 2
@lazyproperty
def perimeter(self):
print('Computing perimeter')
return 2 * math.pi * self.radius
# 模拟交互环境下的操作和输出
if __name__ == '__main__':
c = Circle(4.0)
print(c.radius) # 输出: 4.0
print(c.area) # 输出: Computing area \n 50.26548245743669
print(c.area) # 输出: 50.26548245743669 (不再打印 Computing area)
print(c.perimeter) # 输出: Computing perimeter \n 25.132741228718345
print(c.perimeter) # 输出: 25.132741228718345 (不再打印 Computing perimeter)
当一个描述符是非数据描述符(只实现了 __get__() 没有实现 __set__ 或 __delete__)时,Python 在属性查找时会优先查看实例字典,只有在找不到的情况下才会调用描述符的 __get__() 方法。
c = Circle(4.0)
print(vars(c)) # 输出: {'radius': 4.0}
# 第一次访问 c.area
print(c.area) # 输出: Computing area \n 50.26548245743669
# 再次查看实例字典
print(vars(c)) # 输出: {'area': 50.26548245743669, 'radius': 4.0}
# 第二次访问 c.area(不会重新计算)
print(c.area) # 输出: 50.26548245743669
# 删除 area 属性
del c.area
print(vars(c)) # 输出: {'radius': 4.0}
上述隐患
# 算出的值在创建之后就变成可变的(mutable)了。
c.area
# 输出Computing area 50.26548245743669
c.area = 25
c.area # 输出25
# 可变性问题的解决
def lazyproperty(func):
# 使用私有属性名存储计算结果 避免与原始属性名冲突
name = '_lazy_' + func.__name__
@property
# 使用 @property 装饰一个方法时,这个方法中的 self 就是指调用该属性的那个实例对象
def lazy(self):
# 后续直接返回缓存值
if hasattr(self, name):
return getattr(self, name)
else:
# 第一次访问进行计算并缓存到实例
value = func(self)
setattr(self, name, value)
return value
return lazy
c = Circle(4.0)
c.area
# Computing area 50.26548245743669
c.area
# 50.26548245743669
c.area = 25
# Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: can't set attribute
# 这种方式的缺点就是所有的 get 操作都必须经由属性的 getter 函数来处理。这比 直接在实例字典中查找相应的值要慢一些。
8.11 简化数据结构的初始化过程
# 示例代码 定义多个数据结构 但是都自定义了初始化方法
# 表示股票信息的类
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
# 表示二维坐标点的类
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
# 表示圆的类,包含计算面积的方法
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return math.pi * self.radius ** 2
简单使用
# 定义一个通用结构类,用于处理具有固定字段的类初始化
class Structure:
# 类变量,指定期望的字段
_fields = []
def __init__(self, *args):
if len(args) != len(self._fields):
raise TypeError(f'Expected {len(self._fields)} arguments')
# 将参数按字段名设置为实例属性
for name, value in zip(self._fields, args):
setattr(self, name, value)
# 示例:Stock 类继承 Structure
class Stock(Structure):
_fields = ['name', 'shares', 'price']
class Point(Structure):
_fields = ['x', 'y']
class Circle(Structure):
_fields = ['radius']
def area(self):
return math.pi * self.radius ** 2
if __name__ == '__main__':
try:
# 正常情况
s1 = Stock('Apple', 100, 150.5)
print(f"Stock: {s1.name}, {s1.shares}, {s1.price}")
# Output: Stock: Apple, 100, 150.5
p = Point(2, 3) # 正常创建实例
c = Circle(4.5)
print(f"Circle area: {c.area()}")
# Output: Circle area: 63.61725123519331
# 参数数量不匹配的情况
s2 = Stock('Google', 200)
except TypeError as e:
print(f"Error: {e}")
# Output: Error: Expected 3 arguments
对关键字参数做映射
# 定义一个通用结构类,支持位置参数和关键字参数混合初始化
class Structure:
_fields = [] # 子类中定义字段名列表
def __init__(self, *args, **kwargs):
if len(args) > len(self._fields):
raise TypeError(f'Expected {len(self._fields)} arguments')
# 设置位置参数
for name, value in zip(self._fields, args):
setattr(self, name, value)
# 设置剩余的关键字参数
for name in self._fields[len(args):]:
setattr(self, name, kwargs.pop(name))
# 检查是否还有未识别的关键字参数
if kwargs:
raise TypeError('Invalid argument(s): {}'.format(', '.join(kwargs)))
# 示例类:Stock
class Stock(Structure):
_fields = ['name', 'shares', 'price']
# 测试示例
if __name__ == '__main__':
# 仅使用位置参数
s1 = Stock('ACME', 50, 91.1)
print(f"s1: {s1.name}, {s1.shares}, {s1.price}")
# Output: s1: ACME, 50, 91.1
# 混合使用位置参数和关键字参数
s2 = Stock('ACME', 50, price=91.1)
print(f"s2: {s2.name}, {s2.shares}, {s2.price}")
# Output: s2: ACME, 50, 91.1
# 全部使用关键字参数(顺序无关)
s3 = Stock('ACME', shares=50, price=91.1)
print(f"s3: {s3.name}, {s3.shares}, {s3.price}")
# Output: s3: ACME, 50, 91.1
# 错误用法示例
try:
s4 = Stock('ACME', 50, price=91.1, unknown=100)
except TypeError as e:
print(f"Error: {e}")
# Output: Error: Invalid argument(s): unknown
允许利用关键字添加额外参数
# 定义一个通用结构类,支持固定字段 + 额外关键字参数
class Structure:
# 类变量,指定期望的字段名列表
_fields = []
def __init__(self, *args, **kwargs):
if len(args) != len(self._fields):
raise TypeError(f'Expected {len(self._fields)} arguments')
# 设置位置参数
for name, value in zip(self._fields, args):
setattr(self, name, value)
# 提取额外的关键字参数(不在 _fields 中)
extra_args = kwargs.keys() - self._fields
for name in extra_args:
setattr(self, name, kwargs.pop(name))
# 如果还有未处理完的 kwargs,说明是重复字段
if kwargs:
raise TypeError(f'Duplicate values for {",".join(kwargs)}')
# 示例类:Stock
class Stock(Structure):
_fields = ['name', 'shares', 'price']
# 测试示例
if __name__ == '__main__':
# 正常使用位置参数
s1 = Stock('ACME', 50, 91.1)
print(f"s1: {s1.name}, {s1.shares}, {s1.price}")
# Output: s1: ACME, 50, 91.1
# 使用位置参数 + 额外关键字参数
s2 = Stock('ACME', 50, 91.1, date='8/2/2012')
print(f"s2: {s2.name}, {s2.shares}, {s2.price}, {s2.date}")
# Output: s2: ACME, 50, 91.1, 8/2/2012
# 错误示例:对已有字段重复赋值
try:
s3 = Stock('ACME', 50, 91.1, price=100)
except TypeError as e:
print(f"Error: {e}")
# Output: Error: Duplicate values for price
隐患
# 定义一个通用结构类,用于固定字段的初始化
class Structure:
# 类变量,指定期望的字段名列表
_fields = []
def __init__(self, *args):
if len(args) != len(self._fields):
raise TypeError(f'Expected {len(self._fields)} arguments')
# 直接访问实例字典
# 使用 __dict__.update 批量设置属性
self.__dict__.update(zip(self._fields, args))
存在隐患:
- 如果子类使用了
__slots__或@property(描述符),直接操作__dict__将会失败。 - Python 中有些类并不使用
__dict__存储属性(如__slots__类),这会引发错误。 - 对IDE也会有影响
# 可以正常运行 但是使用help()查看时会出现问题
>>> help(Stock)
Help on class Stock in module __main__:
class Stock(Structure)
| Method resolution order:
| Stock
| Structure
| builtins.object
|
| Methods inherited from Structure:
|
| __init__(self, *args, **kwargs)
| Initialize self. See help(type(self)) for accurate signature.
# 构造函数没有显示出具体的字段名称(如 name, shares, price)
8.12 定义一个接口或抽象基类
一些基本操作
from abc import ABCMeta, abstractmethod
# ABCMeta 是一个元类(metaclass),用于创建抽象基类
class IStream(metaclass=ABCMeta):
# 用于实现声明抽象方法的装饰器
@abstractmethod
def read(self, maxbytes=-1):
pass
@abstractmethod
def write(self, data):
pass
# 抽象类不能被直接实例化
a = IStream()
# TypeError: Can't instantiate abstract class # IStream with abstract methods read, write
# 子类要实现基类中要求的方法
class SocketStream(IStream):
def read(self, maxbytes=-1):
...
def write(self, data):
...
# 抽象积累可以强制规定所需的编程接口
def serialize(obj, stream):
if not isinstance(stream, IStream):
raise TypeError('Expected an IStream')
...
虚拟子类注册
# 将标准库中的 io.IOBase 注册为 IStream 的“虚拟子类”
IStream.register(io.IOBase)
# 打开一个普通文件
f = open('foo.txt', 'w') # 创建一个文件对象用于测试
f.close()
f = open('foo.txt', 'r')
# 检查 f 是否被认为是 IStream 的实例
print(isinstance(f, IStream)) # 输出: True
高级应用
from abc import ABCMeta, abstractmethod
# @abstractmethod 不仅可以用于普通方法,还可以应用于静态方法(@staticmethod)、类方法(@classmethod)以及属性(@property)
class A(metaclass=ABCMeta):
@property
@abstractmethod
# 定义抽象只读属性
# 子类必须实现定义getter方法
def name(self):
pass
@name.setter
@abstractmethod
# @abstractmethod 必须紧挨函数定义
def name(self, value):
pass
@classmethod
@abstractmethod
# 参数是cls 表示类本身
def method1(cls):
pass
@staticmethod
@abstractmethod
def method2():
pass
标准库抽象基类
# 标准库中定义了许多抽象基类
from collections.abc import Sequence, Iterable, Sized, Mapping
def check_interfaces(x):
# 检查是否是序列(支持索引、长度和迭代)
if isinstance(x, Sequence):
print("x 是一个序列(Sequence)")
# 检查是否可迭代(支持 iter())
if isinstance(x, Iterable):
print("x 是可迭代对象(Iterable)")
# 检查是否有大小(支持 len())
if isinstance(x, Sized):
print("x 是有大小的对象(Sized)")
# 检查是否是映射(如字典)
if isinstance(x, Mapping):
print("x 是一个映射(Mapping)")
# 抽象基类的缺陷
from decimal import Decimal
import numbers
# 创建一个 Decimal 实例
x = Decimal('3.4')
# 检查是否是 Real 类型
# 虽然说这个数是一个抽象类 但是由于numbers.Real的实现方式并没有将Decimal注册为其子类
print(isinstance(x, numbers.Real)) # 输出: False
8.13 实现一种数据模型或类型系统
对赋给某些属性的值进行限制
使用描述符机制实现
# 实现系统类型以及对值进行检查的框架
# 使用描述符机制来设置属性值
class Descriptor:
def __init__(self, name=None, **opts):
self.name = name # 属性名称
for key, value in opts.items():
setattr(self, key, value) # 设置额外选项
def __set__(self, instance, value):
# 将值直接存储到实例的 __dict__ 中
instance.__dict__[self.name] = value
# 用于强制类型检查
class Typed(Descriptor):
expected_type = type(None) # 子类应重写此属性以指定期望的类型
def __set__(self, instance, value):
# 检查传入值是否符合预期类型
if not isinstance(value, self.expected_type):
raise TypeError(f'期望类型 {self.expected_type}')
super().__set__(instance, value)
# 用于确保值是非负数(>=0)
class Unsigned(Descriptor):
def __set__(self, instance, value):
# 检查值是否小于0
if value < 0:
raise ValueError('值必须大于等于 0')
super().__set__(instance, value)
# 用于限制值的最大长度
class MaxSized(Descriptor):
def __init__(self, name=None, **opts):
# 必须提供 size 参数,否则抛出异常
if 'size' not in opts:
raise TypeError('缺少 size 参数')
super().__init__(name, **opts)
self.size = opts['size'] # 保存最大尺寸限制
def __set__(self, instance, value):
# 检查值的长度是否超过限制
if len(value) >= self.size:
raise ValueError(f'大小必须小于 {self.size}')
super().__set__(instance, value)
# 定义具体的数据类型描述符类
# 整数类型
class Integer(Typed):
expected_type = int
# 无符号整数(非负整数)
class UnsignedInteger(Integer, Unsigned):
pass
# 浮点数类型
class Float(Typed):
expected_type = float
# 无符号浮点数(非负浮点数)
class UnsignedFloat(Float, Unsigned):
pass
# 字符串类型
class String(Typed):
expected_type = str
# 有大小限制的字符串
class SizedString(String, MaxSized):
pass
class Stock:
# 定义字段及其约束条件
name = SizedString('name', size=8) # 字符串,最大长度为 8
shares = UnsignedInteger('shares') # 整数,且不能为负
price = UnsignedFloat('price') # 浮点数,且不能为负
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
if __name__ == '__main__':
try:
s = Stock('ACME', 50, 91.1)
print("s.name =", s.name) # 输出: 'ACME'
s.shares = 75
# 尝试设置非法值
try:
s.shares = -10
except ValueError as e:
print("错误(shares):", e)
# 错误(shares): 值必须大于等于 0
try:
s.price = 'a lot'
except TypeError as e:
print("错误(price):", e)
# 错误(price): 期望类型 <class 'float'>
try:
s.name = 'ABRACADABRA'
except ValueError as e:
print("错误(name):", e)
# 误(name): 大小必须小于 8
except Exception as e:
print("发生错误:", e)
使用类装饰器实现
# 类装饰器:用于自动绑定描述符属性
def check_attributes(**kwargs):
def decorate(cls):
for key, value in kwargs.items():
# 如果传入的是一个已经实例化的描述符对象
if isinstance(value, Descriptor):
value.name = key
setattr(cls, key, value)
else:
# 如果传入的是描述符类(如 UnsignedInteger)
# 则自动调用构造函数并传入 name
setattr(cls, key, value(key))
return cls
return decorate
# 使用装饰器定义类
@check_attributes(
name=SizedString(size=8),
shares=UnsignedInteger,
price=UnsignedFloat
)
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
使用元类实现
# 使用元类自动绑定描述符属性名
class checkedmeta(type):
"""
元类 checkedmeta 会在类创建时自动为描述符绑定属性名。
这样就不需要手动传入 name 参数,例如:
name = SizedString(size=8)
而不是:
name = SizedString('name', size=8)
"""
def __new__(cls, clsname, bases, methods):
# 遍历类中所有属性
for key, value in methods.items():
# 如果是描述符类型,则自动绑定 name 属性
if isinstance(value, Descriptor):
value.name = key
return type.__new__(cls, clsname, bases, methods)
# 使用元类定义类
class Stock(metaclass=checkedmeta):
# 直接声明描述符,无需手动指定属性名
name = SizedString(size=8) # 字符串,最大长度为 8
shares = UnsignedInteger() # 整数,且不能为负
price = UnsignedFloat() # 浮点数,且不能为负
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
8.14 实现自定义的容器
collections 库中定义了各种各样的抽象基类,当实现自定义的容器类时它们会非常有 用。为了说明清楚,假设我们希望自己的类能够支持迭代操作。要做到这点,只要简 单地从 collections.Iterable 中继承即可,就像下面这样:
# 实现一个自定义的类,用来模仿普通的内建容器类型比如列表或者字典的行为。
# collections 库中定义了许多的抽象基类
import collections
# 继承collections.Iterable 使自定义类可以实现迭代
# 但是使用时要实现__iter()方法 不然会报错
class A(collections.Iterable):
pass
# collextion中的类会按照功能递增的方式形成一个结构层次,如:
Container
└── Iterable
└── Sized
└── Sequence
└── MutableSequence
# 在实现的过程中 要实现一些要求的行为
import collections
try:
seq = collections.Sequence()
except TypeError as e:
print("TypeError occurred:", e)
# TypeError occurred: Can't instantiate abstract class Sequence with abstract methods __getitem__, __len__
# 当前的代码要求实现 支持索引访问的__getitem__(self, index)以及支持获取长度的__len__(self)方法
自定义类的实现
# 使用自定义类实现了所有元素始终以排序后的顺序存储
# 同时支持索引访问(__getitem__)长度查询(__len__) 成员检测(in)迭代(for ... in)切片([start:end])
import collections.abc
import bisect
class SortedItems(collections.abc.Sequence):
def __init__(self, initial=None):
self._items = sorted(initial) if initial is not None else []
def __getitem__(self, index):
return self._items[index]
def __len__(self):
return len(self._items)
def add(self, item):
# 使用 bisect.insort进行插入 保持列表的有序
bisect.insort(self._items, item)
if __name__ == '__main__':
items = SortedItems([5, 1, 3])
print("Initial list:", list(items)) # [1, 3, 5]
print("Index access:", items[0], items[-1]) # 1 5
items.add(2)
items.add(-10)
print("After adding elements:", list(items)) # [-10, 1, 2, 3, 5]
print("Slice [1:4]:", items[1:4]) # [1, 2, 3]
print("Contains 3?", 3 in items) # True
print("Length:", len(items)) # 5
print("Iterating over items:")
for n in items:
print(n)
# 同时自定义容器可以通过各种类型检查
print("Is Iterable? ", isinstance(items, collections.abc.Iterable)) # True
print("Is Sequence? ", isinstance(items, collections.abc.Sequence)) # True
print("Is Container? ", isinstance(items, collections.abc.Container)) # True
print("Is Sized? ", isinstance(items, collections.abc.Sized)) # True
print("Is Mapping? ", isinstance(items, collections.abc.Mapping)) # False
# 通过实现少量核心方法 即可自动获得大量标准容器
# 继承自collections.MutableSequence 则要实现__getitem__、__setitem__、__delitem__、insert、__len__等方法
import collections.abc
class Items(collections.abc.MutableSequence):
def __init__(self, initial=None):
self._items = list(initial) if initial is not None else []
def __getitem__(self, index):
print('Getting:', index)
return self._items[index]
def __setitem__(self, index, value):
print('Setting:', index, value)
self._items[index] = value
def __delitem__(self, index):
print('Deleting:', index)
del self._items[index]
def insert(self, index, value):
print('Inserting:', index, value)
self._items.insert(index, value)
def __len__(self):
print('Len')
return len(self._items)
# 自动实现了append()、remove()、count()、pop()、extend()等方法
if __name__ == '__main__':
a = Items([1, 2, 3])
print(len(a))
a.append(4)
a.append(2)
print(a.count(2))
a.remove(3)
print("\n最终列表:", list(a))
8.15 委托属性的访问
委托的实现
使用内部变量实现
# 访问实例的属性时能将其委托(delegate)到一个内部持有的对象上
# 委托是一种编程模式 将特定的操作委托给另一个不同的对象
class A:
def spam(self, x):
pass
def foo(self):
pass
class B:
def __init__(self):
self._a = A() # 内部持有一个 A 的实例
def spam(self, x):
return self._a.spam(x) # 将 spam 方法委托给 A 实例
def foo(self):
return self._a.foo() # 将 foo 方法委托给 A 实例
def bar(self):
pass
使用__setattr__实现
# 实现具体功能的类
class A:
def spam(self, x):
print(f"A.spam called with {x}")
def foo(self):
print("A.foo called")
class B:
"""通过 __getattr__ 将未定义的方法自动委托给 A 的实例"""
def __init__(self):
self._a = A() # 持有一个 A 类的实例
def bar(self):
print("B.bar called")
# 自动将未在 B 中定义的方法委托给内部的 A 实例
def __getattr__(self, name):
print(f"Delegating {name} to A...")
return getattr(self._a, name)
# 测试代码
b = B()
b.bar() # 调用 B.bar(),直接存在
b.spam(42) # 不存在于 B,触发 __getattr__,委托给 A.spam()
b.foo() # 同样被委托到 A.foo()
使用代理类实现
# Proxy 类:封装一个对象,并将属性操作转发给它
class Proxy:
def __init__(self, obj):
self._obj = obj # 持有被代理的对象
# 当访问不存在的属性时调用
def __getattr__(self, name):
print('getattr:', name)
return getattr(self._obj, name)
# 属性赋值时调用
def __setattr__(self, name, value):
if name.startswith('_'): # 如果是私有属性(如 _obj),使用正常方式设置
super().__setattr__(name, value)
else:
print('setattr:', name, value)
setattr(self._obj, name, value) # 否则,委托给内部对象
# 删除属性时调用
def __delattr__(self, name):
if name.startswith('_'): # 私有属性由父类处理
super().__delattr__(name)
else:
print('delattr:', name)
delattr(self._obj, name) # 否则,委托给内部对象
# 被代理的类
class Spam:
def __init__(self, x):
self.x = x # 实例变量 x
def bar(self, y):
print('Spam.bar:', self.x, y)
# 创建实例
s = Spam(2) # 原始对象
p = Proxy(s) # 创建代理对象
# 测试代理功能
print(p.x) # 访问属性 -> 触发 __getattr__
p.bar(3) # 调用方法 -> 实际上调用了 s.bar()
p.x = 37 # 修改属性 -> 触发 __setattr__
print(s.x) # 验证原始对象的属性确实被修改
委托有时能替代继承
# 不推荐的实现
class A:
def spam(self, x):
print('A.spam', x)
def foo(self):
print('A.foo')
class B(A):
def spam(self, x):
print('B.spam')
super().spam(x)
def bar(self):
print('B.bar')
# 继承是一种很高耦合的操作 父类所有的方法都会暴露出来 控制粒度差
# 多重继承可能导致复杂的方法解析顺序(MRO)
# 使用委托进行处理 可以选择性暴露某些方法 更容易实现接口隔离、行为封装、动态替换等高级特性
class A:
def spam(self, x):
print('A.spam', x)
def foo(self):
print('A.foo')
class B:
def __init__(self):
self._a = A() # 持有一个 A 的实例
def spam(self, x):
print('B.spam', x)
self._a.spam(x) # 显式调用 A 的 spam 方法
def bar(self):
print('B.bar')
def __getattr__(self, name):
return getattr(self._a, name) # 自动委托未定义的方法到 A 实例
# __getattr__ 是一个 fallback 方法 它只在对象本身找不到该属性时才会被调用 如果属性已经定义在实例或类中,则不会触发。
委托的缺陷
# __getattr__ 不会拦截大多数以双下划线(__)开头和结尾的特殊方法(如 __len__, __getitem__ 等)
class ListLike:
def __init__(self):
self._items = []
def __getattr__(self, name):
return getattr(self._items, name)
a = ListLike()
a.append(2) # 成功:append 是普通方法,__getattr__ 拦截成功
a.insert(0, 1) # 成功
a.sort() # 成功
len(a) # ❌ 报错:TypeError: object of type 'ListLike' has no len()
a[0] # ❌ 报错:TypeError: 'ListLike' object does not support indexing
# 特殊方法(如 __len__, __getitem__)是在类本身中直接查找的 ,而不是通过 __getattr__
解决方案
# 普通的方法调用可以用__getattr__实现
# 特殊方法需要手动实现
class ListLike:
def __init__(self):
self._items = [] # 内部使用一个列表来存储数据
def __getattr__(self, name):
# 将未定义的方法委托给内部的 list 对象
return getattr(self._items, name)
# 实现了以下特殊方法以支持常见序列操作:
def __len__(self):
return len(self._items)
def __getitem__(self, index):
return self._items[index]
def __setitem__(self, index, value):
self._items[index] = value
def __delitem__(self, index):
del self._items[index]
8.16 在类中定义多个构造函数
# 使用类方法实现备用构造函数
import time
class Date:
# 主构造函数
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
# 备用构造函数:自动创建“今天”的日期对象
@classmethod
def today(cls):
t = time.localtime()
return cls(t.tm_year, t.tm_mon, t.tm_mday)
# 使用主构造函数创建实例
a = Date(2012, 12, 21) # 指定具体日期
# 使用备用构造函数创建实例
b = Date.today() # 自动获取当前系统日期
# 类方法非常适合用来定义备用构造函数,因为它接收类本身作为参数(cls),能够根据调用者的不同自动创建相应类型的实例,天然支持继承和多态
class NewDate(Date):
pass
c = Date.today() # 创建的是Date的实例 此时的cls是Date
d = NewDate.today() # 创建的是NewDate的实例 此时的cls是NewDate
让初始化支持不同调用(不推荐)
# 一个的demo 这样的代码会难以理解和维护
class Date:
def __init__(self, *args):
if len(args) == 0:
t = time.localtime()
args = (t.tm_year, t.tm_mon, t.tm_mday)
self.year, self.month, self.day = args
a = Date(2012, 12, 21) # 清晰:明确指定日期
# 这样的接口是不直观的 用户需要阅读构造函数才能理解其行为 用户期望构造函数只做简单赋值,不应该隐藏复杂逻辑分支
b = Date() # 不清晰:无参数调用,意图不明
c = Date.today() # 清晰:通过类方法表达“今天”的语义
8.17 不通过调用 init 来创建实例
# 直接调用__new__方法构建一个未初始化的实例
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
# 使用 __new__ 创建一个未初始化的实例
d = Date.__new__(Date)
# 此时访问属性会报错(因为 __init__ 没有被调用)
try:
print(d.year) # AttributeError: 'Date' object has no attribute 'year'
except AttributeError as e:
print(f"Error: {e}")
# 手动进行属性赋值,模拟初始化过程
data = {'year': 2012, 'month': 8, 'day': 29}
for key, value in data.items():
setattr(d, key, value)
# 现在可以正常访问属性
print(d.year) # 2012
print(d.month) # 8
应用场景
# 部分的场景需要使用反序列化或者非标准构造场景 就可能需要绕过init函数
# 如从外部数据结构(如字典、JSON、数据库记录)恢复对象
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
def __repr__(self):
return f"Date({self.year}, {self.month}, {self.day})"
@classmethod
def from_dict(cls, data):
d = cls.__new__(cls) # 创建一个未初始化的对象
for key, value in data.items():
setattr(d, key, value) # 手动设置属性
return d
# 示例数据
data = {'year': 2012, 'month': 8, 'day': 29}
# 从字典恢复对象
d = Date.from_dict(data)
print(d) # 输出: Date(2012, 8, 29)
# 在需要以非标准方式构造对象时,应尽量避免直接操作 __dict__,而应使用 setattr() 来设置属性,这样可以确保你的代码兼容各种类定义方式(包括 __slots__、property、描述符等),提高健壮性和可移植性。
8.18 用 Mixin 技术来扩展类定义
Mixin 是一种设计模式 ,在 Python 中通常表现为一个实现了某些功能的小型类。其不独立使用、没有完整的接口、依赖于其他类提供核心实现、提供的是“增强功能”。主要用于在不改变类继承结构的前提下,为多个无关类添加可复用的功能。
# 日志管理拓展案例
# 本代码在在不修改dict的前提下,为它添加额外功能
#
from collections import defaultdict, OrderedDict
# Mixin 1: 添加日志输出功能
class LoggedMappingMixin:
__slots__ = () # 不分配实例字典,节省内存
def __getitem__(self, key):
print(f'Getting {key}')
# 使用super()将请求交给下一个类处理
return super().__getitem__(key)
def __setitem__(self, key, value):
print(f'Setting {key} = {value!r}')
return super().__setitem__(key, value)
def __delitem__(self, key):
print(f'Deleting {key}')
return super().__delitem__(key)
# Mixin 2: 键只能设置一次
class SetOnceMappingMixin:
__slots__ = ()
def __setitem__(self, key, value):
if key in self:
raise KeyError(f'{key} already set')
return super().__setitem__(key, value)
# Mixin 3: 键必须是字符串类型
class StringKeysMappingMixin:
__slots__ = ()
def __setitem__(self, key, value):
if not isinstance(key, str):
raise TypeError('keys must be strings')
return super().__setitem__(key, value)
# 这些操作最终都会由dict类实现,因为Mixin只是在调用链中添加了一些功能(比如日志、检查等),真正的数据存储和操作还是由 dict 来完成的。
# 示例 1:带日志的 dict
# 通过同时继承dict 和 Mixin,新增了功能,但是没有修改dict类的功能
class LoggedDict(LoggedMappingMixin, dict):
pass
# 示例 2:只允许设置一次的 defaultdict
class SetOnceDefaultDict(SetOnceMappingMixin, defaultdict):
pass
# 示例 3:键必须为字符串 + 只能设置一次 + 有序字典
class StringOrderedDict(StringKeysMappingMixin,
SetOnceMappingMixin,
OrderedDict):
pass
def main():
d = LoggedDict()
d['x'] = 23 # Setting x = 23
print("d['x'] =", d['x']) # Getting x → d['x'] = 23
del d['x'] # Deleting x
d = SetOnceDefaultDict(list)
d['x'].append(2) # Setting x = [2]
d['y'].append(3) # Setting y = [3]
d['x'].append(10) # .append() 是在原对象上进行的操作,并没有调用 __setitem__ 方法,所以不会触发 Mixin 中的“只设置一次”逻辑。
try:
d['x'] = 42 # Setting x = 42 → KeyError: 'x already set'
except KeyError as e:
print("Expected error:", e) # Expected error: x already set
d = StringOrderedDict()
d['x'] = 23 # Setting x = 23
try:
d[42] = 10 # TypeError: keys must be strings
except TypeError as e:
print("Expected type error:", e) # Expected type error: keys must be strings
try:
d['x'] = 42 # KeyError: 'x already set'
except KeyError as e:
print("Expected key error:", e) # Expected key error: x already set
if __name__ == '__main__':
main()
mixin类实现细节
-
Mixin 类不是用来单独使用的,它必须和其他类一起“混合”(多重继承)才能发挥作用。
-
Mixin 类通常不维护自己的状态(即没有实例变量),也不定义
__init__()方法,它只提供方法的增强功能。
给Mixin添加初始化方法和实例变量
给 Mixin 类添加 __init__() 和实例变量是可行但高风险的操作 。你需要通过关键字参数、**kwargs 和良好的命名规范来规避命名冲突与构造函数签名不一致的问题,同时保持 Mixin 的可插拔性和通用性。
class RestrictKeysMixin:
"""
Mixin 类:限制字典的键必须是某种类型。
使用关键字参数 `_restrict_key_type` 指定允许的键类型。
"""
def __init__(self, *args, _restrict_key_type, **kwargs):
# 存储允许的键类型
self._restrict_key_type = _restrict_key_type
# 继续调用父类构造函数(通常是 dict)
super().__init__(*args, **kwargs)
def __setitem__(self, key, value):
# 在设置键之前检查类型
if not isinstance(key, self._restrict_key_type):
raise TypeError(f'Keys must be {self._restrict_key_type}')
# 否则正常执行 setitem(最终交给 dict 处理)
return super().__setitem__(key, value)
# super() 是 Mixin 类能够正常工作的关键,它使得多个 Mixin 可以协同工作,并最终调用到底层的实际类(如 dict)的方法。
# 示例组合类
class RDict(RestrictKeysMixin, dict):
pass
def main():
# 创建一个只允许字符串键的字典
d = RDict(_restrict_key_type=str)
d['name'] = 'Dave'
print(d) # {'name': 'Dave'}
# 从序列初始化
e = RDict([('name', 'Dave'), ('n', 37)], _restrict_key_type=str)
print(e) # {'name': 'Dave', 'n': 37}
# 使用关键字参数初始化
#
f = RDict(name='Dave', n=37, _restrict_key_type=str)
print(f) # {'name': 'Dave', 'n': 37}
# 尝试使用非字符串键 → 应该抛出 TypeError
try:
f[42] = 10
except TypeError as e:
print("Expected error:", e)
if __name__ == '__main__':
main()
使用类装饰器实现
def LoggedMapping(cls):
# 保存原始方法
cls_getitem = cls.__getitem__
cls_setitem = cls.__setitem__
cls_delitem = cls.__delitem__
# 定义新的方法版本,带日志输出
def __getitem__(self, key):
print('Getting ' + str(key))
return cls_getitem(self, key)
def __setitem__(self, key, value):
print('Setting {} = {!r}'.format(key, value))
return cls_setitem(self, key, value)
def __delitem__(self, key):
print('Deleting ' + str(key))
return cls_delitem(self, key)
# 替换类中的特殊方法
cls.__getitem__ = __getitem__
cls.__setitem__ = __setitem__
cls.__delitem__ = __delitem__
return cls
# 使用装饰器增强 dict 子类
@LoggedMapping
class LoggedDict(dict):
pass
8.19 实现带有状态的对象或状态机
# 实现一个状态机 让对象可以在不同的状态中操作
# 这个类中有大量的条件判断 这样并不好
class Connection:
def __init__(self):
self.state = 'CLOSED'
def read(self):
if self.state != 'OPEN':
raise RuntimeError('Not open')
print('reading')
def write(self, data):
if self.state != 'OPEN':
raise RuntimeError('Not open')
print('writing')
def open(self):
if self.state == 'OPEN':
raise RuntimeError('Already open')
self.state = 'OPEN'
def close(self):
if self.state == 'CLOSED':
raise RuntimeError('Already closed')
self.state = 'CLOSED'
# 将每种操作状态以一个单独的类来定义 然后在connection类中使用这些状态
class Connection:
def __init__(self):
self.new_state(ClosedConnectionState)
def new_state(self, newstate):
self._state = newstate
def read(self):
return self._state.read(self)
def write(self, data):
return self._state.write(self, data)
def open(self):
return self._state.open(self)
def close(self):
return self._state.close(self)
# Connection state base class
class ConnectionState:
@staticmethod
def read(conn):
raise NotImplementedError()
@staticmethod
def write(conn, data):
raise NotImplementedError()
@staticmethod
def open(conn):
raise NotImplementedError()
@staticmethod
def close(conn):
raise NotImplementedError()
class ClosedConnectionState(ConnectionState):
@staticmethod
def read(conn):
raise RuntimeError('Not open')
@staticmethod
def write(conn, data):
raise RuntimeError('Not open')
@staticmethod
def open(conn):
conn.new_state(OpenConnectionState)
@staticmethod
def close(conn):
raise RuntimeError('Already closed')
class OpenConnectionState(ConnectionState):
@staticmethod
def read(conn):
print('reading')
@staticmethod
def write(conn, data):
print('writing')
@staticmethod
def open(conn):
raise RuntimeError('Already open')
@staticmethod
def close(conn):
conn.new_state(ClosedConnectionState)
if __name__ == '__main__':
c = Connection() # 创建连接对象
print(c._state) # <class '__main__.ClosedConnectionState'>
try:
c.read()
except Exception as e:
print("Exception:", e) # RuntimeError: Not open
c.open() # 成功打开连接
print(c._state) # <class '__main__.OpenConnectionState'>
c.read() # reading
c.write('hello') # writing: hello
c.close() # 成功关闭连接
print(c._state) # <class '__main__.ClosedConnectionState'>
修改实例类型实现切换
class Connection:
def __init__(self):
self.new_state(ClosedConnection)
def new_state(self, newstate):
# 一些人可能不推荐直接使用修改类类型的方法进行切换 但是相较于上面的代码 这个代码的运行速度确实更快
# 修改当前实例的类型,实现状态切换
self.__class__ = newstate
def read(self):
raise NotImplementedError()
def write(self, data):
raise NotImplementedError()
def open(self):
raise NotImplementedError()
def close(self):
raise NotImplementedError()
class ClosedConnection(Connection):
def read(self):
raise RuntimeError('Not open')
def write(self, data):
raise RuntimeError('Not open')
def open(self):
self.new_state(OpenConnection)
def close(self):
raise RuntimeError('Already closed')
class OpenConnection(Connection):
def read(self):
print('reading')
def write(self, data):
print('writing')
def open(self):
raise RuntimeError('Already open')
def close(self):
self.new_state(ClosedConnection)
if __name__ == '__main__':
c = Connection() # 创建连接对象
print(c) # <ClosedConnection object>
try:
c.read()
except Exception as e:
print("Exception:", e) # Not open
c.open() # 切换为 OpenConnection 状态
print(c) # <OpenConnection object>
c.read() # reading
c.write('hello') # writing
c.close() # 切换回 ClosedConnection 状态
print(c) # <ClosedConnection object>
8.20 调用对象上的方法,方法名以字符串形式给出
使用getattr或opeartor
# 简单的情况 使用getattr方法
# 通过方法名字符串调用方法
import math
import operator
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return 'Point({!r:},{!r:})'.format(self.x, self.y)
def distance(self, x, y):
return math.hypot(self.x - x, self.y - y)
p = Point(2, 3)
d = getattr(p, 'distance')(0, 0) # 调用 p.distance(0, 0)
print(d)
operator.methodcaller('distance', 0, 0)(p) # 等价于p.distance(0, 0)
points = [
Point(1, 2),
Point(3, 0),
Point(10, -3),
Point(-5, -7),
Point(-1, 8),
Point(3, 2)
]
# 按照距离原点的顺序进行排序
points.sort(key=operator.methodcaller('distance', 0, 0))
# 写法一
result = operator.methodcaller('distance', 0, 0)(p)
# 写法二
# operator.methodcall()创建了一个可调用对象,而且把所需的参数提供给了被调用的方 法,只需要传入对应的self参数即可
d = operator.methodcaller('distance', 0, 0)
result = d(p)
# 同样等价于 p.distance(0, 0)
8.21 实现访问者模式
假设有一个复杂的数据结构,其中包含多种不同类型的节点。同时需要遍历这个结构、根据遇到的对象类型执行不同的操作、不希望把每种操作都硬编码到类本身中、希望代码具有良好的扩展性,添加新操作时尽量不修改已有类。这个时候就需要用到访问者模式了。
# 定义抽象语法树 用于表示数学运算
class Node:
pass
class UnaryOperator(Node):
def __init__(self, operand):
self.operand = operand
class BinaryOperator(Node):
def __init__(self, left, right):
self.left = left
self.right = right
# 继承二值操作
class Add(BinaryOperator):
pass
class Sub(BinaryOperator):
pass
class Mul(BinaryOperator):
pass
class Div(BinaryOperator):
pass
#继承单值操作
class Negate(UnaryOperator):
pass
class Number(Node):
def __init__(self, value):
self.value = value
# 表达式:1 + 2 * (3 - 4) / 5
t1 = Sub(Number(3), Number(4)) # (3 - 4)
t2 = Mul(Number(2), t1) # 2 * (3 - 4)
t3 = Div(t2, Number(5)) # 2 * (3 - 4) / 5
t4 = Add(Number(1), t3) # 1 + (2 * (3 - 4) / 5)
实现一
# 创建数据结构是简单的 问题是后续需要对这些结构进行操作。为了让处理过程变得更加通用,就需要使用访问者模式
# 如下Node类本身是不实现操作的 操作都通过visitor实现
# 访问者基类
class NodeVisitor:
def visit(self, node):
# 构造方法名:visit_ + 类型名,例如 visit_Add、visit_Number
methname = 'visit_' + type(node).__name__
# 使用 getattr 查找当前类中是否有这个方法
# 目前实现的代码中self应该是Evaluator和StackCode类
meth = getattr(self, methname, None)
# 如果没有找到,则使用 generic_visit 方法作为默认处理
if meth is None:
meth = self.generic_visit
# 调用该方法并传入 node
return meth(node)
# 默认处理方式:抛出运行时异常,提示未实现对应的 visit_xxx 方法
def generic_visit(self, node):
raise RuntimeError(f'No {type(node).__name__} method')
# 求值访问者 继承了访问者基类
class Evaluator(NodeVisitor):
def visit_Number(self, node):
return node.value
def visit_Add(self, node):
return self.visit(node.left) + self.visit(node.right)
def visit_Sub(self, node):
return self.visit(node.left) - self.visit(node.right)
def visit_Mul(self, node):
return self.visit(node.left) * self.visit(node.right)
def visit_Div(self, node):
return self.visit(node.left) / self.visit(node.right)
def visit_Negate(self, node):
return -self.visit(node.operand)
expr = Add(Number(3), Negate(Number(4))) # 3 + -(4)
e = Evaluator()
print(e.visit(expr)) # 输出: -1
实现二
# 使用堆栈机实现
class StackCode(NodeVisitor):
# 初始化指令列表,调用visit()遍历整个表达式树
def generate_code(self, node):
# 初始化一个空列表 用于储存生成的指令
self.instructions = []
self.visit(node)
# 返回生成的指令列表
return self.instructions
# 遇到数字节点,生成PUSH指令
def visit_Number(self, node):
# 将数字的值压入栈中
self.instructions.append(('PUSH', node.value))
# 处理二元操作符(加减乘除) 先处理左右节点,再添加对应操作指令
def binop(self, node, instruction):
# 递归调用左节点
self.visit(node.left)
# 递归处理右节点
self.visit(node.right)
# 添加对应的指令(数值已经入栈了)
self.instructions.append((instruction,))
def visit_Add(self, node):
self.binop(node, 'ADD')
def visit_Sub(self, node):
self.binop(node, 'SUB')
def visit_Mul(self, node):
self.binop(node, 'MUL')
def visit_Div(self, node):
self.binop(node, 'DIV')
# 处理一元操作符,先处理操作数,再添加操作指令
def unaryop(self, node, instruction):
self.visit(node.operand)
self.instructions.append((instruction,))
def visit_Negate(self, node):
self.unaryop(node, 'NEG')
# 表达式: 1 + 2 * (3 - 4) / 5
t1 = Sub(Number(3), Number(4)) # 3 - 4
t2 = Mul(Number(2), t1) # 2 * (3 - 4)
t3 = Div(t2, Number(5)) # 2 * (3 - 4) / 5
t4 = Add(Number(1), t3) # 1 + (2 * (3 - 4) / 5)
# Add
# / \
# Number Div
# | / \
# 1 Mul Number
# / \ |
# Number Sub 5
# | |
# 2 3 4
s = StackCode()
instructions = s.generate_code(t4)
print(instructions)
# [('PUSH', 1),('PUSH', 2),('PUSH', 3),('PUSH', 4),('SUB',),('MUL',),('PUSH', 5),('DIV',),('ADD',)]
8.22 实现非递归的访问者模式
# 使用递归形式的访问者模式可能超出递归限制,使用堆栈和生成器可以不使用递归
# 使用生成器配合显式栈模拟递归访问,虽然会占用更多内存,但可以突破 Python 默认的递归深度限制(1000 层),从而支持任意深度的表达式树。
import types
class Node:
pass
class NodeVisitor:
def visit(self, node):
# 初始化栈 并放入初始节点
stack = [node]
# 用于存储上一个操作/子节点访问的结果
last_result = None
# 栈不为空时 持续进行循环
while stack:
try:
# 获取栈顶元素(注意不是弹出)
last = stack[-1]
if isinstance(last, types.GeneratorType):
# 如果是生成器,send 结果进去继续执行
stack.append(last.send(last_result))
last_result = None
elif isinstance(last, Node):
# 如果是节点对象,调用 _visit 方法(可能返回生成器)
stack.append(self._visit(stack.pop()))
else:
# 否则认为是结果值,弹出并保存
last_result = stack.pop()
except StopIteration:
# 生成器完成时抛出 StopIteration,直接弹出栈顶
stack.pop()
return last_result
def _visit(self, node):
methname = 'visit_' + type(node).__name__
meth = getattr(self, methname, None)
if meth is None:
meth = self.generic_visit
return meth(node)
def generic_visit(self, node):
raise RuntimeError('No {} method'.format('visit_' + type(node).__name__)
# 下面这个代码会超出递归限制
a = Number(0)
for n in range(1, 100000):
a = Add(a, Number(n))
e = Evaluator()
e.visit(a)
# RecursionError: maximum recursion depth exceeded
# 对Evaluator类进行优化
class Evaluator(NodeVisitor):
def visit_Number(self, node):
return node.value
def visit_Add(self, node):
yield (yield node.left) + (yield node.right)
def visit_Sub(self, node):
yield (yield node.left) - (yield node.right)
def visit_Mul(self, node):
yield (yield node.left) * (yield node.right)
def visit_Div(self, node):
yield (yield node.left) / (yield node.right)
def visit_Negate(self, node):
yield -(yield node.operand)
过程展示
# 变量初始化
N1 = Number(1)
N2 = Number(2)
N3 = Number(3)
N4 = Number(4)
A1 = Add(N1, N2)
S1 = Sub(N3, N4)
M1 = Mul(A1, S1)
evaluator.visit(M1)
# 初始状态
# stack = [M1] last_result = None
# NodeVisitor.visit 循环
# 处理 M1(Mul Node)
# last = M1 (Node)
# -> _visit(M1) 因为是一个节点 所以调用其中的_visit方法
# -> 调用visit_Mul方法 返回一个生成器 gen_M1
# stack = [gen_M1] 将其加入到栈中
# 运行 gen_M1(第一次)
# last = gen_M1 (Generator) 进入生成器分支 -> gen_M1.send(None)
# -> 先yield node.left 返回A1
# 处理 A1 (Add Node)
# last = A1 (Node) -> _visit(A1) -> 返回gen_A1
# stack = [gen_M1, A1]
# 运行 gen_A1(第一次)
# last = gen_A1 (Generator) 进入生成器分支 -> gen_A1.send(None)
# -> visit_Add 执行到 (yield node.left),yield node.left(即N1)
# stack = [gen_M1, gen_A1, N1] 将N1加入到栈中
# last_result = None
# 处理 N1 (Number Node)
# last = N1 (Node)
# -> _visit(N1) 因为是一个节点 所以调用其中的_visit方法
# -> 调用visit_Number(N1) 方法,该方法直接返回 N1.value (即 1)
# stack = [gen_M1, gen_A1, 1] (N1被弹出,其结果1被加入)
# 处理结果 1
# last = 1 (非Node, 非Generator) 进入else结果分支
# last_result = 1 (1被弹出并赋值给last_result)
# stack = [gen_M1, gen_A1]
# 运行 gen_A1(第二次, 恢复处理N1的结果)
# last = gen_A1 (Generator) 进入生成器分支 -> gen_A1.send(last_result) (即 gen_A1.send(1))
# -> visit_Add 中 (yield node.left) 表达式的值变为 1, val_left = 1
# -> 继续执行到 (yield node.right),yield出 node.right (即 N2)
# stack = [gen_M1, gen_A1, N2] 将N2加入到栈中
# last_result = None
# 处理 N2 (Number Node)
# last = N2 (Node)
# -> _visit(N2)
# -> 调用visit_Number(N2) 方法,返回 N2.value (即 2)
# stack = [gen_M1, gen_A1, 2] (N2被弹出,其结果2被加入)
# 处理结果 2
# last = 2 (非Node, 非Generator) 进入else结果分支
# last_result = 2 (2被弹出并赋值给last_result)
# stack = [gen_M1, gen_A1]
# 运行 gen_A1(第三次, 恢复处理N2的结果, 并计算Add)
# last = gen_A1 (Generator) 进入生成器分支 -> gen_A1.send(last_result) (即 gen_A1.send(2))
# -> visit_Add 中 (yield node.right) 表达式的值变为 2, val_right = 2
# -> 计算 result = val_left + val_right (1 + 2 = 3)
# -> visit_Add yield出 result (即 3)
# stack = [gen_M1, gen_A1, 3] 将Add的结果3加入到栈中
# last_result = None
8.23 在环状数据结构中管理内存
案例
# 普通的垃圾回收不适用于环形数据结构
import gc
class Data:
def __del__(self):
print('Data.__del__')
class Node:
def __init__(self):
self.data = Data()
self.parent = None
self.children = []
def add_child(self, child):
self.children.append(child)
child.parent = self
if __name__ == '__main__':
# 删除单个Data()对象 立即删除
a = Data()
del a
gc.collect()
# 删除单个Node对象 立即删除
a = Node()
del a
gc.collect()
# 删除父子关系节点
a = Node()
b = Node()
a.add_child(b)
del a
# 没法立即删除 需要手动回收
gc.collect()
弱引用实现
# 使用树节点展示弱引用机制
# 当两个或多个对象之间互相引用,且没有外部引用指向它们时,就会形成循环引用 。Python 的垃圾回收机制虽然可以处理部分循环引用,但在某些情况下仍可能导致内存泄漏
import weakref
class Node:
def __init__(self, value): # 初始化节点值、父节点和子节点列表
self.value = value # 存储节点值
self._parent = None # 用于保存父节点的弱引用
self.children = [] # 子节点列表
def __repr__(self): # 返回节点的字符串表示
return 'Node({!r})'.format(self.value)
@property
def parent(self):
return self._parent if self._parent is None else self._parent()
@parent.setter
def parent(self, node): # 设置父节点时使用弱引用
# weakref 提供了一种不增加引用计数的方式来引用对象,这样就不会阻止对象被垃圾回收
self._parent = weakref.ref(node)
def add_child(self, child): # 添加子节点,并设置其父节点为当前节点
self.children.append(child)
# 此处实际调用了parent.setter
child.parent = self
# 创建两个节点
root = Node('parent')
c1 = Node('child')
# 建立父子关系
root.add_child(c1)
# 打印子节点的父节点(此时应为 Node('parent'))
print(c1.parent) # 输出: Node('parent')
# 删除父节点引用
del root # 删除对 root 的强引用
print(c1.parent) # 输出: None 说明正确回收了资源
8.24 让类支持比较操作
# 实现__eq__、__lt__、__le__、__gt__、__ge__等方法即可实现各种比较方法
from functools import total_ordering
class Room:
def __init__(self, name, length, width):
self.name = name
self.length = length
self.width = width
self.square_feet = self.length * self.width
# 使用了 @total_ordering 装饰器,只需定义 __eq__ 和 __lt__,其余比较操作符自动可用
@total_ordering
class House:
def __init__(self, name, style):
self.name = name
self.style = style
self.rooms = list()
@property
def living_space_footage(self):
return sum(r.square_feet for r in self.rooms)
def add_room(self, room):
self.rooms.append(room)
def __str__(self):
return '{}: {} square foot {}'.format(self.name,
self.living_space_footage,
self.style)
def __eq__(self, other):
return self.living_space_footage == other.living_space_footage
def __lt__(self, other):
return self.living_space_footage < other.living_space_footage
# Build a few houses, and add rooms to them
h1 = House('h1', 'Cape')
h1.add_room(Room('Master Bedroom', 14, 21))
h1.add_room(Room('Living Room', 18, 20))
h1.add_room(Room('Kitchen', 12, 16))
h1.add_room(Room('Office', 12, 12))
h2 = House('h2', 'Ranch')
h2.add_room(Room('Master Bedroom', 14, 21))
h2.add_room(Room('Living Room', 18, 20))
h2.add_room(Room('Kitchen', 12, 16))
h3 = House('h3', 'Split')
h3.add_room(Room('Master Bedroom', 14, 21))
h3.add_room(Room('Living Room', 18, 20))
h3.add_room(Room('Office', 12, 16))
h3.add_room(Room('Kitchen', 15, 17))
# 输出测试结果(每行右边是预期输出)
print('Is h1 bigger than h2?', h1 > h2) # prints: Is h1 bigger than h2? True
print('Is h2 smaller than h3?', h2 < h3) # prints: Is h2 smaller than h3? True
print('Is h2 greater than or equal to h1?', h2 >= h1) # prints: Is h2 greater than or equal to h1? False
houses = [h1, h2, h3]
# 打印最大和最小的房子
print('Which one is biggest?', max(houses)) # prints: Which one is biggest? h3: 1101 square foot Split
print('Which is smallest?', min(houses)) # prints: Which is smallest? h2: 846 square foot Ranch
8.25 创建缓存实例
普通实现
# 当创建类实例时,返回一个缓存引用,确保相同参数只创建一次实例
# 使用单例变体加缓存机制实现
import weakref
# 原始类定义
class Spam:
def __init__(self, name):
self.name = name
# 使用 WeakValueDictionary 来缓存实例,当实例不再被外部引用时会被自动回收
_spam_cache = weakref.WeakValueDictionary()
# 工厂函数:用于获取缓存中的实例或创建新实例
def get_spam(name):
if name not in _spam_cache:
s = Spam(name)
_spam_cache[name] = s
else:
s = _spam_cache[name]
return s
a = get_spam('foo') # 第一次创建 'foo'
b = get_spam('bar') # 第一次创建 'bar'
print(a is b) # 输出: False (不是同一个实例)
c = get_spam('foo') # 获取已有的 'foo' 实例
print(a is c) # 输出: True (是同一个实例)
del a # 删除对 a 的引用
d = get_spam('foo') # 重新获取 'foo'
print(d is c) # 输出: True (仍然指向同一个实例)
# 注意:只有当所有强引用都被删除后,WeakValueDictionary 才会真正清除缓存中的对象
优雅实现
有点缺陷
# 主要的问题是__init__方法总会被调用 无论对象实例有没有得到缓存都是如此
import weakref
class Spam:
_spam_cache = weakref.WeakValueDictionary()
def __new__(cls, name):
if name in cls._spam_cache:
return cls._spam_cache[name]
else:
self = super().__new__(cls)
cls._spam_cache[name] = self
return self
def __init__(self, name):
print('Initializing Spam')
self.name = name
s = Spam('Dave') # 第一次创建,应调用 __init__
t = Spam('Dave') # 第二次创建,仍会调用 __init__
print(s is t) # 应输出: True
使用全局管理
import weakref
class CachedSpamManager:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_spam(self, name):
if name not in self._cache:
self._cache[name] = Spam(name)
return self._cache[name]
def clear(self):
self._cache.clear()
class Spam:
# 所有 Spam 实例共享同一个 manager
manager = CachedSpamManager()
def __init__(self, name):
self.name = name
@classmethod
def get_spam(cls, name):
return cls.manager.get_spam(name)
if __name__ == '__main__':
a = Spam.get_spam('foo') # 第一次创建
b = Spam.get_spam('foo') # 应该从缓存中获取
print(a is b) # 输出: True (两个变量指向同一个实例)。
1万+

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



