文章目录
- 第八章 类与对象
- 8.1 改变对象的字符串显示
- 8.2 自定义字符串的格式化
- 8.3 让对象支持上下文管理器
- 8.4 创建大量对象时节省内存的方法
- 8.5 在类中封装属性名
- 8.6 创建可管理的属性
- 8.7 调用父类方法
- 8.8 子类中扩展 property
- 8.9 创建新的类或实例属性
- 8.10 使用延迟计算特性
- 8.11 简化数据结构的初始化
- 8.12 定义接口或者抽象基类
- 8.13 实现数据模型的类型约束
- 8.14 实现自定义容器
- 8.15 属性的代理访问
- 8.16 在类中定义多个构造器
- 8.17 创建不调用 init 方法的实例
- 8.18 利用 Mixins 扩展类功能
- 8.19 实现状态对象或者状态机
- 8.20 通过字符串调用对象方法
- 8.21 实现访问者模式
- 8.22 不用递归实现访问者模式
- 8.23 循环引用数据结构的内存管理
- 8.24 让类支持比较操作
- 8.25 创建缓存实例
第八章 类与对象
8.1 改变对象的字符串显示
重新定义类的 __str__() 和 __repr__() 方法。
__repr__() 返回实例的代码表示形式,内置的 repr() 函数返回这个字符串。
__str__() 方法把实例转换为字符串,str() 或 print() 会输出这个字符串。
class Pair:
def __init__(self, x, y):
self.x = x
self.y = y
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)
>>> p
Pair(3, 4)
>>> print(p)
(3, 4)
格式化代码 !r 指明了使用 __repr__() 代替默认的 __str__()。
__str__() 没有定义时会使用 __repr__() 方法代替。
8.2 自定义字符串的格式化
在类上面定义 __format__() 方法。
_format = {
'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
def __format__(self, code):
if code == '':
code = 'ymd'
fmt = _format[code]
return fmt.format(d=self)
>>> d = Date(2019, 10, 1)
>>> format(d)
'2019-10-1'
>>> 'date: {:mdy}'.format(d)
'date: 10/1/2019'
__format__() 方法是字符串格式化的一个钩子函数。格式化代码的解析由类自己实现。
8.3 让对象支持上下文管理器
需要实现 __enter__() 和 __exit__() 方法。出现 with 语句时,__enter__() 方法被触发,返回值被赋给 as 声明的变量;结束时,__exit__() 被触发进行清理工作。
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 = family
self.type = type
self.sock = None
def __enter__(self):
if self.sock is not None:
raise RunTimeError('Already connected')
self.sock = socket(self.family, self.type)
self.sock.connect(self.address)
return self.sock
def __exit__(self):
self.sock.close()
self.sock = None
这个类表示的连接,建立和关闭都是在 with 自动完成。
from functols import partial
conn = LazyConnection(('www.python.org', 80))
with conn as s:
s.send(b'GET /index.html HTTP/1.0\r\n')
s.send(b'Host: ww.python.org\r\n')
resp = b''.join(iter(partial(s.recv, 8192)))
上面的这个类一次只允许一个 socket 连接,不允许多个 with 语句嵌套使用。可以做如下修改:
class LazyConnection:
def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
self.address = address
self.family = family
self.type = type
self.connections = []
def __enter__(self):
sock = socket(self.family, self.type)
sock.connect(self.address)
self.connections.append(sock)
return sock
def __exit__(self, exc_ty, exc_val, tb):
self.connections.pop().close()
在需要管理资源(文件、网络连接、锁)的程序中,上下文管理器被普遍使用。
8.4 创建大量对象时节省内存的方法
对于当做加单数据结构的类,可以定义 __slots__ 属性来减少实例站的内存。定义这个属性后,类的实例会通过一个固定大小的数组构建,而不是每个实例定义一个字典。
定义 __slots__ 后,类的实例不能添加新的属性,类不再支持多继承。
class Date:
__slots__ = ['year', 'month', 'day']
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
8.5 在类中封装属性名
私有数据使用命名规则进行规约。
单下划线开头的名字是内部实现;双下划綫开头的名字的访问名称是不同的形式。
class B:
def __init__(self):
self.__private = 0
def __private_method(self):
print('B')
class C(B):
def __init__(self):
super().__init__()
>>> c = C()
>>> c.__private
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'C' object has no attribute '__private'
>>> b = B()
>>> b.__private
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'B' object has no attribute '__private'
>>>
>>> b._B__private
0
>>> >>> b._B__private_method()
B
通常,隐藏内部名称使用单下划线即可。但在这些内部属性需要对子类隐藏时,就要考虑使用双下划线。
有时定义的变量和保留关键字冲突了,可以用单下划线结尾。
8.6 创建可管理的属性
添加除了访问和修改之外的其它逻辑,可以将该属性定义为一个 property。
class Person:
def __init__(self, first_name):
self.first_name = first_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
@first_name.deleter
def first_name(self):
raise AttrbuteError("Can't delete attrivute.")
使用装饰器时,三个方法的名字必须都一样。第一个方法是一个 getter 函数,使 first_name 成为属性,接下来的两个方法为 first_name 属性添加了 setter 和 deleter 函数。first_name 属性被创建后才能定义 后面的两个装饰器。
在访问 property 时会自动触发 getter、setter 和 deleter 方法。
上面的代码中,初始化为 first_name 赋值时也会进行类型检查。
也能在已经存在的 get、set 方法上定义 property:
class Person:
def __init__(self, first_name):
self.set_first_name(first_name)
def get_first_name(self):
return self._first_name
def set_first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string.')
self._first_name = value
def del_first_name(self):
raise AttributeError("Can't delete attribute.")
name = property(get_first_name, set_first_name, del_first_name)
property 还能定义动态计算的属性,这种属性不会被存储,而是需要的时候才计算出来。
import math
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return math.pi * self.radius ** 2
@property
def diameter(self):
return self.radius * 2
@property
def perimeter(self):
return 2 * math.pi * self.radius
对圆的直径、周长、面积的访问都能通过属性访问。如果不这么做,就要在访问时混合使用属性访问和方法调用。
8.7 调用父类方法
在子类中调用被覆盖的父类方法:super() 函数。
在 __init__() 中使用,确保父类被正确地初始化:
class A:
def __init__(self):
self.x = 0
class B:
def __init__(self):
super().__init__()
self.y = 1
在覆盖特殊方法时使用:
class Proxy:
def __init__(self, obj):
self._obj = obj
def __getattr__(self, name):
return getattr(self._obj, name)
def __setattr__(self, name, value):
if name.startswith('_'):
# 下划线开头交给原生方法解决
super().__setattr__(name, value)
else:
setattr(self._obj, name, value)
继承的时候,会使用 C3 算法构造一个方法解析顺序表,包含子类的所有基类。子类访问属性时,会在这个表上从左到右查找,直到找到匹配这个属性的类。
出现 super() 时,python 会在顺序表上从下一个开始往后搜索。菱形继承的时候,每个子类直接调用父类的方法,最顶层类的方法会调用多次,使用 super() 能使这些方法只被调用一次。
8.8 子类中扩展 property
父类定义了 property:
class Person:
def __init__(self, name):
self.name = name
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string.')
self._name = value
@name.deleter
def name(self):
raise AttributeError("Can't delete attribute.")
子类中扩展这个 property 全部方法:
class SubPerson(Person):
@property
def name(self):
print('Getting name')
return super().name
@name.setter
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
@name.deleter
def name(self):
print('Deleting name')
super(SubPerson, SubPerson).name.__delete__(self)
如果只扩展其中一个方法,使用 property 是不行的。
class SubPerson(Person):
@Person.name.getter
def name(self):
print('Getting name')
return super().name
class SubPerson(Person):
@Person.name.setter
def name(self, value):
print('Setting name to', value)
super(SubPerson, SubPerson).name.__set__(self, value)
8.9 创建新的类或实例属性
简单地对某个类的单个属性添加新的操作或功能,使用 property 即可。但想对大量使用的属性创建一个新的属性类型,可以创建一个描述器类。
class Integer:
def __init__(self, name):
self.name = name
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, int):
raise TypeError('Expected an int')
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
描述器是是实现了三个核心访问操作的类。三个访问方法接收一个实例作为输入,然后操作实例底层的字典。
使用描述器的时候,需要将它的实例作为类属性放到一个类的定义中。
class Point:
x = Integer('x')
y = Integer('y')
def __init__(self, x, y):
self.x = x
self.y = y
描述器的 name 属性是实例字典的 key。
描述器可以实现大部分 python 类特性中的底层魔法。定义描述器可以在底层捕获实例操作并完全自定义它们的行为。利用描述器可以实现很多高级功能。但要注意只能在类级别定义。
描述器通常是使用到装饰器和元类的大型框架的一个组件。
class Typed:
def __init__(self, name, expected_type):
self.name = name
self.expected_type = expected_type
def __get__(self, instannce, 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():
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
8.10 使用延迟计算特性
想将一个只读属性定义为 property,且只在访问的时候才会计算结果,但是访问后,结果值会被保存,下次访问就不用计算了。
使用一个描述器类:
class lazyproperty:
def __init__(self, func):
self.func = func
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
class Circle:
def __init__(self, radius):
self.radius = radius
@lazyproperty
def area(self):
print('Computing area')
return math.pi * self.radius ** 2
@lazyproperty
def permeter(self):
print('Computing perimeter')
return 2 * math.pi * self.radius
描述器只定义了一个 __get__() 方法,当访问的属性不在实例的底层字典时,才会触发 __get__() 方法。
>>> c.area
Computing area
28.274333882308138
>>> vars(c)
{'radius': 3, 'area': 28.274333882308138}
>>> c.area
28.274333882308138
>>> del c.area
>>> vars(c)
{'radius': 3}
>>> c.area
Computing area
28.274333882308138
这个方法的一个缺陷是计算出的值创建后可以被修改。
把 lazyproperty 改为下面的实现能不允许修改。但每次访问都必须使用到 getattr 函数,不如直接读取字典快。
def lazyproperty(func):
# 不改名字,会无限递归
name = '_lazy_' + func.__name__
@property
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(3)
>>> c.area
Computing area
28.274333882308138
>>> c.area = 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> del c.area
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute
>>> c.__dict__
{'radius': 3, '_lazy_area': 28.274333882308138}
8.11 简化数据结构的初始化
好多仅仅作为数据结构的类,不想写太多 __init__() 函数。
可以在一个基类里写一个公共的 __init__() 函数。
class Structure1:
_fields = []
def __init__(self, *args):
if len(args) != len(self._fields):
raise TypeError('Expected {} arguments'.format(len(self._fields)))
for name, value in zip(self._fields, args):
setattr(self, name, value)
使用这个基类:
class Stock(Structure1):
_fields = ['name', 'shares', 'price']
class Point(Structure1):
_fields = ['x', 'y']
import math
class Circle(Structure1):
_fields = ['radius']
def area(self):
return math.pi * self.radius ** 2
支持关键字参数的版本:
class Structure2:
_fields = []
def __init__(self, *args, **kwargs):
if len(args) > len(self._fields):
raise TypeError('Expected {} arguments'.format(len(self._fields)))
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)))
把不在 _fields 中的名称加到属性中去。
class Structure3:
_fields = []
def __init__(self, *args, **kwargs):
if len(args) != len(self._fields):
raise TypeError('Expected {} arguments'.format(len(self._fields)))
for name, value in zip(self._fields, args):
setattr(self, name, value)
extra_args = kwargs.keys() - self._fields
for name in extra_args:
setattr(self, name, kwargs.pop(name))
if kwargs:
raise TypeError('Duplicate values for {}'.format(','.join(kwargs)))
注意 extra_args 的作用,避免同一个属性多次赋值。这种实现和支持关键字参数的版本是冲突的。
使用 setattr 的方法设置属性值,也能直接更新实例的字典。但是在一个子类定义了 __slots__ 或使用 property 包装某个属性时(字典中的键不是这个属性名),直接访问实例字典就不起作用了。
8.12 定义接口或者抽象基类
定义抽象基类,能够检查子类实现了某些特定的方法。
用 abc 模块定义抽象基类。抽象基类不能被实例化,它的目的就是让别的类继承它并实现特定的抽象方法。
from abc import ABCMeta, abstractmethod
class IStream(metaclass=ABCMeta):
@abstractmethod
def read(self, maxbytes=-1):
pass
@abstractmethod
def write(self, data):
pass
class SocketStream(IStream):
def read(self, maxbytes=-1):
pass
def write(self, data):
pass
抽象基类主要用来检查某些类是否为特定类型、实现了某些特定方法。
除了继承外,还能通过注册让某个类实现抽象基类:
import os
IStream.register(io.IOBase)
f = open('test.txt')
isinstance(f, IStream)
@abstractmethod 还能注解静态方法、类方法和 properties,使用时,@abstractmethod 要紧靠函数定义。
标准库中有很多用到抽象基类的地方。collections 定义很多跟容器和迭代器有关的抽象基类,numbers 定义了跟数字对象有关的基类,io库定义了跟 I/O 操作相关的基类。
8.13 实现数据模型的类型约束
定义某些在值上有约束的数据结构。
使用描述器。
# 基类,使用描述器赋值
class Descriptor:
def __init__(self, name=None, **opts):
self.name = name
for key, value in opt.items():
setter(self, key, value)
def __set__(self, instance, value):
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('expected' + str(self.expected_type))
super().__set__(instance, value)
# 限制值的描述器
class Unsigned(Descriptor):
def __set__(self, instance, value):
if value < 0:
raise ValueError('Expected >= 0')
super().__set__(instance, value)
class MaxSized(Descriptor):
def __init__(self, name=None, **opts):
if 'size' not in opts:
raise TypeError('missing size option')
super().__init__(name, **opt)
def __set__(self, instance, value):
if len(value) >= self.size:
raise ValueError('size must be <' + str(self.size))
super().__set__(instance, value)
拿上面的基类定义数据类型:
class Interger(Typed):
excepted_type = int
class UnsignedInterger(Interger, Unsigned):
pass
class UnsignedFloat(Float, 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)
shares = UnsignedInteger('shares')
price = UnsignedFloat('price')
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Stock 实例的属性有赋值约束。上述代码能使用一些技术简化。使用类装饰器:
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:
setattr(cls, key, value(key))
return cls
return decorate
@check_attributes(name=SizedString(size=8),
shares=UnsignedInterger,
price=UnsignedFloat)
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
使用元类简化:
class checkedmeta(type):
def __new__(cls, clsname, bases, methods):
for key, value in methods.items():
if isinstance(value, Descriptor):
value.name = key
return type.__new__(cls, clsname, bases, methods)
class Stock2(metaclass=checkedmeta):
name = SizedString(size=8)
shares = UnsignedInteger()
price = UnsignedFloat()
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Descriptor 基类只定义了 __set__() 方法。如果只从底层字典获取某个属性值的话,就没必要定义 __get__() 方法。
所有描述器类都是基于混入类实现的。Unsigned 和 Maxsized 要和继承自 Typed 类混入。
类装饰器简化代码的方案,不依赖其它新的技术,并且装饰器容易添加或删除。装饰器还能作为混入类的替代方案,且比混入类快几乎 1 倍。
def Typed(expected_type, cls=None):
if cls is None:
return lambda cls: Typed(expected_type, cls)
super_set = cls.__set__
def __set__(self, instance, value):
if not isinstance(value, expected_type):
raise TypeError('expected' + str(expected_type))
super_set(self, instance, value)
cls.__set__ = __set__
return cls
def Unsigned(cls):
super_set = cls.__set__
def __set__(self, instance, value):
if value < 0:
raise ValueError('Excepted >= 0')
super_set(self, instance, value)
cls.__set__ = __set__
return cls
def MaxSized(cls):
super_init = cls.__init__
def __init__(self, name=None, **opts):
if 'size' not in opts:
raise TypeError('missing size option')
super_init(self, name, **opts)
cls.__init__ = __init__
super_set = cls.__set__
def __set__(self, instance, value):
if len(value) >= self.size:
raise ValueError('size must be < ' + str(self.size))
super_set(self, instance, value)
cls.__set__ = __set__
return cls
@Typed(int)
class Integer(Descriptor):
pass
@Unsigned
class UnsignedInteger(Integer):
pass
@Typed(float)
class Float(Descriptor):
pass
@Unsigned
class UnsignedFloat(Float):
pass
@Typed(str)
class String(Descriptor):
pass
@MaxSized
class SizedString(String):
pass
8.14 实现自定义容器
collections 定义了很多抽象基类,想自定义容器类的时候它们会很有用。
如果想让类支持迭代,就让这个类继承 collections.Iterable 即可。但是需要实现 collections.Iterable 的所有抽象方法。可以试着先实例化这个类,在错误提示中找到需要实现的方法。
import bisect
from collections import Sequence
class SortedItems(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._item)
def add(self, item):
bisect.insort(self._item, item)
bisect 可以保证元素插入后还保持顺序。
使用 collections 中的抽象基类能确保你自定义的容器实现了所有必要的方法,并且能简化类型检查。
collections 类会为常见容器操作提供默认的实现。因此只需要实现感兴趣的部分即可。
from collections import MutableSequence
class Items(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._item[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)
>>> a = Items([1, 2, 3])
>>> len(a)
Len
3
>>> a.append(4)
Len
Inserting: 3 4
>>> a.append(2)
Len
Inserting: 4 2
>>> a.count(2)
Getting: 0
Getting: 1
Getting: 2
Getting: 3
Getting: 4
Getting: 5
2
>>> a.remove(3)
Getting: 0
Getting: 1
Getting: 2
Deleting: 2
8.15 属性的代理访问
某个属性的实例访问代理到另一个实例中去。目的是作为继承的一个替代方法或实现代理模式。
class A:
def spam(self, x):
pass
def foo(self):
pass
class B1:
def __init__(self):
self._a = A()
def spam(self, x):
return self._a.spam(x)
def foo(self):
return self._a.foo()
def bar(self):
pass
如果需要代理的方法很多,使用 __getattr__() 方法会更好些:
class B2:
def __init__(self):
self._a = A()
def bar(self):
pass
def __getattr__(self, name):
"""__getattr__()方法在访问的属性不存在的时候才被调用"""
return getattr(self._a, name)
实现代理模式:
# 代理类,包装其它对象,但是显示自身的公共属性
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('_'):
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
def bar(self, y):
print('Spam.bar:', self.x, y)
>>> s = Spam(3)
>>> p = Proxy(s)
>>> p.x
getattr x
3
>>> p.bar(4)
getattr bar
Spam.bar: 3 4
>>> p.x = 6
setattr x 6
自定义属性访问方法,可以用不同方式自定义代理类的行为,如加入日志或只读访问等。
代理类有时候能作为继承的替代方法。
class A:
def spam(self, x):
pass
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')
使用代理:
class A:
def spam(self, x):
print('A.spam', x)
def foo(self):
print('A.foo')
class B:
def __init__(self):
self._a = A()
def spam(self, x):
print('B.spam')
self._a.spam(x)
def bar(self):
print('B.bar')
def __getattr__(self, name):
return getattr(self._a, name)
实现代理模式时,注意 __getattr__() 是一个后备方法,只有在属性不存在的时候才会调用,因此,如果代理类实例本身有这个属性的话,就不会触发这个方法。
__setattr__() 和 __delattr__() 需要额外的魔法来区分代理实例和被代理实例 _obj 的属性。通常约定只代理那些非下划线开头的属性。
__getattr__() 对某些双下划线开头和结尾的属性不适用。这些属性和方法需要一个个重定义。
8.16 在类中定义多个构造器
类除了使用 __init__() 之外,还有其它方式初始化它。
类方法的一个主要用途就是定义多个构造器,接收一个类作为第一个参数,用这个类创建并返回最终的实例。
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)
在继承时也能较好地工作。
8.17 创建不调用 init 方法的实例
__new__() 方法能创建未初始化的实例,这个实例不包含 __init__() 中创建的属性,需要手动初始化。
在反序列化对象或实现某个类方法构造函数时需要绕过 __init__() 方法来创建对象。
from time import localtime
class Date:
def __init__(self, year, month, day):
self.year = year
self.month = month
self.day = day
@classmethod
def today(cls):
d = cls.__new__(cls)
t = localtime()
d.year = t.tm_year
d.month = t.tm_month
d.day = t.tm_day
return d
反序列化时,出现的字典对象想转换成 Date 类型实例,能使用上面的方法。
这种创建实例的方法最好不要直接访问实例字典,因为这个类使用了 __slots__、properties、descriptors 或其它技术的时候代码就失效了。使用 setattr() 方法会让代码更加通用。
8.18 利用 Mixins 扩展类功能
有很多有用的方法,想使用它们来扩展其它类的功能,但是这些类并没有任何的继承关系。
class LoggedMappingMixin:
"""为调试时产生的操作添加日志功能"""
__slots__ = ()
def __getitem__(self, key):
print('Getting ' + str(key))
return super().__getitem__(key)
def __setitem__(self, key, value):
print('Setting {} = {!r}'.format(key, value))
return super().__setitem__(key, value)
def __delitem__(self, key):
print('Dleting ' + str(key))
return super().__delitem__(key)
class SetOnceMappingMixin:
"""一个键只能赋值一次"""
__slots__ = ()
def __setitem__(self, key, value):
if key in self:
raise KeyError(str(key), 'already set')
return super().__setitem__(key, value)
class StringKeysMappingMixin:
"""键强制使用字符串类型"""
__slots__ = ()
def __setitem__(self, key, value):
if not isinstance(key, str):
raise TypeError('keys must be strings')
return super().__setitem__(key, value)
这些类单独使用没意义,要通过继承和其它对象混入使用:
class LoggedDict(LoggedMappingMixin, dict):
pass
混入类不能直接被实例化,没有自己的状态信息和实例属性。
使用类装饰器是另一种实现混入类的方法,这时就不需要多继承了。
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
@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'
太多的条件判断代码复杂且低效,每次执行操作都要先检查条件。更好的方法是为每个状态定义一个对象
class Connection1:
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)
# 连接状态基类
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)
每个状态对象只有静态方法,没有存储任何实例属性。所有的状态信息都在 Connection 实例中。基类中定义 NotImplementedError 是为了确保子类实现了相应的方法。
8.20 通过字符串调用对象方法
使用 getattr():
import math
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(4,5)
>>> p.distance(0,0)
6.4031242374328485
>>> d = getattr(p, 'distance')(0,0)
>>> d
6.4031242374328485
另一种方法是使用 operator.methodcaller()。该方法创建一个可调用对象,已有所需参数。调用的时候把实例对象传给它即可。
import operator
operator.methodcaller('distance', 0, 0)(p)
需要使用相同的参数多次调用某个方法时,operator.methodcaller 很有用:
points = [
Point(1, 2),
Point(3, 0),
Point(10, -1),
Point(-4, -8),
]
# 按到原点的距离排序
points.sort(key=operator.methodcaller('distance', 0, 0))
通过字符串调用方法通常出现在需要模拟 case 语句或实现访问者模式的时候。
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))
t2 = Mul(Number(2), t1)
t3 = Div(t2, Number(5))
t4 = Add(Number(1), t3)
这样做的话,对于每个表达式,每次都要重新定义一遍。访问者模式可以支持所有的数字和操作符。
class NodeVisitor:
def visit(self, node):
methname = 'visit_' + type(node).__name__
meth = getattr(self, methname, Node)
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__))
class Evaluator(NodeVisitor):
def visit_Number(NodeVisitor):
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 -node.operand
e = Evaluator()
e.visit(t4)
定义一个类,在一个栈上面将一个表达式转换为多个操作;
class StackCode(NodeVisitor):
def generate_code(self, node):
self.instructions = []
self.visit(node)
return self.instructions
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.oprand)
self.instructions.append((instruction,))
def visit_Negate(self, node):
self.unaryop(node, 'NEG')
访问者模式的好处就是 getattr() 来获取相应的方法,并利用递归遍历所有节点。这种技术也是实现其他语言中的 switch 或 case 语句的方式。
用这种模式写一个请求分发的控制器:
class HTTPHandler:
def handle(self, request):
methname = 'do_' + request.request_method
getattr(self, methname)(request)
def do_GET(self, request):
pass
def do_POST(self, request):
pass
def do_HEAD(self, request):
pass
访问者模式的缺点是严重依赖递归,数据结构嵌套层次太深可能会超过 Python 的递归深度限制。
8.22 不用递归实现访问者模式
巧妙使用生成器能在消除递归的同时保持访问者模式。
import types
class NodeVisitor:
def visit(self, node):
stack = [node]
last_result = None
while stack:
try:
last = stack[-1]
if isinstance(last, types.GeneratorType):
stack.append(last.send(last_result))
elif isinstance(last, Node):
stack.append(self._visit(stack.pop()))
else:
last_result = stack.pop()
except 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__))
这个类能作为上一节中访问者模式的替代实现。
上一节中的 Evaluator 在处理过深的嵌套层次时会失效:
>>> a = Number(0)
>>> for i in range(1, 100000):
... a = Add(a, Number(i))
...
>>> 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.left)
>>> e = Evaluator()
>>> e.visit(a)
4999950000
碰到 yield 语句时,生成器会返回一个数据并暂时挂起,利用这点代替递归:
value = self.visit(node.left)
# 换成生成器
value = yield node.left
yield 语句会将 node.left 返回给 visit() 方法,然后 visit() 调用这个节点响应的 visit_Name()。
8.23 循环引用数据结构的内存管理
weakref 库中的弱引用。
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 None if self._parent is None else self._parent()
@parent.setter
def parent(self, node):
self._parent = weakref.ref(node)
def add_child(self, child):
self.children.append(child)
child.parent = self
正常的垃圾回收机制是基于应用计数,一个对象的引用数变成 0 的时候才能被删除掉。循环引用时,每个对象的引用计数不会变成 0。
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
>>> a = Data()
>>> del a
Data.__del__
>>> a = Node()
>>> del a
Data.__del__
>>> a = Node()
>>> a.add_child(Node())
>>> del a
>>>
弱引用笑出了这个问题。弱引用本质上是对象的指针,不会增加这个对象的引用计数。访问弱引用所引用的对象,需要调用它,这个对象存在就返回,不存在返回 None。
>>> a = Node()
>>> b = weakref.ref(a)
>>> b
<weakref at 0x0000007A34E4F3B8; to 'Node' at 0x0000007A34E5A0B8>
>>> b()
<__main__.Node object at 0x0000007A34E5A0B8>
>>> del a
>>> print(b())
None
8.24 让类支持比较操作
想让类支持比较运算,但是又不想实现一大堆特殊方法。
装饰器 functools.total_ordering 能简化这个处理。使用它装饰一个类,这个类仅需要定义一个 __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
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
8.25 创建缓存实例
同样的参数再次创建对象时,想返回它的缓存引用。希望相同参数创建的对象是单例的。
需要用一个和类分开的工厂函数:
class Spam:
def __init__(self, name):
self.name = name
import weakref
_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
重新定义类的 __new__() 方法也能达到预期,但是每次都会调用 __init__() 函数,无论该实例是否已缓存。
WeakValueDictionary 的实例只会保存在其他地方还被使用的实例。
把工厂函数跟类放在一块,单独设一个缓存管理器,能更灵活地实现所需的效果。
import weakref
class CachedSpamManager:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_spam(self, name):
if name not in self._cache:
s = Spam(name)
self._cache[name] = s
else:
s = self._cache[name]
return s
def clear(self):
self._cache.clear()
class Spam:
manager = CachedSpamManager()
def __init__(self, name):
self.name = name
def get_spam(name):
return Spam.manager.get_spam(name)
为了防止用户直接使用类实例化,而不是用工厂方法,可以在实例初始化时抛出异常。缓存管理器使用另外的方法来创建实例。
class CachedSpamManager:
def __init__(self):
self._cache = weakref.WeakValueDictionary()
def get_spam(self, name):
if name not in self._cache:
temp = Spam._new(name)
self._cache[name] = temp
else:
temp = self._cache[name]
return temp
def clear(self):
self._cache.clear()
class Spam:
manager = CachedSpamManager()
def __init__(self, *args, **kwargs):
raise RuntimeError("Can't instantiate directly")
@classmethod
def _new(cls, name):
self = cls.__new__(cls)
self.name =name
return self