第三章:类与继承(Ⅱ)
这篇文章是基于 《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 函数来初始化父类。
上一篇: 第三章继承与多态3.2.4
下一篇: 这就是我的SSM