python

이펙티브 파이썬 2nd 정리 #4

기기디 2023. 9. 1. 21:50

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