Python 类 - 类的多态与 @classmethod

一. 类的多态

在 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 的子类必须实现 mapreduce 方法,来实现具体的 “映射-聚合” 操作。

下面我们实现两个具体的子类: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 个换行符,运行结果完全符合预期。

但是,这种写法有个大问题:如果要编写其它的 InputDataWorker 子类,必须得重写 generate_inputscreate_workersmapreduce 方法,以便与之匹配。

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)

测试结果:

重构之后,我们可以随时添加新的 InputDataWorker 子类,而不需要再去修改或定义新的拼接代码,整个代码非常的清晰流畅。

@classmethod 可以用一种与构造器相仿的方式来构造类的对象。通过类方法多态机制,我们能够以更加通用的方式来构建并拼接具体的子类。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容