一. 类的多态
在 Python 中不仅对象支持多态,类也支持多态。类的多态,具体是指继承体系中的多个类能够以各自独有的方式来实现某个方法。这些类都满足相同的接口或继承自相同的抽象类,但却具有各自不同的功能。
下面我们定义 2 个基类,来定义一套 MapReduce
流程:
class InputData:
def read(self):
raise NotImplementedError
class Worker:
def __init__(self, input_data):
self.input_data = input_data
self.result = None
def map(self):
raise NotImplementedError
def reduce(self, other):
raise NotImplementedError
InputData
的子类必须实现 read
方法,用以提供输入数据;Worker
的子类必须实现 map
和 reduce
方法,来实现具体的 “映射-聚合” 操作。
下面我们实现两个具体的子类:PathInputData
用于从文件读取内容并返回,LineCountWorker
则统计每次输入数据中的换行符个数,并将统计到的个数进行累加:
class PathInputData(InputData):
def __init__(self, path):
super().__init__()
self.path = path
def read(self):
return open(self.path).read()
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
的流程。其中,最简单的方式就是手工构建对象,并通过辅助函数将这些对象联系起来:
import os
from threading import Thread
def generate_inputs(data_dir):
for name in os.listdir(data_dir):
yield PathInputData(os.path.join(data_dir, name))
def create_workers(input_list):
workers = []
for input_data in input_list:
workers.append(LineCountWorker(input_data))
return workers
def execute(workers):
threads = [Thread(target=w.map) for w in workers]
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
generate_inputs
列出某个目录的内容,并为该目录下的每个文件创建一个 PathInputData
实例;create_workers
使用上述 generate_inputs
方法返回的 PathInputData
实例来创建 LineCountWorker
实例;execute
函数,将 map
步骤派发到多个线程,最后调用 reduce
方法将所有线程的计算结果聚合成一个值。
最后,是上述辅助函数的有序调用过程:
def mapreduce(data_dir):
inputs = generate_inputs(data_dir)
workers = create_workers(inputs)
return execute(workers)
最后是一个简单的测试过程:
from tempfile import TemporaryDirectory
def write_test_files(tmpdir):
contents = [letter+'\n' for letter in 'PYTHON']
files = list('abc')
for file_name in files:
with open(os.path.join(tmpdir, file_name), 'wt') as f:
f.writelines(contents)
with TemporaryDirectory() as tmpdir:
write_test_files(tmpdir)
result = mapreduce(tmpdir)
运行结果:
>> result
18
我们在 PYTHON
每个字符的后面都插入了一个换行符,一个有 a
b
c
三个文件,因此聚合之后共计 6*3 = 18
个换行符,运行结果完全符合预期。
但是,这种写法有个大问题:如果要编写其它的 InputData
和 Worker
子类,必须得重写 generate_inputs
、create_workers
和 mapreduce
方法,以便与之匹配。
在 Java
语言中,解决这个问题可以通过构造器多态,每个 InputData
子类都提供特殊的构造器,协调 MapReduce
流程的那个辅助方法就可以用它来通用地构造 InputData
对象。Python 不支持构造器多态,但是可以使用 @classmethod
形式的多态。下面,我们就以 @classmethod
形式的多态去通用地构建对象。
二. @classmethod
其实,@classmethod
形式的多态与 InputData.read
那样的实例方法多态非常相似,只不过它针对的是整个类,而不是从该类构建出来的对象。
现在我们重构上面的 2 个基类:
class InputData:
def read(self):
raise NotImplementedError
@classmethod
def generate_inputs(cls, config):
raise NotImplementedError
class Worker:
def __init__(self, input_data):
self.input_data = input_data
self.result = None
def map(self):
raise NotImplementedError
def reduce(self, other):
raise NotImplementedError
@classmethod
def create_workers(cls, input_class, config):
workers = []
for input_data in input_class.generate_inputs(config):
workers.append(cls(input_data))
return workers
InputData
添加了类方法 generate_inputs
:接受一份含有配置参数的字典,具体的子类就可以读取该这些配置参数,来创建独特的 InputData
实例。Worker
基类则添加了类方法 create_workers
,其中 input_class.generate_inputs
是个类级别的多态方法;另外 cls(input_data)
形式来构造了 Worker
实例,而不是向以前那样,使用 __init__
方法。
重构子类:
class PathInputData(InputData):
def __init__(self, path):
super().__init__()
self.path = path
def read(self):
return open(self.path).read()
@classmethod
def generate_inputs(cls, config):
data_dir = config['data_dir']
for name in os.listdir(data_dir):
yield cls(os.path.join(data_dir, name))
class LineCountWorker(Worker):
def map(self):
data = self.input_data.read()
self.result = data.count('\n')
def reduce(self, other):
self.result += other.result
其中,LineCountWorker
没有做任何改动,只需要集成父类的方法即可。PathInputData
需要自行实现类方法 generate_inputs
:通过 config
字典来查询输入文件所在的目录,使用 cls()
的形式返回 PathInputData
实例。
最后,重构 mapreduce
方法,使其更通用:
def mapreduce(worker_class, input_class, config):
workers = worker_class.create_workers(input_class, config)
return execute(workers)
测试结果:
重构之后,我们可以随时添加新的 InputData
和 Worker
子类,而不需要再去修改或定义新的拼接代码,整个代码非常的清晰流畅。
@classmethod
可以用一种与构造器相仿的方式来构造类的对象。通过类方法多态机制,我们能够以更加通用的方式来构建并拼接具体的子类。