8.3 使用标准库中的mixin类
标准库使用了mixin类定义。有许多模块中都有这种例子,包括io、socketserver、urllib.request、contextlib和collections.abc。
当我们基于collections.abc抽象基类自定义集合时,我们会使用mixin类确保容器的横切方面都以一致的方式定义。最上层的集合(Set、Sequence和Mapping)都是基于多个mixin类创建的。仔细读一读Python标准库的8.4节是非常重要的,它介绍了mixin是如何为类提供功能的,因为总体的类结构是由许多不同部分组成的。
只看其中一行,Sequence的总结,我们可以看到它继承自Sized、Iterable和Container。这些mixin类提供了contains()、iter()、reversed()、index()和count()方法。
8.3.1 使用上下文管理器的mixin类
在第 5 章“可调用对象和上下文的使用”中讲解上下文管理器时,我们忽略了ContextDecorator这个mixin类,而是主要关注上下文管理器中的特殊方法。使用这个mixin可以让定义更加清晰。
| 在前面的例子中,我们创建了一个修改全局状态的上下文管理器,它会重置随机数种子。我们会修改这个设计,让Deck类可以作为自己的上下文管理器。当作为上下文管理器使用时,它可以生成一定数量的Card。这并不是为一副牌做单元测试的最好方法。但是,这是一种使用上下文管理器的简单方式。 |
将上下文管理定义为程序中的mixin类时需要注意,我们可能必须重新设计初始化方法,需要抛开一些前提。可能会以下面两种不同的方式来使用程序中的类。
- 当用在with语句之外时,enter()和exit()方法不会被执行。
- 当用在with语句中时,enter()和exit()方法会被执行。
在我们的例子中,我们不能假设在init()中执行shuffle()方法是正确的,因为不知道是否会使用上下文管理器方法。我们也不能将shuffle()延迟到enter()方法中执行,因为这个方法有可能不会被调用。这种复杂性或许在暗示我们提供了过多的灵活性。我们可以延迟执行shuffle()直到第1次调用pop()之前,或者提供一个子类可以禁用的方法函数。下面是一个扩展了list的简单Deck定义。
class Deck( list ):
def init( self, size=1 ):
super().init()
self.rng= random.Random()
for d in range(size):
cards = [ card(r,s) for r in range(13) for s in Suits ]
super().extend( cards )
self._init_shuffle()
def _init_shuffle( self ):
self.rng.shuffle( self )
我们在Deck中定义了一个可删除的_``init_shuffle()方法。当洗牌完成后,子类可以重载这个方法,修改它的逻辑。Deck的子类可以在开始洗牌之前决定随机数生成器的种子,当前版本的类可以禁止在创建过程中洗牌。下面是一个Deck的子类,它包含了mixin——contextlib.ContextDecorator。
class TestDeck( ContextDecorator, Deck ):
def init( self, size= 1, seed= 0 ):
super().init( size=size )
self.seed= seed
def initshuffle( self ):
"""Don't shuffle during init."""
pass
def enter( self ):
self.rng.seed( self.seed, version=1 )
self.rng.shuffle( self )
return self
def __exit( self, exc_type, exc_value, traceback ):
pass
子类通过重载initshuffle()方法,禁止在初始化过程中洗牌。因为这个类混入了ContextDecorator,它也必须定义enter()和exit()。这个Deck的子类可以在with上下文中使用。当使用with语句时,会设定随机数种子,并且会按一个已知的顺序洗牌。如果在with之外使用这个类,就会使用当前的随机数设置洗牌,并且不会执行__enter()。
这种编程风格的目的是将一个类所具有的基本功能与Deck实现的其他方面分离。我们已经将一些随机种子的处理从Deck的其他方面中分离出来了。很明显,如果我们坚持必须使用上下文管理器,就可以大幅度简化设计。这不是 open()函数传统的使用方式。但是,这种简化是非常有益的。我们可以用下面的例子看看会带来哪些不同的行为。
for i in range(3):
d1= Deck(5)
print( d1.pop(), d1.pop(), d1.pop() )
这个例子展示了Deck如何自己生成随机的洗牌顺序。这是用Deck洗牌的简单方法。下一个例子展示了使用一个给定随机数种子的TestDeck。
for i in range(3):
with TestDeck(5, seed=0) as d2:
print( d2.pop(), d2.pop(), d2.pop() )
这段代码展示了如何将Deck的一个子类——TestDeck用作上下文管理器,并生成一系列已知顺序的牌。每次我们调用它,都会得到相同顺序的牌。
8.3.2 禁用类的一个功能
通过重新定义一个方法为只包含pass的方法,我们关闭了初始化时的洗牌功能。对于从一个子类中删除一个功能来说,这个过程显得有些冗长。还有一个方法可以从子类中删除功能:将方法名设置为None。我们可以在TestDeck用这种方式删除初始化时的洗牌操作。
_init_shuffle= None
上面的代码需要在基类中增加一些代码用于兼容方法缺失的情形,如下所示。
try:
self._init_shuffle()
except AttributeError, TypeError:
pass
这是从子类中删除一个功能的方法中比较显式的方式。这段代码说明了方法可能不存在或者是被有意地设为了None。然而另一种设计是将对``initshuffle()的调用从init()中移动到__enter()方法中。这种方法需要使用上下文管理器,它会让对象按我们预期的行为工作。如果在文档中有清楚地记录,这种方法也不会带来困惑。
