[Effective Python] Classes and Interfaces

u8cnk1·2023년 1월 14일
0

Chapter 5: Classes and Interfaces

파이썬은 객체 지향 프로그래밍 언어로서 상속, 다형성, 캡슐화 기능을 지원한다.

파이썬에서 작업을 수행하려면 종종 새로운 클래스를 작성하고 인터페이스와 상호 작용하는 방법을 정의한다. 또한 파이썬의 클래스와 상속을 통해 프로그램의 의도된 동작을 객체로 쉽게 표현할 수 있다.

이는 요구사항이 변화하는 환경에서 유연성을 제공하고, 잘 사용하는 방법을 알면 유지/관리 가능한 코드를 작성할 수 있다.

Item 37: 중첩 대신 Class로 구성하기

예) 학생들의 이름을 미리 알 수 없는 동적인 상황에 성적 기록하기

  1. Dictionary와 관련 타입 사용
class SimpleGradebook:
	def __init__(self):
 		self._grades = {}
        
 	def add_student(self, name):
 		self._grades[name] = []
        
 	def report_grade(self, name, score):
 		self._grades[name].append(score)

 	def average_grade(self, name):	
 		grades = self._grades[name]
 		return sum(grades) / len(grades)
        
 
 # 사용
book = SimpleGradebook() 	# 객체 생성
book.add_student('Isaac Newton')
book.report_grade('Isaac Newton', 90)
book.report_grade('Isaac Newton', 95)
book.report_grade('Isaac Newton', 85)

print(book.average_grade('Isaac Newton'))	 # 90

SimpleGradebook 클래스 확장 - 과목별 성적 list 유지

→ 학생 이름(키)을 다른 dictionary(값)에 매핑하도록 _grade 사전을 변경하여 이 작업을 수행할 수 있다. 가장 안쪽에 있는 dictionary는 과목(키)을 성적(값) 목록에 매핑한다.
→ 누락된 subject를 처리하기 위해 내부 사전에 대한 defaultdict 인스턴스를 사용하여 이 작업을 수행한다.


defaultdict

defaultdict()는 딕셔너리를 만드는 dict의 서브클래스로, 인자로 주어진 객체의 기본값을 딕셔너리의 초기값으로 지정할 수 있다. -> 처음 키를 지정할 때 값을 주지 않으면 해당 키에 대한 값을 디폴트 값으로 지정하고, 키에 명시적으로 값을 지정하게 되면 그 값이 지정된다.
숫자, list, set 등으로 초기화 할 수 있어 여러 용도로 사용할 수 있다.


from collections import defaultdict
 
class BySubjectGradebook:
 	def __init__(self):
 		self._grades = {} # Outer dict
        
 	def add_student(self, name):
		 self._grades[name] = defaultdict(list) # Inner dict
         										# 디폴트값이 list인 딕셔너리, 값을 지정하지 않으면 빈 리스트로 초기화

 	def report_grade(self, name, subject, grade):
 		by_subject = self._grades[name]
 		grade_list = by_subject[subject]
 		grade_list.append(grade)
 
 	def average_grade(self, name):
 		by_subject = self._grades[name]
        
		total, count = 0, 0
		for grades in by_subject.values():
 			total += sum(grades)
 			count += len(grades)
 		return total / count
        
        
 # 사용
book = BySubjectGradebook()
book.add_student('Albert Einstein')
book.report_grade('Albert Einstein', 'Math', 75)
book.report_grade('Albert Einstein', 'Math', 65)
book.report_grade('Albert Einstein', 'Gym', 90)
book.report_grade('Albert Einstein', 'Gym', 95)
print(book.average_grade('Albert Einstein'))	# 81.25

가장 안쪽에 있는 dictionary를 변경하여 중간고사와 기말고사가 퀴즈보다 더 중요하도록 가중치를 부여하기 -> (score, weight) 튜플 사용하여 변경

class WeightedGradebook:
	def __init__(self):
 		self._grades = {}
        
	def add_student(self, name):
 		self._grades[name] = defaultdict(list)
 
 	def report_grade(self, name, subject, score, weight):
 		by_subject = self._grades[name]
 		grade_list = by_subject[subject]
 		grade_list.append((score, weight))
        
 	def average_grade(self, name):
	 	by_subject = self._grades[name]
 
 		score_sum, score_count = 0, 0
 		for subject, scores in by_subject.items():
 			subject_avg, total_weight = 0, 0
		
        for score, weight in scores:
			subject_avg += score * weight
 			total_weight += weight
 
 		score_sum += subject_avg / total_weight
 		score_count += 1
 
 	return score_sum / score_count
    
    
# 사용
book = WeightedGradebook()
book.add_student('Albert Einstein')
book.report_grade('Albert Einstein', 'Math', 75, 0.05)
book.report_grade('Albert Einstein', 'Math', 65, 0.15)
book.report_grade('Albert Einstein', 'Math', 70, 0.80)
book.report_grade('Albert Einstein', 'Gym', 100, 0.40)
book.report_grade('Albert Einstein', 'Gym', 85, 0.60)
print(book.average_grade('Albert Einstein'))	 # 80.25

위 코드의 경우 report_grade에 대한 변경은 간단해 보이지만(grade list에 튜플 인스턴스를 저장), average_grade 메서드는 루프 내에 루프가 있으므로 읽기가 어렵다. 또한 위치 인수의 모든 숫자가 무엇을 의미하는지 명확하지 않아 class를 이용하는 것도 더 어려워졌다.

⇒ dictionary가 포함된 dictionary를 사용하면 다른 프로그래머가 코드를 읽기 어렵고 유지 관리하기도 어려워진다. 이와 같은 복잡성을 볼 때, dinctionary의 내부가 복잡한 경우 dictionary, tuple, set 및 list와 같은 기본 제공 유형에서 class의 계층 구조로 구성하는 것이 좋다.(클래스로 리팩토링)

튜플에 3개 이상의 항목이 있다면 namedtuple을 사용하여 작고 불변의 데이터 클래스를 쉽게 정의할 수 있다.

from collections import namedtuple

Grade = namedtuple('Grade', ('score', 'weight'))

이러한 클래스는 위치 또는 키워드 인수로 구성할 수 있다.
named 특성을 사용하여 필드에 액세스할 수 있으며, 나중에 요구 사항이 다시 변경되고 간단한 데이터 컨테이너에서 가변성 또는 동작을 지원해야 하는 경우 named 튜플에서 클래스로 쉽게 이동할 수 있다.


Limitations of 'namedtuple'

namedtuple은 많은 상황에서 유용하지만, 언제 그것이 득보다 실이 많을 수 있는지 이해하는 것이 중요하다.

  • namedtuple 클래스에는 기본 인수 값을 지정할 수 없다. 따라서 데이터에 많은 선택적 속성이 있을 수 있는 경우 사용할 수 없다.
    (몇 가지 이상의 속성을 사용하는 경우 기본 제공 데이터 클래스 모듈을 사용하는 것이 더 나은 선택일 수 있다.)
  • namedtuple의 속성 값 또한 숫자 인덱스와 반복을 사용하여 액세스할 수 있는데, 이는 특히 외부화된 API에서는 의도하지 않은 사용으로 이어져 나중에 실제 클래스로 이동하기가 더 어려워질 수 있다.

  1. class 사용

    (1) 성적 집합을 포함하는 단일 과목을 나타내는 class 작성

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

(2) 한 학생이 공부하고 있는 일련의 과목들을 나타내기 위한 class

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

(3) 모든 학생들의 이름에 동적으로 맞춰진 컨테이너 작성

class Gradebook:
	def __init__(self):
 		self._students = defaultdict(Student)
 
 	def get_student(self, name):
 		return self._students[name]

(4) 사용

book = Gradebook()
albert = book.get_student('Albert Einstein')
math = albert.get_subject('Math')
math.report_grade(75, 0.05)
math.report_grade(65, 0.15)
math.report_grade(70, 0.80)
gym = albert.get_subject('Gym')
gym.report_grade(100, 0.40)
gym.report_grade(85, 0.60)
print(albert.average_grade())

dictionary를 사용했을때보다 코드의 라인 수는 길어졌지만, 훨씬 읽기 쉽고 명확히 확장 가능하다.

Item 38: 단순 인터페이스에서 Class 대신 함수 사용

파이썬의 많은 내장 API는 함수를 전달함으로써 사용자가 동작을 정의할 수 있게 해준다. 이러한 hook는 API가 실행하는 동안 코드를 다시 호출하는 데 사용한다.

파이썬은 단순한 함수 인터페이스를 만족시키는 다양한 방법을 제공하며, 이 중에서 하나를 선택하여 사용할 수 있다.

파이썬에서 함수와 메서드에 대한 참조는 first class이며, 이는 다른 유형과 마찬가지로 식을 사용할 수 있음을 의미한다. 클래스를 정의하고 인스턴스화하는 대신 구성 요소 간의 간단한 인터페이스를 위한 함수를 사용할 수 있다.

state를 유지하는 기능이 필요한 경우 __call__ 메서드를 제공하는 class를 정의할 수 있다.
__call__을 사용하면 함수처럼 객체를 호출할 수 있다. 또한 일반 함수나 메서드와 마찬가지로 이러한 인스턴스에 대해 callable 내장 함수가 True를 반환합니다.
이러한 방식으로 실행할 수 있는 모든 객체를 callables라고 한다.

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) 	# Relies on __call__
for key, amount in increments:
 	result[key] += amount
assert counter.added == 2

'assert 조건식'은 조건이 True인 경우 다음 명령어로 문제없이 진행되며, False 일 경우 더 이상 프로그램이 실행되지 않고 'AssertionError'로 종료된다.

Item 39: @classmethod Polymorphism

다형성(Polymorphism)은 계층의 여러 클래스가 고유한 버전의 메서드를 구현할 수 있게 한다. 이것은 많은 클래스가 다른 기능을 제공하면서 동일한 인터페이스 또는 추상 기본 클래스를 수행할 수 있음을 의미한다.

특정 기능을 구현하기 위해 구체적인 subclass를 정의하는 방법은 객체가 구성된 후에만 유용하다. 객체를 만들고 orchestrating하기 위한 가장 간단한 접근법은 수동으로 객체를 만들고 helper function으로 연결하는 것이다. 그러나 이 방법은 객체를 구성하는 일반적인 방법이 필요하고 subclass를 작성하려면 각 함수가 일치하도록 다시 작성해야 한다. 클래스당 단일 생성자(__init__ method)만 지원하는 파이썬에서 이 문제를 해결하는 가장 좋은 방법은 class method polymorphism이다.

class method ploymorphism을 사용하여 많은 구체적인 subclass를 만들고 연결하는 일반적인 방법을 제공한다.
@classmethod를 통해 공통 인터페이스를 사용하여 새 인스턴스를 만드는 클래스를 확장할 수 있다.(클래스에 대한 대체 생성자 정의)

Item 40: 'super' 사용하여 부모클래스 초기화

자식 클래스에서 부모 클래스를 초기화하는 간단한 방법은 자식 인스턴스를 사용하여 부모 클래스의 __init__ 메서드를 직접 호출하는 것이다. 이 방식은 클래스가 다중 상속의 영향을 받는 경우 슈퍼클래스의 __init__ 메소드를 직접 호출하면 예측할 수 없는 동작이 발생할 수 있다.
또한 다이아몬드 상속(하위 클래스가 계층 내 어딘가에서 동일한 슈퍼 클래스를 가진 두 개의 개별 클래스로부터 상속될 때)에도 문제가 발생한다. 다이아몬드 상속은 일반적인 슈퍼클래스의 __init__ 메서드를 여러 번 실행하여 예기치 않은 동작을 발생시킨다.

이러한 문제를 해결하기 위해 파이썬은 super built-in function과 표준 MRO(Method Resolution Order)를 가지고 있다.
super는 다이아몬드 계층의 공통 슈퍼클래스가 한 번만 실행되도록 하고, MRO는 C3 선형화라는 알고리즘에 따라 슈퍼클래스가 초기화되는 순서를 정의한다.

class MyBaseClass:
	def __init__(self, value):
 		self.value = value
 
class TimesSevenCorrect(MyBaseClass):
 	def __init__(self, value):
 		super().__init__(value)
 		self.value *= 7
 
class PlusNineCorrect(MyBaseClass):
 	def __init__(self, value):
 		super().__init__(value)
 		self.value += 9
 
 class GoodWay(TimesSevenCorrect, PlusNineCorrect):
 	def __init__(self, value):
 		super().__init__(value)
 
 
foo = GoodWay(5)
print('Should be 7 * (5 + 9) = 98 and is', foo.value)

GoodWay(5)를 실행시켰을 때 foo.value는 ((5 * 7) + 9 = 44이 아닌) 98이 된다.
-> GoodWay(5)를 호출하면, 그것은 차례로 TimesSevenCorrect.__init__, PlusNineCorrect.__init__, MyBaseClass.__init__를 호출한다.
-> 다이아몬드의 꼭대기에 도달하면, 그들의 __init__ 함수들이 호출된 방법과 반대 순서로 그들의 작업을 수행한다.
-> MyBaseClass.__init__는 값을 5에 할당한다.
PlusNineCorrect.__init__는 9를 더하여 값을 14로 만든다.
TimesSevenCorrect.__init__는 값을 98로 만들기 위해 7을 곱한다.

또한 super().__init__에 대한 호출은 하위 클래스 내에서 직접 MyBaseClass.__init__를 호출하는 것보다 유지 관리 측면에서 훨씬 더 유리하다.
나중에 MyBaseClass의 이름을 다른 이름으로 바꾸거나 TimesSevenCorrect 및 PlusNineCorrect가 일치하도록 __init__ 메서드를 업데이트할 필요 없이 다른 슈퍼클래스에서 상속받을 수 있다.

Item 41: Consider Composing Functionality with Mix-in Classes

파이썬은 다중 상속을 다루기 쉽게 만드는 기능이 내장된 객체 지향 언어이다. 그러나 다중 상속은 아예 피하는 것이 좋다.

다중 상속과 함께 제공되는 편리함과 캡슐화를 원하지만 잠재적인 문제를 피하고 싶다면, 대신 mix-in 을 고려해 볼 수 있다.

mix-in은 하위 클래스가 제공할 추가 메서드의 작은 집합만 정의하는 클래스로, 자체 인스턴스 속성을 정의하지 않으며 __init__ 생성자를 호출할 필요가 없다.

반복 코드를 최소화하고 재사용을 극대화하기 위해 min-in을 구성하고 계층화할 수 있다. 또한 일반 기능을 플러그인할 수 있도록 하여 필요할 때 동작을 재정의할 수 있다. 필요에 따라 인스턴스 메소드 또는 클래스 메소드를 포함할 수 있고, 단순한 동작에서 복잡한 기능을 만들기 위해 구성한다.

Item 42: Public Attributes

파이썬에서 클래스 속성에 대한 가시성은 publicprivate의 두 가지 유형이 있다.

public 속성은 개체의 점 연산자를 사용하는 모든 사용자가 액세스할 수 있다.

private field는 속성 이름 앞에 이중 밑줄을 사용하여 지정한다.
클래스 외부에서 private 필드에 직접 액세스하면 예외가 발생한다.
또한 하위 클래스는 상위 클래스의 private 필드에 액세스할 수 없다.

class ApiClass:
 	def __init__(self):
 		self._value = 5
 
 	def get(self):
 		return self._value


class Child(ApiClass):
 	def __init__(self):
 		super().__init__()
 		self._value = 'hello' # Conflicts

a = Child()
print(f'{a.get()} and {a._value} should be different') 		# hello and hello should be different

위 코드처럼 자식 클래스가 자신도 모르게 부모 클래스에서 이미 정의한 속성을 정의할 때에도 문제가 발생한다. 이 문제가 발생할 위험을 줄이기 위해 부모 클래스에서 개인 속성을 사용하여 자식 클래스와 겹치는 속성 이름이 없도록 할 수 있다.

class ApiClass:
	def __init__(self):
 		self.__value = 5 		# Double underscore
 
 	def get(self):
 		return self.__value		 # Double underscore


class Child(ApiClass):
	def __init__(self):
 		super().__init__()
 		self._value = 'hello'
 
a = Child()
print(f'{a.get()} and {a._value} are different') 		# 5 and hello are different

Item 43: collections.abc에서의 상속

모든 파이썬 클래스는 속성과 기능을 함께 캡슐화하는 일종의 컨테이너.
파이썬은 또한 목록, 튜플, 세트, 사전과 같은 데이터를 관리하기 위한 내장 컨테이너 유형을 제공한다. 시퀀스와 같은 간단한 사용 사례를 위한 클래스를 설계할 때 Python의 컨테이너 유형에서 직접 상속한다. 하지만 사용자가 자신의 컨테이너 유형을 정의하는 것은 생각보다 훨씬 어렵다.

파이썬은 이러한 어려움을 피하기 위해 collections.abc 모듈을 제공하여 각 컨테이너 유형에 대한 모든 일반적인 메서드를 제공하는 추상 기본 클래스 집합을 정의한다. -> from collections.abc import Sequence
사용자 지정 컨테이너 유형이 collections.abc에 정의된 인터페이스에서 상속되도록 하여 클래스가 필요한 인터페이스 및 동작과 일치하는지 확인하고, 필요한 메서드를 구현이 누락되면 모듈이 무언가 잘못되었음을 알려준다.

collections.abc 모듈 외에도, 파이썬은 객체 비교와 정렬을 위해 다양한 특별한 방법을 사용하며, 컨테이너 클래스와 비컨테이너 클래스에 의해 제공될 수 있다.

class 참고
assert

0개의 댓글