클래스와 인터페이스 (1)

About_work·2023년 1월 18일
0

python 기초

목록 보기
12/65

interface

  • 추상 class 중에서, 추상 method만 있고, 일반 method는 없는 것 (파이썬에는 없는 개념)
  • 어떤 interface를 상속받았을 때, 사용하지 않을 메서드가 있다면, interface 분리 원칙 위반이다.
  • 그럴 때는, interface를 더 작게 분리해야 한다. (항상 더 작게 쪼갤 수 있을지 고민해야 한다.)
  • 그렇다고 interface 하나당 method 하나만 있을 정도로 잘게 쪼개라는 것은 아니고, 같은 기능이나 역할로 묶어서 interface를 잘 만들어야 한다.

37: 내장 타입을 어러 단계로 내포시키기보다는, class를 합성하라.

  • 3줄 요약
    • (dictionary, long tuple, 다른 내장 타입이 복잡하게 내포된 데이터를 값으로 사용하는) dictionary를 만들지 말라.
    • 완전한 class 가 제공하는 유연성이 필요하지 않고, 가벼운 불변 데이터 container가 필요하면, namedtuple 을 써라.
    • 내부 상태를 표현하는 dictionary가 복잡해지면, 이 데이터를 관리하는 코드를 여러 클래스로 나눠서 재작성하라.
  • 동적 데이터 관리에는 dictionary 타입이 유리하다.
    • 하지만 dictionary 안에 dictionary를 포함시키지 말라. 코드가 읽기 어려워지고, 여러분 스스로도 유지 보수의 ‘악몽’ 속으로 들어가는 셈이다.
    • 어떠 내장 타입이던 내포 단계가 2 단계 이상 되면, 더 이상 dictionary, list, tuple 계층을 추가하지 말라.
  • 원소가 3개 이상인 tuple을 사용하지 말라
  • 대신 collection 내장 모듈의 namedtuple 사용을 고려하라
    • 작은 불변 데이터를 쉽게 정의할 수 있다.
    • 위치 기반 인자를 사용해도 되고, 키워드 인자를 사용해도 된다.
    • 필드에 접근할 때는 attribute 이름을 쓸 수 있다.
    • attribute 값을 숫자 index를 사용해 접근할 수도 있고, iteration도 가능하다.
    • namedtuple의 property가 4-5개보다 더 많아지면, dataclasses 내장 모듈을 사용하라.
  • class는 언제 쓸까?
    • 클래스는 동적(가변성)을 지원해야 하거나, 간단한 데이터 컨테이너 이상의 동작이 필요한 경우부터 사용을 고려하라.
    • namedtuple을 사용하는 모든 부분을 제어할 수 있는 상황이 아니라면, 명시적으로 새로운 class를 정의하는 편이 낫다.
book = Gradebook()
albert = book.get_student('알버트 아인슈타인')
math = albert.get_subject('수학')
math.report_grade(75, 0.05)
math.report_grade(65, 0.15)
math.report_grade(70, 0.80)
gym = albert.get_subject('체육')
gym.report_grade(100, 0.40)
gym.report_grade(85, 0.60)
print(albert.average_grade())

class Gradebook:
    def __init__(self):
        self._students = defaultdict(Student)

    def get_student(self, name):
        return self._students[name]

class Student:
    def __init__(self):
        self._subjects = defaultdict(Subject)

    def get_subject(self, name):
        return self._subjects[name]

    def average_grade(self):
        total, count = 0, 0
        for subject in self._subjects.values():
            total += subject.average_grade()
            count += 1
        return total / count

class Subject:
    def __init__(self):
        self._grades = []

    def report_grade(self, score, weight):
        self._grades.append(Grade(score, weight))

    def average_grade(self):
        total, total_weight = 0, 0
        for grade in self._grades:
            total += grade.score * grade.weight
            total_weight += grade.weight
        return total / total_weight

from collections import namedtuple
Grade = namedtuple('Grade', ('score', 'weight'))

38: 간단한 interface의 경우, class 대신 함수를 받아라.

세줄 요약

  • 파이썬의 여러 component 사이에 간단한 interface가 필요할 때는, class를 정의하여 인스턴스화 하지말고 간단히 함수를 사용하자
  • 파이썬 함수나 메서드는 일급 시민이다. 따라서 (다른 타입의 값과 마찬가지로) 함수나 함수 참조를 식에 사용할 수 있다.
  • __call__ 특별 메서드를 사용하면, class의 instance인 객체를 일반 파이썬 함수처럼 호출할 수 있다.
  • 상태를 유지하기 위한 함수가 필요한 경우에는, 상태가 있는 closure을 정의하는 대신 __call__ 메서드가 있는 클래스를 정의할지 고려해보자.

참고 기초 내용

  • closure 함수

    • 자신이 정의된 영역 밖의 변수를 참조하는 함수
  • 파이썬 함수 = first-class citizen 객체

    • first-class citizen 객체
      • 직접 가리킬 수 있다.
      • 변수에 대입하거나, 다른 함수에 인자로 전달할 수 있다.
      • 식이나 if 문에서 함수를 비교하거나, 함수를 반환하는 것이 가능하다.
  • 식(expression)

    • 하나 이상의 값으로 표현될 수 있다.
    • ex) 1 + 2 + 3
  • API(Application Programming Interface)

    • OS와 응용프로그램 사이의 통신에 사용되는 언어나 메시지 형식
    • 중간 전달자 (요리사-점원-손님) (서버-전달자-클라이언트)
    • 두 소프트웨어 구성 요소가, 서로 통신할 수 있게 하는 메커니즘
    • 두 application 간의 서비스 계약 / 두 app이 서로 통신하는 방법을 정의

본문

  • 파이썬 내장 API 중 상당수는, 함수를 전달해서 동작을 원하는 대로 바꿀 수 있게 해준다.
  • API가 실행되는 과정에서 여러분이 전달한 함수를 실행하는 경우, 이런 함수를 hook 이라고 부른다.
  • 아래의 len 함수가 hook이 된다.
names = [‘asd’, ‘db’ ‘dsdf’]
names.sort(key=len)
>>>
[‘db’, ‘asd’, ‘dsdf’]
  • hook 을 abstract class 를 통해 정의해야 하는 언어도 있지만, python 에서는 단순히 argument와 return 값이 잘 정의된, 상태가 없는 함수를 hook으로 사용하는 경우가 많다.
  • 함수는 class 보다 정의하거나 기술하기가 더 쉬우므로, hook을 사용하기에는 함수가 이상적이다.
  • 아래의 예제도 한번 보자.
from collections import defaultdict

def log_missing():
    print(‘키 추가됨‘)
    return 0

current = {‘초록’: 12, ‘파랑’: 3}
increments = [(‘빨강‘, 5), (’파랑‘, 17), (‘주황’, 9), ]
result = defaultdict(log_missing, current)
for key, amount in increments:
    result[key] += amount

>>>
키 추가됨
키 추가됨
이후: {‘초록’: 12, ‘파랑’: 20, ‘빨강’: 5, ‘주황’: 9}
  • log_missing과 같은 hook을 사용할 수 있으면, 정해진 동작과 부수 효과를 분리할 수 있기 때문에, API를 더 쉽게 만들 수 있다.
  • 그런데, 여기서 없었던 키가 총 몇번 호출되었는지를 세고 싶다면? 어떻게 코딩해야 할까?
class BetterCountMissing:
    def __init__(self):
        self.added = 0
    
    def __call__(self):
        self.added += 1
        return 0

counter= BetterCountMissing()
assert counter() == 0
assert callable(counter)

counter = BetterCountMissing()
result = defaultdict(counter, current) #__call__ 에 의존함
for key, amount in increments:
    result[key] += amount
assert counter.added == 2
  • __call__ 메서드는 (API hook 처럼) 함수가 argument로 쓰일 수 있는 부분에, 이 클래스의 instance를 사용할 수 있다는 사실을 나타낸다.
  • 코드를 처음 읽는 사람도, 이 클래스의 동작을 알아보기 위한 시작점이 __call__ 이라는 사실을 쉽게 알 수 있으며, 이 클래스를 만든 목적이 상태를 저장하는 closure 역할이라는 사실을 잘 알 수 있다.

39: 객체를 generic 하게 구성하려면 @classmethod를 통한 다형성을 활용하라.

세줄 요약

  • 파이썬 클래스에는 생성자가 __init__ 메서드뿐이다.
  • @classmethod를 사용하면, 클래스에 다른 생성자를 정의할 수 있다.
  • 클래스 메서드 다형성을 활용하면, 여러 구체적인 하위 클래스의 객체를 만들고 연결하는 generic한 방법을 제공할 수 있다.

본문 내용

  • 클래스가 다형성을 지원한다??
    • 다형성을 사용하면, 계층을 이루는 여러 class가 자신에게 맞는 유일한 method 버전을 구현할 수 있다.
    • 이는 같은 interface를 만족하거나, 같은 추상 기반 class를 공유하는 많은 class가 서로 다른 기능을 제공할 수 있다는 뜻이다.

import os
import random

tmpdir = 'test_inputs'
write_test_files(tmpdir)

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))

result = mapreduce(tmpdir)
print(f'총 {result} 줄이 있습니다.')

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

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

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()

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

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

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
  • 위 코드의 문제점

    • mapreduce 함수가 전혀 generic 하지 않다는 것이다.
    • 즉, 다른 종류의 InputData나 Worker 하위 클래스를 사용하고 싶다면, 각 하위 클래스에 맞게 generate_inputs, create_workers, mapreduce를 재작성해야 한다.
    • 다른 언어에서는 다형성을 활용해 이 문제를 해결할 수 있다.
      • InputData의 모든 하위 클래스는 mapreduce를 처리하는 도우미 메서드들이 generic하게 사용할 수 있는 특별한 생성자(팩토리 메서드와 비슷한)를 제공한다.
    • 하지만 파이썬에서는 생성자가 __init__밖에 없다는 점이 문제다.
    • InputData의 모든 하위 클래스가 똑같은 생성자만 제공해야 한다고 요구하는 것은 불합리하다.
  • 해결책

    • class method의 다형성을 사용하자.
    • 이 방식은 InputData.read에서 사용했던 instance methd의 다형성과 똑같은데, class로 만들어낸 개별 객체에 적용되지 않고, 클래스 전체에 적용된다는 점만 다르다.

### GenericInputData와 PathInputData를 사용한 방법

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

def mapreduce(worker_class, input_class, config):
    workers = worker_class.create_workers(input_class, config)
    return execute(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

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 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 GenericInputData:
    def read(self):
        raise NotImplementedError

    @classmethod
    def generate_inputs(cls, config):
        raise NotImplementedError
profile
새로운 것이 들어오면 이미 있는 것과 충돌을 시도하라.

0개의 댓글