새소식

AI 개발

[Python] collections & itertools 의 유용한 함수들

  • -

파이썬 collections 모듈

collections는 데이터 처리를 위한 유용한 함수를 제공하는데, dict, list, set, tuple 타입의 자료형에서 확장된 기능을 제공하는 built-in module이다.

 

namedtuple() factory function for creating tuple subclasses with named fields
deque list-like container with fast appends and pops on either end
ChainMap dict-like class for creating a single view of multiple mappings
Counter dict subclass for counting hashable objects
OrderedDict dict subclass that remembers the order entries were added
defaultdict dict subclass that calls a factory function to supply missing values
UserDict wrapper around dictionary objects for easier dict subclassing
UserList wrapper around list objects for easier list subclassing
UserString wrapper around string objects for easier string subclassing

 

ChainMap

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

ChainMap은 여러 dictionary나 key-val 매핑을 묶어 하나의 view로 보여주고 view이기 때문에 underlying mapping이 업데이트 되면 ChainMap도 update된다.(by reference) 

dict에서 update()를 통해 dict를 update하는 것보다 빠르기 때문에 list안에 여러 dictionary를 넣어 뭔가를 처리 할 때 그리고 여러 dictionary의 키 값 등이 모두 필요할 때 사용할 수 있겠지만 그냥 list에 넣는다거나 python에서 새로 추가된 연산자 | 등을 사용해서 대체가 가능하다. 그래서 잘 사용하지 않는 것 같다.

 

참고로 PEP 584 에서 추가된 | 연산자가 알아두면 은근 많이 활용할 일이 많을 것같다. dictionary를 합치고 싶을 때 ChainMap이나 포문을 이용하는 것보다 실용적이다.

>>> d = {'spam': 1, 'eggs': 2, 'cheese': 3}
>>> e = {'cheese': 'cheddar', 'aardvark': 'Ethel'}
>>> d | e
{'spam': 1, 'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel'}
>>> e | d
{'cheese': 3, 'aardvark': 'Ethel', 'spam': 1, 'eggs': 2}

>>> d |= e
>>> d
{'spam': 1, 'eggs': 2, 'cheese': 'cheddar', 'aardvark': 'Ethel'}

같은 key가 있으면 가장 마지막의 값이 저장된다.

 

 

Counter 

>>> # 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)]

리스트에 있는 값을 세서 값: 값의 갯수 로 매핑된 Counter라는 객체를 반환한다. Counter는 dictionary랑 유사하게 생겼는데 key가 없어도 0을 반환한다는 점만 dictionary랑 다르고 거의 같다. dict(cnt) 하면 dictionary로 바로 바꿀 수도 있다.

 

>>> c = Counter(a=4, b=2, c=0, d=-2)
>>> sorted(c.elements())
['a', 'a', 'a', 'a', 'b', 'b']

>>> Counter('abracadabra').most_common(3)
[('a', 5), ('b', 2), ('r', 2)]

>>> c = Counter(a=4, b=2, c=0, d=-2)
>>> d = Counter(a=1, b=2, c=3, d=4)
>>> c.subtract(d)
>>> c
Counter({'a': 3, 'b': 0, 'c': -3, 'd': -6})

>>> c = Counter(a=10, b=5, c=0)
>>> c.total()
15

그리고 dict를 상속받으므로 dict보다 더 많은 기능을 제공하는데 일단 count 수가 높은 대로 정렬되어 있고 elements(), most_common(), subtract() 등의 추가 함수를 제공한다.

그리고 dict에도 total()이 있지만 Counter와 다르게 동작한다. Counter에서 total()은 value들의 합이다. 그리고 fromkeys()는 Counter 객체에 없고 update는 교체하는 대신 value에 값을 더한다.

 

deque

>>> 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는 stack과 queue를 합쳐놓은 자료형이고 'double-ended queue'의 줄임말이다 deck이라고 발음하면 된다고함. 양 끝에서 append하고 pop하는 연산 모두 O(1) 성능으로 지원한다고 한다. deque와 일반 list와의 차이점은 list는 고정 길이 연산에 최적화되어 있어 pop(0)나 insert(0, v) 연산에 대해 O(n) 메모리 이동 비용이 발생한다.

오른쪽에서 데이터를 추가, 확장하고 삭제하는 함수는 append(), extend(), pop()이고 왼쪽에서 데이터를 추가, 확장하고 삭제하는 함수는 appendleft(), extendleft(), popleft()이다. 

 

 

defaultdict

>>> 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])]

defaultdict는 첫번째 인자로 dict의 value의 데이터 타입만 지정하면 키가 없더라도 dict(key) 값을 조작할 수 있게 해준다. 원래 dictionary는 key값을 초기 지정을 해주지 않고 dict(key).append(x) 처럼 key로 접근한 value에 값을 추가, 수정 등을 해주려고 하면 키 에러가 난다. 

 

 

Namedtuple

>>> origin = ('fruits', ['apple', 'banana'])
>>> clone = ('fruits', ['apple', 'banana'])

>>> origin == clone
True
>>> origin is clone
False
>>> clone = origin # Shallow copy
>>> origin == clone
True
>>> origin is clone
True

>>> origin[1] = ['peach']
TypeError
>>> origin[1].append('peach')
>>> origin
('fruits', ['apple', 'banana', 'peach'])

일단 tuple immutable한 객체로 값을 변경할 수 없다. 여기서 값을 변경할 수 없다는 것은 reference를 변경할 수 없다는 의미로 값은 변해도 상관이 없다. append()는 reference를 변경하지 않고 값을 추가하는 함수이기 때문에 reference를 유지하면서 값을 바꿀 수 있다.

 

튜플을 단순 리스트처럼 사용하는 것이 아니라 여러 데이터 자료형을 각 요소로 넣고 딕셔너리처럼 사용하고 싶을 때가 있다. 이때 튜플의 요소에 접근할 때 순서를 기억해서 index로 접근해야 해서 불편할 것이다. 이때 활용할 수 있는 것이 Namedtuple이다.

 

>>> # 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)

namedtuple을 정의할 때 ['x', 'y'] 처럼 리스트로 구분할 수도 있고 'x y' 단순 띄어쓰기나 'x, y' 콤마로 정의할 수 있다. 

 

>>> 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())

그리고 namedtuple도 tuple과 마찬가지로 immutable하기 때문에 값을 직접 변경할 수 없다. 이럴 경우 _replace 함수를 사용해 새로운 값의 객체를 생성해야 한다. 

 

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

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

>>> 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)

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

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

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

tuple을 상속받으므로 tuple에 상속된 함수들 외에도 추가 method와 attribute를 지원한다. field의 이름과 충돌을 방지하기 위해 method와 attribute는 _로 시작된다. _make()은 기존 시퀀스나 iterable로 새로운 instance를 만드는 class method이고, _asdict()는 namedtuple을 dict로 변환하고, _fields()는 field 이름을 나열하는 문자열의 tuple로 새로운 namedtuple을 만들 때 사용할 수 있다. _field_defaults()는 필드 이름을 기본 값으로 매핑하는 dictionary이다. 그리고 문자열에 저장된 field를 조회하려면 getattr() 함수를 이용하면 된다.

 

그리고 collections 모듈에 UserString, UserDict, UserList, OrderedDict 등의 객체가 있는데 아직 써보지 않았는데 나중에 쓸일이 있으면 추가로 정리해야 겠다.

 

 

 

파이썬 itertools 모듈

 

파이썬의 built-in library인 itertools는 iterable한 데이터(e.g. list)에 대해 여러 유용한 반복자를 만들때 사용하는 모듈이다. 잘 알려진 함수로는 permutations, combinatios, product 등 순열과 조합을 구할 때 사용하는 함수가 있다.

 

>>> product('ABCD', repeat=2)
AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD

>>> permutations('ABCD', 2)
AB AC AD BA BC BD CA CB CD DA DB DC

>>> combinations('ABCD', 2)
AB AC AD BC BD CD

>>> combinations_with_replacement('ABCD', 2)
AA AB AC AD BB BC BD CC CD DD

permutations에 iterable한 자료(list, tuple, str 등)을 첫번째 인자로 주고, 두번째 인자로는 갯수를 주면 첫번째 준 요소에 n개의 순열을 뽑은 결과를 반환한다. permutations에 자기 자신까지 포함한 순열을 만들고 싶을 때는 product()를 사용하고, 자기 자신을 포함한 조합을 만들고 싶을 때는 combinations_with_replacement()를 사용하면 된다.

 

Combinatoric iterators:

product() p, q, … [repeat=1] cartesian product, equivalent to a nested for-loop
permutations() p[, r] r-length tuples, all possible orderings, no repeated elements
combinations() p, r r-length tuples, in sorted order, no repeated elements
combinations_with_replacement() p, r r-length tuples, in sorted order, with repeated elements

 

 

그 외에도 itertools들의 함수에는 아래와 같은 종류들이 있다. groupby()나 accumulate()등 for문을 사용할 때 유용한 함수들이 많다.

 

Infinite iterators:

count() start, [step] start, start+step, start+2*step, … count(10) --> 10 11 12 13 14 ...
cycle() p p0, p1, … plast, p0, p1, … cycle('ABCD') --> A B C D A B C D ...
repeat() elem [,n] elem, elem, elem, … endlessly or up to n times repeat(10, 3) --> 10 10 10

 

# count(int/float/complex, step) step 만큼 더해 무한히 반복
>>> for i in count(10):
... print(i)
10, 11, 12, 13, 14, 15 .....

>>> for i in count(10, 2):
... print(i)
10, 12, 14, 16, 18 ....

>>> for i in count(2.5, 0.2):
... print(i)
2.5, 2.7, 2.9, 3.1 ...

# cycle 매개변수를 무한이 반복
>>> for ch in cycle('abcd'):
... print(ch)
a, b, c, d, a, b, c, d, a, b, c ...

>>> for num in cycle([1, 2, 3, 4]):
... print(num)
1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3 ...

# repeat object를 times만큼 반복
>>> for i in repeat(10, 2):
... print(i)
10, 10

>>> for i in repeat('a', 4):
... print(i)
a, a, a, a

>>> for i in repeat([1, 2, 3], 4):
... print(i)
[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 2, 3]

 

iterator를 무한히 반복하고 싶을 때나 여러번 반복하고 싶을 때 사용할 수 있는 함수들로 count, cycle, repeat이 있는데, while 문을 대신해서 쓸 수 있을 것 같은데 많이 사용하지는 않는 것 같다.

 

 

Iterators terminating on the shortest input sequence:

accumulate() p [,func] p0, p0+p1, p0+p1+p2, … accumulate([1,2,3,4,5]) --> 1 3 6 10 15
chain() p, q, … p0, p1, … plast, q0, q1, … chain('ABC', 'DEF') --> A B C D E F
chain.from_iterable() iterable p0, p1, … plast, q0, q1, … chain.from_iterable(['ABC', 'DEF']) --> A B C D E F
compress() data, selectors (d[0] if s[0]), (d[1] if s[1]), … compress('ABCDEF', [1,0,1,0,1,1]) --> A C E F
dropwhile() pred, seq seq[n], seq[n+1], starting when pred fails dropwhile(lambda x: x<5, [1,4,6,4,1]) --> 6 4 1
filterfalse() pred, seq elements of seq where pred(elem) is false filterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
groupby() iterable[, key] sub-iterators grouped by value of key(v) 아래에 예시 있음
islice() seq, [start,] stop [, step] elements from seq[start:stop:step] islice('ABCDEFG', 2, None) --> C D E F G
pairwise() iterable (p[0], p[1]), (p[1], p[2]) pairwise('ABCDEFG') --> AB BC CD DE EF FG
starmap() func, seq func(*seq[0]), func(*seq[1]), … starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000
takewhile() pred, seq seq[0], seq[1], until pred fails takewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4
tee() it, n it1, it2, … itn splits one iterator into n  
zip_longest() p, q, … (p[0], q[0]), (p[1], q[1]), … zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-

 

>>> from itertools import groupby

>>> [k for k, g in groupby('AAAABBBCCDAABBB')]
['A', 'B', 'C', 'D', 'A', 'B']
>>> [list(g) for k, g in groupby('AAAABBBCCD')]
[['A', 'A', 'A', 'A'], ['B', 'B', 'B'], ['C', 'C'], ['D']]

# Define a printer for comparing outputs
>>> def print_groupby(iterable, keyfunc=None):
...    for k, g in it.groupby(iterable, keyfunc):
...        print("key: '{}'--> group: {}".format(k, list(g)))

# Feature A: group consecutive occurrences
>>> print_groupby("BCAACACAADBBB")
key: 'B'--> group: ['B']
key: 'C'--> group: ['C']
key: 'A'--> group: ['A', 'A']
key: 'C'--> group: ['C']
key: 'A'--> group: ['A']
key: 'C'--> group: ['C']
key: 'A'--> group: ['A', 'A']
key: 'D'--> group: ['D']
key: 'B'--> group: ['B', 'B', 'B']

# Feature B: group all occurrences
>>> print_groupby(sorted("BCAACACAADBBB"))
key: 'A'--> group: ['A', 'A', 'A', 'A', 'A']
key: 'B'--> group: ['B', 'B', 'B', 'B']
key: 'C'--> group: ['C', 'C', 'C']
key: 'D'--> group: ['D']

특히 유용하고 자주 사용되는 함수라고 생각하는 groupby()

딕셔너리에서 특정 value를 key로 지정한 dictionary를 만들어야 했을 때 유용했던 함수이다. 첫번째 인자로 준 iterable한 자료에서 두번째 인자로 준 key와 group을 반환하는 iterator를 만들어주는 함수이다. groupby()는 SQL GROUPBY와는 다른 것이 iterable 자료가 동일한 키 함수에 대해 이미 정렬되어 있어야 한다. 유닉스의 uniq 필터와 유사하게 작동하기 때문에 키 함수의 값이 변경될 때마다 break나 새 그룹을 생성하기 때문이다. 

 

 

from itertools import groupby

val = [{'name': 'satyajit', 'address': 'btm', 'pin': 560076}, 
       {'name': 'Mukul', 'address': 'Silk board', 'pin': 560078},
       {'name': 'Preetam', 'address': 'btm', 'pin': 560076}]


for pin, list_data in groupby(sorted(val, key=lambda k: k['pin']),lambda x: x['pin']):
...     print(pin)
...     for rec in list_data:
...             print(rec)
... 


560076
{'name': 'satyajit', 'pin': 560076, 'address': 'btm'}
{'name': 'Preetam', 'pin': 560076, 'address': 'btm'}
560078
{'name': 'Mukul', 'pin': 560078, 'address': 'Silk board'}

 

data = [
    {'name': '이민서', 'blood': 'O'},
    {'name': '이영순', 'blood': 'B'},
    {'name': '이상호', 'blood': 'AB'},
    {'name': '김지민', 'blood': 'B'},
    {'name': '최상현', 'blood': 'AB'},
    {'name': '김지아', 'blood': 'A'},
    {'name': '손우진', 'blood': 'A'},
    {'name': '박은주', 'blood': 'A'}
]

# 위 데이터를 blood 기준으로 묶으려면

# 먼저 정렬 후
>>> import operator
>>> data = sorted(data, key=operator.itemgetter('blood'))
>>> data
[{'blood': 'A', 'name': '김지아'},
 {'blood': 'A', 'name': '손우진'},
 {'blood': 'A', 'name': '박은주'},
 {'blood': 'AB', 'name': '이상호'},
 {'blood': 'AB', 'name': '최상현'},
 {'blood': 'B', 'name': '이영순'},
 {'blood': 'B', 'name': '김지민'},
 {'blood': 'O', 'name': '이민서'}]
 
# groupby 사용
>>> import itertools
>>> grouped_data = itertools.groupby(data, key=operator.itemgetter('blood'))
>>> result = {}
>>> for key, group_data in grouped_data:
...     result[key] = list(group_data)
...

>>> pprint.pprint(result)
{'A': [{'blood': 'A', 'name': '김지아'},
       {'blood': 'A', 'name': '손우진'},
       {'blood': 'A', 'name': '박은주'}],
 'AB': [{'blood': 'AB', 'name': '이상호'}, {'blood': 'AB', 'name': '최상현'}],
 'B': [{'blood': 'B', 'name': '이영순'}, {'blood': 'B', 'name': '김지민'}],
 'O': [{'blood': 'O', 'name': '이민서'}]}

# 출처 : https://wikidocs.net/108940

 

위처럼 기존 dictionary가 있을 때 dictionary의 특정 key-val 값을 기준으로 데이터를 만들고 싶을 때 사용할 수 있다.

 

 

 

 

Referecnes

 

https://docs.python.org/3/library/collections.html?highlight=collections#collections.ChainMap 

https://docs.python.org/3/library/itertools.html?highlight=itertools 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.