다형성을 이용해 Generic한 클래스 구성하기

 
  • 클래스 다형성의 필요성을 알아볼 것임
    • 아래와 같이 추상 인터페이스와, 그를 활용해 구현한 PathInputData, LineConterWorker 두 클래스가 있음

이해를 위해 필요한 개념

  • 다형성(Polymophism)
    • 하나의 객체(Object)가 여러가지 타입을 가질 수 있는 것
    • 구현 방법
      1. 오버로딩
        1. 메소드 이름을 같게
        2. 매개변수의 개수 또는 타입을 다르게
      2. 오버라이딩 (상속이 선행되야함)
        1. 메소드 이름 같게
        2. 매개변수 완전 같게
        3. 처리를 다르게
      3. 함수형 인터페이스
    • 이해를 위한 전체 구조 그림
    • 지금부터 설명할 코드들은 상속 이후의 다형성을 설명하는 것이므로 오버라이딩임
import os
import random
import threading

class InputData:
    def read(self):
        raise NotImplementedError

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

    def read(self):
        with open(self.path) as f:
            return f.read()


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

class LineCountWorker(Worker):
    def map(self):
        data = self.input_data.read()
        self.result = data.count('\n')

    def reduce(self, other):
        self.result += other.result

def write_test_files(tmpdir):
    os.makedirs(tmpdir)
    for i in range(100):
        with open(os.path.join(tmpdir, str(i)), 'w') as f:
            f.write('\n' * random.randint(0, 100))

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

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
    for worker in rest:
        first.reduce(worker)
    return first.result

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


from threading import Thread
def mapreduce(data_dir):
    inputs = generate_inputs(data_dir)
    workers = create_workers(inputs)
    return execute(workers)

tmpdir = 'test_inputs'
# write_test_files(tmpdir)

result = mapreduce(tmpdir)
print(f'총 {result} 줄이 있습니다.')
총 4709 줄이 있습니다.
  • generate_inputs
    • 지정한 경로에 존재하는 모든 dir에 대해 PathInputData 이터레이터를 발생시키는 제너레이터 함수
  • create_workers
    • 전체 입력 경로 리스트에 대응되게끔 하나씩 LineCounterWorker 인스턴스를 생성

답안 코드

### GenericInputData와 PathInputData를 사용한 방법
class GenericInputData:
    def read(self):
        raise NotImplementedError

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

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

    def read(self):
        with open(self.path) as f:
            return f.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 GenericWorker:
    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


class LineCountWorker(GenericWorker):
    def map(self):
        data = self.input_data.read()
        self.result = data.count('\n')

    def reduce(self, other):
        self.result += other.result

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

config = {'data_dir': tmpdir}
result = mapreduce(LineCountWorker, PathInputData, config)
print(f'총 {result} 줄이 있습니다.')

내 코드

### GenericInputData와 PathInputData를 사용한 방법
class GenericInputData:
    def read(self):
        raise NotImplementedError

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

class PathInputData(GenericInputData):
    def __init__(self, path, config):
        super().__init__()
        self.path = path
        self.data_dir = config
        # self.data_dir = config['data_dir']

    def read(self):
        with open(self.path) as f:
            return f.read()

    # @classmethod
    # def generate_inputs(cls, config):
    def generate_inputs(self, config):
        # data_dir = config['data_dir']
        for name in os.listdir(self.data_dir['data_dir']):
            yield PathInputData(os.path.join(self.data_dir['data_dir'], name), config)


class GenericWorker:
    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


class LineCountWorker(GenericWorker):
    def map(self):
        data = self.input_data.read()
        self.result = data.count('\n')

    def reduce(self, other):
        self.result += other.result

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

config = {'data_dir': tmpdir}
result = mapreduce(LineCountWorker, PathInputData(tmpdir, config))
print(f'총 {result} 줄이 있습니다.')
총 4709 줄이 있습니다.

아직 해결 안된 의문

  • 내가 작성한 코드처럼 @classmethod가 아니라 __init__에서 config에 대한 데이터 처리를 진행하는식으로 해도 mapreduce 함수는 제너릭함.
    • 답안 코드와 내 코드의 차이로 인해 발생되는 안좋은 점이 있는가..? 아직은 내가 내공이 부족해서 그런지 보이지 않음