ch01

  |   Source

1. 자료구조와 알고리즘

  • start: 2014-09-30
  • end: 2014-10-02

Python3

정의

  • 파이썬은 list, set, dictionary 같이 유용한 자료 구조 내장
  • 이러한 구조체의 장점은 사용이 편리
  • 검색, 정렬, 순서, 여과 등에 대한 질문이 종종 생김

1.1 시퀀스를 개별 변수로 나누기

문제

  • N개의 요소를 가진 튜플이나 시퀀스가 있다. 이를 변수 N개로 나누어야 한다.

해결

  • 모든 시퀀스(혹은 이터레이팅 가능한 것)는 간단한 할당문을 사용해서 개별 변수로 나눔
  • 주의사항: 변수의 개수가 시퀀스에 일치해야 한다는 것뿐
In [17]:
!python --version
Python 3.4.1
In [2]:
p = (4, 5)
In [3]:
x, y = p
In [4]:
x
Out[4]:
4
In [5]:
y
Out[5]:
5
In [6]:
data = ['ACME', 50, 91.1, (2012, 12, 21)]
In [7]:
name, shares, price, date = data
In [8]:
name
Out[8]:
'ACME'
In [9]:
shares
Out[9]:
50
In [10]:
price
Out[10]:
91.1
In [11]:
date
Out[11]:
(2012, 12, 21)
In [12]:
name, shares, price, (year, mon, day) = data
In [13]:
name
Out[13]:
'ACME'
In [14]:
year
Out[14]:
2012
In [15]:
mon
Out[15]:
12
In [16]:
day
Out[16]:
21

요소 개수가 일치하지 않으면 에러 발생

In [17]:
p = (4, 5)
In [18]:
x, y, z = p
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-18-abf102a00aa7> in <module>()
----> 1 x, y, z = p

ValueError: need more than 2 values to unpack

토론

  • unpacking은 사실 튜플이나 리스트뿐만 아니라 순환 가능한 모든 객체에 적용 가능
  • 문자열, 파일, iterator, generator도 포함
In [19]:
s = 'Hello'
In [20]:
a, b, c, d, e = s
In [21]:
a
Out[21]:
'H'
In [22]:
b
Out[22]:
'e'
In [23]:
c
Out[23]:
'l'
In [24]:
d
Out[24]:
'l'
In [25]:
e
Out[25]:
'o'
In [26]:
data = ['ACME', 50, 91.1, (2012, 12, 21)]
In [27]:
# _는 이전 셀의 결과값 저장
_, shares, price, _ = data
In [28]:
shares
Out[28]:
50
In [29]:
price
Out[29]:
91.1

1.2 임의 순환체의 요소 나누기

문제

  • 순환체를 언패킹하려는데 요소가 N개 이상 포함되어 "값이 너무 많습니다"" 라는 예외가 발생

해결

In [1]:
ll = range(10)
first, *middle, last = ll
In [13]:
# only Python3

def avg(lst):
    return sum(lst) / float(len(lst))

def drop_first_last(grades):
    first, *middle, last = grades
    return avg(middle)
In [4]:
# python2 version
def drop_first_last2(grades):
    lst = grades[1:-1]
    return sum(lst) / float(len(lst))
In [5]:
import random
l = [random.randint(1, 20) for i in range(10)] 
l
Out[5]:
[15, 10, 1, 4, 3, 18, 3, 9, 4, 16]
In [6]:
sum(l)
Out[6]:
83
In [8]:
sum(l) / float(len(l))
Out[8]:
8.3
In [9]:
l[1:-1]
Out[9]:
[10, 1, 4, 3, 18, 3, 9, 4]
In [10]:
sum(l[1:-1]) / float(len(l[1:-1]))
Out[10]:
6.5
In [14]:
drop_first_last(l)
Out[14]:
6.5
In [15]:
drop_first_last2(l)
Out[15]:
6.5
In [18]:
# numpy version
import numpy as np
np.mean(l)
Out[18]:
8.3000000000000007
In [20]:
# python2 version
def drop_first_last3(grades):
    lst = grades[1:-1]
    return np.mean(lst)
In [22]:
drop_first_last3(l)
Out[22]:
6.5
사용자 주소
In [23]:
record = ('Dave', 'dave@example.com', '773-555-1212', '847-555-1212')
In [24]:
name, email, *phone_numbers = record
In [25]:
name
Out[25]:
'Dave'
In [26]:
email
Out[26]:
'dave@example.com'
In [27]:
phone_numbers
Out[27]:
['773-555-1212', '847-555-1212']
In [28]:
def compare_trailing_current(sales_record):
    *trailing_qtrs, current_qtr = sales_record
    trailing_avg = sum(trailing_qtrs) / len(trailing_qtrs)
    return avg_comparison(trailing_avg, current_qtr)
In [29]:
*trailing, current = [10, 8, 7, 1, 9, 5, 10, 3]
In [30]:
trailing
Out[30]:
[10, 8, 7, 1, 9, 5, 10]
In [31]:
current
Out[31]:
3

토론

  • 언패킹 방식은 길이를 알 수 없는 순환체에 안성맞춤
  • 떄때로 순환체에 들어있는 패턴이나 구조(예를 들어 1 뒤에 나오는 요소는 무조건 전화번호)를 가지고 있는데, 이럴 때도 별표 구문을 사용하면 개발자의 수고를 많이 덜어줌
  • 길이가 일정하지 않은 튜플에 사용하면 상당히 편리
In [32]:
records = [
('foo', 1, 2),
('bar', 'hello'),
('foo', 3, 4)
]
In [33]:
def do_foo(x, y):
    print('foo', x, y)
In [34]:
def do_bar(s):
    print('bar', s)
In [37]:
# records에서 tag빼고 나머지는 *args 로 받고
# 그 받은 *args를 다시 함수에 *args로 넘겨주네
# 받는 함수는 x, y로 되어있는것 하나와 s로 되어 있는것 하나
for tag, *args in records:
    if tag == 'foo':
        do_foo(*args)
    elif tag == 'bar':
        do_bar(*args)
foo 1 2
bar hello
foo 3 4
  • 문자열 프로세싱에도 사용
In [38]:
line = 'nobody:*:-2:-2:Unprivileged user:/var/empty:/usr/bin/false'
In [39]:
line.split(':')
Out[39]:
['nobody',
 '*',
 '-2',
 '-2',
 'Unprivileged user',
 '/var/empty',
 '/usr/bin/false']
In [40]:
uname, *fields, homedir, sh = line.split(':')
In [41]:
uname
Out[41]:
'nobody'
In [43]:
homedir
Out[43]:
'/var/empty'
In [44]:
sh
Out[44]:
'/usr/bin/false'
  • Unpacking 후에 특정값을 버리고 싶다면?
In [45]:
record = ('ACME', 50, 123.45, (12, 18, 2012))
In [46]:
# *fields 대신에 *_
name, *_, (*_, year) = record
In [47]:
name
Out[47]:
'ACME'
In [48]:
year
Out[48]:
2012
In [50]:
items = [1, 10, 7, 4, 5, 9]
In [51]:
head, *tail = items
In [52]:
head
Out[52]:
1
In [53]:
tails
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-53-74d54ee14e9f> in <module>()
----> 1 tails

NameError: name 'tails' is not defined
In [54]:
def sum2(items):
    head, *tail = items
    return head + sum2(tail) if tail else head
In [55]:
sum2(items)
Out[55]:
36
In [56]:
sum(items)
Out[56]:
36
  • 하지만 파이썬의 재귀적 제약이 존재하므로 마지막에 예로 든 함수는 학문적 호기심 이외는 쓸모없음
파이썬의 재귀적 재약
  • 사실 팩토리얼 정도의 코드라면 C언어에서의 제한은 그렇게 크지 않습니다. 어떤 제한이 걸리냐 하면 운영체제마다 구조는 좀 다를 수는 있지만, C에서 스택의 크기는 쓰레드 안에서 작은 것은 64kB, 큰 것은 2MB정도 됩니다. 거기서 팩토리얼 함수는 지역변수를 전혀 안 쓰고도 구현할 수 있기 떄문에 지역변수는 스택포인터 증가에 영향을 주지 않고, 인수 전달과 리턴값, 상태가 저장되는 레지스터의 저장 등 때문에 아키텍처마다 다르지만 OS X에서는 48바이트 정도 다른 곳에서도 대략 8~128바이트 사이로 쓰게 됩니다. 따라서, 계산해보면 스택이 넘치지 않으면서 재귀호출을 할 수 있는 것은 대략 10000~100000번 내외라고 볼 수 있습니다.

  • 파이썬도 C프로그램이기 때문에 이런 스택 크기의 제약을 받는데, 파이썬 인터프리터는 팩토리얼 함수보다 훨씬 지역함수도 많고, 한 번 호출 때 한 함수만 거치는 것이 아니라서, 팩토리얼 함수보다 한 번 재귀 호출에 쓰는 스택이 많이 늘어납니다. 그래서 보통 작은 데서는 500회, 많은 데서는 5000회 정도 재귀호출을 할 수 있고, 그 이상 되면 스택 오버플로우로 파이썬 VM 자체가 죽어버립니다. 그 것을 막으려고 파이썬에서는 재귀호출을 파이썬 내부에서 관리하고 있습니다.

  • 스택리스 파이썬은 이런 파이썬 함수가 파이썬 함수를 부르는 과정을 재귀함수로 처리하지 않고 메모리를 힙에 할당해서 씁니다. 그래서 시스템 스택 크기 제약을 받지 않게 되는 덕에 메모리가 허용하는 한 무한히 재귀호출을 할 수 있습니다. http://www.stackless.com/

1.3 마지막 N개 아이템 유지

문제

  • 순환이나 프로세싱 중 마지막으로 발견한 N개의 아이템을 유지하고 싶다.

해결

  • 이와 같은 용도로 collections.deque
In [63]:
from collections import deque

def search(lines, pattern, history=5):
    previous_lines = deque(maxlen=history)
    for line in lines:
        if pattern in line:
            yield line, previous_lines
        previous_lines.append(line)
        
if __name__ == '__main__':
    with open('README.md') as f:
        for line, prevlines in search(f, 'Python', 5):
            for pline in prevlines:
                print(pline, end='')
            print(line, end='')
            print('-' * 20)
# Python Cookbook - Third Edition
--------------------
# Python Cookbook - Third Edition

![Python cookbook](http://image.yes24.com/goods/11949491/L)
--------------------
- [Source Code](https://github.com/dabeaz/python-cookbook)

## 이 책을 선택한 이유

- 기본 서적을 보기에는 너무 지루한 사람들을 위한 책
- Python 중수에게 필요한 책
--------------------
## 이 책을 선택한 이유

- 기본 서적을 보기에는 너무 지루한 사람들을 위한 책
- Python 중수에게 필요한 책
- 유명한 라이브러리들을 살펴보는데 몇 가지 익숙하지 않은 문법들이 존재했다. 나름 파이썬 공부 많이했다고 생각했는데 이것들을 이해할 수가 없으니 짜증이 났는데 한글책이 나왔다길래 바로 구매!
- Python3로 작성되서 음..? 난 Python 2.7.2
--------------------
- Python3로 작성되서 음..? 난 Python 2.7.2
- 안되는거 있으면 [stackoverflow](www.stackoverflow.com/) 찾아보면서 헤쳐나가자!

## 이 책을 정리하면서 들었던 생각

## IPython으로 정리한 내용
--------------------
In [65]:
help(deque)
Help on class deque in module collections:

class deque(builtins.object)
 |  deque([iterable[, maxlen]]) --> deque object
 |  
 |  Build an ordered collection with optimized access from its endpoints.
 |  
 |  Methods defined here:
 |  
 |  __copy__(...)
 |      Return a shallow copy of a deque.
 |  
 |  __delitem__(self, key, /)
 |      Delete self[key].
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getitem__(self, key, /)
 |      Return self[key].
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __iadd__(self, value, /)
 |      Implement self+=value.
 |  
 |  __init__(self, /, *args, **kwargs)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |  
 |  __iter__(self, /)
 |      Implement iter(self).
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __len__(self, /)
 |      Return len(self).
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  __reduce__(...)
 |      Return state information for pickling.
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __reversed__(...)
 |      D.__reversed__() -- return a reverse iterator over the deque
 |  
 |  __setitem__(self, key, value, /)
 |      Set self[key] to value.
 |  
 |  __sizeof__(...)
 |      D.__sizeof__() -- size of D in memory, in bytes
 |  
 |  append(...)
 |      Add an element to the right side of the deque.
 |  
 |  appendleft(...)
 |      Add an element to the left side of the deque.
 |  
 |  clear(...)
 |      Remove all elements from the deque.
 |  
 |  count(...)
 |      D.count(value) -> integer -- return number of occurrences of value
 |  
 |  extend(...)
 |      Extend the right side of the deque with elements from the iterable
 |  
 |  extendleft(...)
 |      Extend the left side of the deque with elements from the iterable
 |  
 |  pop(...)
 |      Remove and return the rightmost element.
 |  
 |  popleft(...)
 |      Remove and return the leftmost element.
 |  
 |  remove(...)
 |      D.remove(value) -- remove first occurrence of value.
 |  
 |  reverse(...)
 |      D.reverse() -- reverse *IN PLACE*
 |  
 |  rotate(...)
 |      Rotate the deque n steps to the right (default n=1).  If n is negative, rotates left.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  maxlen
 |      maximum size of a deque or None if unbounded
 |  
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |  
 |  __hash__ = None

토론

  • 아이템을 찾는 코드를 작성할 때, 주로 yield를 포함한 제너레이터 함수를 만들곤 함
  • 이렇게 하면 검색 과정과 결과를 사용하는 코드를 분리 가능
  • 제너레이터가 무엇인지 레시피 4.3 참고
  • dque(maxlen=N)으로 고정 크기 큐를 생성
  • 큐가 꽉 찬 상태에서 새로운 아이템을 넣으면 가장 마지막 아이템이 자동으로 삭제됨
In [66]:
q = deque(maxlen=3)
In [68]:
q.append(1)
In [69]:
q.append(2)
In [70]:
q.append(3)
In [71]:
q
Out[71]:
deque([1, 2, 3], maxlen=3)
In [72]:
q.append(4)
In [73]:
q
Out[73]:
deque([2, 3, 4], maxlen=3)
In [74]:
q.append(5)
In [75]:
q
Out[75]:
deque([3, 4, 5], maxlen=3)
  • 리스트를 사용해서 이런 과정을 수동으로 처리할 수도 있지만, 큐를 사용하는 것이 더 빠르고 보기도 좋음
  • 큐 구조체가 필요할 때 deque를 사용 가능
  • 최대 크기를 지정하지 않으면 제한없이 양쪽에 아이템을 넣거나 빼는 작업 가능
In [76]:
q = deque()
In [77]:
q.append(1)
In [78]:
q.append(2)
In [79]:
q.append(3)
In [80]:
q
Out[80]:
deque([1, 2, 3])
In [81]:
q.appendleft(4)
In [82]:
q
Out[82]:
deque([4, 1, 2, 3])
In [83]:
q.pop()
Out[83]:
3
In [84]:
q
Out[84]:
deque([4, 1, 2])
In [85]:
q.popleft()
Out[85]:
4
In [86]:
q
Out[86]:
deque([1, 2])

1.4 N 아이템의 최대 혹은 최소값 찾기

문제

  • 컬렉션 내부에서 가장 크거나 작은 N개의 아이템을 찾아야 한다.

해결

  • heapq 모듈에는 이 용도에 적합한 nlargest()와 nsmallest() 두 함수가 있다.

  • 와 굉장히 편하네
  • 내가 만든다고 삽질 안해도 그냥 가져다 쓰면 되겠네
  • 이 리스트 중에서 가장 큰 or 작은 3개를 가져오려고 할때 꽤 되잖아.

In [88]:
import heapq

nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
print(heapq.nlargest(3, nums))
print(heapq.nsmallest(3, nums))
[42, 37, 23]
[-4, 1, 2]
In [89]:
portfolio = [
    {'name': 'IBM', 'shares': 100, 'price': 91.1},
    {'name': 'AAPL', 'shares': 50, 'price': 5432.22},
    {'name': 'FB', 'shares': 200, 'price': 21.09},
    {'name': 'HPQ', 'shares': 35, 'price': 31.75},
    {'name': 'YAHOO', 'shares': 45, 'price': 16.35},
    {'name': 'ACME', 'shares': 75, 'price': 115.65}
]
  • key에 정렬해주고 싶은값을 넘겨주면 된다.
  • lambda 형식으로
In [91]:
cheap = heapq.nsmallest(3, portfolio, key=lambda s: s['price'])
cheap
Out[91]:
[{'price': 16.35, 'shares': 45, 'name': 'YAHOO'},
 {'price': 21.09, 'shares': 200, 'name': 'FB'},
 {'price': 31.75, 'shares': 35, 'name': 'HPQ'}]
In [92]:
expensive = heapq.nlargest(3, portfolio, key=lambda s: s['price'])
expensive
Out[92]:
[{'price': 5432.22, 'shares': 50, 'name': 'AAPL'},
 {'price': 115.65, 'shares': 75, 'name': 'ACME'},
 {'price': 91.1, 'shares': 100, 'name': 'IBM'}]
In [93]:
cheap2 = heapq.nsmallest(3, portfolio, key=lambda s: s['shares'])
cheap2
Out[93]:
[{'price': 31.75, 'shares': 35, 'name': 'HPQ'},
 {'price': 16.35, 'shares': 45, 'name': 'YAHOO'},
 {'price': 5432.22, 'shares': 50, 'name': 'AAPL'}]
In [106]:
# sorted를 활용한 연산
# 똑같기는 한데 nsmallest가 훨씬 가독성이 좋다.
sorted(portfolio, key=lambda s: s['shares'])[:3]
Out[106]:
[{'price': 31.75, 'shares': 35, 'name': 'HPQ'},
 {'price': 16.35, 'shares': 45, 'name': 'YAHOO'},
 {'price': 5432.22, 'shares': 50, 'name': 'AAPL'}]
In [96]:
expensive2 = heapq.nlargest(3, portfolio, key=lambda s: s['shares'])
expensive2
Out[96]:
[{'price': 21.09, 'shares': 200, 'name': 'FB'},
 {'price': 91.1, 'shares': 100, 'name': 'IBM'},
 {'price': 115.65, 'shares': 75, 'name': 'ACME'}]
In [105]:
sorted(portfolio, key=lambda s: s['shares'], reverse=True)[:3]
Out[105]:
[{'price': 21.09, 'shares': 200, 'name': 'FB'},
 {'price': 91.1, 'shares': 100, 'name': 'IBM'},
 {'price': 115.65, 'shares': 75, 'name': 'ACME'}]
In [97]:
help(heapq.nlargest)
Help on function nlargest in module heapq:

nlargest(n, iterable, key=None)
    Find the n largest elements in a dataset.
    
    Equivalent to:  sorted(iterable, key=key, reverse=True)[:n]

토론

  • 가장 작거나 큰 N개의 아이템을 찾고 있고 N이 컬렉션 전체 크기보다 작다면 앞에 나온 함수가 더 나은 성능을 제공한다.
  • 내부 구조
In [107]:
nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
In [108]:
import heapq
In [109]:
heap = list(nums)
heap
Out[109]:
[1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
In [110]:
heapq.heapify(heap)
In [123]:
help(heapq.heapify)
Help on built-in function heapify in module _heapq:

heapify(...)
    Transform list into a heap, in-place, in O(len(heap)) time.

In [111]:
heap
Out[111]:
[-4, 2, 1, 23, 7, 2, 18, 23, 42, 37, 8]
  • 힙의 가장 중요한 기능: heap[0]이 가장 작은 아이템이 된다는 사실
  • N을 힙의 크기라 하면 O(log N)의 시간 복잡도 소요
  • 왜 자기 맘대로 순서가 바뀌지..??
    • heap이라는 list를 heapq.heappop에 넣으면 여기에서 가장 작은게 pop 된다.
In [112]:
heapq.heappop(heap)
Out[112]:
-4
In [113]:
heapq.heappop(heap)
Out[113]:
1
In [114]:
heapq.heappop(heap)
Out[114]:
2
In [115]:
heapq.heappop(heap)
Out[115]:
2
In [116]:
heap
Out[116]:
[7, 23, 8, 23, 42, 37, 18]
In [117]:
heapq.heappop(heap)
Out[117]:
7
In [118]:
heap
Out[118]:
[8, 23, 18, 23, 42, 37]
In [119]:
heapq.heappop(heap)
Out[119]:
8
In [120]:
heap
Out[120]:
[18, 23, 37, 23, 42]
In [121]:
heapq.heappop(heap)
Out[121]:
18
In [122]:
heap
Out[122]:
[23, 23, 37, 42]
  • nlargest()와 nsmallest() 함수는 찾고자 하는 아이템의 개수가 상대적으로 작을때 가장 알맞음
  • 만약 최소값이나 최대값을 구하려 한다면(N이 1), min()과 max()를 사용하는 것이 더 빠름
  • N의 크기가 컬렉션 크기와 비슷해지면 우선 컬렉션을 정렬해 놓고 그 조각을 사용하는 것이 더 빠르다.
  • sorted(items)[:N] 이나 sorted(items)[-N:]

  • 알고리즘이 재밌구만ㅎㅎ
  • 모르면 진짜 하기 싫은데 조금씩 알아가는 재미가 쏠쏠하다

1.5 우선 순위 큐 구현

문제

  • 주어진 우선 순위에 따라 아이템을 정렬하는 큐를 구현하고 항상 우선순위가 가장 높은 아이템을 먼저 팝하도록 만들어야 한다.

해결

  • heapq 모듈을 사용해 간단한 우선순위 큐 구현
In [128]:
import heapq

class PriorityQueue:
    def __init__(self):
        self._queue = []
        self._index = 0
        
    def push(self, item, priority):
        heapq.heappush(self._queue, (-priority, self._index, item))
        self._index += 1
        
    def pop(self):
        return heapq.heappop(self._queue)[-1]
In [129]:
class Item:
    def __init__(self, name):
        self.name = name
    
    def __repr__(self):
        # !r: Calls repr() on the argument first
        return 'Item({!r})'.format(self.name)
In [130]:
q = PriorityQueue()
q.push(Item('foo'), 1)
q.push(Item('bar'), 5)
q.push(Item('spam'), 4)
q.push(Item('grok'), 1)
q
Out[130]:
<__main__.PriorityQueue at 0x10fda1940>
In [131]:
q.pop()
Out[131]:
Item('bar')
In [132]:
q.pop()
Out[132]:
Item('spam')
In [133]:
q.pop()
Out[133]:
Item('foo')
In [134]:
q.pop()
Out[134]:
Item('grok')

토론

  • 가장 중요한 부분: heapq 모듈의 사용법
  • heapq.heappush()와 heapq.heappop()은 list_queue의 첫번째 아이템이 가장 작은 우선 순위를 가진 것처럼 아이템을 삽입하거나 제거한다.
  • 힙의 크기가 N일 때 푸시와 팝의 시간 복잡도가 O(logN)이므로 N이 아주 커진다 해도 상당히 효율적
  • 큐가 튜플 형태로 구성(-priority, index, item)
  • priority값은 큐 내부 아이템을 가장 높은 우선 순위에서 낮은 우선 순위로 정렬하기 위해 무효화된다?!
  • 이는 가장 낮은 값에서 높은 값으로 정렬되는 일반적인 힙과는 반대다.
  • index 변수는 우선 순위가 동일한 아이템의 순서를 정할 때 사용
  • 아이템이 삽입된 순서대로 정렬
  • 인덱스는 우선 순위 값이 동일할 때도 중요한 역할
priority, index의 중요성
In [135]:
a = Item('foo')
In [136]:
b = Item('bar')
In [137]:
a < b
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-137-be58db8c10ac> in <module>()
----> 1 a < b

TypeError: unorderable types: Item() < Item()
  • (priority, item) 튜플을 만들었다면 우선 순위 값이 달라야만 비교가 가능
  • 하지만 동일한 우선 순위를 가진 두 아이템의 비교는 앞에 나온 바와 같이 실패
In [138]:
a = (1, Item('foo'))
In [139]:
b = (5, Item('bar'))
In [140]:
a < b
Out[140]:
True
In [141]:
c = (1, Item('grok'))
In [142]:
a < c
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-142-47626f73af81> in <module>()
----> 1 a < c

TypeError: unorderable types: Item() < Item()
  • 여기에 인덱스 값을 추가해서 튜플을 만들면(priority, index, item), 어떠한 튜플도 동일한 인덱스 값을 가질 수 없으므로 이 문제를 원천적으로 해결 가능(파이썬은 일단 비교 결과가 나오고 나면 뒤에 남아있는 튜플 값을 비교하려고 시도하지 않음)
In [143]:
a = (1, 0, Item('foo'))
In [144]:
b = (5, 1, Item('bar'))
In [145]:
c = (1, 2, Item('grok'))
In [146]:
a < b
Out[146]:
True
In [147]:
a < c
Out[147]:
True
  • 쓰레드 간 통신에 이 큐를 사용하려면 올바른 락시그널 사용

1.6 딕셔너리의 키를 여러 값에 매핑하기

문제

  • 딕셔너리의 키를 하나 이상의 값에 매핑하고 싶다(소위 "multidict")

해결

  • 하나의 키에 하나의 값이 매핑되어 있는 것을 딕셔너리
  • 키에 여러값을 매핑하려면, 그 여러값을 리스트나 세트와 같은 컨테이너에 따로 저장해 둬야 됨
In [149]:
d = {
    'a': [1,2,3],
    'b': [4,5]
}
In [150]:
e = {
    'a': {1, 2, 3},
    'b': {4, 5}
}
In [151]:
d
Out[151]:
{'a': [1, 2, 3], 'b': [4, 5]}
In [152]:
e
Out[152]:
{'a': {1, 2, 3}, 'b': {4, 5}}
  • 리스트: 아이템의 삽입 순서 유지
  • 세트: 순서x, 중복x
  • 딕셔너리를 쉽게 만들기 위해서 collections 모듈의 defaultdict 사용
  • defaultdict의 기능 중에는 첫번째 값을 자동으로 초기화하는 것이 있음
In [153]:
from collections import defaultdict
In [180]:
d = defaultdict(list)
In [181]:
d['a'].append(1)
In [182]:
d
Out[182]:
defaultdict(<class 'list'>, {'a': [1]})
In [183]:
d['a'].append(2)
In [184]:
d
Out[184]:
defaultdict(<class 'list'>, {'a': [1, 2]})
In [185]:
d['b'].append(4)
In [187]:
d['b'].append(4)
In [188]:
d
Out[188]:
defaultdict(<class 'list'>, {'a': [1, 2], 'b': [4, 4]})
In [160]:
ss = defaultdict(set)
In [161]:
ss['a'].add(1)
In [162]:
ss
Out[162]:
defaultdict(<class 'set'>, {'a': {1}})
In [163]:
ss['a'].add(2)
In [164]:
ss['b'].add(4)
In [190]:
ss['b'].add(4)
In [191]:
ss
Out[191]:
defaultdict(<class 'set'>, {'a': {1, 2}, 'b': {4}})
  • 다만 defaultdict를 사용할 때는 딕셔너리에 존재하지 않는 값이라도 한 번이라도 접근했던 키의 엔트리를 자동으로 생성
  • 마음에 들지 않는다면 일반 딕셔너리의 setdefault 사용
In [175]:
# 뭔 얘기인가 했더니 여러값을 저장하기 위해서 []를 만들었다는 얘기고만
# 4,4를 저장해야 되는데 그냥 변수로 할당하면 안되고 리스트로 만들어야 하니까
d = {}
d.setdefault('a', []).append(1)
In [176]:
d.setdefault('a', []).append(2)
In [177]:
d.setdefault('b', []).append(4)
In [178]:
d.setdefault('b', []).append(4)
In [179]:
d
Out[179]:
{'a': [1, 2], 'b': [4, 4]}
  • 하지만 많은 프로그래머들은 setdefault()가 자연스럽지 않다고 생각
  • 첫번째 값에 대해서 항상 새로운 인스턴스를 생성한다는 점은 언급하지 않더라도 말이다

토론

  • 이론적으로 여러값을 가지는 딕셔너리를 만드는 것이 복잡하지는 않음
  • 첫번째 값에 대한 초기화를 스스려 하려면 꽤나 복잡한 과정
In [193]:
from random import randint
In [203]:
pairs = [(randint(1,3), i) for i in range(10)]
In [204]:
d = {}
from random import randint

for key, value in pairs:
    if key not in d:
        d[key] = []
    d[key].append(value)
d
Out[204]:
{1: [0, 2, 3, 8, 9], 2: [1, 6], 3: [4, 5, 7]}
In [207]:
d = defaultdict(list)
for key, value in pairs:
    d[key].append(value)
d
Out[207]:
defaultdict(<class 'list'>, {1: [0, 2, 3, 8, 9], 2: [1, 6], 3: [4, 5, 7]})

1.7 딕셔너리 순서 유지

문제

  • 딕셔너리를 만들고, 순환이나 직렬화할 때 순서를 조절하고 싶다

해결

  • 딕셔너리 내부 아이템의 순서를 조절하려면 collections 모듈의 OrderedDict를 사용
  • 삽입 초기의 순서를 그대로 기억
In [208]:
from collections import OrderedDict

d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4
d
Out[208]:
OrderedDict([('foo', 1), ('bar', 2), ('spam', 3), ('grok', 4)])
In [209]:
for key in d:
    print(key, d[key])
foo 1
bar 2
spam 3
grok 4
In [210]:
for key, value in d.items():
    print(key, value)
foo 1
bar 2
spam 3
grok 4
  • OrderedDict는 나중에 직렬화하거나 다른 포맷으로 인코딩할 다른 매핑을 만들 때 특히 유용
  • JSON 인코딩에 나타나는 특정 필드의 순서를 조절하기 위해서 OrderedDict에 다음과 같이 데이터를 생성
In [211]:
import json
json.dumps(d)
Out[211]:
'{"foo": 1, "bar": 2, "spam": 3, "grok": 4}'

토론

  • OrderedDict는 내부적으로 doubly linked list로 삽입 순서와 관련있는 키를 기억
  • 새로운 아이템을 처음으로 삽입하면 리스트의 제일 끝에 위치시킴
  • 기존 키에 재할당을 한다해도 순서에는 변화가 생기지 않음
  • 더블 링크드 리스트를 사용하기 때문에 일반적인 딕셔너리에 비해서 2배로 크다.
  • 매우 큰 데이터 구조체를 만든다면 고심 해봐야 됨(10만 라인 csv 정도)

1.8 딕셔너리 계산

문제

  • 딕셔너리 데이터에 여러 계산을 수행하고 싶다(최소값, 최대값, 정렬 등)

해결

  • 딕셔너리에 주식 이름과 가격
In [212]:
prices = {
'ACME': 45.23,
'AAPL': 612.78,
'IBM': 205.55,
'HPQ': 37.20,
'FB': 10.75
}
  • 딕셔너리 내용에 대해 유용한 계산을 하려면 딕셔너리의 키와 값zip()으로 뒤집어 주는 것이 좋음
  • 최소 주가와 최대 주가를 찾는 코드를 살펴보자
In [225]:
aa = zip(prices.values(), prices.keys())
print(aa)
for i in aa:
    print(i)
<zip object at 0x10fd9ee88>
(612.78, 'AAPL')
(37.2, 'HPQ')
(205.55, 'IBM')
(10.75, 'FB')
(45.23, 'ACME')
In [234]:
bb = zip(prices.keys(), prices.values())
print(type(bb))
print(bb)
for i in bb:
    print(i)
<class 'zip'>
<zip object at 0x10fd932c8>
('AAPL', 612.78)
('HPQ', 37.2)
('IBM', 205.55)
('FB', 10.75)
('ACME', 45.23)
In [213]:
min_price = min(zip(prices.values(), prices.keys()))
min_price
Out[213]:
(10.75, 'FB')
In [229]:
max_price = max(zip(prices.values(), prices.keys()))
max_price
Out[229]:
(612.78, 'AAPL')
In [227]:
min_price2 = min(zip(prices.keys(), prices.values()))
min_price2
Out[227]:
('AAPL', 612.78)
In [228]:
min_price3 = max(zip(prices.keys(), prices.values()))
min_price3
Out[228]:
('IBM', 205.55)
  • 이와 유사하게 데이터의 순서를 매기려면 zip()과 sorted()를 함께 사용
In [244]:
cc = zip(prices.values(), prices.keys())
print(cc)
sorted(cc)
<zip object at 0x10fdb9e48>
Out[244]:
[(10.75, 'FB'),
 (37.2, 'HPQ'),
 (45.23, 'ACME'),
 (205.55, 'IBM'),
 (612.78, 'AAPL')]
In [235]:
prices_sorted = sorted(zip(prices.values(), prices.keys()))
prices_sorted
Out[235]:
[(10.75, 'FB'),
 (37.2, 'HPQ'),
 (45.23, 'ACME'),
 (205.55, 'IBM'),
 (612.78, 'AAPL')]
In [250]:
prices_sorted = sorted(zip(prices.values(), prices.keys()), reverse=True)
prices_sorted
Out[250]:
[(612.78, 'AAPL'),
 (205.55, 'IBM'),
 (45.23, 'ACME'),
 (37.2, 'HPQ'),
 (10.75, 'FB')]
  • 계산을 할 때, zip()은 단 한 번만 소비할 수 있는 iterator를 생성
In [247]:
prices_and_names = zip(prices.values(), prices.keys())
print(prices_and_names)
print(min(prices_and_names))
# 왜 에러가 나는지 모르겠다..
print(max(prices_and_names))
<zip object at 0x10fdb9c88>
(10.75, 'FB')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-247-eae855c708ab> in <module>()
      3 print(min(prices_and_names))
      4 # 왜 에러가 나는지 모르겠다..
----> 5 print(max(prices_and_names))

ValueError: max() arg is an empty sequence
In [251]:
prices.values()
Out[251]:
dict_values([612.78, 37.2, 205.55, 10.75, 45.23])
In [252]:
prices_and_names = zip(prices.values(), prices.keys())
print(prices_and_names)
print(min(prices_and_names))
# 왜 에러가 나는지 모르겠다..
print(max(prices_and_names))
<zip object at 0x10fda2f88>
(10.75, 'FB')
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-252-eae855c708ab> in <module>()
      3 print(min(prices_and_names))
      4 # 왜 에러가 나는지 모르겠다..
----> 5 print(max(prices_and_names))

ValueError: max() arg is an empty sequence
왜 max만 Error가 발생하지?
In [248]:
max([1, 10, 99, 33])
Out[248]:
99
In [249]:
min([1, 10, 99, 33])
Out[249]:
1

토론

  • 딕셔너리에서 일반적인 데이터 축소를 시도하면, 오직 키에 대해서만 작업이 이루어짐
In [255]:
prices
Out[255]:
{'AAPL': 612.78, 'HPQ': 37.2, 'IBM': 205.55, 'FB': 10.75, 'ACME': 45.23}
In [253]:
min(prices)
Out[253]:
'AAPL'
In [254]:
max(prices)
Out[254]:
'IBM'
  • 딕셔너리의 값에 대해서 알고 싶음
In [256]:
min(prices.values())
Out[256]:
10.75
In [257]:
max(prices.values())
Out[257]:
612.78
  • 키에 일치하는 값 정보까지 알고 싶다면?(어떤 주식의 값이 가장 낮을까?)
In [260]:
min(prices, key=lambda k: prices[k])
Out[260]:
'FB'
In [261]:
max(prices, key=lambda k: prices[k])
Out[261]:
'AAPL'
  • 하지만 최소값을 얻기 위해서 한 번 더 살펴보는 작업이 필요함
In [262]:
min_value = prices[min(prices, key=lambda k: prices[k])]
min_value
Out[262]:
10.75
In [263]:
prices['FB']
Out[263]:
10.75
  • zip()을 포함한 해결책은 딕셔너리의 시퀀스를 (value, key) 페어로 뒤집는 것으로 문제 해결
  • 이런 튜플에 비교를 수행하면 value 요소를 먼저 비교하고 뒤이어 key를 비교
  • 명령어 하나만으로 정확히 우리가 원하는 데이터 축소화 정렬을 수행
  • 여러 엔트리가 동일한 값을 가지고 있을 때 비교 결과를 결정하기 위해서 키를 사용한다는 점 주목
  • min()과 max()를 계산할 때 중복된 값이 있으면 가장 작거나 큰 키를 가지공 ㅣㅆ는 엔트리를 반환
In [264]:
prices = {'AAA': 45.23,
          'ZZZ': 45.23}
In [266]:
min(zip(prices.values(), prices.keys()))
Out[266]:
(45.23, 'AAA')
In [267]:
max(zip(prices.values(), prices.keys()))
Out[267]:
(45.23, 'ZZZ')

1.9 두 딕셔너리의 유사점 찾기

문제

  • 두 딕셔너리가 있고 여기서 유사점을 찾고 싶다(동일한 키, 동일한 값 등)

해결

In [268]:
a = {
'x': 1,
'y': 2,
'z': 3
}
In [269]:
b = {
'w': 10,
'x': 11,
'y': 2
}
  • 두 딕셔너리의 유사점을 찾으려면 keys()와 items() 메소드에 집한 연산 수행
In [271]:
# 동일한 키 찾기
a.keys() & b.keys()
Out[271]:
{'x', 'y'}
In [272]:
# a에만 있고 b에는 없는 키 찾기
a.keys() - b.keys()
Out[272]:
{'z'}
  • 이런 연산을 사용해서 딕셔너리의 내용을 수정하거나 걸러낼 수 있음
  • 선택한 키를 삭제한 새로운 딕셔너리를 만들고 시ㅣㅍ을 때는
In [273]:
# 특정 키를 제거한 새로운 딕셔너리 만들기
c = {key:a[key] for key in a.keys() - {'z', 'w'}}
c
Out[273]:
{'x': 1, 'y': 2}

토론

  • 딕셔너리는 키와 값의 매핑으로 이루어짐
  • 집합 연산 기능도 있음(합집합, 교집합, 여집합)
  • items() 메소드는 key,value 페어로 구성된 item-view 객체를 반환
  • values() 메소드는 앞에 나온 집합 연산을 지원하지 않음, key처럼 유일하다는 보장이 없기 때문

1.10 순서를 깨지 않고 시퀀스의 중복 없애기

문제

  • 시퀀스에서 중복된 값을 없애고 싶지만, 아이템의 순서는 유지하고 싶다

해결

  • 시퀀스의 값이 hash 가능하다면 이 문제는 set, generator를 사용해 쉽게 해결
  • yield를 사용하는게 익숙하지가 않네

  • 그냥 list로 해도 해결되지 않나??
  • 함수 안에서 모두 연산하고 최종적으로 return만 돌려주면 되지 않나? 굳이 yield를 사용해야 되나?

In [290]:
def dedupe(items):
    seen = set()
    for item in items:
        if item not in seen:
            yield item
            seen.add(item)
In [291]:
a = [1, 5, 2, 1, 9, 1, 5, 10]
In [292]:
# 받을 때 yield generator로 계속 받으니까 최종적으로 list로 변경해줘야 되네
list(dedupe(a))
Out[292]:
[1, 5, 2, 9, 10]
In [293]:
dedupe(a)
Out[293]:
<generator object dedupe at 0x10fdb6f78>
In [294]:
print(dedupe(a))
<generator object dedupe at 0x10fdb4990>
In [295]:
# 그냥 list 이용해도 되잖아..?
# 근데 list를 이용하면 hash값이 아니기 때문에 list의 길이가 길어질수록 시간복잡도가 상승하겠네
# set은 해쉬값으로 계산하기 때문에 시간복잡도는 항상 일정!(구종만님 위대한 dict 이해하기 참고)
def dedupe_my(items):
    seen = []
    for item in items:
        if item not in seen:
            seen.append(item)
    return seen
dedupe_my(a)
Out[295]:
[1, 5, 2, 9, 10]
  • 시퀀스의 아이템이 해시 가능한 경우에만 사용 가능
  • 해시 불가능한 타입(dict)의 중복을 없애려면 레시피에 약간의 수정이 필요
In [296]:
def dedupe_dict(items, key=None):
    seen = set()
    for item in items:
        val = item if key is None else key(item)
        if val not in seen:
            yield item
            seen.add(val)
  • key 인자의 목적: 중복 검사를 위해 함수가 시퀀스 아이템을 해시 가능한 타입으로 변환한다고 명시
In [297]:
a = [
{'x': 1, 'y': 2},
{'x': 1, 'y': 3},
{'x': 1, 'y': 2},
{'x': 2, 'y': 4}
]
In [298]:
# 아...이게 뭔 귀신 신나락 까먹는 소리인가 한참 고민했네
# 내가 아직 내공이 부족하구나 라는 자괴감에 잠시 빠짐..
# a라는 dictionary에서 x, y 2개를 key로 잡아서 중복을 제거하겠다는 의미
# 총 4개가 들어가는데 1,2가 중복이 되기 때문에 총 3개만 출력 됨
list(dedupe_dict(a, key=lambda d: (d['x'], d['y'])))
Out[298]:
[{'x': 1, 'y': 2}, {'x': 1, 'y': 3}, {'x': 2, 'y': 4}]
In [300]:
# 이건 x값으로 중복 체크하겠다는 의미
# 그래서 1,2만 통과되서 총 2개 출력
list(dedupe_dict(a, key=lambda d: d['x']))
Out[300]:
[{'x': 1, 'y': 2}, {'x': 2, 'y': 4}]
  • 뒤에 나온 해결책은 필드가 하나이거나 커다란 자료 구조에 기반한 값의 중복을 없앨 때도 잘 동작

  • 자세한건 나중에 직접 써 먹을 때 다시 한 번 확인해야겠다.
  • 지금은 대충 훑어봤다. 지금 완전히 이해할 수 없으니 나중에 내가 꼭 써야 할 때 이 문서를 찾으면 훨씬 이해도가 빠를 것이다.

토론

  • 중복을 없애려면 대게 세트를 만드는 것이 가장 쉬움
In [302]:
a = [1, 5, 2, 1, 9, 1, 5, 10]
In [303]:
set(a)
Out[303]:
{1, 2, 5, 9, 10}
  • set을 이용하면 기존 데이터의 순서가 훼손 됨
  • generator 함수를 사용했기 때문에 단순히 리스트 프로세싱 말고도 아주 일반적인 목적으로 함수를 사용할 수 있음
  • 파일을 읽어 들일 때 중복된 라인을 무시하려면 단순히 다음과 같은 코드 사용
In [306]:
!cat 01/duplicate.txt
dup
dup

good job?
ok..
dup

hoho
good job?
end
In [305]:
with open('01/duplicate.txt') as f:
    for line in dedupe(f):
        print(line.strip())
dup

good job?
ok..
hoho
end
  • key 함수의 스펙은 파이썬 내장 함수인 sorted(), min(), max() 등의 기능을 흉내 내고 있음

1.11 슬라이스 이름 붙이기

문제

  • 프로그램 코드에 slice를 지시하는 하드코딩이 너무 많아 이해하기 어려운 상황이다. 이를 정리해야 한다.

해결

  • 고정된 문자열로부터 특정 데이터를 추출하는 코드가 있다고 가정
In [323]:
record = '....................100         ........513.25    ..........'
In [324]:
record[20:32]
Out[324]:
'100         '
In [325]:
record[40:48]
Out[325]:
'513.25  '
In [326]:
cost = int(record[20:32]) * float(record[40:48])
In [327]:
cost
Out[327]:
51325.0
In [329]:
SHARES = slice(20, 32)
SHARES
Out[329]:
slice(20, 32, None)
In [330]:
PRICE = slice(40, 48)
PRICE
Out[330]:
slice(40, 48, None)
In [331]:
cost = int(record[SHARES]) * float(record[PRICE])
cost
Out[331]:
51325.0
  • 이렇게 하는 것도 나쁘지 않지만 나중에 자리 위치가 바뀌게 되면 어떻게 할거냐?
  • bio python에서 나왔던 struct 모듈을 사용하는게 좋아 보인다.
  • 아니면 개념을 잡아주는 프로그래밍 정석에 나왔던대로 각각의 튜플로 만들어 주던가

토론

  • 일반적으로 내장 함수인 slice()는 슬라이스 받는 모든 곳에 사용할 수 있는 조각을 생성
In [340]:
items = [0, 1, 2, 3, 4, 5, 6]
In [341]:
a = slice(2, 4)
a
Out[341]:
slice(2, 4, None)
In [342]:
items[2:4]
Out[342]:
[2, 3]
In [343]:
items[a]
Out[343]:
[2, 3]
In [344]:
items[a] = [10, 11]
In [345]:
items
Out[345]:
[0, 1, 10, 11, 4, 5, 6]
In [346]:
del items[a]
In [347]:
items
Out[347]:
[0, 1, 4, 5, 6]
  • slice 인스턴스 s가 있다면 s.start와 s.stop, s.step 속성을 통해 좀 더 많은 정보를 얻을 수 있음
In [384]:
# 책에 오타로 10으로 써져 있어서 한참 고생
a = slice(5, 50, 2)
In [385]:
a.start
Out[385]:
5
In [386]:
a.stop
Out[386]:
50
In [387]:
a.step
Out[387]:
2
  • 추가적으로 indices(size) 메소드를 사용하면 특정 크기의 슬라이스를 매핑
  • 튜플(start, stop, step)을 반환하는데, 모든 값은 경계를 넘어서지 않도록 제약이 걸려있다(인덱스에 접근할 때 IndexError 예외가 발생하지 않도록 하기 위함)
In [388]:
s = 'HelloWorld'
In [389]:
len(s)
Out[389]:
10
In [390]:
# a slice 설정값에 len(s)가 max가 되네
a.indices(len(s))
Out[390]:
(5, 10, 2)
In [391]:
range(*a.indices(len(s)))
Out[391]:
range(5, 10, 2)
In [392]:
for i in range(*a.indices(len(s))):
    print(s[i])
W
r
d

1.12 시퀀스에 가장 많은 아이템 찾기

문제

  • 시퀀스에 가장 많이 나타난 아이템을 찾고 싶다

해결

  • 이러한 문제를 해결하기 위해 존재하는 클래스가 바로 collections.Counter
  • most_common() 메소드도 제공
  • 단어가 들어있는 리스트에서 가장 자주 나타나는 단어를 찾아보자
In [396]:
words = [
'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes',
'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the',
'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into',
'my', 'eyes', "you're", 'under'
]
In [399]:
from collections import Counter
word_counts = Counter(words)
top_three = word_counts.most_common(3)
print(top_three)
print(word_counts)
[('eyes', 8), ('the', 5), ('look', 4)]
Counter({'eyes': 8, 'the': 5, 'look': 4, 'my': 3, 'into': 3, 'around': 2, "don't": 1, "you're": 1, 'under': 1, 'not': 1})

토론

  • Counter 객체에는 해시 가능한 모든 아이템을 입력 가능
  • 내부적으로 Counter는 아이템이 나타난 횟수를 가리키는 딕셔너리
In [400]:
word_counts['not']
Out[400]:
1
In [401]:
word_counts['eyes']
Out[401]:
8
In [402]:
morewords = ['why', 'are', 'you', 'not', 'looking', 'in', 'my', 'eyes']
In [403]:
for word in morewords:
    word_counts[word] += 1
In [404]:
word_counts['eyes']
Out[404]:
9
In [405]:
word_counts
Out[405]:
Counter({'eyes': 9, 'the': 5, 'look': 4, 'my': 4, 'into': 3, 'not': 2, 'around': 2, 'in': 1, "you're": 1, "don't": 1, 'you': 1, 'under': 1, 'are': 1, 'looking': 1, 'why': 1})
In [406]:
# list로 줄 수도 있네
word_counts.update(morewords)
In [407]:
word_counts
Out[407]:
Counter({'eyes': 10, 'my': 5, 'the': 5, 'look': 4, 'not': 3, 'into': 3, 'in': 2, 'around': 2, 'you': 2, 'are': 2, 'looking': 2, 'why': 2, "you're": 1, "don't": 1, 'under': 1})
  • 수식도 가능
In [408]:
a = Counter(words)
In [409]:
b = Counter(morewords)
In [410]:
a
Out[410]:
Counter({'eyes': 8, 'the': 5, 'look': 4, 'my': 3, 'into': 3, 'around': 2, "don't": 1, "you're": 1, 'under': 1, 'not': 1})
In [411]:
b
Out[411]:
Counter({'in': 1, 'not': 1, 'looking': 1, 'why': 1, 'eyes': 1, 'you': 1, 'are': 1, 'my': 1})
In [412]:
c = a + b
In [413]:
c
Out[413]:
Counter({'eyes': 9, 'the': 5, 'look': 4, 'my': 4, 'into': 3, 'not': 2, 'around': 2, 'in': 1, "you're": 1, "don't": 1, 'you': 1, 'under': 1, 'are': 1, 'looking': 1, 'why': 1})
In [414]:
d = a - b
In [415]:
d
Out[415]:
Counter({'eyes': 7, 'the': 5, 'look': 4, 'into': 3, 'my': 2, 'around': 2, "you're": 1, 'under': 1, "don't": 1})
  • 데이터의 개수를 파악해야 하는 문제에 있어 Counter 객체는 매우 유용

1.13 일반 키로 딕셔너리 리스트 정렬

문제

  • 딕셔너리 리스트가 있고, 하나 혹은 그 이상의 딕셔너리 값으로 이를 정렬하고 싶다

해결

  • operator 모듈의 itemgetter 함수를 사용하면 쉽게 정렬
In [1]:
rows = [
{'fname': 'Brian', 'lname': 'Jones', 'uid': 1003},
{'fname': 'David', 'lname': 'Beazley', 'uid': 1002},
{'fname': 'John', 'lname': 'Cleese', 'uid': 1001},
{'fname': 'Big', 'lname': 'Jones', 'uid': 1004}
]
In [37]:
from operator import itemgetter
In [5]:
rows_by_fname = sorted(rows, key=itemgetter('fname'))
rows_by_fname
Out[5]:
[{'uid': 1004, 'fname': 'Big', 'lname': 'Jones'},
 {'uid': 1003, 'fname': 'Brian', 'lname': 'Jones'},
 {'uid': 1002, 'fname': 'David', 'lname': 'Beazley'},
 {'uid': 1001, 'fname': 'John', 'lname': 'Cleese'}]
In [6]:
rows_by_uid = sorted(rows, key=itemgetter('uid'))
rows_by_uid
Out[6]:
[{'uid': 1001, 'fname': 'John', 'lname': 'Cleese'},
 {'uid': 1002, 'fname': 'David', 'lname': 'Beazley'},
 {'uid': 1003, 'fname': 'Brian', 'lname': 'Jones'},
 {'uid': 1004, 'fname': 'Big', 'lname': 'Jones'}]
In [7]:
rows_by_lfname = sorted(rows, key=itemgetter('lname', 'fname'))
rows_by_lfname
Out[7]:
[{'uid': 1002, 'fname': 'David', 'lname': 'Beazley'},
 {'uid': 1001, 'fname': 'John', 'lname': 'Cleese'},
 {'uid': 1004, 'fname': 'Big', 'lname': 'Jones'},
 {'uid': 1003, 'fname': 'Brian', 'lname': 'Jones'}]

토론

  • 키워드 인자 key를 받는 내장 함수 sorted()에 rows를 전달
  • rows로부터 단일 아이템을 받는 호출 가능 객체를 입력으로 받고 정렬의 기본이 되는 값을 반환
  • itemgetter() 함수는 그런 호출 가능 객체를 생성
  • operator.itemgetter() 함수는 rows 레코드에서 원하는 값을 추출하는데 사용하는 인덱스를 인자로 받는다. 딕셔너리 키 이름이나 숫자 리스트 요소가 될 수도 있고, 객체의 __getitem__() 메소드에 넣을 수 있는 모든 값이 가능
  • itemgetter()에 여러 인덱스를 전달하면, 생성한 호출 가능 객체가 모든 요소를 가지고 있는 튜플을 반환하고, sorted()가 튜플의 정렬 순서에 따라 결과의 순서를 잡음. 여러 필드를 동시에 정렬할 때 유용
itemgetter() -> lambda 표현식
In [8]:
rows_by_fname = sorted(rows, key=lambda r: r['fname'])
rows_by_fname
Out[8]:
[{'uid': 1004, 'fname': 'Big', 'lname': 'Jones'},
 {'uid': 1003, 'fname': 'Brian', 'lname': 'Jones'},
 {'uid': 1002, 'fname': 'David', 'lname': 'Beazley'},
 {'uid': 1001, 'fname': 'John', 'lname': 'Cleese'}]
In [10]:
rows_by_lfname = sorted(rows, key=lambda r: (r['lname'], r['fname']))
rows_by_lfname
Out[10]:
[{'uid': 1002, 'fname': 'David', 'lname': 'Beazley'},
 {'uid': 1001, 'fname': 'John', 'lname': 'Cleese'},
 {'uid': 1004, 'fname': 'Big', 'lname': 'Jones'},
 {'uid': 1003, 'fname': 'Brian', 'lname': 'Jones'}]
  • itemgetter()를 사용한 코드의 실행 속도가 조금 더 빠르다.
  • 그리고 더 쉽다.
In [11]:
min(rows, key=itemgetter('uid'))
Out[11]:
{'uid': 1001, 'fname': 'John', 'lname': 'Cleese'}
In [12]:
max(rows, key=itemgetter('uid'))
Out[12]:
{'uid': 1004, 'fname': 'Big', 'lname': 'Jones'}

1.14 기본 비교 기능없이 객체 정렬

문제

  • 동일한 클래스 객체를 정렬해야 하는데, 이 클래스는 기본적인 비교 연산을 제공하지 않는다.

해결

  • 내장 함수 sorted()는 key 인자에 호출 가능 객체를 받아 sorted가 객체 비교에 사용할 수 있는 값을 반환
  • 애플리케이션에 User 인스턴스를 시퀀스로 갖고 있고 이를 user_id 요소를 기반으로 정렬하고 싶다.
  • 이럴 때는 User 인스턴스를 입력으로 받고 user_id를 반환하는 코드 작성
In [25]:
class User:
    def __init__(self, user_id):
        self.user_id = user_id
    def __repr__(self):
        return 'User({})'.format(self.user_id)
In [26]:
users = [User(23), User(3), User(99)]
In [27]:
users
Out[27]:
[User(23), User(3), User(99)]
In [28]:
sorted(users, key=lambda u: u.user_id)
Out[28]:
[User(3), User(23), User(99)]
  • lambda를 사용하는 대신 operator.attrgetter()를 사용해도 됨
In [38]:
from operator import attrgetter
In [30]:
sorted(users, key=attrgetter('user_id'))
Out[30]:
[User(3), User(23), User(99)]
In [31]:
sorted(users, key=itemgetter('user_id'))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-31-d23a30526307> in <module>()
----> 1 sorted(users, key=itemgetter('user_id'))

TypeError: 'User' object is not subscriptable

토론

  • lambda를 사용할지 attrgetter()를 사용할지 여부는 개인의 선호도
  • attrgetter()의 속도가 빠른 경우가 종종 있고 동시에 여러 필드를 추출하는 기능이 추가되어 있음
In [19]:
class User:
    def __init__(self, last_name, first_name):
        self.last_name = last_name
        self.first_name = first_name
    def __repr__(self):
        return 'User({} {})'.format(self.last_name, self.first_name)
In [21]:
users = [User('ABC', 'ZXY'), User('BCD', 'XYZ'), User('BCD', 'XXY')]
users
Out[21]:
[User(ABC ZXY), User(BCD XYZ), User(BCD XXY)]
In [22]:
by_name = sorted(users, key=attrgetter('last_name', 'first_name'))
by_name
Out[22]:
[User(ABC ZXY), User(BCD XXY), User(BCD XYZ)]
itemgetter vs attrgetter
In [43]:
mydict = { 'a1': ['g',6],
           'a2': ['e',2],
           'a3': ['h',3],
           'a4': ['s',2],
           'a5': ['j',9],
           'a6': ['y',7] }
In [49]:
mydict
Out[49]:
{'a1': ['g', 6],
 'a6': ['y', 7],
 'a3': ['h', 3],
 'a2': ['e', 2],
 'a4': ['s', 2],
 'a5': ['j', 9]}
In [47]:
sorted(mydict.values(), key=itemgetter(1))
Out[47]:
[['e', 2], ['s', 2], ['h', 3], ['g', 6], ['y', 7], ['j', 9]]
In [63]:
sorted(mydict.items(), key=itemgetter(1))
Out[63]:
[('a2', ['e', 2]),
 ('a1', ['g', 6]),
 ('a3', ['h', 3]),
 ('a5', ['j', 9]),
 ('a4', ['s', 2]),
 ('a6', ['y', 7])]
In [65]:
sorted(mydict.items(), key=itemgetter(0))
Out[65]:
[('a1', ['g', 6]),
 ('a2', ['e', 2]),
 ('a3', ['h', 3]),
 ('a4', ['s', 2]),
 ('a5', ['j', 9]),
 ('a6', ['y', 7])]
In [67]:
sorted(mydict.items(), key=itemgetter(1)(1))
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-67-a4efbb75a113> in <module>()
----> 1 sorted(mydict.items(), key=itemgetter(1)(1))

TypeError: 'int' object is not subscriptable
In [78]:
sorted(mydict.items(), key=lambda (k,v): itemgetter(1)(v))
  File "<ipython-input-78-c394b7465c10>", line 1
    sorted(mydict.items(), key=lambda (k,v): itemgetter(1)(v))
                                      ^
SyntaxError: invalid syntax
In [74]:
import sys
list_of_tuples = [(1,2), (3,4), (5,0)]
min_tuple = None
minimum = sys.maxsize
for pair in list_of_tuples:
    x, y = pair
    if y < minimum:
        min_tuple = pair
print(min_tuple)
(5, 0)
In [75]:
def snd(pair):
    x, y = pair
    return y

list_of_tuples = [(1,2), (3,4), (5,0)]
min(list_of_tuples, key=snd)
Out[75]:
(5, 0)
  • itemgetter는 positon을 파라미터로 보내주면
  • seqeunce에서 __getitem__ 으로 sequence의 position 값이 무엇인지 알려준다.
Using itemgetter
In [81]:
from operator import itemgetter
list__ = [1,2,3]
print(itemgetter(1)(list__))
print(itemgetter(2)(list__))
2
3
In [79]:
def __itemgetter(position):
    def applier(sequence):
        return sequence.__getitem__(position)
    return applier
In [84]:
__itemgetter(2)(list__)
Out[84]:
3
In [80]:
from operator import itemgetter
list_of_tuples = [(1,2), (3,4), (5,0)]
min(list_of_tuples, key=itemgetter(1))
Out[80]:
(5, 0)
Using Attrgetter
  • index 대신에 attribute
In [90]:
class Student(object):
    def __init__(self, id, name, marks):
        self.id = id
        self.name = name
        self.marks = marks
        
    def __str__(self):
        return '{0} has marks {1}'.format(self.name, self.marks)
In [91]:
students = [Student(0, 'Foo', 30), 
            Student(1, 'Bar', 95), 
            Student(2, 'Baz', 80)]
 
best_student = max(students, key=attrgetter('marks')) # don't forget the quotes
print(best_student)
Bar has marks 95

1.15 필드에 따라 레코드 묶기

문제

  • 일련의 딕셔너리나 인스턴스가 있고 특정 필드 값에 기반한 그룹의 데이터를 순환하고 싶다.

해결

  • itertools.groupby() 함수는 이와 같은 데이터를 묶는 데 유용
In [92]:
rows = [
{'address': '5412 N CLARK', 'date': '07/01/2012'},
{'address': '5148 N CLARK', 'date': '07/04/2012'},
{'address': '5800 E 58TH', 'date': '07/02/2012'},
{'address': '2122 N CLARK', 'date': '07/03/2012'},
{'address': '5645 N REVENSWOOD', 'date': '07/02/2012'},
{'address': '1060 W ADDISON', 'date': '07/02/2012'},
{'address': '4801 N BROADWAY', 'date': '07/01/2012'},
{'address': '1039 W GRANVILLE', 'date': '07/04/2012'},
]
  • 날짜로 구분 지을 데이터 조각을 순환
  • 우선 원하는 필드에 따라 정렬(이번 경우엔 date field)
  • 그 후에 itertools.groupby()
In [93]:
from operator import itemgetter
from itertools import groupby

rows.sort(key=itemgetter('date'))
rows
Out[93]:
[{'address': '5412 N CLARK', 'date': '07/01/2012'},
 {'address': '4801 N BROADWAY', 'date': '07/01/2012'},
 {'address': '5800 E 58TH', 'date': '07/02/2012'},
 {'address': '5645 N REVENSWOOD', 'date': '07/02/2012'},
 {'address': '1060 W ADDISON', 'date': '07/02/2012'},
 {'address': '2122 N CLARK', 'date': '07/03/2012'},
 {'address': '5148 N CLARK', 'date': '07/04/2012'},
 {'address': '1039 W GRANVILLE', 'date': '07/04/2012'}]
In [94]:
for date, items in groupby(rows, key=itemgetter('date')):
    print(date)
    for i in items:
        print('    ', i)
07/01/2012
     {'address': '5412 N CLARK', 'date': '07/01/2012'}
     {'address': '4801 N BROADWAY', 'date': '07/01/2012'}
07/02/2012
     {'address': '5800 E 58TH', 'date': '07/02/2012'}
     {'address': '5645 N REVENSWOOD', 'date': '07/02/2012'}
     {'address': '1060 W ADDISON', 'date': '07/02/2012'}
07/03/2012
     {'address': '2122 N CLARK', 'date': '07/03/2012'}
07/04/2012
     {'address': '5148 N CLARK', 'date': '07/04/2012'}
     {'address': '1039 W GRANVILLE', 'date': '07/04/2012'}

토론

  • groupby() 함수는 시퀀스를 검색하고 동일한 값(혹은 키 함수에서 반환한 값)에 대한 일련의 '실행'을 찾는다.
  • 개별 순환에 대해서 값, 그리고 같은 값을 가진 그룹의 모든 아이템을 만드는 iterator를 함께 반환
  • 원하는 필드에 따라 데이터를 정렬해야 하는 과정이 중요
  • groupby() 함수는 연속된 아이템에만 동작하기 때문에 정렬 과정을 생략하면 원하는 대로 함수를 실행할 수 없음
  • 단순히 날짜에 따라 데이터를 묶어서 커다란 자료 구조에 넣어 놓고 원할 때마다 접근하려는 것
  • defaultdict()를 사용해서 multidict를 구성하는게 나을 수 있음
In [96]:
from pprint import pprint
In [99]:
from collections import defaultdict
rows_by_date = defaultdict(list)
for row in rows:
    rows_by_date[row['date']].append(row)
In [100]:
for r in rows_by_date['07/01/2012']:
    print(r)
{'address': '5412 N CLARK', 'date': '07/01/2012'}
{'address': '4801 N BROADWAY', 'date': '07/01/2012'}
  • 메모리 사용량에 크게 구애 받지 않는다면 이 방식을 사용하는 것이 정렬한 후에 groupby()를 사용하는 첫 번째 방법보다 더 빠를 것

1.16 시퀀스 필터링

문제

  • 시퀀스 내부에 데이터가 있고 특정 조건에 따라 값을 추출하거나 줄이고 싶다

해결

  • 가장 간단한 해결책은 list comprehension
In [101]:
mylist = [1, 4, -5, 10, -7, 2, 3, -1]
In [102]:
[n for n in mylist if n > 0]
Out[102]:
[1, 4, 10, 2, 3]
In [103]:
[n for n in mylist if n < 0]
Out[103]:
[-5, -7, -1]
  • 단점: 입력된 내용이 크면 매우 큰 결과가 생성될 수도 있다.
  • 생성자 표현식으로 해결
In [127]:
# generator를 생성했기 때문에 이걸로 list comprehension처럼 바로 결과값을 얻을 수 없다.
pos = (n for n in mylist if n > 0)
pos
Out[127]:
<generator object <genexpr> at 0x1068d6af8>
In [128]:
type(pos)
Out[128]:
generator
In [129]:
# generator라서 한 번 돌면 pos에 아무것도 남지 않는구나
list(pos)
Out[129]:
[1, 4, 10, 2, 3]
In [130]:
type(pos)
Out[130]:
generator
In [131]:
# generator는 for문에서 실행될 때 결과값을 확인할 수 있다.
for x in pos:
    print(x)
  • list comprehension이나 생성자 표현식에 필터 필터 조건을 만드는 것이 쉽지 않을 때가 존재
  • 필터링 도중에 예외 처리를 해야 한다거나 다른 복잡한 내용이 들어가야 한다면 어떻게?
    • filter()
In [107]:
values = ['1', '2', '-3', '-', '4', 'N/A', '5']
In [108]:
def is_int(val):
    try:
        x = int(val)
        return True
    except ValueError:
        return False
In [110]:
# filter: func, iterable
isvals = list(filter(is_int, values))
isvals
Out[110]:
['1', '2', '-3', '4', '5']
  • filter()는 iterator를 생성한다.
  • 따라서 결과의 리스트를 만들고 싶다면 위에 나온대로 list()도 함께 사용해야 함

토론

  • list comprehension과 생성자 표현식은 간단한 데이터를 걸러내기 위한 가장 쉽고 직관적인 방법
  • 또한 동시에 데이터 변형 기능도 있음
In [132]:
mylist = [1, 4, -5, 10, -7, 2, 3, -1]
In [133]:
import math
[math.sqrt(n) for n in mylist if n > 0]
Out[133]:
[1.0, 2.0, 3.1622776601683795, 1.4142135623730951, 1.7320508075688772]
  • 필터링에는 조건을 만족하지 않는 값을 걸러내는 것 외에도 새로운 값으로 치환하는 방식도 존재
  • 양수만 찾아내는 필터링뿐 아니라 잘못된 값을 특정 범위에 들어가도록 수정
  • 필터링 조건을 조건 표현식으로 변경하면 간단히 해결
In [138]:
[n if n > 0 else 0 for n in mylist]
Out[138]:
[1, 4, 0, 10, 0, 2, 3, 0]
In [139]:
[math.sqrt(n) if n > 0 else 0 for n in mylist]
Out[139]:
[1.0, 2.0, 0, 3.1622776601683795, 0, 1.4142135623730951, 1.7320508075688772, 0]
In [140]:
clip_pos = [n if n < 0 else 0 for n in mylist]
clip_pos
Out[140]:
[0, 0, -5, 0, -7, 0, 0, -1]
In [141]:
addresses = [
'5412 N CLARK',
'5148 N CLARK',
'5800 E 58TH',
'2122 N CLARK',
'5645 N REVENSWOOD',
'1060 W ADDISON',
'4801 N BROADWAY',
'1039 W GRANVILLE',
]
In [142]:
counts = [0, 3, 10, 4, 1, 7, 6, 1]
  • 카운트 값이 5 이상인 주소만 남기려 한다면..?
In [143]:
from itertools import compress
In [144]:
# 이거 numpy에 있던 기능 같은데..?
more5 = [n > 5 for n in counts]
more5
Out[144]:
[False, False, True, False, False, True, True, False]
In [146]:
compress(addresses, more5)
Out[146]:
<itertools.compress at 0x1068c8cf8>
In [145]:
list(compress(addresses, more5))
Out[145]:
['5800 E 58TH', '1060 W ADDISON', '4801 N BROADWAY']
  • 우선 주어진 조건에 만족하는지 여부를 담은 Boolean 시퀀스를 만들어 두는 것이 포인트
  • compress() 함수로 True에 일치하는 값만 골라냄
  • filter()와 마찬가지로, compress()는 일반적으로 이터레이터를 반환
  • 따라서 실행 결과를 리스트에 담고 싶다면 list()를 사용해야 함
In [156]:
d = dict(zip(addresses, counts))
d
Out[156]:
{'5412 N CLARK': 0,
 '1039 W GRANVILLE': 1,
 '4801 N BROADWAY': 6,
 '1060 W ADDISON': 7,
 '5148 N CLARK': 3,
 '5800 E 58TH': 10,
 '5645 N REVENSWOOD': 1,
 '2122 N CLARK': 4}
In [155]:
# map, zil, filter 이런 종류들 매번 햇깔린다.
for i in map(lambda a: a*a, range(6)):
    print(i)
0
1
4
9
16
25
In [160]:
d.items()
Out[160]:
dict_items([('5412 N CLARK', 0), ('1039 W GRANVILLE', 1), ('4801 N BROADWAY', 6), ('1060 W ADDISON', 7), ('5148 N CLARK', 3), ('5800 E 58TH', 10), ('5645 N REVENSWOOD', 1), ('2122 N CLARK', 4)])
Python Tips, Tricks, and Hacks: Dictionary Comprehensions로 해보려고 참고
In [163]:
[k for k, v in d.items() if v > 5]
Out[163]:
['4801 N BROADWAY', '1060 W ADDISON', '5800 E 58TH']
In [181]:
dict(['a', 1])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-181-9439c3a1a806> in <module>()
----> 1 dict(['a', 1])

ValueError: dictionary update sequence element #0 has length 1; 2 is required
In [186]:
dict_as_list = [['a', 1], ['b', 2], ['c', 3]]
dict(dict_as_list)
Out[186]:
{'c': 3, 'a': 1, 'b': 2}
In [189]:
# error 발생
dict(['a', 1])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-189-9439c3a1a806> in <module>()
----> 1 dict(['a', 1])

ValueError: dictionary update sequence element #0 has length 1; 2 is required
In [188]:
dict([['a', 1]])
Out[188]:
{'a': 1}
In [176]:
# dictionary 만들 때 [k, v] 로 감싸줘야 되는군..
dict([[k, v] for k, v in d.items() if v > 5])
Out[176]:
{'5800 E 58TH': 10, '4801 N BROADWAY': 6, '1060 W ADDISON': 7}
In [185]:
dict([k, v] for k, v in d.items() if v > 5)
Out[185]:
{'5800 E 58TH': 10, '4801 N BROADWAY': 6, '1060 W ADDISON': 7}
In [203]:
# 아 이걸 찾으려고 엄청 삽질했네 ㅡ.ㅡ
# 그래 {}로 감싸주고 k:v 하면 되는거잖아!
{k:v for k, v in d.items() if v > 5}
Out[203]:
{'5800 E 58TH': 10, '4801 N BROADWAY': 6, '1060 W ADDISON': 7}
In [193]:
# 이렇게 만들어주면 compress를 사용하는 것과 같다.
# 그런데 바로 적용하는 것은 compress가 좀 더 쉽다고 생각되네
# compress 사용할 상황이 되면 compress를 쓰자.
list(k for k, v in d.items() if v > 5)
Out[193]:
['4801 N BROADWAY', '1060 W ADDISON', '5800 E 58TH']

1.17 딕셔너리 부분 추출

문제

  • 딕셔너리의 특정 부분으로부터 다른 딕셔너리를 만들고 싶다.

해결

  • dictionary comprehension을 사용하면 간단하게 해결
In [194]:
prices = {
'ACME': 45.23,
'AAPL': 612.78,
'IBM': 205.55,
'HPQ': 37.20,
'FB': 10.75
}
In [205]:
# 가격이 200 이상인 것에 대한 딕셔너리
p1 = {key:value for key, value in prices.items() if value > 200}
p1
Out[205]:
{'AAPL': 612.78, 'IBM': 205.55}
In [206]:
# 기술 관련 주식으로 딕셔너리 구성
tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'}
p2 = {key:value for key, value in prices.items() if key in tech_names}
p2
Out[206]:
{'HPQ': 37.2, 'AAPL': 612.78, 'IBM': 205.55}

토론

  • dictionary comprehension으로 할 수 있는 대부분의 일은 tuple sequence를 만들고 dict() 함수에 전달하는 것으로도 할 수 있음
In [208]:
p1 = dict((key, value) for key, value in prices.items() if value > 200)
p1
Out[208]:
{'AAPL': 612.78, 'IBM': 205.55}
  • dictionary comprehension을 사용하는 것이 더 깔끔하고 실행 속도 측면에서 보아도 조금 더 유리(prices 딕셔너리의 경우 실행 속도에서 2배 차이 발생)
  • 동일한 문제를 해결하는 데는 언제나 많은 방법이 존재
In [209]:
tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'}
p2 = { key:prices[key] for key in prices.keys() & tech_names}
p2
Out[209]:
{'HPQ': 37.2, 'AAPL': 612.78, 'IBM': 205.55}

1.18 시퀀스 요소에 이름 매핑

문제

  • 리스트나 튜플의 위치로 요소에 접근하는 코드 존재
  • 하지만 때론 이런 코드의 가독성이 떨어짐
  • 또한 위치에 의존하는 코드의 구조도 이름으로 접근 가능하도록 수정하고 싶음

해결

  • collections.namedtuple()을 사용하면 일반적인 튜플 객체를 사용하는 것에 비해 그리 크지 않은 오버헤드로 이 기능을 구현
  • collections.namedtuple()은 실제로 표준 파이썬 tuple 타입의 서브 클래스를 반환하는 팩토리 메소드
  • 타입 이름과, 포함해야 할 필드를 전달하면 인스턴스화 가능한 클래스를 반환
  • 여기에 필드의 값을 전달하는 식으로 사용 가능
In [210]:
from collections import namedtuple
In [211]:
Subscriber = namedtuple('Subscriber', ['addr', 'joined'])
In [212]:
sub = Subscriber('jonesy@example.com', '2012-10-19')
In [213]:
sub
Out[213]:
Subscriber(addr='jonesy@example.com', joined='2012-10-19')
In [214]:
sub.addr
Out[214]:
'jonesy@example.com'
In [215]:
sub.joined
Out[215]:
'2012-10-19'
  • namedtuple의 인스턴스는 일반적인 클래스 인스턴스와 비슷해 보이지만 튜플과 교환이 가능하고, 인덱싱이나 언패킹과 같은 튜플의 일반적인 기능을 모두 지원
In [216]:
len(sub)
Out[216]:
2
In [217]:
addr, joined = sub
In [218]:
addr
Out[218]:
'jonesy@example.com'
In [219]:
joined
Out[219]:
'2012-10-19'
  • namedtuple은 주로 요소의 위치를 기반으로 구현되어 있는 코드를 분리
  • 요소의 위치로 접근하는 코드가 있을 때, 예를 들어 테이블에 새로운 열이 추가되었다거나 할 때 문제가 발생할 수 있음
  • 반환된 튜플을 네임드 튜플로 변환했다면 이런 문제를 예방할 수 있음
In [243]:
def compute_cost(records):
    total = 0.0
    for rec in records:
        total += rec[1] * rec[2]
    return total
In [244]:
records = [['name', 20, 1000], ['name2', 30, 2000]]
In [245]:
compute_cost(records)
Out[245]:
80000.0
In [286]:
Stock = namedtuple('Stock', ['name', 'shares', 'price'])

  • 자료구조를 많이 알고 있으면 쓸데없는 삽질을 방지해 줄 수 있겠네.
  • 괜히 구현한다고 힘 빼지 않고 그냥 가져다 쓰면 되잖아..
  • 이미 잘 만들어져 있는것 가져다 쓰면 된다.
    • 자동차 만드는데 바퀴를 다시 발명할 이유는 없잖아?
    • 그런데 그 함수에 버그가 존재한다면? SSL heartbleed, bash bug

In [247]:
def compute_cost(records):
    total = 0.0
    for rec in records:
        print(*rec)
        s = Stock(*rec)
        print(s)
        total += s.shares * s.price
    return total
In [248]:
compute_cost(records)
name 20 1000
Stock(name='name', shares=20, price=1000)
name2 30 2000
Stock(name='name2', shares=30, price=2000)
Out[248]:
80000.0
  • 만약에 name과 price 자리가 바뀌게 된다면?
  • 제일 처음에 한 방법은 rec[1] rec[2] -> rec[0] rec[1]로 변경해야 한다.
  • 하지만 namedtuple은 제일 처음 namedtuple을 설정할 때만 변경해주면 된다.
    • Stock = namedtuple('Stock', ['name', 'shares', 'price'])
    • Stock = namedtuple('Stock', ['shares', 'price', 'name'])
In [276]:
Stock_change = namedtuple('Stock', ['shares', 'price', 'name'])
In [277]:
records = [[20, 1000, 'name'], [30, 2000, 'name2']]
In [280]:
def compute_cost_change(records):
    total = 0.0
    for rec in records:
        s = Stock_change(*rec)
        total += s.shares * s.price
    return total
In [281]:
compute_cost_change(records)
Out[281]:
80000.0

토론

  • namedtuple은 저장 공간을 더 필요로 하는 딕셔너리 대신 사용 가능
  • 딕셔너리를 포함한 방대한 자료 구조를 구상하고 있다면 namedtuple을 사용하는 것이 더 효율적
  • 딕셔너리와는 다르게 namedtuple은 수정 불가
In [288]:
s = Stock('ACME', 100, 123.45)
In [289]:
s
Out[289]:
Stock(name='ACME', shares=100, price=123.45)
In [290]:
s.shares
Out[290]:
100
In [291]:
s.shares = 75
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-291-b592c031d9bc> in <module>()
----> 1 s.shares = 75

AttributeError: can't set attribute
  • 속성을 수정하려 한다면 _replace() 메소드를 사용
  • 완전히 새로운 namedtuple 생성
In [292]:
s = s._replace(shares=75)
s
Out[292]:
Stock(name='ACME', shares=75, price=123.45)
  • _replace() 메소드를 사용하면 옵션이나 빈 필드를 가진 namedtuple을 간단히 만들 수 있음
  • 일단 기본값을 가진 프로토타입 튜플을 만들고, _replace()로 치환된 값을 가진 새로운 인스턴스를 만듦
In [295]:
from collections import namedtuple

Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time'])

# 프로토타입 인스턴스 생성
stock_prototype = Stock('', 0, 0.0, None, None)

# 딕셔너리를 Stock으로 변환하는 함수
def dict_to_stock(s):
    return stock_prototype._replace(**s)
In [296]:
a = {'name': 'ACME', 'shares': 100, 'price': 123.45}
In [297]:
dict_to_stock(a)
Out[297]:
Stock(name='ACME', shares=100, price=123.45, date=None, time=None)
In [298]:
b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'}
In [299]:
dict_to_stock(b)
Out[299]:
Stock(name='ACME', shares=100, price=123.45, date='12/17/2012', time=None)

1.19 데이터를 변환하면서 줄이기

문제

  • 감소 함수(sum(), min(), max())를 실행해야 하는데, 먼저 데이터를 변환하거나 필터링 해야 됨

해결

  • 데이터를 줄이면서 변형하는 가장 우아한 방식은 생성자 표현식
In [300]:
nums = [1, 2, 3, 4, 5]
In [302]:
s = sum(x * x for x in nums)
s
Out[302]:
55
In [397]:
# 디렉토리에 또 다른 .py 파일이 있는지 살펴보기
import os
files = os.listdir('.')
if any(name.endswith('.ipynb') for name in files):
    print('There by python!')
else:
    print('Sorry, no python.')
There by python!
In [398]:
!ls -l *.ipynb
-rw-r--r--  1 re4lfl0w  staff    6135 10  2 06:49 Untitled0.ipynb
-rw-r--r--  1 re4lfl0w  staff  245530 10  2 09:13 ch01.ipynb
In [308]:
# 튜플을 CSV로 출력한다.
s = ('ACME', 50, 123.45)
print(', '.join(str(x) for x in s))
ACME, 50, 123.45
In [310]:
# 자료 구조의 필드를 줄인다.
portfolio = [
{'name': 'GOOG', 'shares': 50},
{'name': 'YHOO', 'shares': 75},
{'name': 'AOL', 'shares': 20},
{'name': 'SCOX', 'shares': 65}
]
In [316]:
list(s['shares'] for s in portfolio)
Out[316]:
[50, 75, 20, 65]
In [312]:
min_shares = min(s['shares'] for s in portfolio)
min_shares
Out[312]:
20

토론

  • 함수에 인자로 전달된 생성자 표현식의 문법적인 측면을 보여줌(예: 즉, 반복적인 괄호를 할 필요가 없음)
In [318]:
# 생성자 표현식을 인자로 전달
s = sum((x * x for x in nums))
s
Out[318]:
55
In [319]:
# 더 우아한 방식
# 고민을 많이했는데 이게 좀 더 우아한 방식이군..
s = sum(x * x for x in nums)
s
Out[319]:
55
  • 생성자 인자를 사용하면 임시 리스트를 만드는 것보다 더 효율적이고 코드가 간결한 경우가 많음
In [320]:
nums = [1, 2, 3, 4, 5]
s = sum([x * x for x in nums])
s
Out[320]:
55
  • 물론 이 코드도 동작하지만 추가적인 리스트를 생성해야 한다는 번거로움
  • num의 크기가 방대해지면 한 번 쓰고 버릴 임시 리스트의 크기도 커진다는 문제 발생
  • 생성자를 사용하면 데이터를 순환 가능하게 변형하므로 메모리 측면에서 훨씬 유리
  • min(), max() 같은 함수는 key라는 여러 상황에서 유용한 인자를 받기 때문에 생성자 방식을 사용해야 하는 이유를 한 가지 더 만들어 줌
In [322]:
# 원본: 20을 반환
min_shares = min(s['shares'] for s in portfolio)
min_shares
Out[322]:
20
In [324]:
# 대안: {'name': 'AOL', 'shares': 20}을 반환
# 으.. 람다 적응 아직도 잘 안된다.
min_shares = min(portfolio, key=lambda s: s['shares'])
min_shares
Out[324]:
{'shares': 20, 'name': 'AOL'}
In [326]:
portfolio
Out[326]:
[{'shares': 50, 'name': 'GOOG'},
 {'shares': 75, 'name': 'YHOO'},
 {'shares': 20, 'name': 'AOL'},
 {'shares': 65, 'name': 'SCOX'}]

1.20 여러 매핑을 단일 매핑으로 합치기

  • 오! 드디어 1장의 마지막!!

문제

  • 딕셔너리나 매핑이 여러 개 있고, 자료 검색이나 데이터 확인을 위해서 하나의 매핑으로 합치고 싶다

해결

In [353]:
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
In [354]:
from collections import ChainMap
c = ChainMap(b,a)
print(c['x'])
print(c['y'])
print(c['z']) # b가 먼저 들어갔으니 우선권
1
2
4
In [355]:
from collections import ChainMap
c = ChainMap(a, b)
print(c['x'])
print(c['y'])
print(c['z']) # a가 먼저 들어갔으니 우선권이네
1
2
3

토론

  • ChainMap: 매핑을 여러 개 받아서 하나처럼 보이게 만듦
  • 그렇게 보이는 것일뿐 하나로 합치는 것은 아니다.
  • 단지 매핑에 대한 리스트를 유지하면서 리스트를 스캔하도록 일반적인 딕셔너리 동작을 재정의한다.
In [356]:
len(c)
Out[356]:
3
In [357]:
list(c.keys())
Out[357]:
['x', 'y', 'z']
In [358]:
list(c.values())
Out[358]:
[1, 2, 3]
  • 중복 키가 있으면 첫번째 매핑의 값을 사용함
  • c['z]는 언제나 딕셔너리의 a의 값을 참조하며 b의 값은 참조하지 않음(Chainmap(b,a)로 넘겨주면 b의 값을 참조)
In [359]:
c['z'] = 10
In [360]:
c['w'] = 40
In [361]:
c
Out[361]:
ChainMap({'w': 40, 'x': 1, 'z': 10}, {'y': 2, 'z': 4})
In [362]:
del c['x']
In [363]:
c
Out[363]:
ChainMap({'w': 40, 'z': 10}, {'y': 2, 'z': 4})
In [364]:
a
Out[364]:
{'w': 40, 'z': 10}
In [365]:
del c['y']
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-365-df3e26fa6544> in <module>()
----> 1 del c['y']

/Users/re4lfl0w/virtualenv/python3_test/lib/python3.4/collections/__init__.py in __delitem__(self, key)
    839             del self.maps[0][key]
    840         except KeyError:
--> 841             raise KeyError('Key not found in the first mapping: {!r}'.format(key))
    842 
    843     def popitem(self):

KeyError: "Key not found in the first mapping: 'y'"
  • ChainMap은 프로그래밍 언어의 변수와 같이 범위가 있는 값(즉, 전역변수, 지역변수)에서 사용하면 유용
  • 이 동작을 쉽게 만들어주는 메소드가 존재
In [369]:
values = ChainMap()
In [371]:
values['x'] = 1
values
Out[371]:
ChainMap({'x': 1})
In [372]:
# 새로운 매핑 추가
values = values.new_child()
In [373]:
values['x'] = 2
values
Out[373]:
ChainMap({'x': 2}, {'x': 1})
In [374]:
# 새로운 매핑 추가
values = values.new_child()
In [375]:
values['x'] = 3
values
Out[375]:
ChainMap({'x': 3}, {'x': 2}, {'x': 1})
In [376]:
# 마지막 매핑 삭제
values = values.parents
values
Out[376]:
ChainMap({'x': 2}, {'x': 1})
In [377]:
values['x']
Out[377]:
2
In [378]:
# 마지막 매핑 삭제
values = values.parents
values
Out[378]:
ChainMap({'x': 1})
In [379]:
values['x']
Out[379]:
1
In [380]:
values
Out[380]:
ChainMap({'x': 1})
  • ChainMap의 대안으로 update()를 사용해 딕셔너리를 하나로 합칠 수동 ㅣㅆ음
In [381]:
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
In [382]:
merged = dict(b)
merged
Out[382]:
{'y': 2, 'z': 4}
In [383]:
# z값이 update가 되네
merged.update(a)
merged
Out[383]:
{'x': 1, 'y': 2, 'z': 3}
In [384]:
merged['x']
Out[384]:
1
In [385]:
merged['y']
Out[385]:
2
In [386]:
merged['z']
Out[386]:
3
  • 이렇게 해도 잘 동작히지만, 완젼히 별개의 딕셔너리 객체를 새로 만들어야 한다.(혹은 기존 딕셔너리의 내용을 변경해야 한다.)
  • 또한 원본 딕셔너리의 내용이 변경된다 해도 합쳐 놓은 딕셔너리에 반영되지 않는다.
In [388]:
a['x'] = 13
In [389]:
merged['x']
Out[389]:
1
  • ChainMap은 원본 딕셔너리를 참조하기 때문에 이와 같은 문제 발생하지 않음. 쉽게 이야기 해서 C언어의 pointer를 이용한거네.
In [390]:
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
In [391]:
merged = ChainMap(a, b)
In [392]:
merged['x']
Out[392]:
1
In [393]:
a['x']  = 42
In [395]:
merged['x'] # pointer를 사용했기 때문에 합친 딕셔너리에도 변경됨.
Out[395]:
42
Comments powered by Disqus