34. send로 제네레이터에 데이터를 주입하지 말라
요약
- send()를 통해서 데이터를 제네레이터에 주입할 수 있다. 제네레이터는 send()로 주입돤 값을 yield식이 반환하는 값을 통해 받으며 이 값을 변수에 저장하여 활용 할 수 있다.
- send()와 yield from식을 함께 사용하면 제네레이터의 출혁에 None이 반환되는 의도하지 않은 결과를 얻을수도 있다.
- 합성할 제네레이터들의 입력으로 이터레이터를 전달하는 방식이 send()를 사용하는 방식보다 더 낫다. send()는 가급적 사용하지 않는것을 권장한다
제네레이터는 이터레이터를 생성하는 함수로, 함수 실행 중 yield 키워드를 사용하여 값을 반환하고 함수의 상태를 유지합니다. 제네레이터는 함수 호출과 달리 값을 한 번에 하나씩 반환하며, 메모리 사용량을 최소화합니다.
제네레이터는 send() 메서드를 사용하여 값도 주입할 수 있습니다. send() 메서드는 yield 키워드와 함께 사용되며, 제네레이터에 값을 주입할 수 있습니다. 제네레이터는 send() 메서드에 의해 주입된 값을 yield 식이 반환하는 변수에 저장합니다. 이를 통해 제네레이터의 실행을 제어하고 값을 주입할 수 있습니다.
def generator():
while True:
value = yield
print(f'Got value: {value}')
g = generator()
next(g) # Start the generator
g.send(1) # Send value 1 to the generator
이 예시에서는 제네레이터 함수를 정의하고, send() 메서드를 사용하여 값을 주입합니다. 이를 통해 제네레이터 함수의 실행을 제어하고 값을 주입할 수 있습니다.
그러나 send() 메서드와 yield from 식을 함께 사용하면 예상치 않은 결과가 발생할 수 있습니다. send() 메서드와 yield from 식을 함께 사용하면 제네레이터의 출현에 None이 반환되는 경우가 발생할 수 있습니다.
def generator():
yield from range(10)
g = generator()
g.send(None)
이 예시에서는 제네레이터 함수를 정의하고, yield from 식을 사용하여 0부터 9까지의 정수를 반환합니다. 그런 다음 send() 메서드를 사용하여 None 값을 주입합니다. 이 경우 제네레이터의 출현에 None이 반환되는 예기치 않은 결과가 발생합니다.
제네레이터에 값을 주입하는 대신, 제네레이터를 합성하는 방식으로 이터레이터를 전달하는 것이 좋습니다. 이를 통해 제네레이터의 실행을 제어하고, 예기치 않은 결과를 방지할 수 있습니다.
send() 메서드를 사용하여 제네레이터에 값을 주입하지 않는 것이 좋습니다. 대신, 제네레이터를 합성하는 방식으로 이터레이터를 전달하는 것이 좋습니다. 이를 통해 제네레이터의 실행을 제어하고, 예기치 않은 결과를 방지할 수 있습니다.
35. 제네레이터 안에서 throw로 상태를 변화시키지 말라
요약
- throw메소드를 사용하면 제네레이터가 마지막으로 실행한 yield식의 위치에서 예외를 다시 발생시킬 수 있다.
- throw를 사용하면 가독성이 나빠진다. 예외를 잡아내고 다시 발생시키는데 준비 코드가 필요하며 내포 단게가 깊어지기 때문이다.
- 제네레이터에서 예외적인 동작을 제공하고 싶다면 __iter__메소드를 구현하는 클래스를 사용하면서 예외적인 경우에 상태를 전이시키는게 좋다.
예를 들어 다음과 같은 제네레이터가 있다고 가정해보겠습니다.
def my_generator():
while True:
try:
value = yield
print(f"Got value: {value}")
except ValueError:
print("Oops! Invalid value provided.")
이제 my_generator를 실행하면서 throw 메서드를 사용하여 예외를 발생시켜 보겠습니다.
g = my_generator()
next(g) # Start the generator
g.send(1) # Send value 1 to the generator
g.throw(ValueError) # Raise a ValueError inside the generator
g.send(2) # Send value 2 to the generator
위 코드에서 throw 메서드를 사용하여 ValueError 예외를 발생시켰습니다. 이를 통해 제네레이터 내부에서 예외 처리를 수행할 수 있습니다. 하지만 이와 같은 방법은 코드의 가독성이 나빠지며 디버깅이 어려워집니다.
따라서 예외적인 상황에서 제네레이터의 상태를 변경하려면 iter 메서드를 사용하는 클래스를 만들고 예외 처리를 수행하는 것이 좋습니다. 이를 통해 코드의 가독성을 높일 수 있으며 예외 처리를 간편하게 할 수 있습니다.
class MyGenerator:
def __init__(self):
self._state = "init"
def __iter__(self):
self._state = "iter"
return self
def __next__(self):
self._state = "next"
return None
def throw(self, exc_type, exc_value=None, traceback=None):
if self._state == "iter":
raise exc_type(exc_value).with_traceback(traceback)
elif self._state == "next":
return self.send(None)
else:
raise ValueError("Invalid state")
위와 같이 MyGenerator 클래스를 구현하면 throw 메서드를 사용하여 예외를 발생시키는 것이 아니라 예외를 일으키는 상황에서 self._state를 변경하여 제네레이터의 상태를 변경할 수 있습니다. 이를 통해 코드의 가독성을 높일 수 있습니다.
g = MyGenerator()
iter(g) # Start the generator
next(g) # First `yield`
g.throw(ValueError) # Raise a ValueError inside the generator
next(g) # Second `yield`
위 코드에서 MyGenerator 클래스를 사용하여 상태를 변경하면서 예외 처리를 수행합니다. 이를 통해 코드의 가독성을 높일 수 있습니다.
37. 내장 타입을 여러 단계로 내포시키기 보다는 클래스를 합성하라
요약
- 딕셔너리의 내포 단계가 두 단계 이상이 된다면 더 이상 새로운 딕셔너리, 리스트, 튜플을 추가하지 않아야한다.
- 완전한 클래스가 제공하는 유연성이 필요하지 않고 가벼운 불변 데이터를 담는 컨테이가 필요하다면 namedtuple을 사용하자
- 내부 상태를 표현하는 딕셔너리가 복잡해지는 상황이라면 딕셔너리가 아닌 클래스로 분리하라
내장 타입을 여러 단계로 내포시키기보다는 클래스를 합성하라.
- 파이썬 내장 딕셔너리 타입을 사용하면 객체의 생명 주기 동안 동적인 내부 상태를 잘 유지할 수 있다. 여기서 동적(dynamic) 이라는 말은 어떤 값이 들어올지 미리 알 수 없는 식별자들을 유지해야 한다는 뜻이다.
예시:
문제: 학생들의 성적을 관리하는 시스템을 만들려고 합니다. 각 학생은 다수의 과목을 수강하며, 각 과목에는 여러 시험 점수가 있습니다.
나쁜 방법: 딕셔너리 내포 사용
pythonCopy code
grades = {}
grades['이수영'] = {}
grades['이수영']['수학'] = [80, 90, 85]
grades['이수영']['영어'] = [90, 85, 88]
이러한 방식은 간단한 경우에는 적절해 보일 수 있지만, 코드의 복잡성이 증가하면 관리하기 어려워집니다. 또한 데이터의 유효성 검사, 기본값 설정 등의 추가 로직을 구현하기가 번거롭습니다.
좋은 방법: 클래스 사용
from collections import defaultdict
class Subject:
def __init__(self):
self.scores = []
def add_score(self, score):
self.scores.append(score)
class Student:
def __init__(self):
self.subjects = defaultdict(Subject)
def add_score(self, subject_name, score):
subject = self.subjects[subject_name]
subject.add_score(score)
grades = {}
grades['이수영'] = Student()
grades['이수영'].add_score('수학', 80)
grades['이수영'].add_score('수학', 90)
grades['이수영'].add_score('영어', 90)
이 방식은 초기에는 더 많은 코드를 필요로 하지만, 확장성과 유지 보수성 면에서 훨씬 우수합니다. 예를 들어, 학생마다의 평균 점수 계산, 특정 과목의 최고점 계산 등의 기능을 추가하기가 더 쉽습니다.
결론
- 내장 타입의 내포를 과도하게 사용하는 것은 복잡성과 버그를 초래할 수 있습니다. 데이터의 구조가 복잡해지면, 해당 구조를 잘 표현하는 클래스를 사용하는 것이 좋습니다.
namedtuple의 한계
- namedtuple은 가벼운 불변 데이터 컨테이너를 만드는 데 유용하지만 한계가 있습니다. 첫 번째는 필드에 기본값을 지원하지 않는다는 것입니다. 두 번째는 상속을 지원하지 않는다는 것이며 namedtuple을 확장하거나 수정해야 할 경우 문제가 될 수 있습니다. 이러한 경우에는 완전한 클래스를 사용하는 것이 더 나은 선택일 수 있습니다.
'python' 카테고리의 다른 글
이펙티브 파이썬 2nd 정리 #5 (0) | 2023.09.08 |
---|---|
이펙티브 파이썬 2nd 정리 #3 (0) | 2023.08.18 |
이펙티브 파이썬 2nd 정리 #2 (0) | 2023.08.05 |
이펙티브 파이썬 2nd 정리 #1 (1) | 2023.07.28 |