7 분 소요

요즘 파이썬을 사용하면서 자연스레 좀 더 효율적으로 코드를 짜고 싶은 마음이 커졌습니다. 그래서 이 책을 공부하기 시작했습니다.
이 글은 개인 공부를 목적으로 작성되었습니다.
혹시 오타나 글의 수정사항이 있어 알려주시면 감사하겠습니다.





  • 파이썬에서는 컴프리헨션을 이용해 리스트, 딕셔너리, 집합 등 타입을 간결하게 이터레이션하면서 원소로부터 파생되는 데이터 구조를 생성할 수 있다.
  • 제너레이터는 함수가 점진적으로 반환하는 값으로 이뤄지는 스트림을 만들어준다.

Better way 27: map과 filter 대신 컴프리헨션을 사용하라

  • 파이썬은 다른 시퀀스나 이터러블에서 새 리스트를 만들어내는 간결한 구문을 제공하는 데, 이런 식을 리스트 컴프리헨션이라고 한다.

      a = [x for x in range(1,11)]
      # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
      squares = [x**2 for x in a]
      print(squares)
      # [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
    


  • 리스트 컴프리헨션은 입력 리스트에서 원소를 쉽게 필터링해 결과에서 원하는 원소를 제거할 수 있다.

      even_squares = [x**2 for x in a if x % 2 ==0]
      even_squares
      # [4, 16, 36, 64, 100]
    


  • 또한 딕셔너리 컴프리헨션과 집합 컴프리헨션이 있다. 이를 사용하면 알고리즘을 작성할 때 딕셔너리나 집합에서 파생된 데이터 구조를 쉽게 만들 수 있다.

      even_squares_dict = {x: x**2 for x in a if x % 2 == 0}
      even_squares_set = {x**3 for x in a if x % 3 == 0}
      print(even_squares_dict) # {2: 4, 4: 16, 6: 36, 8: 64, 10: 100}
      print(even_squares_set) # {216, 729, 27}
    
  • 각각의 호출을 적절한 생성자로 감싸면 같은 결과를 map과 filter를 사용해 만들 수 있지만, 코드가 너무 길어지기 때문에 이를 사용하는 것은 가급적 피한다.



Better way 28: 컴프리헨션 내부에 제어 하위 식을 세 개 이상 사용하지 말라

  • 컴프리헨션은 기본적인 사용법 외에도 루프를 여러 수준으로 내포하도록 허용한다.
    • 아래 코드에서 각각의 하위 식은 컴프리헨션에 들어간 순서대로 왼쪽에서 오른쪽으로 실행된다.

      matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
      flat = [x for row in matrix for x in row]
      print(flat)
      # [1, 2, 3, 4, 5, 6, 7, 8, 9]
      


  • 컴프리헨션은 여러 if 조건을 허용한다.
  • 여러 조건을 같은 수준의 루프에 사용하면 암시적으로 and 식을 의미한다.

      a = [x for x in range(1,11)]
      b = [x for x in a if x > 4 if x % 2 == 0]
      print(b) # [6, 8, 10]
    


  • 하지만, 제어 하위 식이 세 개 이상인 컴프리헨션은 이해하기 매우 어려우므로 가능하면 피해야 한다.



Better way 29: 대입식을 사용해 컴프리헨션 안에서 반복 작업을 피하라

  • 컴프리헨션에서 같은 계산을 여러 위치에서 공유하는 경우가 많다.
    • 아래 코드는 왈러스 연산자를 사용하여 컴프리헨션의 일부분에 대입식을 만들었다.
      • 왈러스 연산자를 더 알고 싶다면, 이 링크를 참조한다.
      stock = {
          '못': 125,
          '나사못': 35,
          '나비너트': 8,
          '와셔': 24,
      }
      
      order = ['나사못', '나비너트', '클립']
      
      def get_batches(count, size):
          return count // size
      
      found = {name: batches for name in order
              if (batches := get_batches(stock.get(name, 0 ), 8))
              }
      print(found) # {'나사못': 4, '나비너트': 1}
      
  • 대입식(batch := get_batches(...))을 사용하면 stock 딕셔너리에서 각 order 키를 한 번만 조회하고 get_batches를 한 번만 호출해서 그 결과를 batches 변수에 저장할 수 있다.


  • 하지만 컴프리헨션이 값 부분에서 왈러스 연산자를 사용할 때 그 값에 대한 조건 부분이 없다면 루프 밖 영역으로 루프 변수가 누출된다.
    • 루프 변수를 누출하지 않는 편이 낫기 때문에 컴프리헨션에서 대입식을 조건에만 사용하는 것을 권장한다.



Better way 30: 리스트를 반환하기보다는 제너레이터를 사용하라.

  • 다음은 문자열에서 띄어쓰기 위치를 반환할 때의 코드이다.

    def index_words(text):
        result = []
        if text:
            result.append(0)
        for index, letter in enumerate(text):
            if letter == ' ':
                result.append(index + 1)
        return result
        
    address = '영어로 된 코드(해석)는 잘 봐야 한다.'
    result = index_words(address)
    print(result) # [0, 4, 6, 14, 16, 19]
    


  • 하지만, 위 코드는 핵심을 알아보기 힘들다. 그리고 리스트에 모든 결과를 다 저장해야 한다. 만약 입력 데이터가 크면 메모리가 부족할 수 있다.
    • 위 함수를 개선시키고자 제너레이트를 사용한다.
  • 제너레이터yield 식을 사용하는 함수에 의해 만들어진다.
    • 제너레이터(generator): 여러 개의 데이터를 미리 만들어 놓지 않고 필요할 때마다 즉석해서 하나씩 만들어낼 수 있는 객체 1
    • yield를 사용하면 제너레이터를 반환한다.
    • 제너레이터가 yield에 전달하는 값은 이터레이터에 의해 호출하는 쪽에 반환된다.
    • 매번 제너레이터가 호출되면 yield에서 지정한 값을 반환한 후 다음 호출이 있을 때까지 자신의 상태를 정지시킨다.
    def index_words_iter(text):
        if text:
            yield 0
        for index, letter in enumerate(text):
            if letter == ' ':
                yield index + 1 
    
    address = '영어로 된 코드(해석)는 잘 봐야 한다.'
    it = index_words_iter(address)
    print(next(it)) # 0
    print(next(it)) # 4
    print(next(it)) # 6
    


  • 제너레이터가 반환하는 이터레이터를 리스트 내장 함수에 넘기면 필요할 때 제너레이터를 쉽게 리스트로 변환할 수 있다.
  • iter 용어 정리 2
    • 이터레이트(iterate): 객체 안의 값을 차례대로 받는 것을 뜻한다.
    • 이터러블(iterable): 받은 값을 순환값이라하고, 순한 과능한 객체를 이터러블이라 한다. __iter__이 정의된 객체
    • 이터레이터(iterator): __next__가 정의된 객체
    result = list(index_words_iter(address))
    print(result) # [0, 4, 6, 14, 16, 19]
    



Better way 31: 인자에 대해 이터레이션할 때는 방어적이 돼라

  • 이터레이터는 결과를 단 한 번만 만들어낸다.
  • 재사용 목적이 있다면, 이터레이터의 전체 내용을 리스트에 넣어 사용한다.
    • 하지만, 이터레이터의 내용을 복사하면 메모리가 엄청 소모될 수 있다.
  • 이터레이터 프로토콜(iterator protocol)은 파이썬의 for 루프나 그와 연관된 식들이 컨테이너 타입의 내용을 방문할 때 사용하는 절차다.
    • 파이썬에서 for x in foo와 같은 구문을 사용하면, 실제로는 iter(foo)를 호출한다.
    • iter 내장 함수는 foo.__iter__라는 특별 메서드를 호출한다.
    • __iter__ 메서드는 반드시 이터레이터 객체를 반환해야 한다.
    • for 루프는 반환받은 이터레이터 객체가 데이터를 소진할 때까지 반복적으로 이터레이터 객체에 대해 next 내장 함수(__next__)를 호출한다.
  • __iter__ 메서드를 제너레이터로 정의하면 쉽게 이터러블 컨테이너 타입을 정의할 수 있다.

    class Test:
        def __init__(self, data_path):
            self.data_path = data_path
          
        def __iter__(self):
            with open(self.data_path) as f:
                for line in f:
                    yield int(line)
              
    path = 'f:/data/test.txt'
    
    test = Test(path)
    list(test) # [12, 1, 2, 3, 4, 7]
    

with : 파일을 접근할 때 with 문으로 실행하면 오류 발생 여부와 관계없이 마지막에 close를 해주는 기능을 한다. 3



Better way 32: 긴 리스트 컴프리헨션보다는 제너레이터 식을 사용하라

  • 입력이 작은 경우만 처리할 수 있는 방식으로 리스트 컴프리헨션을 사용한다.

    value = [int(x) for x in open('test.txt')]
    print(value) # [100, 57, 15, 1, 12, 75, 5, 86, 89, 11]
    


  • 제너레이터 식(generator expression)은 리스트 컴프리헨션과 제너레이터를 일반화한 것이다.
    • 제너레이터 식을 실행해도 출력 시퀀스 전체가 실체화되지는 않는다.
    • 그 대신 제너레이터 식에 들어 있는 식으로부터 원소를 하나씩 만들어내는 이터레이터가 생성된다.
    • ( ) 사이에 리스트 컴프리헨션과 비슷한 구문을 넣어 제너레이터 식을 만들 수 있다.
  • next 내장 함수를 사용하여 다음 값을 가져온다.
  • 제너레이터 식을 사용하면 메모리를 모두 소모하는 것을 염려할 필요 없이 원소를 원하는 대로 가져와 소비할 수 있다.

    value = (int(x) for x in open('test.txt'))
    print(value)
    # <generator object <genexpr> at 0x0000017CF87E97B0>
    
    print(next(value)) # 100
    print(next(value)) # 57
    


  • 제너레이터 식의 또 다른 특징은 두 제너레이터 식을 합성할 수 있다는 점이다.

    roots = ((x, x**0.5) for x in value)
    
    print(next(roots)) # (15, 3.872983346207417)
    print(next(roots)) # (1, 1.0)
    



Better way 33: yield from을 사용해 여러 제너레이터를 합성하라

  • yield from은 파이썬 인터프리터가 사용자 대신 for 루프를 내포시키고 yield 식을 처리하도록 만든다.
  • yield from 식을 사용하면 여러 내장 제너레이터를 모아서 제너레이터 하나로 합성할 수 있다.

    def num_generator():
        x= [1, 2, 3]
        y = [2, 3]
        yield from x
        yield from y
          
      
    for i in num_generator():
        print(i)
      
    # 1
    # 2
    # 3
    # 2
    # 3
    
  • yield from에 반복가능한 객체(ex. 리스트)를 지정하면 리스트에 들어있는 요소를 한 개씩 바깥으로 전달한다. 4 위의 코드는 총 next 함수를 5번 호출한다.



Better way 34: send로 제너레이터에 데이터를 주입하지 말라

  • send 메서드를 사용해 데이터를 제너레이터에 주입할 수 있다. 제너레이터는 send로 주입된 값을 yield 식이 반환하는 값을 통해 받으며, 이 값을 변수에 저장해 활용할 수 있다.
  • 하지만 합성할 제너레이터들의 입력으로 이터레이터를 전달하는 방식이 send를 사용하는 방식보다 더 낫다. send는 가급적 사용하지 말라고 조언한다.



Better way 35: 제너레이터 안에서 throw로 상태를 변화시키지 말라

  • 제너레이터의 고급 기능으로 제너레이터 안에서 Exception을 다시 던질 수 있는 throw 메서드가 있다.
    • 어떤 제너레이터에 대해 throw가 호출되면 이 제너레이터는 값을 내놓은 yield로부터 평소처럼 제너레이터 실행을 계속하는 대신 throw가 제공한 Exception을 다시 던진다.
  • 이 기능은 제너레이터와 제너레이터를 호출하는 쪽 사이에 양방향 통신 수단을 제공한다.

    class MyError(Exception):
        pass
    
    def my_generator():
        yield 1
          
        try:
            yield 2
        except MyError:
            print('MyError 발생')
        else:
            yield 3
          
        yield 4
    
    it = my_generator()
    print(next(it)) 
    print(next(it))
    print(it.throw(MyError('test error')))
    # 1
    # 2
    # MyError 발생
    # 4
    


  • 하지만 throw를 사용하면 가독성이 나빠지기 때문에 제너레이터에서 예외적인 동작을 제공하는 더 나은 방법은 __iter__ 메서드를 구현하는 클래스를 사용하면서 예외적인 경우에 상태를 전이시키는 것이다.



Better way 36: 이터레이터나 제너레이터를 다룰 때는 itertools를 사용하라

  • itertools 내장 모듈에는 이터레이터를 조직화하거나 사용할 때 쓸모 있는 여러 함수가 들어 있다.
  • 이터레이터나 제너레이터를 다루는 itertools 함수는 세 가지 범주로 나눌 수 있다.
    • 여러 이터레이터를 연결함
    • 이터레이터의 원소를 걸러냄
    • 원소의 조합을 만들어냄


여러 이터레이터 연결하기

chain

  • 여러 이터레이터를 하나의 순차적인 이터레이터로 합치고 싶을 때 chain을 사용한다.

    import itertools
    
    it = itertools.chain([1,2,3], [4,5,6])
    list(it) # [1, 2, 3, 4, 5, 6]
    


repeat

  • 한 값을 계속 반복해 내놓고 싶을 때 repeat를 사용한다.
  • 이터레이터가 값을 내놓는 횟수를 제한하려면 repeat의 두 번째 인자로 최대 횟수를 지정하면 된다.

    it = itertools.repeat('안녕', 3)
    list(it) # ['안녕', '안녕', '안녕']
    


cycle

  • 어떤 이터레이터가 내놓는 원소들을 계속 반복하고 싶을 때는 cycle을 사용한다.

    it = itertools.cycle([1, 2])
    result = [next(it) for _ in range(10)]
    print(result) # [1, 2, 1, 2, 1, 2, 1, 2, 1, 2]
    


tee

  • 한 이터레이터를 병렬적으로 두 번째 인자로 지정된 개수의 이터레이터로 만들고 싶을 때 tee를 사용한다.

    it1, it2 = itertools.tee(['하나', '둘'], 2)
    print(list(it1))
    print(list(it2))
    
    # ['하나', '둘']
    # ['하나', '둘']
    


zip_longest

  • zip_longest는 여러 이터레이터 중 짧은 쪽 이터레이터의 원소를 다 사용한 경우 fillvalue로 지정한 값을 채워 넣어준다.
    • fillvalue로 아무 값도 지정하지 않으면 None을 넣는다.
    keys = ['하나', '둘', '셋']
    values = [1, 2]
    
    normal = list(zip(keys, values))
    print('zip: ', normal) 
    
    it = itertools.zip_longest(keys, values, fillvalue='없음')
    longest = list(it)
    print('zip_longest: ', longest)
    
    # zip:  [('하나', 1), ('둘', 2)]
    # zip_longest:  [('하나', 1), ('둘', 2), ('셋', '없음')]
    



이터레이터에서 원소 거르기

islice

  • 이터레이터를 복사하지 않으면서 원소 인덱스를 이용해 슬라이싱하고 싶을 때 islice를 사용한다.

    values = [x for x in range(1, 11)]
    
    first_five = itertools.islice(values, 5)
    print('앞에서 다섯 개:', list(first_five))
    
    middle_odds = itertools.islice(values, 2, 8, 2)
    print('중간의 홀수들: ', list(middle_odds))
    
    # 앞에서 다섯 개: [1, 2, 3, 4, 5]
    # 중간의 홀수들:  [3, 5, 7]
    


takewhile

  • takewhile은 이터레이터에서 주어진 술어가 False를 반환하는 첫 원소가 나타날 때까지 원소를 돌려준다.

    values = [x for x in range(1, 11)]
    less_than_seven = lambda x: x<7
    it = itertools.takewhile(less_than_seven, values)
    print(list(it))
    # [1, 2, 3, 4, 5, 6]
    


filterfalse

  • filterfalse는 filter 내장 함수의 반대다.

    values = [x for x in range(1, 11)]
    evens = lambda x: x % 2 == 0
    
    filter_false_result = itertools.filterfalse(evens, values)
    print(list(filter_false_result))
    # [1, 3, 5, 7, 9]
    



이터레이터에서 원소의 조합 만들어내기

accumulate

  • accumulate는 파라미터를 두 개 받는 함수를 반복 적용하면서 이터레이터 원소를 값 하나로 줄여준다.
  • 이 함수가 돌려주는 이터레이터는 원본 이터레이터의 각 원소에 대해 누적된 결과를 내놓는다.

    values = [x for x in range(1, 11)]
    sum_reduce = itertools.accumulate(values)
    print('합계: ', list(sum_reduce))
    
    def sum_modulo_20(first, second):
        output = first + second 
        return output % 20
    
    modulo_reduce = itertools.accumulate(values, sum_modulo_20)
    print('20으로 나눈 나머지의 합계: ', list(modulo_reduce))
    
    # 합계:  [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
    # 20으로 나눈 나머지의 합계:  [1, 3, 6, 10, 15, 1, 8, 16, 5, 15]
    


product

  • product는 하나 이상의 이터레이터에 들어 있는 아이템들의 데카르트 곱(Cartesian product)을 반환한다.

    single = itertools.product([1,2], repeat=2)
    print(list(single))
    
    multiple = itertools.product([1,2], ['a', 'b'])
    print(list((multiple)))
    
    # [(1, 1), (1, 2), (2, 1), (2, 2)]
    # [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b')]
    


permutations

  • permutations는 이터레이터가 내놓는 원소들로부터 만들어낸 길이 N인 순열을 돌려준다.

    it = itertools.permutations([1,2,3], 2)
    print(list(it))
    # [(1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)]
    


combinations

  • combinations는 이터레이터가 내놓는 원소들로부터 만들어낸 길이 N인 조합을 돌려준다.

    it = itertools.combinations([1,2,3], 2)
    print(list(it))
    # [(1, 2), (1, 3), (2, 3)]
    





References

댓글남기기