第24條 以@classmethod形式的多態通用地構建對象

在Python中,不僅對象支持多態,類也支持多態。

多態,使得繼承體系中的多個類能夠以各自獨有的方式來實現某個方法。這些類,都滿足相同的接口或繼承自相同的抽象類,但卻有着各自不同的功能。

案例1:實現MapReduce流程,定義表示輸入數據的公共基類。

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

現在編寫InputData類的具體子類,實現磁盤文件中的數據讀取。

class PathInputData(InputData):
    def __init__(self,path) -> None:
        super().__init__()
        self.path = path
    
    def read(self):
        return open(self.path).read()

同時,我們可能需要多個像PathInputData這樣的類充當InputData的子類,以實現多個標準接口的read方法,比如可以實現網絡讀取並解壓數據。

此外,我們還需要爲MapReduce工作線程定義一套類似的抽象接口,以便於處理輸入的數據。

class Worker(object):
    def __init__(self,input_data) -> None:
        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的各個組件後,需要將各個組件串聯起來,以實現整個流程,通常的方法是編寫輔助函數將這些類對象聯繫起來。

##生成器函數,生成數據
def generate_inputs(data_dir):
    for name in os.listdir(data_dir):
        yield PathInputData(os.path.join(data_dir,name))

### 創建多個worker對象
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

### 執行mapreduce函數
def mapreduce(data_dir):
    inputs = generate_inputs(data_dir)
    workers = create_workers(inputs)
    return execute(workers)

整個調用的流程圖如下所示:
在這裏插入圖片描述
上述寫法存在的主要問題是MapReduce函數不夠通用。如果編寫其他的InputDataWorker子類,那就得重寫generate_inputscreate_workersmapreduce函數。

其實,在C++或者Java當中,可以通過構造函數的重載來解決,但是在Python中只允許名爲__init__的構造器方法,所以不能提供多個不同輸入參數的__init__方法。

@classmethod

解決這個問題最好的方法,是使用@classmethod形式的多態,即類方法的多態機制

首先修改InputData類,爲它添加通用的generate_inputs類方法,該方法會根據通用的接口來創建新的InputData實例

class GenericInputData(object):
    def read(self):
        raise NotImplementedError
    @classmethod
    def generate_inputs(cls,config):
        raise NotImplementerError
 
 ## 修改子類
 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))

按照同樣的方法修改Worker類,並添加create_workers方法。

class GenericWorker(object):
    #...
    def map(self):
        raise NotImplementedError
    def reduce(self,other):
        raise NotImplementedError
    @classmethod
    def create_inputs(cls,input_class,config):
        workers = []
        for input_data in input_class.generate_inputs(config):
            worker.append(cls(input_data))
        return workers

上述代碼的重點在於input_class.generate_inputs的調用,是類級別的多態調用,同時GenericWorker對象通過cls形式構造。

具體的GenericWorker子類,只需修改繼承的父類即可。

class LineCountWorker(GenericWorker):
    #...

最後,重寫mapreduce函數:

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

if __name__=='__main__':
    config = {'data_dir':'./data/'}
    result = mapreduce(LineCountWorker,PathInputData,config)

最後,我們可以編寫GenericInputDataGenericWorker的其他子類,而無需修改函數代碼,僅需要改動的是mapreduce的輸入參數即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章