Home [Better Way #39] 객체를 제너릭하게 구성하려면 @classmethod를 통한 다형성을 활용하라
Post
Cancel

[Better Way #39] 객체를 제너릭하게 구성하려면 @classmethod를 통한 다형성을 활용하라

본문은 “파이썬 코딩의 기술 (Effective Python, 2판)”“Chapter 05. Classes and Interfaces”을 읽고 정리한 내용입니다.


파이썬에서는 객체뿐 아니라 클래스다형성을 지원한다.

이처럼 클래스의 다형성을 이용하면 같은 인터페이스를 만족하거나 같은 추상 기반 클래스를 공유하는 많은 클래스가 서로 다른 기능을 제공할 수 있게 된다.


클래스의 다형성을 이용하여 MapReduce를 구현해보자!


[제너릭하지 않은 방법] 인스턴스 메서드 다형성 활용

(1) 인스턴스 메서드 다형성을 활용하여 MapReduce의 각 요소를 구현한다.

  1. 입력 데이터이 입력 데이터를 소비하는 워커에 대한 추상 인터페이스

    하위 클래스에서 다시 정의해야 하는 메서드, 즉 공통 인터페이스에 대해서는 NotImplementedError를 발생시키도록 한다.

    • 입력 데이터에 대한 추상 인터페이스

      1
      2
      3
      
      class InputData:
          def read(self):
              raise NotImplementedError
      
    • 워커에 대한 추상 인터페이스

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      
      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
      
  2. 각 추상 인터페이스에 대한 구체적인 하위 클래스

    공통 인터페이스를 상속 받아 하위 클래스를 작성한다. 이때, 공통 인터페이스를 구현해야 한다.

    • 입력 데이터에 대한 구체적 하위 클래스 (디스크에서 파일을 읽는 동작)

      1
      2
      3
      4
      5
      6
      7
      8
      
      class PathInputData(InputData):
          def __init__(self, path):
              super().__init__()
              self.path = path
                
          def read(self):
              with open(self.path) as f:
                  return f.read()
      
    • 워커에 대한 구체적 하위 클래스 (\n 문자의 개수를 세는 동작)

      1
      2
      3
      4
      5
      6
      7
      
      class LineCountWorker(Worker):
          def map(self):
              data = self.input_data.read()
              self.result = data.count("\n")
                
          def reduce(self, other):
              self.result += other.result
      
  3. 동작 테스트를 위한 dummy file

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     import os
     import random
        
     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))
        
     tmpdir = "dummy_file"
     write_test_files(tmpdir)
    


(2) 도우미 함수를 활용하여 각 부분에 해당하는 객체를 직접 만들고 연결한다.

  • 🧑🏻‍💻 전체 코드 🧑🏻‍💻
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    import os
    
    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
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    from threading import Thread
    
    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
    
    1
    2
    3
    4
    
    def mapreduce(data_dir):
        inputs = generate_inputs(data_dir)
        workers = create_workers(inputs)
        return execute(workers)
    
    1
    2
    3
    
    tmpdir = "dummy_file"
    result = mapreduce(tmpdir)
    print(f"{result} 줄이 있습니다.") # 총 5008 줄이 있습니다.
    
  • Worker 인스턴스의 map을 여러 스레드에 공급하여 실행할 수 있고, 그 후 reduce를 반복적으로 호출하여 결과를 최종 값으로 합친다.
  • 잘 작동하지만, 함수가 전혀 제너릭(generic)하지 않다!

    즉, 다른 InputDataWorker의 하위 클래스를 사용하고 싶다면, 각각에 맞게 generate_inputs(), create_workers(), mapreduce() 함수를 재작성 해야 한다.


객체를 구성할 수 있는 제네릭한 방법이 필요하다. 다른 언어에서는 다형성을 활용하여 이 문제를 해결할 수 있지만, 파이썬에서는 생성자 메서드가 __init__ 밖에 없다. 하지만 하위 클래스가 똑같은 생성자만 제공해야 하는 것은 불합리하다!

클래스 메서드 다형성을 사용하자!!


[제너릭한 방법] 클래스 메서드 다형성 활용

클래스 메서드(class method) 다형성을 사용한다면, 다형성이 클래스로 만들어낸 개별 객체에 적용되는 것이 아니라 클래스 전체에 적용된다.

(1) 클래스 메서드 다형성을 활용하여 MapReduce의 각 요소를 구현한다.

  1. 입력 데이터이 입력 데이터를 소비하는 워커에 대한 추상 인터페이스

    클래스 메서드를 사용하려면 제너릭 @classmethod를 적용한다.

    • 입력 데이터에 대한 추상 인터페이스

      1
      2
      3
      4
      5
      6
      7
      
      class GenericInputData:
          def read(self):
              raise NotImplementedError
                
          @classmethod
          def generate_inputs(cls, config):
              raise NotImplementedError
      
    • 워커에 대한 추상 인터페이스

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      
      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):   # input_class: GenericInputData의 하위 타입
              workers = []
              # 클래스 다형성: input_class.generate_inputs
              for input_data in input_class.generate_inputs(config):
                  workers.append(cls(input_data)) # __init__ 메서드가 아닌, 제너릭 생성자 cls()를 호출함으로써 GenericWorker 객체를 만들 수 있다!
              return workers
      

      파이썬 클래스의 생성자__init__ 메서드 뿐인데, @classmethod를 사용하면 클래스에 __init__ 메서드가 아닌 다른 (제너릭) 생성자를 정의할 수 있다.

  2. 각 추상 인터페이스에 대한 구체적인 하위 클래스

    • 입력 데이터에 대한 구체적 하위 클래스 (디스크에서 파일을 읽는 동작)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      
      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))
      
    • 워커에 대한 구체적 하위 클래스 (\n 문자의 개수를 세는 동작)

      1
      2
      
      class LineCountWorker(GenericWorker):
          # 내용은 기존 LineCountWorker와 동일
      


(2) 완전히 제너릭mapreduce 함수를 작성한다.

제너릭한 동작을 위해 클래스 자체를 파라미터로 받도록 한다.

1
2
3
def mapreduce(worker_class, input_class, config):   # -- 완전히 제너릭하다!
    workers = worker_class.create_workers(input_class, config)
    return execute(workers)
1
2
3
4
config = {"data_dir": "dummy_file"}
# 제너릭하게 작동하므로, 더 많은 파라미터가 필요하다.
result = mapreduce(LineCountWorker, PathInputData, config)
print(f"{result} 줄이 있습니다.") # 총 5008 줄이 있습니다.


각 하위 클래스의 인스턴스 캑체를 결합하는 코드를 변경하지 않아도, GenericInputDataGenericWorker의 하위 클래스를 원하는 대로 변경할 수 있다!

This post is licensed under CC BY 4.0 by the author.

[Better Way #38] 간단한 인터페이스의 경우 클래스 대신 함수를 받아라

[Better Way #40] super로 부모 클래스를 초기화하라 (+ MRO)