ch01
1. 자료구조와 알고리즘¶
- start: 2014-09-30
- end: 2014-10-02
Python3¶
- Python in Real World: virtualenv를 사용하자 - 가상 개발환경 구축하기
- Python 3 & Python 2.7 dual install - the right way on OSX - YouTube
- 파이썬3를 설치하지 않고 python2로 해보려다가 예제들이 실행이 안되서 굴복하게 됐다. -_-
정의¶
- 파이썬은 list, set, dictionary 같이 유용한 자료 구조 내장
- 이러한 구조체의 장점은 사용이 편리
- 검색, 정렬, 순서, 여과 등에 대한 질문이 종종 생김
1.1 시퀀스를 개별 변수로 나누기¶
문제¶
- N개의 요소를 가진 튜플이나 시퀀스가 있다. 이를 변수 N개로 나누어야 한다.
해결¶
- 모든 시퀀스(혹은 이터레이팅 가능한 것)는 간단한 할당문을 사용해서 개별 변수로 나눔
- 주의사항: 변수의 개수가 시퀀스에 일치해야 한다는 것뿐
!python --version
p = (4, 5)
x, y = p
x
y
data = ['ACME', 50, 91.1, (2012, 12, 21)]
name, shares, price, date = data
name
shares
price
date
name, shares, price, (year, mon, day) = data
name
year
mon
day
요소 개수가 일치하지 않으면 에러 발생
p = (4, 5)
x, y, z = p
토론¶
- unpacking은 사실 튜플이나 리스트뿐만 아니라 순환 가능한 모든 객체에 적용 가능
- 문자열, 파일, iterator, generator도 포함
s = 'Hello'
a, b, c, d, e = s
a
b
c
d
e
data = ['ACME', 50, 91.1, (2012, 12, 21)]
# _는 이전 셀의 결과값 저장
_, shares, price, _ = data
shares
price
1.2 임의 순환체의 요소 나누기¶
문제¶
- 순환체를 언패킹하려는데 요소가 N개 이상 포함되어 "값이 너무 많습니다"" 라는 예외가 발생
해결¶
- "별 표현식" 사용(only python 3)
- PEP 3132 -- Extended Iterable Unpacking
- python - Finding the average of a list - Stack Overflow
ll = range(10)
first, *middle, last = ll
# only Python3
def avg(lst):
return sum(lst) / float(len(lst))
def drop_first_last(grades):
first, *middle, last = grades
return avg(middle)
# python2 version
def drop_first_last2(grades):
lst = grades[1:-1]
return sum(lst) / float(len(lst))
import random
l = [random.randint(1, 20) for i in range(10)]
l
sum(l)
sum(l) / float(len(l))
l[1:-1]
sum(l[1:-1]) / float(len(l[1:-1]))
drop_first_last(l)
drop_first_last2(l)
# numpy version
import numpy as np
np.mean(l)
# python2 version
def drop_first_last3(grades):
lst = grades[1:-1]
return np.mean(lst)
drop_first_last3(l)
사용자 주소¶
record = ('Dave', 'dave@example.com', '773-555-1212', '847-555-1212')
name, email, *phone_numbers = record
name
email
phone_numbers
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)
*trailing, current = [10, 8, 7, 1, 9, 5, 10, 3]
trailing
current
토론¶
- 언패킹 방식은 길이를 알 수 없는 순환체에 안성맞춤
- 떄때로 순환체에 들어있는 패턴이나 구조(예를 들어 1 뒤에 나오는 요소는 무조건 전화번호)를 가지고 있는데, 이럴 때도 별표 구문을 사용하면 개발자의 수고를 많이 덜어줌
- 길이가 일정하지 않은 튜플에 사용하면 상당히 편리
records = [
('foo', 1, 2),
('bar', 'hello'),
('foo', 3, 4)
]
def do_foo(x, y):
print('foo', x, y)
def do_bar(s):
print('bar', s)
# 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)
- 문자열 프로세싱에도 사용
line = 'nobody:*:-2:-2:Unprivileged user:/var/empty:/usr/bin/false'
line.split(':')
uname, *fields, homedir, sh = line.split(':')
uname
homedir
sh
- Unpacking 후에 특정값을 버리고 싶다면?
record = ('ACME', 50, 123.45, (12, 18, 2012))
# *fields 대신에 *_
name, *_, (*_, year) = record
name
year
items = [1, 10, 7, 4, 5, 9]
head, *tail = items
head
tails
def sum2(items):
head, *tail = items
return head + sum2(tail) if tail else head
sum2(items)
sum(items)
- 하지만 파이썬의 재귀적 제약이 존재하므로 마지막에 예로 든 함수는 학문적 호기심 이외는 쓸모없음
파이썬의 재귀적 재약¶
-
사실 팩토리얼 정도의 코드라면 C언어에서의 제한은 그렇게 크지 않습니다. 어떤 제한이 걸리냐 하면 운영체제마다 구조는 좀 다를 수는 있지만, C에서 스택의 크기는 쓰레드 안에서 작은 것은 64kB, 큰 것은 2MB정도 됩니다. 거기서 팩토리얼 함수는 지역변수를 전혀 안 쓰고도 구현할 수 있기 떄문에 지역변수는 스택포인터 증가에 영향을 주지 않고, 인수 전달과 리턴값, 상태가 저장되는 레지스터의 저장 등 때문에 아키텍처마다 다르지만 OS X에서는 48바이트 정도 다른 곳에서도 대략 8~128바이트 사이로 쓰게 됩니다. 따라서, 계산해보면 스택이 넘치지 않으면서 재귀호출을 할 수 있는 것은 대략 10000~100000번 내외라고 볼 수 있습니다.
-
파이썬도 C프로그램이기 때문에 이런 스택 크기의 제약을 받는데, 파이썬 인터프리터는 팩토리얼 함수보다 훨씬 지역함수도 많고, 한 번 호출 때 한 함수만 거치는 것이 아니라서, 팩토리얼 함수보다 한 번 재귀 호출에 쓰는 스택이 많이 늘어납니다. 그래서 보통 작은 데서는 500회, 많은 데서는 5000회 정도 재귀호출을 할 수 있고, 그 이상 되면 스택 오버플로우로 파이썬 VM 자체가 죽어버립니다. 그 것을 막으려고 파이썬에서는 재귀호출을 파이썬 내부에서 관리하고 있습니다.
-
스택리스 파이썬은 이런 파이썬 함수가 파이썬 함수를 부르는 과정을 재귀함수로 처리하지 않고 메모리를 힙에 할당해서 씁니다. 그래서 시스템 스택 크기 제약을 받지 않게 되는 덕에 메모리가 허용하는 한 무한히 재귀호출을 할 수 있습니다. http://www.stackless.com/
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)
help(deque)
토론¶
- 아이템을 찾는 코드를 작성할 때, 주로 yield를 포함한 제너레이터 함수를 만들곤 함
- 이렇게 하면 검색 과정과 결과를 사용하는 코드를 분리 가능
- 제너레이터가 무엇인지 레시피 4.3 참고
- dque(maxlen=N)으로 고정 크기 큐를 생성
- 큐가 꽉 찬 상태에서 새로운 아이템을 넣으면 가장 마지막 아이템이 자동으로 삭제됨
q = deque(maxlen=3)
q.append(1)
q.append(2)
q.append(3)
q
q.append(4)
q
q.append(5)
q
- 리스트를 사용해서 이런 과정을 수동으로 처리할 수도 있지만, 큐를 사용하는 것이 더 빠르고 보기도 좋음
- 큐 구조체가 필요할 때 deque를 사용 가능
- 최대 크기를 지정하지 않으면 제한없이 양쪽에 아이템을 넣거나 빼는 작업 가능
q = deque()
q.append(1)
q.append(2)
q.append(3)
q
q.appendleft(4)
q
q.pop()
q
q.popleft()
q
import heapq
nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
print(heapq.nlargest(3, nums))
print(heapq.nsmallest(3, nums))
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 형식으로
cheap = heapq.nsmallest(3, portfolio, key=lambda s: s['price'])
cheap
expensive = heapq.nlargest(3, portfolio, key=lambda s: s['price'])
expensive
cheap2 = heapq.nsmallest(3, portfolio, key=lambda s: s['shares'])
cheap2
# sorted를 활용한 연산
# 똑같기는 한데 nsmallest가 훨씬 가독성이 좋다.
sorted(portfolio, key=lambda s: s['shares'])[:3]
expensive2 = heapq.nlargest(3, portfolio, key=lambda s: s['shares'])
expensive2
sorted(portfolio, key=lambda s: s['shares'], reverse=True)[:3]
help(heapq.nlargest)
nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2]
import heapq
heap = list(nums)
heap
heapq.heapify(heap)
help(heapq.heapify)
heap
- 힙의 가장 중요한 기능: heap[0]이 가장 작은 아이템이 된다는 사실
- N을 힙의 크기라 하면 O(log N)의 시간 복잡도 소요
- 왜 자기 맘대로 순서가 바뀌지..??
- heap이라는 list를 heapq.heappop에 넣으면 여기에서 가장 작은게 pop 된다.
heapq.heappop(heap)
heapq.heappop(heap)
heapq.heappop(heap)
heapq.heappop(heap)
heap
heapq.heappop(heap)
heap
heapq.heappop(heap)
heap
heapq.heappop(heap)
heap
- nlargest()와 nsmallest() 함수는 찾고자 하는 아이템의 개수가 상대적으로 작을때 가장 알맞음
- 만약 최소값이나 최대값을 구하려 한다면(N이 1), min()과 max()를 사용하는 것이 더 빠름
- N의 크기가 컬렉션 크기와 비슷해지면 우선 컬렉션을 정렬해 놓고 그 조각을 사용하는 것이 더 빠르다.
- sorted(items)[:N] 이나 sorted(items)[-N:]
- 힙의 구현 방식?!
- 8.4. heapq â Heap queue algorithm — Python 2.7.8 documentation
- 힙 (자료 구조) - 위키백과, 우리 모두의 백과사전): 설명이 어렵다.
-
중성자 별의 충돌 에너지 :: 자료구조 #5 힙(Heap)
- 아하! min heap은 가장 작은 숫자를 유지하기 위해 끊임없이 교환을 하는구만. 그래서 heap의 heap[0]은 가장 작을 수 밖에 없네
- 완전이진트리 유지
- 홀수, 짝수에 의해 인덱스값 결정
-
양군&우자 :: [ 자료구조 ] 힙(Heap) 자료구조
- 각각의 노드는 유일한 키 값을 가지게 됨
- 노드 중에서 최대값이나 최소값을 빠른시간 내에 찾기 위해서 만든 자료구조
- 키값과는 별개로 중복된 값은 허용 됨
- 정렬알고리즘 종류및 간단한 설명
- 자료구조 5강. 힙(Heap) :: 금지된 엑시노아의 비공정
- 알고리즘이 재밌구만ㅎㅎ
- 모르면 진짜 하기 싫은데 조금씩 알아가는 재미가 쏠쏠하다
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]
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)
q = PriorityQueue()
q.push(Item('foo'), 1)
q.push(Item('bar'), 5)
q.push(Item('spam'), 4)
q.push(Item('grok'), 1)
q
q.pop()
q.pop()
q.pop()
q.pop()
- 첫번째 pop()이 어떻게 가장 높은 우선 순위 아이템을 반환하는지 관찰
- 두 아이템의 우선 순위가 같은 경우(foo와 grok)에는 큐에 삽입된 순서와 동일하게 반환
- 7.1. string â Common string operations — Python 2.7.8 documentation: !r 검색
토론¶
- 가장 중요한 부분: heapq 모듈의 사용법
- heapq.heappush()와 heapq.heappop()은 list_queue의 첫번째 아이템이 가장 작은 우선 순위를 가진 것처럼 아이템을 삽입하거나 제거한다.
- 힙의 크기가 N일 때 푸시와 팝의 시간 복잡도가 O(logN)이므로 N이 아주 커진다 해도 상당히 효율적
- 큐가 튜플 형태로 구성(-priority, index, item)
- priority값은 큐 내부 아이템을 가장 높은 우선 순위에서 낮은 우선 순위로 정렬하기 위해 무효화된다?!
- 이는 가장 낮은 값에서 높은 값으로 정렬되는 일반적인 힙과는 반대다.
- index 변수는 우선 순위가 동일한 아이템의 순서를 정할 때 사용
- 아이템이 삽입된 순서대로 정렬
- 인덱스는 우선 순위 값이 동일할 때도 중요한 역할
priority, index의 중요성¶
a = Item('foo')
b = Item('bar')
a < b
- (priority, item) 튜플을 만들었다면 우선 순위 값이 달라야만 비교가 가능
- 하지만 동일한 우선 순위를 가진 두 아이템의 비교는 앞에 나온 바와 같이 실패
a = (1, Item('foo'))
b = (5, Item('bar'))
a < b
c = (1, Item('grok'))
a < c
- 여기에 인덱스 값을 추가해서 튜플을 만들면(priority, index, item), 어떠한 튜플도 동일한 인덱스 값을 가질 수 없으므로 이 문제를 원천적으로 해결 가능(파이썬은 일단 비교 결과가 나오고 나면 뒤에 남아있는 튜플 값을 비교하려고 시도하지 않음)
a = (1, 0, Item('foo'))
b = (5, 1, Item('bar'))
c = (1, 2, Item('grok'))
a < b
a < c
- 쓰레드 간 통신에 이 큐를 사용하려면 올바른 락과 시그널 사용
d = {
'a': [1,2,3],
'b': [4,5]
}
e = {
'a': {1, 2, 3},
'b': {4, 5}
}
d
e
- 리스트: 아이템의 삽입 순서 유지
- 세트: 순서x, 중복x
- 딕셔너리를 쉽게 만들기 위해서 collections 모듈의 defaultdict 사용
- defaultdict의 기능 중에는 첫번째 값을 자동으로 초기화하는 것이 있음
from collections import defaultdict
d = defaultdict(list)
d['a'].append(1)
d
d['a'].append(2)
d
d['b'].append(4)
d['b'].append(4)
d
ss = defaultdict(set)
ss['a'].add(1)
ss
ss['a'].add(2)
ss['b'].add(4)
ss['b'].add(4)
ss
- 다만 defaultdict를 사용할 때는 딕셔너리에 존재하지 않는 값이라도 한 번이라도 접근했던 키의 엔트리를 자동으로 생성
- 마음에 들지 않는다면 일반 딕셔너리의 setdefault 사용
# 뭔 얘기인가 했더니 여러값을 저장하기 위해서 []를 만들었다는 얘기고만
# 4,4를 저장해야 되는데 그냥 변수로 할당하면 안되고 리스트로 만들어야 하니까
d = {}
d.setdefault('a', []).append(1)
d.setdefault('a', []).append(2)
d.setdefault('b', []).append(4)
d.setdefault('b', []).append(4)
d
- 하지만 많은 프로그래머들은 setdefault()가 자연스럽지 않다고 생각
- 첫번째 값에 대해서 항상 새로운 인스턴스를 생성한다는 점은 언급하지 않더라도 말이다
토론¶
- 이론적으로 여러값을 가지는 딕셔너리를 만드는 것이 복잡하지는 않음
- 첫번째 값에 대한 초기화를 스스려 하려면 꽤나 복잡한 과정
from random import randint
pairs = [(randint(1,3), i) for i in range(10)]
d = {}
from random import randint
for key, value in pairs:
if key not in d:
d[key] = []
d[key].append(value)
d
d = defaultdict(list)
for key, value in pairs:
d[key].append(value)
d
from collections import OrderedDict
d = OrderedDict()
d['foo'] = 1
d['bar'] = 2
d['spam'] = 3
d['grok'] = 4
d
for key in d:
print(key, d[key])
for key, value in d.items():
print(key, value)
- OrderedDict는 나중에 직렬화하거나 다른 포맷으로 인코딩할 다른 매핑을 만들 때 특히 유용
- JSON 인코딩에 나타나는 특정 필드의 순서를 조절하기 위해서 OrderedDict에 다음과 같이 데이터를 생성
import json
json.dumps(d)
토론¶
- OrderedDict는 내부적으로 doubly linked list로 삽입 순서와 관련있는 키를 기억
- 새로운 아이템을 처음으로 삽입하면 리스트의 제일 끝에 위치시킴
- 기존 키에 재할당을 한다해도 순서에는 변화가 생기지 않음
- 더블 링크드 리스트를 사용하기 때문에 일반적인 딕셔너리에 비해서 2배로 크다.
- 매우 큰 데이터 구조체를 만든다면 고심 해봐야 됨(10만 라인 csv 정도)
prices = {
'ACME': 45.23,
'AAPL': 612.78,
'IBM': 205.55,
'HPQ': 37.20,
'FB': 10.75
}
- 딕셔너리 내용에 대해 유용한 계산을 하려면 딕셔너리의 키와 값을 zip()으로 뒤집어 주는 것이 좋음
- 최소 주가와 최대 주가를 찾는 코드를 살펴보자
aa = zip(prices.values(), prices.keys())
print(aa)
for i in aa:
print(i)
bb = zip(prices.keys(), prices.values())
print(type(bb))
print(bb)
for i in bb:
print(i)
min_price = min(zip(prices.values(), prices.keys()))
min_price
max_price = max(zip(prices.values(), prices.keys()))
max_price
min_price2 = min(zip(prices.keys(), prices.values()))
min_price2
min_price3 = max(zip(prices.keys(), prices.values()))
min_price3
- 이와 유사하게 데이터의 순서를 매기려면 zip()과 sorted()를 함께 사용
cc = zip(prices.values(), prices.keys())
print(cc)
sorted(cc)
prices_sorted = sorted(zip(prices.values(), prices.keys()))
prices_sorted
prices_sorted = sorted(zip(prices.values(), prices.keys()), reverse=True)
prices_sorted
- 계산을 할 때, zip()은 단 한 번만 소비할 수 있는 iterator를 생성
prices_and_names = zip(prices.values(), prices.keys())
print(prices_and_names)
print(min(prices_and_names))
# 왜 에러가 나는지 모르겠다..
print(max(prices_and_names))
prices.values()
prices_and_names = zip(prices.values(), prices.keys())
print(prices_and_names)
print(min(prices_and_names))
# 왜 에러가 나는지 모르겠다..
print(max(prices_and_names))
왜 max만 Error가 발생하지?¶
- min은 발생하지 않고..?
- min과 max의 로직이 달라서 그런것 같은데?
- 2. Built-in Functions — Python 2.7.8 documentation: min, max
max([1, 10, 99, 33])
min([1, 10, 99, 33])
토론¶
- 딕셔너리에서 일반적인 데이터 축소를 시도하면, 오직 키에 대해서만 작업이 이루어짐
prices
min(prices)
max(prices)
- 딕셔너리의 값에 대해서 알고 싶음
min(prices.values())
max(prices.values())
- 키에 일치하는 값 정보까지 알고 싶다면?(어떤 주식의 값이 가장 낮을까?)
min(prices, key=lambda k: prices[k])
max(prices, key=lambda k: prices[k])
- 하지만 최소값을 얻기 위해서 한 번 더 살펴보는 작업이 필요함
min_value = prices[min(prices, key=lambda k: prices[k])]
min_value
prices['FB']
- zip()을 포함한 해결책은 딕셔너리의 시퀀스를 (value, key) 페어로 뒤집는 것으로 문제 해결
- 이런 튜플에 비교를 수행하면 value 요소를 먼저 비교하고 뒤이어 key를 비교
- 명령어 하나만으로 정확히 우리가 원하는 데이터 축소화 정렬을 수행
- 여러 엔트리가 동일한 값을 가지고 있을 때 비교 결과를 결정하기 위해서 키를 사용한다는 점 주목
- min()과 max()를 계산할 때 중복된 값이 있으면 가장 작거나 큰 키를 가지공 ㅣㅆ는 엔트리를 반환
prices = {'AAA': 45.23,
'ZZZ': 45.23}
min(zip(prices.values(), prices.keys()))
max(zip(prices.values(), prices.keys()))
a = {
'x': 1,
'y': 2,
'z': 3
}
b = {
'w': 10,
'x': 11,
'y': 2
}
- 두 딕셔너리의 유사점을 찾으려면 keys()와 items() 메소드에 집한 연산 수행
# 동일한 키 찾기
a.keys() & b.keys()
# a에만 있고 b에는 없는 키 찾기
a.keys() - b.keys()
- 이런 연산을 사용해서 딕셔너리의 내용을 수정하거나 걸러낼 수 있음
- 선택한 키를 삭제한 새로운 딕셔너리를 만들고 시ㅣㅍ을 때는
# 특정 키를 제거한 새로운 딕셔너리 만들기
c = {key:a[key] for key in a.keys() - {'z', 'w'}}
c
토론¶
- 딕셔너리는 키와 값의 매핑으로 이루어짐
- 집합 연산 기능도 있음(합집합, 교집합, 여집합)
- items() 메소드는 key,value 페어로 구성된 item-view 객체를 반환
- values() 메소드는 앞에 나온 집합 연산을 지원하지 않음, key처럼 유일하다는 보장이 없기 때문
def dedupe(items):
seen = set()
for item in items:
if item not in seen:
yield item
seen.add(item)
a = [1, 5, 2, 1, 9, 1, 5, 10]
# 받을 때 yield generator로 계속 받으니까 최종적으로 list로 변경해줘야 되네
list(dedupe(a))
dedupe(a)
print(dedupe(a))
# 그냥 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)
- 시퀀스의 아이템이 해시 가능한 경우에만 사용 가능
- 해시 불가능한 타입(dict)의 중복을 없애려면 레시피에 약간의 수정이 필요
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 인자의 목적: 중복 검사를 위해 함수가 시퀀스 아이템을 해시 가능한 타입으로 변환한다고 명시
a = [
{'x': 1, 'y': 2},
{'x': 1, 'y': 3},
{'x': 1, 'y': 2},
{'x': 2, 'y': 4}
]
# 아...이게 뭔 귀신 신나락 까먹는 소리인가 한참 고민했네
# 내가 아직 내공이 부족하구나 라는 자괴감에 잠시 빠짐..
# a라는 dictionary에서 x, y 2개를 key로 잡아서 중복을 제거하겠다는 의미
# 총 4개가 들어가는데 1,2가 중복이 되기 때문에 총 3개만 출력 됨
list(dedupe_dict(a, key=lambda d: (d['x'], d['y'])))
# 이건 x값으로 중복 체크하겠다는 의미
# 그래서 1,2만 통과되서 총 2개 출력
list(dedupe_dict(a, key=lambda d: d['x']))
- 뒤에 나온 해결책은 필드가 하나이거나 커다란 자료 구조에 기반한 값의 중복을 없앨 때도 잘 동작
- 자세한건 나중에 직접 써 먹을 때 다시 한 번 확인해야겠다.
- 지금은 대충 훑어봤다. 지금 완전히 이해할 수 없으니 나중에 내가 꼭 써야 할 때 이 문서를 찾으면 훨씬 이해도가 빠를 것이다.
토론¶
- 중복을 없애려면 대게 세트를 만드는 것이 가장 쉬움
a = [1, 5, 2, 1, 9, 1, 5, 10]
set(a)
- set을 이용하면 기존 데이터의 순서가 훼손 됨
- generator 함수를 사용했기 때문에 단순히 리스트 프로세싱 말고도 아주 일반적인 목적으로 함수를 사용할 수 있음
- 파일을 읽어 들일 때 중복된 라인을 무시하려면 단순히 다음과 같은 코드 사용
!cat 01/duplicate.txt
with open('01/duplicate.txt') as f:
for line in dedupe(f):
print(line.strip())
- key 함수의 스펙은 파이썬 내장 함수인 sorted(), min(), max() 등의 기능을 흉내 내고 있음
record = '....................100 ........513.25 ..........'
record[20:32]
record[40:48]
cost = int(record[20:32]) * float(record[40:48])
cost
SHARES = slice(20, 32)
SHARES
PRICE = slice(40, 48)
PRICE
cost = int(record[SHARES]) * float(record[PRICE])
cost
- 이렇게 하는 것도 나쁘지 않지만 나중에 자리 위치가 바뀌게 되면 어떻게 할거냐?
- bio python에서 나왔던 struct 모듈을 사용하는게 좋아 보인다.
- 아니면 개념을 잡아주는 프로그래밍 정석에 나왔던대로 각각의 튜플로 만들어 주던가
토론¶
- 일반적으로 내장 함수인 slice()는 슬라이스 받는 모든 곳에 사용할 수 있는 조각을 생성
items = [0, 1, 2, 3, 4, 5, 6]
a = slice(2, 4)
a
items[2:4]
items[a]
items[a] = [10, 11]
items
del items[a]
items
- slice 인스턴스 s가 있다면 s.start와 s.stop, s.step 속성을 통해 좀 더 많은 정보를 얻을 수 있음
# 책에 오타로 10으로 써져 있어서 한참 고생
a = slice(5, 50, 2)
a.start
a.stop
a.step
- 추가적으로 indices(size) 메소드를 사용하면 특정 크기의 슬라이스를 매핑
- 튜플(start, stop, step)을 반환하는데, 모든 값은 경계를 넘어서지 않도록 제약이 걸려있다(인덱스에 접근할 때 IndexError 예외가 발생하지 않도록 하기 위함)
s = 'HelloWorld'
len(s)
# a slice 설정값에 len(s)가 max가 되네
a.indices(len(s))
range(*a.indices(len(s)))
for i in range(*a.indices(len(s))):
print(s[i])
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'
]
from collections import Counter
word_counts = Counter(words)
top_three = word_counts.most_common(3)
print(top_three)
print(word_counts)
토론¶
- Counter 객체에는 해시 가능한 모든 아이템을 입력 가능
- 내부적으로 Counter는 아이템이 나타난 횟수를 가리키는 딕셔너리
word_counts['not']
word_counts['eyes']
morewords = ['why', 'are', 'you', 'not', 'looking', 'in', 'my', 'eyes']
for word in morewords:
word_counts[word] += 1
word_counts['eyes']
word_counts
# list로 줄 수도 있네
word_counts.update(morewords)
word_counts
- 수식도 가능
a = Counter(words)
b = Counter(morewords)
a
b
c = a + b
c
d = a - b
d
- 데이터의 개수를 파악해야 하는 문제에 있어 Counter 객체는 매우 유용
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}
]
from operator import itemgetter
rows_by_fname = sorted(rows, key=itemgetter('fname'))
rows_by_fname
rows_by_uid = sorted(rows, key=itemgetter('uid'))
rows_by_uid
rows_by_lfname = sorted(rows, key=itemgetter('lname', 'fname'))
rows_by_lfname
토론¶
- 키워드 인자 key를 받는 내장 함수 sorted()에 rows를 전달
- rows로부터 단일 아이템을 받는 호출 가능 객체를 입력으로 받고 정렬의 기본이 되는 값을 반환
- itemgetter() 함수는 그런 호출 가능 객체를 생성
- operator.itemgetter() 함수는 rows 레코드에서 원하는 값을 추출하는데 사용하는 인덱스를 인자로 받는다. 딕셔너리 키 이름이나 숫자 리스트 요소가 될 수도 있고, 객체의 __getitem__() 메소드에 넣을 수 있는 모든 값이 가능
- itemgetter()에 여러 인덱스를 전달하면, 생성한 호출 가능 객체가 모든 요소를 가지고 있는 튜플을 반환하고, sorted()가 튜플의 정렬 순서에 따라 결과의 순서를 잡음. 여러 필드를 동시에 정렬할 때 유용
itemgetter() -> lambda 표현식¶
rows_by_fname = sorted(rows, key=lambda r: r['fname'])
rows_by_fname
rows_by_lfname = sorted(rows, key=lambda r: (r['lname'], r['fname']))
rows_by_lfname
- itemgetter()를 사용한 코드의 실행 속도가 조금 더 빠르다.
- 그리고 더 쉽다.
min(rows, key=itemgetter('uid'))
max(rows, key=itemgetter('uid'))
class User:
def __init__(self, user_id):
self.user_id = user_id
def __repr__(self):
return 'User({})'.format(self.user_id)
users = [User(23), User(3), User(99)]
users
sorted(users, key=lambda u: u.user_id)
- lambda를 사용하는 대신 operator.attrgetter()를 사용해도 됨
from operator import attrgetter
sorted(users, key=attrgetter('user_id'))
sorted(users, key=itemgetter('user_id'))
토론¶
- lambda를 사용할지 attrgetter()를 사용할지 여부는 개인의 선호도
- attrgetter()의 속도가 빠른 경우가 종종 있고 동시에 여러 필드를 추출하는 기능이 추가되어 있음
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)
users = [User('ABC', 'ZXY'), User('BCD', 'XYZ'), User('BCD', 'XXY')]
users
by_name = sorted(users, key=attrgetter('last_name', 'first_name'))
by_name
itemgetter vs attrgetter¶
- itemgetter: dictionary
- attrgetter: class
-
python - Sorting dictionary using operator.itemgetter - Stack Overflow
- python3에서 되는 방법은 나중에 찾아보자! skip!
mydict = { 'a1': ['g',6],
'a2': ['e',2],
'a3': ['h',3],
'a4': ['s',2],
'a5': ['j',9],
'a6': ['y',7] }
mydict
sorted(mydict.values(), key=itemgetter(1))
sorted(mydict.items(), key=itemgetter(1))
sorted(mydict.items(), key=itemgetter(0))
sorted(mydict.items(), key=itemgetter(1)(1))
sorted(mydict.items(), key=lambda (k,v): itemgetter(1)(v))
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)
def snd(pair):
x, y = pair
return y
list_of_tuples = [(1,2), (3,4), (5,0)]
min(list_of_tuples, key=snd)
- itemgetter는 positon을 파라미터로 보내주면
- seqeunce에서 __getitem__ 으로 sequence의 position 값이 무엇인지 알려준다.
Using itemgetter¶
from operator import itemgetter
list__ = [1,2,3]
print(itemgetter(1)(list__))
print(itemgetter(2)(list__))
def __itemgetter(position):
def applier(sequence):
return sequence.__getitem__(position)
return applier
__itemgetter(2)(list__)
from operator import itemgetter
list_of_tuples = [(1,2), (3,4), (5,0)]
min(list_of_tuples, key=itemgetter(1))
Using Attrgetter¶
- index 대신에 attribute
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)
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)
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()
from operator import itemgetter
from itertools import groupby
rows.sort(key=itemgetter('date'))
rows
for date, items in groupby(rows, key=itemgetter('date')):
print(date)
for i in items:
print(' ', i)
토론¶
- groupby() 함수는 시퀀스를 검색하고 동일한 값(혹은 키 함수에서 반환한 값)에 대한 일련의 '실행'을 찾는다.
- 개별 순환에 대해서 값, 그리고 같은 값을 가진 그룹의 모든 아이템을 만드는 iterator를 함께 반환
- 원하는 필드에 따라 데이터를 정렬해야 하는 과정이 중요
- groupby() 함수는 연속된 아이템에만 동작하기 때문에 정렬 과정을 생략하면 원하는 대로 함수를 실행할 수 없음
- 단순히 날짜에 따라 데이터를 묶어서 커다란 자료 구조에 넣어 놓고 원할 때마다 접근하려는 것
- defaultdict()를 사용해서 multidict를 구성하는게 나을 수 있음
from pprint import pprint
from collections import defaultdict
rows_by_date = defaultdict(list)
for row in rows:
rows_by_date[row['date']].append(row)
for r in rows_by_date['07/01/2012']:
print(r)
- 메모리 사용량에 크게 구애 받지 않는다면 이 방식을 사용하는 것이 정렬한 후에 groupby()를 사용하는 첫 번째 방법보다 더 빠를 것
mylist = [1, 4, -5, 10, -7, 2, 3, -1]
[n for n in mylist if n > 0]
[n for n in mylist if n < 0]
- 단점: 입력된 내용이 크면 매우 큰 결과가 생성될 수도 있다.
- 생성자 표현식으로 해결
# generator를 생성했기 때문에 이걸로 list comprehension처럼 바로 결과값을 얻을 수 없다.
pos = (n for n in mylist if n > 0)
pos
type(pos)
# generator라서 한 번 돌면 pos에 아무것도 남지 않는구나
list(pos)
type(pos)
# generator는 for문에서 실행될 때 결과값을 확인할 수 있다.
for x in pos:
print(x)
- list comprehension이나 생성자 표현식에 필터 필터 조건을 만드는 것이 쉽지 않을 때가 존재
- 필터링 도중에 예외 처리를 해야 한다거나 다른 복잡한 내용이 들어가야 한다면 어떻게?
- filter()
values = ['1', '2', '-3', '-', '4', 'N/A', '5']
def is_int(val):
try:
x = int(val)
return True
except ValueError:
return False
# filter: func, iterable
isvals = list(filter(is_int, values))
isvals
- filter()는 iterator를 생성한다.
- 따라서 결과의 리스트를 만들고 싶다면 위에 나온대로 list()도 함께 사용해야 함
토론¶
- list comprehension과 생성자 표현식은 간단한 데이터를 걸러내기 위한 가장 쉽고 직관적인 방법
- 또한 동시에 데이터 변형 기능도 있음
mylist = [1, 4, -5, 10, -7, 2, 3, -1]
import math
[math.sqrt(n) for n in mylist if n > 0]
- 필터링에는 조건을 만족하지 않는 값을 걸러내는 것 외에도 새로운 값으로 치환하는 방식도 존재
- 양수만 찾아내는 필터링뿐 아니라 잘못된 값을 특정 범위에 들어가도록 수정
- 필터링 조건을 조건 표현식으로 변경하면 간단히 해결
[n if n > 0 else 0 for n in mylist]
[math.sqrt(n) if n > 0 else 0 for n in mylist]
clip_pos = [n if n < 0 else 0 for n in mylist]
clip_pos
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',
]
counts = [0, 3, 10, 4, 1, 7, 6, 1]
- 카운트 값이 5 이상인 주소만 남기려 한다면..?
from itertools import compress
# 이거 numpy에 있던 기능 같은데..?
more5 = [n > 5 for n in counts]
more5
compress(addresses, more5)
list(compress(addresses, more5))
- 우선 주어진 조건에 만족하는지 여부를 담은 Boolean 시퀀스를 만들어 두는 것이 포인트
- compress() 함수로 True에 일치하는 값만 골라냄
- filter()와 마찬가지로, compress()는 일반적으로 이터레이터를 반환
- 따라서 실행 결과를 리스트에 담고 싶다면 list()를 사용해야 함
d = dict(zip(addresses, counts))
d
# map, zil, filter 이런 종류들 매번 햇깔린다.
for i in map(lambda a: a*a, range(6)):
print(i)
d.items()
Python Tips, Tricks, and Hacks: Dictionary Comprehensions로 해보려고 참고¶
[k for k, v in d.items() if v > 5]
dict(['a', 1])
dict_as_list = [['a', 1], ['b', 2], ['c', 3]]
dict(dict_as_list)
# error 발생
dict(['a', 1])
dict([['a', 1]])
# dictionary 만들 때 [k, v] 로 감싸줘야 되는군..
dict([[k, v] for k, v in d.items() if v > 5])
dict([k, v] for k, v in d.items() if v > 5)
# 아 이걸 찾으려고 엄청 삽질했네 ㅡ.ㅡ
# 그래 {}로 감싸주고 k:v 하면 되는거잖아!
{k:v for k, v in d.items() if v > 5}
# 이렇게 만들어주면 compress를 사용하는 것과 같다.
# 그런데 바로 적용하는 것은 compress가 좀 더 쉽다고 생각되네
# compress 사용할 상황이 되면 compress를 쓰자.
list(k for k, v in d.items() if v > 5)
prices = {
'ACME': 45.23,
'AAPL': 612.78,
'IBM': 205.55,
'HPQ': 37.20,
'FB': 10.75
}
# 가격이 200 이상인 것에 대한 딕셔너리
p1 = {key:value for key, value in prices.items() if value > 200}
p1
# 기술 관련 주식으로 딕셔너리 구성
tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'}
p2 = {key:value for key, value in prices.items() if key in tech_names}
p2
토론¶
- dictionary comprehension으로 할 수 있는 대부분의 일은 tuple sequence를 만들고 dict() 함수에 전달하는 것으로도 할 수 있음
p1 = dict((key, value) for key, value in prices.items() if value > 200)
p1
- dictionary comprehension을 사용하는 것이 더 깔끔하고 실행 속도 측면에서 보아도 조금 더 유리(prices 딕셔너리의 경우 실행 속도에서 2배 차이 발생)
- 동일한 문제를 해결하는 데는 언제나 많은 방법이 존재
tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'}
p2 = { key:prices[key] for key in prices.keys() & tech_names}
p2
1.18 시퀀스 요소에 이름 매핑¶
문제¶
- 리스트나 튜플의 위치로 요소에 접근하는 코드 존재
- 하지만 때론 이런 코드의 가독성이 떨어짐
- 또한 위치에 의존하는 코드의 구조도 이름으로 접근 가능하도록 수정하고 싶음
해결¶
- collections.namedtuple()을 사용하면 일반적인 튜플 객체를 사용하는 것에 비해 그리 크지 않은 오버헤드로 이 기능을 구현
- collections.namedtuple()은 실제로 표준 파이썬 tuple 타입의 서브 클래스를 반환하는 팩토리 메소드
- 타입 이름과, 포함해야 할 필드를 전달하면 인스턴스화 가능한 클래스를 반환
- 여기에 필드의 값을 전달하는 식으로 사용 가능
from collections import namedtuple
Subscriber = namedtuple('Subscriber', ['addr', 'joined'])
sub = Subscriber('jonesy@example.com', '2012-10-19')
sub
sub.addr
sub.joined
- namedtuple의 인스턴스는 일반적인 클래스 인스턴스와 비슷해 보이지만 튜플과 교환이 가능하고, 인덱싱이나 언패킹과 같은 튜플의 일반적인 기능을 모두 지원
len(sub)
addr, joined = sub
addr
joined
- namedtuple은 주로 요소의 위치를 기반으로 구현되어 있는 코드를 분리
- 요소의 위치로 접근하는 코드가 있을 때, 예를 들어 테이블에 새로운 열이 추가되었다거나 할 때 문제가 발생할 수 있음
- 반환된 튜플을 네임드 튜플로 변환했다면 이런 문제를 예방할 수 있음
def compute_cost(records):
total = 0.0
for rec in records:
total += rec[1] * rec[2]
return total
records = [['name', 20, 1000], ['name2', 30, 2000]]
compute_cost(records)
Stock = namedtuple('Stock', ['name', 'shares', 'price'])
- 자료구조를 많이 알고 있으면 쓸데없는 삽질을 방지해 줄 수 있겠네.
- 괜히 구현한다고 힘 빼지 않고 그냥 가져다 쓰면 되잖아..
- 이미 잘 만들어져 있는것 가져다 쓰면 된다.
- 자동차 만드는데 바퀴를 다시 발명할 이유는 없잖아?
- 그런데 그 함수에 버그가 존재한다면? SSL heartbleed, bash bug
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
compute_cost(records)
- 만약에 name과 price 자리가 바뀌게 된다면?
- 제일 처음에 한 방법은 rec[1] rec[2] -> rec[0] rec[1]로 변경해야 한다.
- 하지만 namedtuple은 제일 처음 namedtuple을 설정할 때만 변경해주면 된다.
- Stock = namedtuple('Stock', ['name', 'shares', 'price'])
- Stock = namedtuple('Stock', ['shares', 'price', 'name'])
Stock_change = namedtuple('Stock', ['shares', 'price', 'name'])
records = [[20, 1000, 'name'], [30, 2000, 'name2']]
def compute_cost_change(records):
total = 0.0
for rec in records:
s = Stock_change(*rec)
total += s.shares * s.price
return total
compute_cost_change(records)
토론¶
- namedtuple은 저장 공간을 더 필요로 하는 딕셔너리 대신 사용 가능
- 딕셔너리를 포함한 방대한 자료 구조를 구상하고 있다면 namedtuple을 사용하는 것이 더 효율적
- 딕셔너리와는 다르게 namedtuple은 수정 불가
s = Stock('ACME', 100, 123.45)
s
s.shares
s.shares = 75
- 속성을 수정하려 한다면 _replace() 메소드를 사용
- 완전히 새로운 namedtuple 생성
s = s._replace(shares=75)
s
- _replace() 메소드를 사용하면 옵션이나 빈 필드를 가진 namedtuple을 간단히 만들 수 있음
- 일단 기본값을 가진 프로토타입 튜플을 만들고, _replace()로 치환된 값을 가진 새로운 인스턴스를 만듦
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)
a = {'name': 'ACME', 'shares': 100, 'price': 123.45}
dict_to_stock(a)
b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'}
dict_to_stock(b)
nums = [1, 2, 3, 4, 5]
s = sum(x * x for x in nums)
s
# 디렉토리에 또 다른 .py 파일이 있는지 살펴보기
import os
files = os.listdir('.')
if any(name.endswith('.ipynb') for name in files):
print('There by python!')
else:
print('Sorry, no python.')
!ls -l *.ipynb
# 튜플을 CSV로 출력한다.
s = ('ACME', 50, 123.45)
print(', '.join(str(x) for x in s))
# 자료 구조의 필드를 줄인다.
portfolio = [
{'name': 'GOOG', 'shares': 50},
{'name': 'YHOO', 'shares': 75},
{'name': 'AOL', 'shares': 20},
{'name': 'SCOX', 'shares': 65}
]
list(s['shares'] for s in portfolio)
min_shares = min(s['shares'] for s in portfolio)
min_shares
토론¶
- 함수에 인자로 전달된 생성자 표현식의 문법적인 측면을 보여줌(예: 즉, 반복적인 괄호를 할 필요가 없음)
# 생성자 표현식을 인자로 전달
s = sum((x * x for x in nums))
s
# 더 우아한 방식
# 고민을 많이했는데 이게 좀 더 우아한 방식이군..
s = sum(x * x for x in nums)
s
- 생성자 인자를 사용하면 임시 리스트를 만드는 것보다 더 효율적이고 코드가 간결한 경우가 많음
nums = [1, 2, 3, 4, 5]
s = sum([x * x for x in nums])
s
- 물론 이 코드도 동작하지만 추가적인 리스트를 생성해야 한다는 번거로움
- num의 크기가 방대해지면 한 번 쓰고 버릴 임시 리스트의 크기도 커진다는 문제 발생
- 생성자를 사용하면 데이터를 순환 가능하게 변형하므로 메모리 측면에서 훨씬 유리
- min(), max() 같은 함수는 key라는 여러 상황에서 유용한 인자를 받기 때문에 생성자 방식을 사용해야 하는 이유를 한 가지 더 만들어 줌
# 원본: 20을 반환
min_shares = min(s['shares'] for s in portfolio)
min_shares
# 대안: {'name': 'AOL', 'shares': 20}을 반환
# 으.. 람다 적응 아직도 잘 안된다.
min_shares = min(portfolio, key=lambda s: s['shares'])
min_shares
portfolio
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
from collections import ChainMap
c = ChainMap(b,a)
print(c['x'])
print(c['y'])
print(c['z']) # b가 먼저 들어갔으니 우선권
from collections import ChainMap
c = ChainMap(a, b)
print(c['x'])
print(c['y'])
print(c['z']) # a가 먼저 들어갔으니 우선권이네
토론¶
- ChainMap: 매핑을 여러 개 받아서 하나처럼 보이게 만듦
- 그렇게 보이는 것일뿐 하나로 합치는 것은 아니다.
- 단지 매핑에 대한 리스트를 유지하면서 리스트를 스캔하도록 일반적인 딕셔너리 동작을 재정의한다.
len(c)
list(c.keys())
list(c.values())
- 중복 키가 있으면 첫번째 매핑의 값을 사용함
- c['z]는 언제나 딕셔너리의 a의 값을 참조하며 b의 값은 참조하지 않음(Chainmap(b,a)로 넘겨주면 b의 값을 참조)
c['z'] = 10
c['w'] = 40
c
del c['x']
c
a
del c['y']
- ChainMap은 프로그래밍 언어의 변수와 같이 범위가 있는 값(즉, 전역변수, 지역변수)에서 사용하면 유용
- 이 동작을 쉽게 만들어주는 메소드가 존재
values = ChainMap()
values['x'] = 1
values
# 새로운 매핑 추가
values = values.new_child()
values['x'] = 2
values
# 새로운 매핑 추가
values = values.new_child()
values['x'] = 3
values
# 마지막 매핑 삭제
values = values.parents
values
values['x']
# 마지막 매핑 삭제
values = values.parents
values
values['x']
values
- ChainMap의 대안으로 update()를 사용해 딕셔너리를 하나로 합칠 수동 ㅣㅆ음
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
merged = dict(b)
merged
# z값이 update가 되네
merged.update(a)
merged
merged['x']
merged['y']
merged['z']
- 이렇게 해도 잘 동작히지만, 완젼히 별개의 딕셔너리 객체를 새로 만들어야 한다.(혹은 기존 딕셔너리의 내용을 변경해야 한다.)
- 또한 원본 딕셔너리의 내용이 변경된다 해도 합쳐 놓은 딕셔너리에 반영되지 않는다.
a['x'] = 13
merged['x']
- ChainMap은 원본 딕셔너리를 참조하기 때문에 이와 같은 문제 발생하지 않음. 쉽게 이야기 해서 C언어의 pointer를 이용한거네.
a = {'x': 1, 'z': 3}
b = {'y': 2, 'z': 4}
merged = ChainMap(a, b)
merged['x']
a['x'] = 42
merged['x'] # pointer를 사용했기 때문에 합친 딕셔너리에도 변경됨.