每日10行代码121: 编写高质量python代码的方法24:以@classmethod 形式的多态去通用地构建对象

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

class InputData(object):
    def read(self):
        raise NotImplementedError

现在编写 InputData 类的具体子类,以便从磁盘文件里读取数据:

class PathInputData(InputData):
    def __init__(self, path):
        super().__init__()
        self.path = PathInputData

    def read(self):
        return open(self.path).read()

我们可能需要很多像PathInputData 这样的类来充当 InputData 的子类,每个子类都需要实现标准接口中的 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

下面定义具体的 Worker 子类,以实现我们想要的 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 实现方式,看上去很好,但接下来却会遇到一个大问题,那就是如何把这些组件拼接起来。 上面写的那些类,都具备合理的接口与适当的抽象,但我们必须把对象构建出来才能体现出那些类的意义。现在,由谁来负责构建对象并协调 MapReduce 流程呢?
最简单的办法是手工构建相关对象,并通过某些辅助函数将这些对象联系起来。下面这段代码可以列出某个目录的内容,并为该目录下的每个文件创建一个 PathInputData 实例:

def generate_inputs(data_dir):
    for name in os.listdir(data_dir):
        yield PathInputData(os.path.join(data_dir, name))

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

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

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

def execute(worders):
    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

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

def mapreduce(data_dir):
    inputs = generate_inputs(data_dir)
    workers = create_workers(inputs)
    return execute(workers)

用一系列输入文件来测试 mapreduce 函数,可以得到正常的结果。

for tempfile import TemporaryDirectory

def write_test_files(tmpdir):
    # ...

with TmporaryDirectory() as tmpdir:
    wirte_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 流程的那个辅助方法可以用它来通用地构造 InputData 对象。但是, Python 只允许名为 __init__ 的构造器方法,所以我们不能要求每个 InputData 子类都提供兼容的构造器。
解决这个问题的最佳方案,是使用 @classmethod 形式的多态。这种多态形式,其实与 InputData.read 那样的实例方法多态非常相似,只不过它针对的是整个类,而不是从该类构建出来的对象。
现在我们用这套累路来实现与 MapReduce 流程有关的类。首先,修改 InputData 类,为它添加通用的 generate_inputs 类方法,该方法会根据通用的接口来创建新的 InputData 实例。


class GenericInputData(object):
    def read(self):
        raise NotImplementedError

    @classmethod
    def generate_inputs(cls,config):
        raise NotImplementedError

新添加的 generate_inputs 方法,接受一份含有配置参数的字典 ,而具体的 Generic-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))

接下来,按照类似方式实现 GenericWorker 类的 create_workers 辅助方法。为了生成必要的输入数据,调用者必须把 GenericInputData 的子类传给该方法的 input_class 参数。该方法用 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,它是个类级别的多态方法。此外,我们还看到: create_workers 方法用另外一种方式构造 了 GenericWorker 对象,它是通过cls形式来构造 的,而不是像以前那样,直接用 __init__ 方法。
至于具体的 GenericWorker 子类,则只需修改它所继承的父类即可:

class LineCountWorker(GenericWorker):
    # ...  

最后,重写mapreduce 函数,令其变得完全通用。

def mapreduce(worker_class, input_class, config):
    workers = worker_class.create_workers(input_class, config)
    return execute(workers)

在测试文件上面执行修改后的 MapReduce 程序,所得结果与原来那套实现方式 是相同的。 区别在于,现在的 mapreduce 函数需要更多的参数,以便用更加通用的方式来操作相关对象。

with TemporaryDirectory() as tmpdir:
    write_test_files(tmpdir)
    config = {'data_dir':tmpdir}
    result = mapreduce(LineCountWorker, PathInputData, config)

我们可以继续编写其他的 GenericInputData 和 GenericWorker 子类,而且不用再修改刚才写好的那些拼接代码了。

要点:

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

后记:这篇文章没看懂,主要是我还缺一些基础,一是不太熟悉python里的类继承,二是不太熟悉mapreduce的概念。《effectivepython》这本书中间有好大一部分都是关于类的,而Python中的面象对象我用的比较少,理解不够透彻,所以我决定把这部分内容先放一放,后面主要学一些自己能看懂的内容,类的部分留待以后使用的多了再来学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值