欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

第三章:类与继承(Ⅱ)

程序员文章站 2022-07-16 09:24:37
...

这篇文章是基于 《Effective Python——编写高质量Python代码的59个有效方法》[美] 布雷特·斯拉特金 著 爱飞翔 译 这本书中的内容,写写自己在某方面的感悟,并摘录一些作为读书笔记供今后鞭策。侵删。

第 24 条:以 @classmethod 形式的多态取通用地构建对象

在 python 中,不仅对象支持多态,类也支持多态,那么类的多态是什么意思呢?
多态,使得继承体系中多个类都能以各自所独有的方式来实现某个方法。这些类,都满足相同的接口或继承自相同的抽象类,但却有着不同的功能。
例如,为了实现一套 MapReduce 流程,我们需要定义公共基类来表示输入的数据。下面这段代码就定义了这样的基类,它的 read 方法必须由子类来实现。

In [1]: class InputData(object):
   ...:     def read(self):
   ...:         raise NotImplementedError
   ...:

In [2]: class PathInputData(InputData):
   ...:     def __init__(self, path):
   ...:         super().__init__()
   ...:         self.path = path
   ...:     def read(self):
   ...:         return open(self.path).read()
   ...:

我们可能需要很多像 PathInputData 这样的类来充当 InputData 的子类,每个子类都需要实现标准接口中的 read 方法,并以字节的形式返回待处理的数据。其他的 InputData 子类可能会通过网络读取并解压缩数据。
此外,我们还需要为 MapReduce 工作线程定义一套类似的抽象接口,以便用标准的方式来处理输入的数据。

In [3]: class Worker(object):
   ...:     def __init__(self, input_data):
   ...:         self.input_data = input_data
   ...:         self.result = None
   ...:     def map(self):
   ...:         raise NotImplementedError
   ...:     def reduce(self, other):
   ...:         raise NotImplementedError
   ...:

下面定义具体的 Worker 子类,以实现我们想要的 MapReduce 功能。本例所实现的功能,是一个简单的换行符计数器。

In [5]: class LineCountWorker(Worker):
   ...:     def map(self):
   ...:         data = self.input_data.read()
   ...:         self.result = data.count("\n")
   ...:     def reduce(self, other):
   ...:         self.result += other.result
   ...:

刚才这套 MapReduce 实现方式,看上去很好,但接下来却会遇到一个大问题,那就是如何把这些组件拼接起来。上面写的那些类,都具备合理的接口与适当的抽象,但我们必须把对象构建出来才能体现出那些类的意义,那么,有谁来负责构建对象并协调 MapReduce 流程呢?
下面这段代码可以列出某个目录的内容,并为该目录下的每个文件创建一个 PathInputData 实例:

In [6]: def generate_input(data_dir):
   ...:     for name in os.listdir(data_dir):
   ...:         yield PathInputData(os.path.join(data_dir,
   ...: name))
   ...:

然后,用 generate_inputs 方法所返回的 InputData 实例来创建 LineCountWorker 实例。

In [7]: def create_workers(input_list):
   ...:     workers = []
   ...:     for input_data in input_list:
   ...:         workers.append(LineCountWorker(input_data))
   ...:
   ...:     return workers
   ...:

现在执行这些 Worker 实例,以便将 MapReduce 流程中的 map 步骤派发到多个线程之中。接下来,反复调用 reduce 方法,将 mao 步骤的结果合并成一个最终值。

In [8]: def execute(workers):
   ...:     threads = [Thread(target=w.map) for w in worker
   ...: s]
   ...:     for thread in threads: thread.start()
   ...:     for thread in threads: thread.join()
   ...:     first, rest = workers[0], workers[1]
   ...:     for worker in rest:
   ...:         first.reduce(worker)
   ...:     return first.result

最后,把上面这些代码片段都拼装到函数里面,以便执行 MapReduce 流程的每个步骤。

In [9]: def mapreduce(data_dir):
   ...:     inputs = generate_inputs(data_dir)
   ...:     workers = create_workers(inputs)
   ...:     return execute(workers)

但是,这种写法有个大问题,那就是 MapReduce 函数不够通用。如果要编写其他的 InputData 或 Worker 子类,那就得重写 generate_inputs、create_workers 和 mapreduce 函数,以便与之匹配。

memo

  • 在 python 程序中,每个类都只能有一个构造器,也就是 __init __ 方法。
  • 通过 @classmethod 机制,可以用一种与构造器相仿的方式来构造类的对象。
  • 通过类方法多态机制,我们能够更加通用的方式来构建并拼接具体的子类。

第 25 条:用 super 初始化父类。

初始化父类的传统方法,就是在子类里用子类实例直接调用父类的 __init __ 方法。

In [10]: class MyBaseClass(object):
    ...:     def __init__(self, value):
    ...:         self.value = value
    ...:

In [11]: class MyChildClass(MyBaseClass):
    ...:     def __init__(self):
    ...:         MyChildClass.__init__(self, s)
    ...:

这种办法对于简单的继承体系是可行的,但是在许多情况下会出现问题。
如果子类受到了多重继承的影响,那么直接调用超类的 __init __ 方法,可能会产生无法预知的行为。

问题一

In [12]: class TimesTwo(object):
    ...:     def __init__(self):
    ...:         self.value*= 2
    ...:

In [13]: class PlusFive(object):
    ...:     def __init__(self):
    ...:         self.value += 5
    ...:

我们使用其中一种顺序来定义它所继承的各个超类。

In [12]: class TimesTwo(object):
    ...:     def __init__(self):
    ...:         self.value*= 2
    ...:

In [13]: class PlusFive(object):
    ...:     def __init__(self):
    ...:         self.value += 5
    ...:

In [14]: class OneWay(MyBaseClass, TimesTwo, PlusFive):
    ...:     def __init__(self, value):
    ...:         MyBaseClass.__init__(self, value)
    ...:         TimesTwo.__init__(self)
    ...:         PlusFive.__init__(self)
    ...:

In [15]: foo = OneWay(5)

In [16]: foo.value
Out[16]: 15

构建该类实例之后,我们发现,它所产生的结果与继承时的超类顺序相符。
下面,我们使用另外一种顺序来定义它所继承的各个超类。

In [17]: class AnotherWay(MyBaseClass, PlusFive, TimesTwo):
    ...:
    ...:     def __init__(self, value):
    ...:         MyBaseClass.__init__(self, value)
    ...:         TimesTwo.__init__(self)
    ...:         PlusFive.__init__(self)
    ...:

但是,上面这段代码并没有修改超类构造器的调用顺序,它还是和以前一样,先调用TimesTwo.__init __,然后才调用 PlusFive.__init __,这就导致该类所产生的结果与其超类定义的顺序不相符。

In [18]: bar = AnotherWay(5)

In [19]: bar.value
Out[19]: 15

问题二

如果子类继承自两个单独的超类,而那两个超类又继承自同一个公共基类,那么就构成了钻石型继承体系。这种继承会使钻石顶部那个公共基类多次执行其__init __方法,从而产生意想不到的行为。

In [20]: class TimeFive(MyBaseClass):
    ...:     def __init__(self, value):
    ...:         MyBaseClass.__init__(self, value)
    ...:         self.value *= 5
    ...:

In [21]: class PlusTwo(MyBaseClass):
    ...:     def __init__(self, value):
    ...:         MyBaseClass.__init__(self, value)
    ...:         self.value += 2
    ...:

然后再定义一个子类,同时继承上面这两个类,这样 MyBaseClass 就成了钻石顶部的那个公共基类。

In [23]: class ThisWay(TimeFive, PlusTwo):
    ...:     def __init__(self, value):
    ...:         TimeFive.__init__(self, value)
    ...:         PlusTwo.__init__(self, value)
    ...:

In [24]: foo = ThisWay(5)

In [25]: foo.value
Out[25]: 7

我们可能认为输出的结果会是27,因为(5*5)+2=27,但实际上却是7,因为在调用第二个超类的构造器,也就是 PlusTwo.__init __时,它会再度调用MyBaseClass.__init __,从而导致self.value 重新变成5。
内置的 super 函数确实可以正常运作,但在 Python2 中有两个问题值得注意:

  • super 语句写起来有点麻烦。我们必须指定当前所在的类和 self 对象,而且还要指定相关的方法的名称,以及那个方法的参数。对于 Python 变成新手来说,这种构造方式有些费解。
  • 调用 super 时,必须写出当前类的名称。由于我们以后很可能会修改类体系,所以类的名称也可能会变换,那时,必须修改每一条super调用语句才行。

Python 3 则没有这些问题,因为它提供了一种不带参数的 super 调用方式,该方式的效果与用 __class __ 和 self 来调用 super 相同。 Python 3 总是可以通过 super 写出清晰、精练而又准确的代码。

In [26]: class Explicit(MyBaseClass):
    ...:     def __init__(self, value):
    ...:         super(__class__, self).__init__(value * 2)
    ...:
    ...:

In [27]: class Implicit(MyBaseClass):
    ...:     def __init__(self, value):
    ...:         super().__init__(value * 2)
    ...:

由于 Python 3 程序可以在方法中通过 __class __ 变量准确地引用当前类,所以上面这种写法能够正常运作,而 Python 2 则没有定义__class __,故不能采用这种写法。

memo

  • Python 采用标准的方法解析顺序来解决超类初始化次序及钻石继承问题。
  • 总是应该使用内置的 super 函数来初始化父类。

第三章:类与继承(Ⅱ)