Chapter 2. 리스트와 딕셔너리를 읽으면서 정리합니다.

새로 알게 되었거나 중요하다고 생각되는 부분 위주로 정리하고자 합니다.

Effective Python 2nd

Chapter 2. 리스트와 딕셔너리

13. 슬라이싱보다는 나머지를 모두 잡아내는 언패킹을 사용하라

  • 언패킹 대입에 별표 식을 사용하면 언패킹 패턴에 대입되지 않는 모든 부분을 리스트에 잡아낼 수 있다.

    car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
    car_ages_descending = sorted(car_ages, reverse=True)
    oldest, second_oldest, *others = car_ages_descending
    print(oldest, second_oldest, others)
    
  • 리스트를 서로 겹치지 않게 여러 조각으로 나눌 경우, 슬라이싱과 인덱싱을 사용하기보다는 나머지를 모드 잡아내는 언패킹을 사용해야 실수할 여지가 훨씬 줄어든다.

    # 아래처럼이 아니라
    all_csv_rows = list(generate_csv())
    header = all_csv_rows[0]
    rows = all_csv_rows[1:]
    print('CSV 헤더:', header)
    print('행 수: ', len(rows))
      
    # 이렇게 구현하자
    it = generate_csv()
    header, *rows = it
    print('CSV 헤더:', header)
    print('행 수: ', len(rows))
    

14. 복잡한 기준을 사용해 정렬할 때는 key 파라미터를 사용하라

  • sort 메서드의 key 파라미터를 사용하면 리스트의 각 원소 대신 비교에 사용할 객체를 반환하는 도우미 함수를 제공할 수 있다.

    class Tool:
        def __init__(self, name, weight):
            self.name = name
            self.weight = weight
      
        def __repr__(self):
            return f'Tool({self.name!r}, {self.weight})'
      
    tools = [
        Tool('수준계', 3.5),
        Tool('해머', 1.25),
        Tool('스크류드라이버', 0.5),
        Tool('끌', 0.25),
    ]
      
    print('미정렬:', repr(tools))
    tools.sort(key=lambda x: x.name)
    print('\n정렬: ', tools)
    
  • key 함수에서 튜플을 반환하면 여러 정렬 기준을 하나로 엮을 수 있다.

    단항 부호 반전 연산자를 사요하면 정렬 순서를 반대로 바꿀 수 있다.

    power_tools.sort(key=lambda x: (-x.weight, x.name))
    print(power_tools)
    
  • 부호를 바꿀 수 없는 타입의 경우 reverse 값으로 정렬 순서를 지정하면서 sort 메서드를 여러 번 사용해야 한다.

    power_tools.sort(key=lambda x: x.name)   # name 기준 오름차순
    power_tools.sort(key=lambda x: x.weight, # weight 기준 내림차순
                     reverse=True)
    

15. 딕셔너리 삽입 순서에 의존할 때는 조심하라

  • python 3.7부터는 키를 삽입한 순서대로 돌려받는다.

  • 딕셔너리와 비슷한 객체를 쉽게 만들 수 있다.

    from collections.abc import MutableMapping
      
    # 알파벳 순으로 표시해야 할 때
    class SortedDict(MutableMapping):
        def __init__(self):
            self.data = {}
      
        def __getitem__(self, key):
            return self.data[key]
      
        def __setitem__(self, key, value):
            self.data[key] = value
      
        def __delitem__(self, key):
            del self.data[key]
      
        def __iter__(self):
            keys = list(self.data.keys())
            keys.sort()
            for key in keys:
                yield key
      
        def __len__(self):
            return len(self.data)
    
  • 딕셔너리와 비슷한 클래스를 조심스럽게 다루는 방법으로

    • dict 인스턴스의 삽입 순서 보존에 의존하지 않고 코드를 작성하는 방법

      def get_winner(ranks):
          for name, rank in ranks.items():
              if rank == 1:
                  return name
      
    • 실행 시점에 명시적으로 dict 타입 검사하는 방법

      def get_winner(ranks):
          if not isinstance(ranks, dict):
              raise TypeError('dict 인스턴스가 필요합니다')
          return next(iter(ranks))
      
    • 타입 애너테이션과 정적 분석을 사용해 dict 값을 요구하는 방법이 있다.

      # example.py
      from typing import Dict, MutableMapping
          
      def populate_ranks(votes: Dict[str, int],
                         ranks: Dict[str, int]) -> None:
          names = list(votes.keys())
          names.sort(key=votes.get, reverse=True)
          for i, name in enumerate(names, 1):
              ranks[name] = i
          
      def get_winner(ranks: Dict[str, int]) -> str:
          return next(iter(ranks))
          
      class SortedDict(MutableMapping[str, int]):
          def __init__(self):
              self.data = {}
          
          def __getitem__(self, key):
              return self.data[key]
          
          def __setitem__(self, key, value):
              self.data[key] = value
          
          def __delitem__(self, key):
              del self.data[key]
          
          def __iter__(self):
              keys = list(self.data.keys())
              keys.sort()
              for key in keys:
                  yield key
          
          def __len__(self):
              return len(self.data)
      
      • mypy 도구를 엄격한 모드로 사용한다

        $ python -m mypy --strict example.py
        

16. in을 사용하고 딕셔너리 키를 없을 때 KeyError를 처리하기보다는 get을 사용하라

  • 기존에는 아래처럼 사용했을 것이다.

    counters = {
        '품퍼니켈': 2,
        '사워도우': 1,
    }
    key = '밀'
      
    # 첫 번째 방법
    if key in counters:
        count = counters[key]
    else:
        count = 0
    counters[key] = count + 1
    # 두 번째 방법 (더 효율적)
    try:
        count = counters[key]
    except KeyError:
        count = 0
    counters[key] = count + 1
    
  • get 메서드를 이용하자

    count = counters.get(key, 0)
    counters[key] = count + 1
    
    if (names := votes.get(key)) is None:
        votes[key] = names = []
    

Note. 카운터로 이뤄진 딕셔너리를 유지해야 하는 경우 collections.Counter 클래스를 고려하자.

17. 내부 상태에서 원소가 없는 경우를 처리할 때는 setdefault보다 defaultdict를 사용하라

  • 키로 어떤 값이 들어올지 모르는 딕셔너리를 관리해야 할 때 defaultdict를 사용하라

    class Visits:
        def __init__(self):
            self.data = defaultdict(set)
      
        def add(self, country, city):
            self.data[country].add(city)
      
    visits = Visits()
    visits.add('영국', '바스')
    visits.add('영국', '런던')
    print(visits.data)
    
  • setdefault가 더 짧은 코드를 만들어내는 경우 setdefault를 사용하는 것도 고려해볼 만하다.

    visits = {
        '미국': {'뉴욕', '로스엔젤레스'},
        '일본': {'하코네'},
    }
    visits.setdefault('프랑스', set()).add('칸') # 짧다
      
    if (japan := visits.get('일본')) is None: # 길다
        visits['일본'] = japan = set()
    japan.add('교토')
      
    print(visits)
    

18. __ missing __을 사용해 키에 따라 다른 디폴트 값을 생성하는 방법을 알아두라

  • 디폴트 값을 만드는 계산 비용이 높거나 예외가 발생할 수 있는 상황에서는 setdefault 메서드를 사용하지 말라.

  • defaultdict에 전달되는 함수는 인자를 받지 않으므로 접근에 사용한 키 값에 맞는 디폴트 값을 생성하는 것을 불가능하다.

  • 디폴트 키를 만들 때 어떤 키를 사용했는지 반드시 알아야 하는 상황이라면 직접 dict의 하위 클래스와 __ missing __ 메서드를 정의하면 된다.

    class Pictures(dict):
        def __missing__(self, key):
            value = open_picture(key)
            self[key] = value
            return value
      
    pictures = Pictures()
    handle = pictures[path]
    handle.seek(0)
    image_data = handle.read()