使用@classmethod多态性构造对象
多态
在Python
中,不仅对象支持多态,类也是支持多态的。这意味着什么呢?又有什么好处咧?
多态是一个对于分层良好的类树中,不同类之间相同名称的方法却实现了不同的功能的体现。这也使得许多提供了不同功能的类来来实现相同的接口或者抽象的基类(详见第28
项:从ollections.abc
继承来实现自定义的容器类型)。
例如:你想写一个MapReduce
的实现类,并且使用一个普通类作为输入数据。这里我定义了一个这样一个带有read
方法的类,但是为了下面的继承,它必须是一个超类。
class InputData(object):
def read(self):
raise NotImplementedError
# 现在实现一个继承了InputData的子类,并使用read方法来读取电脑上硬盘中的数据。
class PathInputData(InputData):
def ____init__(self, path):
super().__init__()
self.path = path
def read(self):
return open(self.path).read()
你可以实现很多的像PathInputData
这样的子类,每一个都可是实现标准接口中的read
方法来返回他们各自处理过的数据。相比于从磁盘上read
数据,其他的InputData
子类可以从网络,解压缩的透明数据等等。你还可能使用一个相似的MapREduce
的抽象的接口,来以一个标准的方式来处理输入数据。
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
# 现在可以定义一个子类来实现一个特殊的我想实现的MapReduce函数:一个简单的行数的计数器。
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
的构建和策划负责呢?怎样才能更简洁,更有效率,这值得我们思考一番。
人工构建和连接这些对象的最简单的方式就是使用一些工具函数。这里,我列出了一个目录下的内容,并且为每一个它包含的文件构造了一个PathInputData
实例。
def generate_inputs(data_dir):
for name in os.listdir(data_dir):
yield PathInputData(os.path.join(data_dir, name))
# 下一步就是创建LineCountWorker实例来使用generate_inputs返回的InputData实例。
def create_workers(input_list):
workers = []
for input_data in input_list:
workers.append(LineCountWork(input_data))
return workers
# 我通过多线程的方式取出map中的元素之星这些Worker的实例(详见第37项:阻塞IO的情况下使用线程,避免并行计算)。然后,我调用了reduce方法来重复的把临时计算结果累加到最终结果上。
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, reset in rest:
first.reduce(worker)
return first.result
# 每一步的调用看起来很繁琐,于是我封装了一个函数来交替的执行全部任务。
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):
# ···
with TemporaryDirectory() as tmpdir:
write_test_files(tmpdir)
result = mapreduce(tmpdir)
print("There are :", result, "lines")
>>>
There are : 4360 lines
虽然我们可以通过这段代码获取我们想要的结果,但是却存在一个巨大的问题,那就是mapreduce
函数不够通用。如果你想编写另一个InputData
或者Worker
子类,你就不得不要重写generate_inputs
, create_workers
以及mapreduce
函数了。
解决这个问题我们需要一个通用的构造对象的方式。在别的编程语言中,你需要重载构造函数来解决这个问题,需要每一个InputData
的子类提供一个特殊的构造方法,这样才能借助工具方法来更加通用的编排MapReduce
类。然而问题就是Python
中只允许使用__init__
方法来构造,所以要兼容每一个nputData
的子类是不可能的了。
@classmethod
下面终于到正题了,一个比较好的解决办法就是使用@classmethod
多态性。和我在多态性的那个例子中对Input.read
解释类似,除了这个是被附加到整个类上而不是它们的构造器对象上。这样说起来有点让人摸不着头脑,下面我就把这个想法附加到MapReduce
类上吧。这里,我用一个通用的类方法拓展了InputData
类,那就是借助于普通接口来创建InputData
的实例。
class GenericInputData(object):
def read(self):
raise NotImplementedError
@classmethod
def generate_inputs(cls, config):
raise NotImplementedError
我通过接受一个配置参数信息的集合的函数generate_inputs
来创建InputData
子类来实现,这里我使用config
把为输入的文件转换成目录路径的集合。
class PathInputData(GenericInputData):
# ···
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))
相似的,我可以使用create_workers
改善GenericWorker
类。这里我使用input_class
参数(必须为GenericInputData
类的子类)来生成必须的输入。我使用cls
方法来作为一个通用的构造器来构造了GenericWorker
的实例。
class GenericWorker(object):
# ···
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
上面多态类中的input_class.generate_inputs
方法的调用就是我尝试着讲解的了。其实也就是怎么可以不通过__init__
方法来实现类似于重载的构造方法的体现。没有什么高深的地方,用得多了自然也就熟悉了。
在我的GenericWorker
子类中,除了改变其父类的行为之外,也没什么了。
class LineCountWorker(GenericWorker):
# ···
# 最终,我只需要重写一下mapreduce方法就可以完成这个通用类的编写了
def mapreduce(worker_class, input_class, config):
workers = worker_class.create_workers(input_class, config)
return execute(workers)
# 测试部分的代码与之前的相比,除了参数树木上的不同,也没什么区别了。
with TemporaryDirectory() as tmpdir:
write_test_files(tmpdir)
config = {'data_dir': tmpdir}
result = mapreduce(LineCountWorker, PathInputData, config)
现在,你可以如你所愿地编写其他的GenericInputData
和GenericWorker
类,而不需要修改代码了。
备忘录
Python
的每个类只支持单个的构造方法,__init__
。- 使用
@classmethod
可以为你的类定义可替代构造方法的方法。 - 类的多态为具体子类的组合提供了一种更加通用的方式。