collections — 컨테이너 데이터 타입

collections — 컨테이너 데이터 타입

collections 모듈은 Python의 범용 내장 컨테이너인 dict, list, set, tuple에 대한 대안을 제공하는 특화된 컨테이너 데이터 타입을 구현해요.

출처: Python 표준 라이브러리

본문

namedtuple() 이름 붙은 필드를 가진 tuple 서브클래스를 만드는 팩토리 함수
deque 양끝에서 빠른 추가·제거가 가능한 list형 컨테이너
ChainMap 여러 매핑을 하나의 뷰로 묶는 dict형 클래스
Counter 해시 가능한 객체를 세기 위한 dict 서브클래스
OrderedDict 항목이 추가된 순서를 기억하는 dict 서브클래스
defaultdict 빠진 값을 공급하는 팩토리 함수를 호출하는 dict 서브클래스
UserDict dict 서브클래싱을 쉽게 하기 위한 딕셔너리 객체 래퍼
UserList list 서브클래싱을 쉽게 하기 위한 리스트 객체 래퍼
UserString string 서브클래싱을 쉽게 하기 위한 문자열 객체 래퍼

ChainMap 객체

ChainMap 클래스는 여러 매핑을 빠르게 연결해 하나의 단위로 취급할 수 있게 해 줘요. 새 딕셔너리를 만들고 update()를 여러 번 호출하는 것보다 훨씬 빠른 경우가 많아요. 중첩 스코프를 시뮬레이션하는 데 쓸 수 있고 템플릿 작업에도 유용합니다.

class collections.ChainMap(*maps) — 여러 dict나 다른 매핑을 함께 묶어 하나의 갱신 가능한 뷰를 만들어요. maps를 지정하지 않으면 빈 딕셔너리 하나를 제공해서 새 체인이 항상 최소한 하나의 매핑을 갖도록 합니다.

기반 매핑은 리스트에 저장되는데, 이 리스트는 공개라서 maps 속성으로 접근·갱신할 수 있어요. 다른 상태는 없습니다. 조회는 키를 찾을 때까지 기반 매핑을 차례로 검색하고, 반대로 쓰기·갱신·삭제는 첫 번째 매핑에만 동작해요. ChainMap은 기반 매핑을 참조로 통합하므로, 기반 매핑 중 하나가 갱신되면 그 변경이 ChainMap에 반영됩니다.

일반적인 딕셔너리 메서드를 모두 지원하고, 추가로 maps 속성, 새 하위 컨텍스트를 만드는 메서드, 첫 번째 매핑을 제외한 나머지에 접근하는 프로퍼티가 있어요.

ChainMap의 반복 순서는 매핑을 마지막에서 첫 번째로 훑으면서 결정돼요.

>>> baseline = {'music': 'bach', 'art': 'rembrandt'}
>>> adjustments = {'art': 'van gogh', 'opera': 'carmen'}
>>> list(ChainMap(adjustments, baseline))
['music', 'art', 'opera']

이것은 마지막 매핑부터 시작하는 일련의 dict.update() 호출과 같은 순서를 주어요.

>>> combined = baseline.copy()
>>> combined.update(adjustments)
>>> list(combined)
['music', 'art', 'opera']

버전 3.9 변경: PEP 584에 지정된 ||= 연산자 지원 추가.

참고: Enthought CodeTools 패키지의 MultiContext 클래스는 체인 안의 어떤 매핑에도 쓸 수 있는 옵션을 제공해요. 템플릿용 Django의 Context 클래스는 읽기 전용 매핑 체인이에요. new_child() 메서드와 parents 프로퍼티처럼 컨텍스트 푸시·팝 기능도 있죠. Nested Contexts 레시피는 쓰기·기타 변경을 첫 번째 매핑에만 적용할지 체인 안의 어떤 매핑에도 적용할지 제어하는 옵션이 있어요.

ChainMap 예제와 레시피

Python 내부 조회 체인을 시뮬레이션하는 예:

import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))

사용자가 지정한 명령줄 인자가 환경 변수보다, 환경 변수가 기본값보다 우선하도록 하는 예:

import os, argparse

defaults = {'color': 'red', 'user': 'guest'}

parser = argparse.ArgumentParser()
parser.add_argument('-u', '--user')
parser.add_argument('-c', '--color')
namespace = parser.parse_args()
command_line_args = {k: v for k, v in vars(namespace).items() if v is not None}

combined = ChainMap(command_line_args, os.environ, defaults)
print(combined['color'])
print(combined['user'])

중첩 컨텍스트를 시뮬레이션하기 위한 ChainMap 사용 패턴:

c = ChainMap()        # Create root context
d = c.new_child()     # Create nested child context
e = c.new_child()     # Child of c, independent from d
e.maps[0]             # Current context dictionary -- like Python's locals()
e.maps[-1]            # Root context -- like Python's globals()
e.parents             # Enclosing context chain -- like Python's nonlocals

d['x'] = 1            # Set value in current context
d['x']                # Get first key in the chain of contexts
del d['x']            # Delete from current context
list(d)               # All nested values
k in d                # Check all nested values
len(d)                # Number of nested values
d.items()             # All nested items
dict(d)               # Flatten into a regular dictionary

ChainMap 클래스는 갱신(쓰기·삭제)을 체인 첫 매핑에만 하고 조회는 전체 체인을 검색해요. 하지만 깊은 쓰기·삭제가 필요하면 체인 안쪽에서 찾은 키를 갱신하는 서브클래스를 만들기 쉽습니다.

class DeepChainMap(ChainMap):
    'Variant of ChainMap that allows direct updates to inner scopes'

    def __setitem__(self, key, value):
        for mapping in self.maps:
            if key in mapping:
                mapping[key] = value
                return
        self.maps[0][key] = value

    def __delitem__(self, key):
        for mapping in self.maps:
            if key in mapping:
                del mapping[key]
                return
        raise KeyError(key)

>>> d = DeepChainMap({'zebra': 'black'}, {'elephant': 'blue'}, {'lion': 'yellow'})
>>> d['lion'] = 'orange'         # update an existing key two levels down
>>> d['snake'] = 'red'           # new keys get added to the topmost dict
>>> del d['elephant']            # remove an existing key one level down
>>> d                            # display result
DeepChainMap({'zebra': 'black', 'snake': 'red'}, {}, {'lion': 'orange'})

Counter 객체

편리하고 빠른 집계(tally)를 위한 카운터 도구가 제공돼요. 예:

>>> # Tally occurrences of words in a list
>>> cnt = Counter()
>>> for word in ['red', 'blue', 'red', 'green', 'blue', 'blue']:
...     cnt[word] += 1
...
>>> cnt
Counter({'blue': 3, 'red': 2, 'green': 1})

>>> # Find the ten most common words in Hamlet
>>> import re
>>> words = re.findall(r'\w+', open('hamlet.txt').read().lower())
>>> Counter(words).most_common(10)
[('the', 1143), ('and', 966), ('to', 762), ('of', 669), ('i', 631),
 ('you', 554),  ('a', 546), ('my', 514), ('hamlet', 471), ('in', 451)]

class collections.Counter(**kwargs), class collections.Counter(iterable, /, **kwargs), class collections.Counter(mapping, /, **kwargs) — 해시 가능한 객체를 세기 위한 dict 서브클래스예요. 요소는 딕셔너리 키로, 개수는 딕셔너리 값으로 저장되는 컬렉션이에요. 개수는 0이나 음수까지 포함한 모든 정수가 허용됩니다. Counter는 다른 언어의 bag이나 multiset과 비슷해요.

요소는 iterable에서 세거나 다른 매핑(또는 카운터)에서 초기화해요.

>>> c = Counter()                           # a new, empty counter
>>> c = Counter('gallahad')                 # a new counter from an iterable
>>> c = Counter({'red': 4, 'blue': 2})      # a new counter from a mapping
>>> c = Counter(cats=4, dogs=8)             # a new counter from keyword args

Counter 객체는 딕셔너리 인터페이스를 가지되, 없는 항목에 대해 KeyError를 일으키는 대신 0 개수를 반환한다는 점이 달라요.

>>> c = Counter(['eggs', 'ham'])
>>> c['bacon']                              # count of a missing element is zero
0

개수를 0으로 설정해도 카운터에서 요소가 제거되지는 않아요. 완전히 제거하려면 del을 쓰세요.

>>> c['sausage'] = 0                        # counter entry with a zero count
>>> del c['sausage']                        # del actually removes the entry

버전 3.1 추가. 버전 3.7 변경: dict 서브클래스로서 Counter는 삽입 순서 기억 능력을 상속받았어요. Counter 객체의 수학 연산도 순서를 보존합니다. 결과는 왼쪽 피연산자에서 요소가 처음 등장한 순서, 그다음 오른쪽 피연산자에서 등장한 순서로 정렬됩니다.

Counter 객체는 모든 딕셔너리에서 쓸 수 있는 메서드 외에 추가 메서드를 지원하고, 두 메서드는 카운터에서 다르게 동작해요.

Counter==, !=, <, <=, >, >= 같은 동등·부분집합·상위집합 관계의 비교 연산자를 지원해요. 모든 비교는 없는 요소를 0 개수로 취급해서 Counter(a=1) == Counter(a=1, b=0)이 참이 됩니다.

버전 3.10 변경: 비교(rich comparison) 연산이 추가되었어요. 버전 3.10 변경: 동등 비교에서 없는 요소를 0 개수로 취급해요. 이전에는 Counter(a=3)Counter(a=3, b=0)을 구별했어요.

Counter 객체 작업의 일반적인 패턴:

c.total()                       # total of all counts
c.clear()                       # reset all counts
list(c)                         # list unique elements
set(c)                          # convert to a set
dict(c)                         # convert to a regular dictionary
c.items()                       # access the (elem, cnt) pairs
Counter(dict(list_of_pairs))    # convert from a list of (elem, cnt) pairs
c.most_common()[:-n-1:-1]       # n least common elements
+c                              # remove zero and negative counts

Counter 객체를 결합해 multiset(0보다 큰 개수를 가진 카운터)을 만드는 여러 수학 연산이 제공돼요. 덧셈·뺄셈은 대응 요소의 개수를 더하거나 빼서 카운터를 결합하고, 교집합·합집합은 대응 개수의 최소·최대를 반환해요. 동등·포함 비교는 대응 개수를 비교합니다. 각 연산은 부호 있는 개수를 가진 입력을 받아들일 수 있지만, 출력은 개수가 0 이하인 결과는 제외해요.

>>> c = Counter(a=3, b=1)
>>> d = Counter(a=1, b=2)
>>> c + d                       # add two counters together:  c[x] + d[x]
Counter({'a': 4, 'b': 3})
>>> c - d                       # subtract (keeping only positive counts)
Counter({'a': 2})
>>> c & d                       # intersection:  min(c[x], d[x])
Counter({'a': 1, 'b': 1})
>>> c | d                       # union:  max(c[x], d[x])
Counter({'a': 3, 'b': 2})
>>> c == d                      # equality:  c[x] == d[x]
False
>>> c <= d                      # inclusion:  c[x] <= d[x]
False

단항 덧셈·뺄셈은 빈 카운터를 더하거나 빈 카운터에서 빼는 단축 표기예요.

>>> c = Counter(a=2, b=-4)
>>> +c
Counter({'a': 2})
>>> -c
Counter({'b': 4})

버전 3.3 추가: 단항 덧셈·단항 뺄셈·제자리 multiset 연산 지원 추가.

참고: Counter는 주로 계속 증가하는 개수를 나타내는 양의 정수와 함께 쓰도록 설계됐어요. 하지만 다른 타입이나 음수 값이 필요한 경우를 불필요하게 배제하지 않도록 신경을 썼습니다. Counter 클래스 자체는 키·값에 제한이 없는 딕셔너리 서브클래스예요. 값은 개수를 나타내는 숫자가 의도지만 값 필드에 무엇이든 저장할 수 있어요. most_common() 메서드는 값이 정렬 가능하기만 하면 돼요. c[key] += 1 같은 제자리 연산은 값 타입이 덧셈·뺄셈만 지원하면 되므로 분수·float·decimal도 동작하고 음수도 지원됩니다. update()subtract()도 입력·출력 모두 음수·0 값을 허용해요. multiset 메서드는 양수 값 사용 사례만 대상으로 설계됐어요. 입력은 음수·0이어도 되지만 양수 값을 가진 출력만 만들어집니다. 타입 제한은 없지만 값 타입이 덧셈·뺄셈·비교를 지원해야 해요. elements() 메서드는 정수 개수를 요구하고 0·음수 개수는 무시합니다.

참고: Smalltalk의 Bag 클래스, multiset에 대한 위키백과 항목, C++ multiset 튜토리얼을 참고하세요. multiset의 수학 연산과 사용 사례는 Knuth, Donald, The Art of Computer Programming Volume II, Section 4.6.3, Exercise 19를 보세요. 주어진 요소 집합 위에서 주어진 크기의 서로 다른 모든 multiset을 나열하려면 itertools.combinations_with_replacement()를 쓰세요: map(Counter, combinations_with_replacement('ABC', 2)) → AA AB AC BB BC CC.

deque 객체

class collections.deque([iterable[, maxlen]])iterable의 데이터로 왼쪽에서 오른쪽으로(append() 사용) 초기화된 새 deque 객체를 반환해요. iterable을 지정하지 않으면 새 deque는 비어 있습니다.

Deque는 스택과 큐의 일반화예요(발음은 "deck"이고 "double-ended queue"의 줄임말). deque의 양쪽에서 스레드 안전하고 메모리 효율적인 append·pop을 지원하며, 양방향 모두 대략 O(1) 성능이에요. list 객체도 비슷한 연산을 지원하지만 고정 길이 연산에 최적화돼 있고, pop(0)insert(0, v)처럼 기반 데이터 표현의 크기와 위치를 모두 바꾸는 연산에서는 O(n) 메모리 이동 비용이 발생합니다.

maxlen을 지정하지 않거나 None이면 deque는 임의 길이로 커질 수 있어요. 그 외에는 지정된 최대 길이로 제한됩니다. 제한 길이 deque가 꽉 차면 새 항목이 추가될 때 반대쪽 끝에서 같은 수의 항목이 버려져요. 제한 길이 deque는 Unix의 tail 필터와 비슷한 기능을 제공하고, 가장 최근 활동만 관심 있는 트랜잭션·데이터 풀 추적에도 유용합니다.

Deque는 내용물 타입에 대해 제네릭이에요. deque 객체는 다음 메서드와 읽기 전용 속성 하나를 지원합니다.

그 외에도 deque는 반복, pickle, len(d), reversed(d), copy.copy(d), copy.deepcopy(d), in 연산자로 멤버십 테스트, d[0] 같은 첨자 참조(첫 요소 접근)를 지원해요. 인덱스 접근은 양끝에서 O(1)이지만 중간으로 갈수록 O(n)으로 느려져요. 빠른 임의 접근이 필요하면 리스트를 쓰세요.

버전 3.5부터 deque는 __add__(), __mul__(), __imul__()을 지원합니다.

예:

>>> from collections import deque
>>> d = deque('ghi')                 # make a new deque with three items
>>> for elem in d:                   # iterate over the deque's elements
...     print(elem.upper())
G
H
I

>>> d.append('j')                    # add a new entry to the right side
>>> d.appendleft('f')                # add a new entry to the left side
>>> d                                # show the representation of the deque
deque(['f', 'g', 'h', 'i', 'j'])

>>> d.pop()                          # return and remove the rightmost item
'j'
>>> d.popleft()                      # return and remove the leftmost item
'f'
>>> list(d)                          # list the contents of the deque
['g', 'h', 'i']
>>> d[0]                             # peek at leftmost item
'g'
>>> d[-1]                            # peek at rightmost item
'i'

>>> list(reversed(d))                # list the contents of a deque in reverse
['i', 'h', 'g']
>>> 'h' in d                         # search the deque
True
>>> d.extend('jkl')                  # add multiple elements at once
>>> d
deque(['g', 'h', 'i', 'j', 'k', 'l'])
>>> d.rotate(1)                      # right rotation
>>> d
deque(['l', 'g', 'h', 'i', 'j', 'k'])
>>> d.rotate(-1)                     # left rotation
>>> d
deque(['g', 'h', 'i', 'j', 'k', 'l'])

>>> deque(reversed(d))               # make a new deque in reverse order
deque(['l', 'k', 'j', 'i', 'h', 'g'])
>>> d.clear()                        # empty the deque
>>> d.pop()                          # cannot pop from an empty deque
Traceback (most recent call last):
    File "<pyshell#6>", line 1, in -toplevel-
        d.pop()
IndexError: pop from an empty deque

>>> d.extendleft('abc')              # extendleft() reverses the input order
>>> d
deque(['c', 'b', 'a'])

deque 레시피

제한 길이 deque는 Unix의 tail 필터와 비슷한 기능을 제공해요.

def tail(filename, n=10):
    'Return the last n lines of a file'
    with open(filename) as f:
        return deque(f, n)

deque를 쓰는 또 다른 접근은 오른쪽에 append하고 왼쪽에서 pop해서 최근에 추가된 요소 시퀀스를 유지하는 거예요.

def moving_average(iterable, n=3):
    # moving_average([40, 30, 50, 46, 39, 44]) --> 40.0 42.0 45.0 43.0
    # https://en.wikipedia.org/wiki/Moving_average
    it = iter(iterable)
    d = deque(itertools.islice(it, n-1))
    d.appendleft(0)
    s = sum(d)
    for elem in it:
        s += elem - d.popleft()
        d.append(elem)
        yield s / n

라운드로빈 스케줄러는 deque에 저장된 입력 이터레이터로 구현할 수 있어요. 0번 위치의 활성 이터레이터에서 값을 내놓고, 그 이터레이터가 소진되면 popleft()로 제거하고, 아니면 rotate() 메서드로 끝에 다시 순환시켜요.

def roundrobin(*iterables):
    "roundrobin('ABC', 'D', 'EF') --> A D E B F C"
    iterators = deque(map(iter, iterables))
    while iterators:
        try:
            while True:
                yield next(iterators[0])
                iterators.rotate(-1)
        except StopIteration:
            # Remove an exhausted iterator.
            iterators.popleft()

rotate() 메서드는 deque 슬라이싱·삭제를 구현하는 방법을 제공해요. 예를 들어 del d[n]의 순수 Python 구현은 pop할 요소를 배치하는 데 rotate()에 의존합니다.

def delete_nth(d, n):
    d.rotate(-n)
    d.popleft()
    d.rotate(n)

deque 슬라이싱을 구현하려면 비슷한 접근으로 rotate()를 적용해 대상 요소를 deque 왼쪽으로 가져오고, popleft()로 옛 항목을 제거하고 extend()로 새 항목을 추가한 뒤 회전을 되돌려요. 이 접근을 조금 변형하면 dup, drop, swap, over, pick, rot, roll 같은 Forth 스타일 스택 조작을 쉽게 구현할 수 있습니다.

defaultdict 객체

class collections.defaultdict(default_factory=None, /, **kwargs), class collections.defaultdict(default_factory, mapping, /, **kwargs), class collections.defaultdict(default_factory, iterable, /, **kwargs) — 새 딕셔너리형 객체를 반환해요. defaultdict는 내장 dict 클래스의 서브클래스로, 한 메서드를 오버라이드하고 쓰기 가능한 인스턴스 변수 하나를 추가합니다. 나머지 기능은 dict 클래스와 같아서 여기선 다루지 않아요.

첫 번째 인자는 default_factory 속성의 초기값을 제공하며 기본값은 None이에요. 나머지 인자는 키워드 인자를 포함해 dict 생성자에 전달된 것과 똑같이 취급됩니다. defaultdict는 키와 값의 타입에 대해 제네릭이에요. 표준 dict 연산 외에 한 메서드와 인스턴스 변수를 지원합니다.

버전 3.9 변경: PEP 584에 지정된 merge(|)와 update(|=) 연산자 추가.

defaultdict 예제

default_factorylist를 쓰면 키-값 쌍 시퀀스를 리스트의 딕셔너리로 쉽게 그룹화할 수 있어요.

>>> s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
>>> d = defaultdict(list)
>>> for k, v in s:
...     d[k].append(v)
...
>>> sorted(d.items())
[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]

각 키가 처음 만날 때 매핑에 없으므로, 빈 list를 반환하는 default_factory 함수로 항목이 자동 생성된 뒤 list.append() 연산이 값을 새 리스트에 붙입니다. 키를 다시 만나면 조회는 정상적으로 진행되고(그 키의 리스트 반환) list.append()가 리스트에 또 다른 값을 추가해요. 이 기법은 dict.setdefault()를 쓰는 동등한 기법보다 단순하고 빠릅니다.

>>> d = {}
>>> for k, v in s:
...     d.setdefault(k, []).append(v)
...
>>> sorted(d.items())
[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]

default_factoryint로 설정하면 defaultdict를 세기(다른 언어의 bag이나 multiset처럼)에 유용하게 쓸 수 있어요.

>>> s = 'mississippi'
>>> d = defaultdict(int)
>>> for k in s:
...     d[k] += 1
...
>>> sorted(d.items())
[('i', 4), ('m', 1), ('p', 2), ('s', 4)]

문자가 처음 만날 때 매핑에 없으므로 default_factory 함수가 int()를 호출해 기본 개수 0을 공급하고, 그다음 증가 연산이 각 문자에 대한 개수를 쌓아 올려요.

항상 0을 반환하는 int() 함수는 상수 함수의 특수한 경우일 뿐이에요. 상수 함수를 더 빠르고 유연하게 만드는 방법은 0뿐 아니라 어떤 상수 값(0 이외도)도 공급할 수 있는 람다 함수를 쓰는 거예요.

>>> def constant_factory(value):
...     return lambda: value
...
>>> d = defaultdict(constant_factory('<missing>'))
>>> d.update(name='John', action='ran')
>>> '%(name)s %(action)s to %(object)s' % d
'John ran to <missing>'

default_factoryset으로 설정하면 defaultdict를 집합의 딕셔너리를 만드는 데 유용하게 쓸 수 있어요.

>>> s = [('red', 1), ('blue', 2), ('red', 3), ('blue', 4), ('red', 1), ('blue', 4)]
>>> d = defaultdict(set)
>>> for k, v in s:
...     d[k].add(v)
...
>>> sorted(d.items())
[('blue', {2, 4}), ('red', {1, 3})]

이름 붙은 필드를 가진 튜플: namedtuple() 팩토리 함수

명명된 튜플은 튜플의 각 위치에 의미를 부여하고 더 읽기 쉽고 자기 문서화된 코드를 허용해요. 일반 튜플을 쓸 수 있는 곳이면 어디든 쓸 수 있고, 위치 인덱스 대신 이름으로 필드에 접근하는 능력을 추가해요.

collections.namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)typename이라는 새 튜플 서브클래스를 반환해요. 새 서브클래스는 속성 조회로 접근 가능하고 동시에 인덱싱·반복 가능한 필드를 가진 튜플형 객체를 만드는 데 쓰입니다. 서브클래스 인스턴스에는 유용한 docstring(typenamefield_names 포함)과 name=value 형식으로 튜플 내용을 나열하는 유용한 __repr__() 메서드도 있어요.

field_names['x', 'y'] 같은 문자열 시퀀스예요. 또는 각 필드 이름이 공백·쉼표로 구분된 단일 문자열, 예를 들어 'x y''x, y'일 수도 있어요.

밑줄로 시작하는 이름을 제외한 모든 유효한 Python 식별자를 필드 이름으로 쓸 수 있어요. 유효한 식별자는 문자·숫자·밑줄로 이루어지고 숫자나 밑줄로 시작하지 않으며, class, for, return, global, pass, raise 같은 키워드가 될 수 없어요.

rename이 참이면 잘못된 필드 이름이 자동으로 위치 이름으로 대체됩니다. 예를 들어 ['abc', 'def', 'ghi', 'abc']['abc', '_1', 'ghi', '_3']으로 변환되어 키워드 def와 중복 필드 이름 abc를 없애요.

defaultsNone이거나 기본값의 iterable일 수 있어요. 기본값을 가진 필드는 기본값이 없는 필드 뒤에 와야 하므로, defaults는 가장 오른쪽 매개변수에 적용됩니다. 예를 들어 fieldnames가 ['x', 'y', 'z']이고 defaults가 (1, 2)면, x는 필수 인자가 되고 y는 기본값 1, z는 기본값 2를 가집니다.

module이 정의되면 명명된 튜플의 __module__ 속성이 그 값으로 설정됩니다.

명명된 튜플 인스턴스는 인스턴스별 딕셔너리를 가지지 않아 가벼우며 일반 튜플보다 더 많은 메모리를 요구하지 않아요. pickle을 지원하려면 명명된 튜플 클래스를 typename과 일치하는 변수에 할당해야 해요.

버전 3.1 변경: rename 지원 추가. 버전 3.6 변경: verboserename 매개변수가 키워드 전용 인자가 됐어요. module 매개변수 추가. 버전 3.7 변경: verbose 매개변수와 _source 속성 제거. defaults 매개변수와 _field_defaults 속성 추가.

>>> # Basic example
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(11, y=22)     # instantiate with positional or keyword arguments
>>> p[0] + p[1]             # indexable like the plain tuple (11, 22)
33
>>> x, y = p                # unpack like a regular tuple
>>> x, y
(11, 22)
>>> p.x + p.y               # fields also accessible by name
33
>>> p                       # readable __repr__ with a name=value style
Point(x=11, y=22)

명명된 튜플은 csvsqlite3 모듈이 반환하는 결과 튜플에 필드 이름을 부여하는 데 특히 유용해요.

EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')

import csv
for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))):
    print(emp.name, emp.title)

import sqlite3
conn = sqlite3.connect('/companydata')
cursor = conn.cursor()
cursor.execute('SELECT name, age, title, department, paygrade FROM employees')
for emp in map(EmployeeRecord._make, cursor.fetchall()):
    print(emp.name, emp.title)

튜플에서 상속받은 메서드 외에 명명된 튜플은 추가 메서드 세 개와 속성 두 개를 지원해요. 필드 이름과의 충돌을 막기 위해 메서드·속성 이름은 밑줄로 시작합니다.

classmethod somenamedtuple._make(iterable, /) — 기존 시퀀스나 iterable에서 새 인스턴스를 만드는 클래스 메서드.

>>> t = [11, 22]
>>> Point._make(t)
Point(x=11, y=22)

somenamedtuple._asdict() — 필드 이름을 해당 값에 매핑하는 새 dict를 반환해요.

>>> p = Point(x=11, y=22)
>>> p._asdict()
{'x': 11, 'y': 22}

버전 3.1 변경: 일반 dict 대신 OrderedDict 반환. 버전 3.8 변경: OrderedDict 대신 일반 dict 반환. Python 3.7부터 일반 dict도 정렬이 보장됩니다. OrderedDict의 추가 기능이 필요하면 OrderedDict(nt._asdict())처럼 결과를 원하는 타입으로 변환하세요.

somenamedtuple._replace(**kwargs) — 지정된 필드를 새 값으로 바꾼 명명된 튜플의 새 인스턴스를 반환해요.

>>> p = Point(x=11, y=22)
>>> p._replace(x=33)
Point(x=33, y=22)

>>> for partnum, record in inventory.items():
...     inventory[partnum] = record._replace(price=newprices[partnum], timestamp=time.now())

명명된 튜플은 제네릭 함수 copy.replace()의 지원도 받아요.

버전 3.13 변경: 잘못된 키워드 인자에 ValueError 대신 TypeError 발생.

somenamedtuple._fields — 필드 이름을 나열하는 문자열 튜플. 내부 검사와 기존 명명된 튜플에서 새 명명된 튜플 타입을 만드는 데 유용해요.

>>> p._fields            # view the field names
('x', 'y')

>>> Color = namedtuple('Color', 'red green blue')
>>> Pixel = namedtuple('Pixel', Point._fields + Color._fields)
>>> Pixel(11, 22, 128, 255, 0)
Pixel(x=11, y=22, red=128, green=255, blue=0)

somenamedtuple._field_defaults — 필드 이름을 기본값에 매핑하는 딕셔너리.

>>> Account = namedtuple('Account', ['type', 'balance'], defaults=[0])
>>> Account._field_defaults
{'balance': 0}
>>> Account('premium')
Account(type='premium', balance=0)

이름이 문자열에 저장된 필드를 가져오려면 getattr() 함수를 쓰세요.

>>> getattr(p, 'x')
11

딕셔너리를 명명된 튜플로 변환하려면 이중 별표 연산자(인자 목록 풀기 참고)를 쓰세요.

>>> d = {'x': 11, 'y': 22}
>>> Point(**d)
Point(x=11, y=22)

명명된 튜플은 일반 Python 클래스라서 서브클래스로 기능을 쉽게 추가·변경할 수 있어요. 계산 필드와 고정 폭 출력 형식을 추가하는 방법은 다음과 같습니다.

>>> class Point(namedtuple('Point', ['x', 'y'])):
...     __slots__ = ()
...     @property
...     def hypot(self):
...         return (self.x ** 2 + self.y ** 2) ** 0.5
...     def __str__(self):
...         return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

>>> for p in Point(3, 4), Point(14, 5/7):
...     print(p)
Point: x= 3.000  y= 4.000  hypot= 5.000
Point: x=14.000  y= 0.714  hypot=14.018

위 서브클래스는 __slots__을 빈 튜플로 설정하는데, 인스턴스 딕셔너리 생성을 막아 메모리 요구를 낮춰요.

새로 저장되는 필드를 추가하는 데 서브클래싱은 유용하지 않아요. 대신 _fields 속성에서 새 명명된 튜플 타입을 만들면 됩니다.

>>> Point3D = namedtuple('Point3D', Point._fields + ('z',))

docstring은 __doc__ 필드에 직접 할당해 커스터마이즈할 수 있어요.

>>> Book = namedtuple('Book', ['id', 'title', 'authors'])
>>> Book.__doc__ += ': Hardcover book in active collection'
>>> Book.id.__doc__ = '13-digit ISBN'
>>> Book.title.__doc__ = 'Title of first printing'
>>> Book.authors.__doc__ = 'List of authors sorted by last name'

버전 3.5 변경: 프로퍼티 docstring이 쓰기 가능해졌어요.

참고: 명명된 튜플에 타입 힌트를 추가하려면 typing.NamedTuple을 보세요. class Component(NamedTuple): part_number: int; weight: float; description: Optional[str] = None처럼 class 키워드를 쓰는 우아한 표기도 제공해요. 튜플 대신 기반 딕셔너리를 쓰는 변경 가능한 네임스페이스는 types.SimpleNamespace()를 참고하세요. dataclasses 모듈은 사용자 정의 클래스에 생성된 특수 메서드를 자동으로 추가하는 데코레이터·함수를 제공해요.

OrderedDict 객체

정렬 딕셔너리는 일반 딕셔너리와 같지만 순서 관련 연산에 몇 가지 추가 기능이 있어요. 내장 dict 클래스가 삽입 순서 기억 능력을 얻으면서(Python 3.7에서 보장됨) 중요도는 줄었습니다.

dict와의 몇 가지 차이는 여전히 남아 있어요.

  • 일반 dict는 매핑 연산에 매우 뛰어나도록 설계됐어요. 삽입 순서 추적은 부차적이었어요.
  • OrderedDict는 재정렬 연산에 뛰어나도록 설계됐어요. 공간 효율성·반복 속도·갱신 연산 성능은 부차적이었어요.
  • OrderedDict 알고리즘은 dict보다 빈번한 재정렬 연산을 더 잘 처리해요. 아래 레시피처럼 다양한 종류의 LRU 캐시 구현에 적합한 이유예요.
  • OrderedDict의 동등 연산은 순서 일치를 확인해요. 일반 dict는 p == q and all(k1 == k2 for k1, k2 in zip(p, q))로 순서 민감 동등 테스트를 흉내 낼 수 있어요.
  • OrderedDictpopitem() 메서드는 시그니처가 달라요. 어느 항목을 pop할지 지정하는 선택적 인자를 받습니다. 일반 dict는 d.popitem()으로 OrderedDictod.popitem(last=True)를 흉내 내는데, 가장 오른쪽(마지막) 항목을 pop한다는 게 보장돼요. (k := next(iter(d)), d.pop(k))od.popitem(last=False)를 흉내 내서 존재하면 가장 왼쪽(첫) 항목을 반환·제거할 수 있어요.
  • OrderedDict에는 요소를 끝점으로 효율적으로 재배치하는 move_to_end() 메서드가 있어요. 일반 dict는 d[k] = d.pop(k)od.move_to_end(k, last=True)를 흉내 내서 키·연관 값을 가장 오른쪽(마지막) 위치로 옮겨요. OrderedDictod.move_to_end(k, last=False)(가장 왼쪽/첫 위치로 이동)에 대한 효율적인 동등 구현은 일반 dict에 없어요.
  • Python 3.8까지 dict에는 __reversed__() 메서드가 없었어요.

class collections.OrderedDict(**kwargs), class collections.OrderedDict(mapping, /, **kwargs), class collections.OrderedDict(iterable, /, **kwargs) — 딕셔너리 순서 재배치에 특화된 메서드를 가진 dict 서브클래스 인스턴스를 반환해요.

버전 3.1 추가.

일반 매핑 메서드 외에도 정렬 딕셔너리는 reversed()를 사용한 역방향 반복을 지원해요. OrderedDict 객체 간 동등 테스트는 순서에 민감하고 대략 list(od1.items())==list(od2.items())와 같습니다. OrderedDict 객체와 다른 Mapping 객체 사이의 동등 테스트는 일반 딕셔러리처럼 순서에 둔감해요. 그래서 OrderedDict 객체를 일반 딕셔너리가 쓰이는 곳 어디든 대체할 수 있어요.

버전 3.5 변경: OrderedDict의 items·keys·values 뷰가 reversed()로 역방향 반복을 지원. 버전 3.6 변경: PEP 468 수용으로 OrderedDict 생성자와 update() 메서드에 전달된 키워드 인자의 순서가 유지됩니다. 버전 3.9 변경: PEP 584에 지정된 merge(|)와 update(|=) 연산자 추가.

OrderedDict 예제와 레시피

키가 마지막으로 삽입된 순서를 기억하는 정렬 딕셔너리 변형을 만드는 건 간단해요. 새 항목이 기존 항목을 덮어쓰면 원래 삽입 위치가 바뀌어 끝으로 이동합니다.

class LastUpdatedOrderedDict(OrderedDict):
    'Store items in the order the keys were last added'

    def __setitem__(self, key, value):
        super().__setitem__(key, value)
        self.move_to_end(key)

OrderedDict@functools.lru_cache의 변형을 구현하는 데도 유용해요.

from collections import OrderedDict
from time import monotonic

class TimeBoundedLRU:
    "LRU Cache that invalidates and refreshes old entries."

    def __init__(self, func, maxsize=128, maxage=30):
        self.cache = OrderedDict()      # { args : (timestamp, result)}
        self.func = func
        self.maxsize = maxsize
        self.maxage = maxage

    def __call__(self, *args):
        if args in self.cache:
            self.cache.move_to_end(args)
            timestamp, result = self.cache[args]
            if monotonic() - timestamp <= self.maxage:
                return result
        result = self.func(*args)
        self.cache[args] = monotonic(), result
        if len(self.cache) > self.maxsize:
            self.cache.popitem(last=False)
        return result
class MultiHitLRUCache:
    """ LRU cache that defers caching a result until
        it has been requested multiple times.

        To avoid flushing the LRU cache with one-time requests,
        we don't cache until a request has been made more than once.

    """

    def __init__(self, func, maxsize=128, maxrequests=4096, cache_after=1):
        self.requests = OrderedDict()   # { uncached_key : request_count }
        self.cache = OrderedDict()      # { cached_key : function_result }
        self.func = func
        self.maxrequests = maxrequests  # max number of uncached requests
        self.maxsize = maxsize          # max number of stored return values
        self.cache_after = cache_after

    def __call__(self, *args):
        if args in self.cache:
            self.cache.move_to_end(args)
            return self.cache[args]
        result = self.func(*args)
        self.requests[args] = self.requests.get(args, 0) + 1
        if self.requests[args] <= self.cache_after:
            self.requests.move_to_end(args)
            if len(self.requests) > self.maxrequests:
                self.requests.popitem(last=False)
        else:
            self.requests.pop(args, None)
            self.cache[args] = result
            if len(self.cache) > self.maxsize:
                self.cache.popitem(last=False)
        return result

UserDict 객체

UserDict 클래스는 딕셔너리 객체를 감싸는 래퍼 역할을 해요. dict에서 직접 서브클래싱할 수 있게 되면서 이 클래스의 필요성은 다소 줄었지만, 기반 딕셔너리를 속성으로 접근할 수 있어서 다루기 더 쉬울 수 있어요.

class collections.UserDict(**kwargs), class collections.UserDict(mapping, /, **kwargs), class collections.UserDict(iterable, /, **kwargs) — 딕셔너리를 시뮬레이션하는 클래스예요. 인스턴스 내용은 일반 딕셔너리에 저장되며 UserDict 인스턴스의 data 속성으로 접근할 수 있어요. 인자가 주어지면 일반 딕셔너리처럼 data 초기화에 사용됩니다. 매핑의 메서드·연산을 지원하는 것 외에 UserDict 인스턴스는 data 속성을 제공해요.

UserList 객체

이 클래스는 리스트 객체를 감싸는 래퍼 역할을 해요. 상속받아 기존 메서드를 오버라이드하거나 새 메서드를 추가할 수 있는 자신만의 리스트형 클래스의 유용한 기본 클래스예요. 이렇게 해서 리스트에 새 동작을 추가할 수 있습니다. list에서 직접 서브클래싱할 수 있게 되면서 필요성은 다소 줄었지만, 기반 리스트를 속성으로 접근할 수 있어 다루기 더 쉬울 수 있어요.

class collections.UserList([list]) — 리스트를 시뮬레이션하는 클래스예요. 인스턴스 내용은 일반 리스트에 저장되며 UserList 인스턴스의 data 속성으로 접근할 수 있어요. 인스턴스 내용은 처음에 list의 복사본으로 설정되며 기본값은 빈 리스트 []예요. list는 실제 Python 리스트나 UserList 객체 같은 모든 iterable일 수 있어요. 변경 가능 시퀀스의 메서드·연산을 지원하는 것 외에 data 속성을 제공합니다.

서브클래싱 요구사항: UserList의 서브클래스는 인자 없이 또는 인자 하나로 호출할 수 있는 생성자를 제공해야 해요. 새 시퀀스를 반환하는 리스트 연산은 실제 구현 클래스의 인스턴스를 만들려 시도하는데, 그러려면 데이터 소스로 쓰일 시퀀스 객체 하나를 매개변수로 받는 생성자가 호출 가능하다고 가정합니다. 파생 클래스가 이 요구사항을 지키고 싶지 않으면 이 클래스가 지원하는 모든 특수 메서드를 오버라이드해야 해요. 그 경우 제공해야 할 메서드에 대한 정보는 소스를 참고하세요.

UserString 객체

UserString 클래스는 문자열 객체를 감싸는 래퍼 역할을 해요. str에서 직접 서브클래싱할 수 있게 되면서 필요성은 다소 줄었지만, 기반 문자열을 속성으로 접근할 수 있어 다루기 더 쉬울 수 있어요.

class collections.UserString(seq) — 문자열 객체를 시뮬레이션하는 클래스예요. 인스턴스 내용은 일반 문자열 객체에 저장되며 UserString 인스턴스의 data 속성으로 접근할 수 있어요. 인스턴스 내용은 처음에 seq의 복사본으로 설정되고, seq 인자는 내장 str() 함수로 문자열로 변환할 수 있는 어떤 객체든 될 수 있어요. 문자열의 메서드·연산을 지원하는 것 외에 data 속성을 제공합니다.

버전 3.5 변경: __getnewargs__, __rmod__, casefold, format_map, isprintable, maketrans 메서드 추가.

더 알아보기