파이썬 코딩의 기술
Chapter3. 클래스와 상속
22. 딕셔너리와 튜플보다는 헬퍼 클래스로 관리하자
- 다른 딕셔너리나 긴 튜플을 값으로 담은 딕셔너리를 생성하지 말자
- 정식 클래스의 유연성이 필요 없다면 가변운 불변 데이터 컨테이너에는 namedtuple을 사용하자
- 내부 상태를 관리하는 딕셔너리가 복잡해지면 여러 헬퍼 클래스를 사용하는 방식으로 관리 코드를 바꾸자
class WieghtedGradebook(object):
def __init__(self):
self._grades = {}
def add_student(self, name):
self._grades[name] = {}
def report_grade(self, name, subject, score, weight):
by_subject = self._grades[name]
grade_list = by_subject.setdefault(subject, [])
grade_list.append((score, weight))
def average(self, name):
by_subject = self._grades[name]
score_num, score_count = 0, 0
for subject, scores in by_subject.items():
subject_avg, total_wight = 0, 0
for score, weight in scores:
# ...
return score_sum / score_count
book.report_grade('Albert', 'Math', 80, 0,10)
- 값을 튜플로 만든 것뿐이라서 report_grade를 수정한 내역은 간단해 보이지만
- average_grade 메서드는 루프 안에 루프가 생겨서 이해하기 어려워졌다
- 클래스를 사용하는 면에서도 위치 인수에 있는 숫자들이 무엇을 의미하는지도 불명확
import collections
Grade = collections.namedtuple('Grade', ('score', 'weight'))
class Subject(object):
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
class Student(object):
def __init__(self):
self._subjects = {}
def subject(self, name):
if name not in self._subjects:
self._subjects[name] = Subject()
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 Gradebook(object):
def __init__(self):
self._students = {}
def student(self, name):
if name not in self._students:
self._students[name] = Student()
return self._students[name]
book = Gradebook()
albert = book.student('Albert')
math = albert.subject('Math')
math.report_grade(80, 0.10)
# ...
print(albert.average_grade())
23. 인터페이스가 간단하면 클래스 대신 함수를 받자
- 파이썬에서 컴포넌트 사이의 간단한 인터페이스용으로 클래스를 정의하고 인스턴스를 생성하는 대신에 함수만 써도 종종 충분하다
- 파이썬에서 함수와 메서드에 대한 참조는 일급이다. 즉, 다른 타입처럼 표현식에서 사용할 수 있다
- call이라는 특별한 메서드는 클래스의 인스턴스를 일반 파이썬 함수처럼 호출할 수 있게 해준다
- 상태를 보존하는 함수가 필요할 때 상태 보존 클로저를 정의하는 대신 call 메서드를 제공하는 클래스를 정의하는 방안을 고려하자
상태 보존 클로저를 기본값 후크로 사용하는 헬퍼함수
def increment_with_report(current, increments):
added_count = 0
def missing():
nonlocal added_count # 상태보존클로저
added_count += 1
return 0
result = defaultdict(missing, current)
for key, amount in increments:
result[key] += amount
return result, added_count
result, count = increment_with_report(current, increments)
assert count == 2
-
클로저 안에 상태를 숨기면 나중에 기능을 추가하기 쉽다
-
이해하기 어렵다
보존할 상태를 캡슐화하는 작은 클래스를 정의
class CountMissing(object): def __init__(self): self.added = 0 def missing(self): self.added += 1 return 0
counter = CounterMissing()
result = defaultdict(counter.missing, current) # 메서드 참조
for key, amount in increments:
result[key] += amount
assert counter.added == 2
- 헬퍼 클래스로 상태 보존 클로저의 동작을 제공하는 방법이 보다 명확
- 클래스 자체만으로는 용도가 무엇인지 바로 이해하기 어렵다
> __call__ 메서드
```python
class BetterCountMissing(object):
def __init__(self):
self.added = 0
def __call__(self):
self.added += 1
return 0
counter = BetterCountMissing()
counter()
assert callable(counter)
counter = BetterCountMissing()
result = defaultdict(counter, current) # __call__이 필요함
for key, amount in increments:
result[key] += amount
assert counter.added == 2
24. 객체를 범용으로 생성하려면 @classmethod 다형성을 이용하자
- 파이썬에서는 클래스별로 생성자를 한 개(init 메서드)만 만들 수 있다
- 클래스에 필요한 다른 생성자를 정의하려면 @classmethod를 사용하자
- 구체 서브 클래스들을 만들고 연결하는 범용적인 방법을 제공하려면 클래스 메서드 다형성을 이용하자
class InputData(object): def read(self): raise NotImplementedError
class PathInputData(InputData):
def init(self, path):
super().init()
self.path = path
def read(self):
return open(self.path).read()
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
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 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
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
def mapreduce(data_dir):
inputs = generate_inputs(data_dir)
workers = create_workers(inputs)
return execute(workers)
- mapreduce 함수가 전혀 범용적이지 않음
> @classmethod 다형성 이용
```python
class GenericInputData(object):
def read(self):
raise NotImplementedError
@classmethod
def generate_inputs(cls, config):
raise NotImplementedError
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))
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
def mapreduce(worker_class, input_class, config):
workers = worker_class.create_workers(input_class, config)
return execute(workers)
with TemporaryDirectory() as tmpir:
write_test_files(tmpdir)
config = {'data_dir': tmpdir}
result = mapreduce(LineCounterWorker, PathInputData, config)
25. super로 부모 클래스를 초기화하자
- 파이썬의 표준 메서드 해석 순서(MRO)는 슈퍼클래스의 초기화 순서와 다이아몬드 상속 문제를 해결한다
- 항상 내장 함수 super로 부모 클래스를 초기화하자
# 파이썬2 class MyBaseClass(object): def __init__(self, value): self.value = value
class TimesFiveCorrect(MyBaseClass):
def init(self, value):
super(TimesFiveCorrect, self).init(value)
self.value *= 5
class PlusTwoCorrect(MyBaseClass):
def init(self, value):
super(PlusTwoCorrect, self).init(value)
self.value += 2
class GoodWay(TimesFiveCorrect, PlusTwoCorrect):
def init(self, value):
super(GoodWay, self).init(value)
foo = GoodWay(5)
5 * (5 + 2) = 35
- 순서는 뒤에서부터 시작하는 것과 같다(MRO)
- 모든 초기화 메서드는 실제 __init__ 함수가 호출된 순서의 역순으로 실행된다
- 문법이 장황하다
- super를 호출하면서 현재 클래스의 이름을 지정해야 한다
> MRO 순서는 mro 클래스 메서드로 알 수 있다.
```python
from pprint import pprint
pprint(GoodWay.mro())
# 파이썬3
class Explicit(MyBaseClass):
def __init__(self, value):
super(__class__, self).__init__(value*2)
class Implicit(MyBaseClass):
def __init(self, value):
super().__init__(value*2)
assert Explicit(10).value == Implicit(10).value
- 파이썬3에서는 super를 인수 없이 호출하면 class와 self를 인수로 넘겨서 호출한 것으로 처리해서 이 문제를 해결
26. 믹스인 유틸리티 클래스에만 다중 상속을 사용하자
-
믹스인이란 클래스에서 제공해야 하는 추가적인 메서드만 정의하는 작은 클래스를 말한다
-
믹스인 클래스로 같은 결과를 얻을 수 있다면 다중 상속을 사용하지 말자
-
인스턴스 수준에서 동작을 교체할 수 있게 만들어서 믹스인 클래스가 요구할 때 클래스별로 원하는 동작을 하게 하자
-
간단한 동작으로 복잡한 기능을 생성하려면 믹스인을 조합하자
class ToDictMixin(object): def to_dict(self): return self._traverse_dict(self.__dict__) def _traverse_dict(self, instance_dict): output = {} for key, value in instance_dict.items(): output[key] = self._traverse(key, value) return output def _traverse(self, key, value): if isinstance(value, ToDictMixin): return value.to_dict() elif isinstance(value, dict): return self._traverse_dict(value) if isinstance(value, list): return [self._traverse(key, i) for i in value] if hasattr(value, '__dict__'): return self._traverse_dict(value.__dict__) else: return value
class BinaryTree(ToDictMixin):
def init(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
tree = BinaryTree(10,
left = BinaryTree(7, right=BinaryTree(9)),
right = BinaryTree(13, left=BinaryTree(11)))
print(tree.to_dict())
- 믹스인의 가장 큰 장점은 범용 기능을 교체할 수 있게 만들어서 필요할 때 동작을 오버라이드 할 수 있다는 점
```python
class BinaryTreeWithParent(BinaryTree):
def __init__(self, value, left=None, right=None, parent=None):
super().__init__(value, left=left, right=right)
self.parent = parent
def _traverse(self, key, value):
if(isinstance(value, BinaryTreeWithParent) and
key == 'parent'):
return value.value # 순환 방지
else:
return super()._traverse(key, value)
root BinaryTreeWithParent(10)
root.left = BinaryTreeWithParent(7, parent=root)
root.left.right = BinaryTreeWithParent(9, parent=root.left)
print(root.to_dict())
- _traverse 메서드를 오버라이드해서 부모를 탐색하지 않고 부모의 숫자 값만 꺼내오게 만든 예제다
class NamedSubTree(ToDictMixin):
def __init__(self, name, tree_with_parent):
self.name = name
self.tree_with_parent = tree_with_parent
my_tree = NamedSubTree('foobar', root.left.right)
print(my_tree.to_dict()) # 무한 루프를 돌지 않음
- BinaryTreeWithParent 타입의 속성이 있는 클래스라면 무엇이든 자동으로 ToDictMixin으로 동작할 수 있게 되었다
- 믹스인을 조합할 수도 있다
class JsonMix(object):
@classmethod
def from_json(cls, data):
kwargs = json.loads(data)
return cls(**kwargs)
def to_json(self):
return json.dumps(self.to_dict())
class DatacenterRack(ToDictMixin, JsonMixin):
def __init__(self, switch=None, machines=None):
self.switch = Switch(**switch)
self.machines = [
Machine(**kwargs) for kwargs in machines
]
class Switch(ToDictMixin, JsonMixin):
# ...
class Machine(ToDictMixin, JsonMixin):
# ...
deserialized = DatacenterRack.from_json(serialized)
roundtrip = deserialized.to_json()
assert json.loads(serialized) == json.loads(roundtrip)
27. 공개 속성보다는 비공개 속성을 사용하자
- 파이썬 컴파일러는 비공개 속성을 엄격하게 강요하지 않는다
- 서브클래스가 내부 API와 속성에 접근하지 못하게 막기보다는 처음부터 내부 API와 속성으로 더 많은 일을 할 수 있게 설계하자
- 비공개 속성에 대한 접근을 강제로 제어하지 말고 보호 필드를 문서화해서 서브클래스에 필요한 지침을 제공하자
- 직접 제어할 수 없는 서브클래스와 이름이 충돌하지 않게 할 때만 비공개 속성을 사용하는 방안을 고려하자
비고개 필드는 속성 이름 앞에 밑줄 두 개를 붙여 지정한다
class MyObject(object):
def __init__(self):
self.public_field = 5
self.__private_field = 10
def get_private_field(self):
return self.__private_field
foo = MyObject()
assert foo.public_field == 5
assert foo.get_private_field() == 10
클래스 메서드도 같은 class 블록에 선언되어 있으므로 비공개 속성에 접근할 수 있다
class MyOtherObject(object):
def __init__(self):
self.__private_field = 71
@classmethod
def get_private_field_of_instance(cls, instance):
return instance.__private_field
bar = MyOtherObject()
assert MyOtherObject.get_private_field_of_instance(bar) == 71
class MyParentObject(object):
def __init__(self):
self.__private_field = 71
class MyChildObject(MyParentObject):
def get_private_field(self):
return self.__private_field
bas = MyChildObject()
baz.get_private_field() # error
assert baz._MyParentObject__private_field == 71
- 서브 클래스에서는 부모 클래스의 비공개 필드에 접근할 수 없다
- 비공개 속성의 동작은 간단하게 속성 이름을 변환하는 방식으로 구현된다
class ApiClass(object):
def __init__(self):
self.__value = 5
def get(self):
return self.__value
class Child(ApiClass):
def __init__(self):
super().__init()
self._value = 'hello' # OK!
a = Child()
print(a.get(), 'and', a._value, 'are different') # 5 and hello are different
- 부모 클래스에서 비공개 속성을 사용해서 자식 클래스와 속성 이름이 겹치지 않게 하면 된다
28. 커스텀 컨테이너 타입은 collections.abc의 클래스를 상속 받게 만들자
- 쓰임새간 간단할 때는 list나 dict 같은 파이썬의 컨테이너 타입에서 직접 상속받게 하자
- 커스텀 컨테이너 타입을 올바르게 구현하는 데 필요한 많은 메서드에 주의해야 한다
- 커스텀 컨테이너 타입이 collections.abc에 정의된 인터페이스에서 상속받게 만들어서 클래스가 필요한 이넡페이스, 동작과 일치하게 하자
class BinaryNode(object):
def __init__(self, value, left=None, right=None):
self.value = value
self.left = left
self.right = right
- 이 클래스가 시퀀스 타입처럼 동작하게 하려면?
- 파이썬은 특별한 이름을 붙이 인스턴스 메서드로 컨테이너 동작을 구현한다
예시
bar = [1,2,3]
assert bar[0] == bar.__getitem__(0)
- BinaryNode 클래스가 시퀀스처럼 동작하게 하려면 객체의 트리를 깊이 우선으로 탐색하는 getitem을 구현하면 된다
class IndexableNode(BinaryNode):
def _search(self, count, index):
# ...
# (found, count) 반환
def __getitem__(self, index):
found, _ = self._search(0, index)
if not found:
raise IndexError('Index out of range')
return found.value
tree = IndexableNode(
10,
left = IndexableNode(,
5,
left = IndexableNode(2),
right = IndexableNode(
6, right = IndexableNode(7))),
right = IndexableNode(
15, left = IndexableNode(11)))
- 트리 탐색은 물론이고 list처럼 접근할 수도 있다
- 문제는 getitem을 구현하는 것만으로는 기대하는 시퀀스 시맨틱을 모두 제공하지 못한다는 점이다 (ex. len(tree))
class SequenceNode(IndexableNode):
def __len__(self):
_, count = self._search(0, None)
return count
tree = SequenceNode(
# ...
)
print('Tree has %d nodes' % len(tree))
- count, index 메서드가 빠졌다...
from collections.abc import Sequence
class BetterNode(SequenceNode, Sequence):
pass
tree = BetterNode(
# ...
)
print('Index of 7 is', tree.index(7))
print('Count of 10 is', tree.cound(10))
'프로그래머 > Python' 카테고리의 다른 글
[Effective Python 복습] Chapter 5. 병행성과 병렬성 (0) | 2020.12.04 |
---|---|
[Effective Python 복습] Chapter 4. 메타클래스와 속성 (0) | 2020.12.03 |
[파이썬] 파이썬 정리 for me (feat. FastCampus) (0) | 2020.05.30 |
Python - 문법 함수 | 생활코딩 강의 복습 | 프로그래밍 공부 (0) | 2018.12.30 |
Python - 삭제 구현 | 생활코딩 강의 복습 | 프로그래밍 공부 (0) | 2018.12.29 |