8.3 使用标准库中的mixin类

    标准库使用了mixin类定义。有许多模块中都有这种例子,包括iosocketserverurllib.requestcontextlibcollections.abc

    当我们基于collections.abc抽象基类自定义集合时,我们会使用mixin类确保容器的横切方面都以一致的方式定义。最上层的集合(SetSequenceMapping)都是基于多个mixin类创建的。仔细读一读Python标准库的8.4节是非常重要的,它介绍了mixin是如何为类提供功能的,因为总体的类结构是由许多不同部分组成的。

    只看其中一行,Sequence的总结,我们可以看到它继承自SizedIterableContainer。这些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()方法中。这种方法需要使用上下文管理器,它会让对象按我们预期的行为工作。如果在文档中有清楚地记录,这种方法也不会带来困惑。