CHAPTER 03
컬렉션 자료구조
셋
딕셔너리
파이썬 컬렉션 데이터 타입
연습문제
2장에서는 시퀸스 자료구조로 데이터를 슬라이싱하거나 정렬했다. 컬렉션(collection)자료구조는 시퀸스 자료구조와 달리, 데이터를 서로 연관시키지(relating) 않고 모아두는 컨테이너다(container). 컬렉션 자료구조는 시퀸스 자료구조에서 봤던 속성 중 세 가지 속성을 지닌다.
- 멤버십 연산자 : in
- 크기 함수 : len(seq)
- 반복성 : 반복문의 데이터를 순회한다.
파이썬의 내장 컬렉션 데이터 타입에는 셋과 딕셔너리가 있다. 이 장의 마지막 절에서는 collections 모듈이 제공하는 다른 유용한 컬렉션 데이터 타입들도 살펴본다.
3.1 셋
파이썬의 셋(집합,set)은 반복 가능하고, 가변적이며, 중복 요소가 없고, 정렬되지 않은 컬렉션 데이터 타입이다.
인덱스 연산은 할 수 없다. 셋은 멤버십 테스트 및 중복항목 제거에 사용된다. 셋의 삽입 시간복잡도는 O(1)이고, 합집합(union)의 시간 복잡도는 O(m+n)이다. 교집합(intersection)의 경우, 두 셋 중에서 더 작은 셋에 대해서만 계싼하면 되므로, 시간복잡도는 O(n)이다.
NOTE_ 프로즌 셋(frozen set)은 셋과 달리 불변 객체이며, 셋에서 사용할 수 있는 일부 메서드를 사용할 수 없다. 곧, 프로즌 셋의 요소를 변경하는 메서드를 사용할 수 없다. frozenset()으로 생성한다.Python 3.7.4 (tags/v3.7.4:e09359112e, Jul 8 2019, 19:29:22) [MSC v.1916 32 bit (Intel)] on win32Type "help", "copyright", "credits" or "license" for more information.>>> dir(set())['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__iand__', '__init__', '__init_subclass__', '__ior__', '__isub__', '__iter__', '__ixor__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'add', 'clear', 'copy', 'difference', 'difference_update', 'discard', 'intersection', 'intersection_update', 'isdisjoint', 'issubset', 'issuperset', 'pop', 'remove', 'symmetric_difference', 'symmetric_difference_update', 'union', 'update']>>> >>> >>> dir(frozenset())['__and__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__or__', '__rand__', '__reduce__', '__reduce_ex__', '__repr__', '__ror__', '__rsub__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__xor__', 'copy', 'difference', 'intersection', 'isdisjoint', 'issubset', 'issuperset', 'symmetric_difference', 'union']>>> >>> >>> fs = frozenset((0,1,2,3,4))>>> 2 in fsTrue>>> len(fs)5
add()
A.add(x)는 셋A에 x가 없는 경우 x를 추가한다.
>>> people = {"버피", "에인절", "자일스"}>>> people.add("윌로")>>> people{'에인절', '버피', '자일스', '윌로'}
update()와 != 연산자
A.update(B)와 혹은 A |= B는 A를 B에 추가한다.(합집합)
>>> people = {"버피", "에인절", "자일스"} >>> people.update({"로미오", "줄리엣", "에인절"})>>> people{'로미오', '에인절', '버피', '자일스', '줄리엣'}>>>>>> people |= {"리키","유진"}>>> people{'로미오', '유진', '에인절', '리키', '버피', '자일스', '줄리엣
union()과 | 연산자
A.union(B)와 A와 | B는 앞에서 본 update() 메서드와 같지만, 연산 결과를 복사본으로 반환한다.
>>> people = {"버피", "에인절", "자일스"}>>> people.union({"로미오", "줄리엣"}){'로미오', '에인절', '버피', '자일스', '줄리엣'}>>>>>> people{'에인절', '버피', '자일스'}>>>>>> people | {"브라이언"}{'브라이언', '에인절', '버피', '자일스'}>>>>>>
intersection()과 &연산자
A.intersection(B)와 A & B는 A와 B의 교집합의 복사본을 반환한다.
>>> people = {"버피", "에인절", "자일스", "이안"}>>> vampires= {"에인절", "자일스","윌로"}>>> people.intersection(vampires){'에인절', '자일스'}>>> people & vampires{'에인절', '자일스'}>>>
difference()와 연산자
A.difference(B)와 A-B와 A와 B의 차집합의 복사본을 반환한다.
>>> people = { "버피", "에인절", "자일스", "아영"}>>> vampires = {"스파이크","에인절", "상민"}>>> people.difference(vampires){'자일스', '버피', '아영'}>>> people - vampires{'자일스', '버피', '아영'}>>>
clear()
A.clear()는 A의 모든 항목을 제거한다.
>>> people={"버피","자일스","에인절"}>>> people.clear()>>> peopleset()
discard(),remove(),pop()
A.discard(x)는 A의 항목 x를 제거하며 반환값은 없다. A.remove()는 A.discard()와 같지만, 항목 X가 없을 경우 KeyError 예외를 발생시킨다.
A.pop()는 A에서 한 항목을 무작위로 제거하고 그 항목을 반환한다. 셋이 비어있으면 KeyError 예외를 발생시킨다.
>>> countries={"프랑스", "스페인", "영국"}>>> countries.discard("한국")>>> countries.remove("일본")Traceback (most recent call last): File "<stdin>", line 1, in <module>KeyError: '일본'>>>>>> countries.pop() #무작위'스페인'>>> countries.discard("스페인")>>> countries.remove("영국")>>> countries.pop()'프랑스'>>> countries.pop()Traceback (most recent call last): File "<stdin>", line 1, in <module>KeyError: 'pop from an empty set'>>>KeyboardInterrupt
3.1.2 셋과 리스트
리스트 타입은 셋 타입으로 변환(casting)할 수 있다. 다음 예제를 살펴보자.
def remove_dup(l1): '''리스트의 중복된 항목을 제거한 후 반환한다.''' print("remove_dup l1",list(set(l1))) return list(set(l1))def intersection(l1, l2): '''교집합 결과를 반환한다.''' print("intersection l1",list(set(l1) & set(l2))) return list(set(l1) & set(l2))def union(l1, l2): '''합집합 결과를 반환한다.''' print("union set(l1), set(l2)",list(set(l1) | set(l2) )) return list(set(l1) | set(l2) )def test_sets_operation_with_lists(): l1 = [1, 2, 3, 4, 5, 5, 9, 11, 11, 15] l2 = [4, 5, 6, 7, 8] l3 = [] assert(remove_dup(l1) == [1, 2, 3, 4, 5, 9, 11, 15]) assert(intersection(l1, l2) == [4,5]) assert(union(l1, l2) == [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 15]) assert(remove_dup(l3) == []) assert(intersection(l3, l2) == l3) assert(sorted(union(l3, l2)) == sorted(l2)) print("테스트 통과")if __name__ == '__main__': test_sets_operation_with_lists()
결과
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\1_set_operations_with_lists.pyremove_dup l1 [1, 2, 3, 4, 5, 9, 11, 15]intersection l1 [4, 5]union set(l1), set(l2) [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 15]remove_dup l1 []intersection l1 []union set(l1), set(l2) [4, 5, 6, 7, 8]테스트 통과
딕셔너리에서도 셋 속성을 사용할 수 있다. 딕셔너리는 바로 다음 절에서 자세히 살펴본다.
def set_operations_with_dict(): pairs = [("a",1), ("b",2), ("c",3)] d1 = dict(pairs) print("딕셔너리1\t:{0}".format(d1)) d2 = {"a":1, "c":2, "d":3, "e":4} print("딕셔너리2\t: {0}".format(d2)) intersection = d1.keys() & d2.keys() print("d1 n d2 (키)\t: {0}".format(intersection)) intersection_items = d1.items() & d2.items() print("d1 n d2 (키,값)\t: {0}".format(intersection_items) ) subtraction1 = d1.keys() - d2.keys() print("d1 - d2 (키)\t: {0}".format(subtraction1)) subtraction2 = d2.keys() - d1.keys() print("d2 - d1 (키)\t:{0}".format(subtraction2)) subtraction_items = d1.items() -d2.items() print("d1 - d2 (키,값)\t: {0}".format(subtraction_items)) """딕셔너리의 특정 키를 제외한다.""" d3 = {key: d2[key] for key in d2.keys() - {"c","d"}} print("d2 - {{c,d}}\t: {0}".format(d3))if __name__ == "__main__": set_operations_with_dict()
결과
딕셔너리1 :{'a': 1, 'b': 2, 'c': 3}딕셔너리2 : {'a': 1, 'c': 2, 'd': 3, 'e': 4}d1 n d2 (키) : {'c', 'a'}d1 n d2 (키,값) : {('a', 1)}d1 - d2 (키) : {'b'}d2 - d1 (키) :{'d', 'e'}d1 - d2 (키,값) : {('c', 3), ('b', 2)}d2 - {c,d} : {'a': 1, 'e': 4}
3.2.1 딕셔너리 메서드
setdefault()
setdefault() 메서드는 딕셔너리에서 키의 존재 여부를 모른 채 접근할 때 사용된다. (딕셔너리에 존재하지 않는 키에접근하면 예외가 발생한다) A.setdefault(key,default)를 사용하면 딕셔너리 A에 key가 존재할 경우 키에 해당하는 값을 얻을 수 있고, key가 존재하지 않는다면, 새 키와 기본값 default가 딕셔너리에 저장된다.
def usual_dict(dict_data): """dict[key]사용""" newdata={} for k, v in dict_data: print("k",k) print("v",v) print("dict_data",dict_data) if k in newdata: newdata[k].append(v) print("if newdata[k]",newdata[k]) else: newdata[k] = [v] print("else newdata[k]",newdata[k]) return newdatadef setdefault_dict(dict_data): """ setdefault()메서드 사용""" newdata = {} for k, v in dict_data: newdata.setdefault(k,[]).append(v) return newdatadef test_setdef(): dict_data = (("key1", "value1"), ("key1","value2"), ("key2", "value3"), ("key2", " value4"), ("key2", "value5"),) print(usual_dict(dict_data)) print(setdefault_dict(dict_data))if __name__ == "__main__": test_setdef()
결과
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\3_setdefault_example.pyk key1v value1dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))else newdata[k] ['value1']k key1v value2dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))if newdata[k] ['value1', 'value2']k key2v value3dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))else newdata[k] ['value3']k key2v value4dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))if newdata[k] ['value3', ' value4']k key2v value5dict_data (('key1', 'value1'), ('key1', 'value2'), ('key2', 'value3'), ('key2', ' value4'), ('key2', 'value5'))if newdata[k] ['value3', ' value4', 'value5']{'key1': ['value1', 'value2'], 'key2': ['value3', ' value4', 'value5']}{'key1': ['value1', 'value2'], 'key2': ['value3', ' value4', 'value5']}
A.update(B)는 딕셔너리 A에 딕셔너리 B의 키가 존재한다면, 기존 A의 (키, 값)을 B의 (키,값)으로 갱신한다. B의 키가 A에 존재하지 않는다면, B의 (키,값)을 A에 추가한다.
>>> d = {'a':1, 'b': 2}>>> d.update({'b':10})>>> d{'a': 1, 'b': 10}>>> d.update({'c':100})>>> d{'a': 1, 'b': 10, 'c': 100}>>>
get()
A.get(key)는 딕셔너리A의 key값을 반환한다. key가 존재하지 않으면 아무것도 반환하지 않는다.
>>> sunnydale = dict(name="잰더", age=17, hobby='게임')>>> sunnydale.get("hobby")'게임'>>> sunnydale['hobby']'게임'>>> sunnydale.get("hello") >>> sunnydale['hello'] Traceback (most recent call last): File "<stdin>", line 1, in <module>KeyError: 'hello'
items(), values(),keys()
items(), keys(), values() 메서드는 딕셔너리 뷰(view)다. 딕셔너리 뷰란 딕셔너리의 항목(키 또는 값)을 조회하는 읽기 전용의 반복 가능한 객체다.
>>> sunnydale = dict(name="잰더", age=17, hobby='게임')>>> sunnydale.items() dict_items([('name', '잰더'), ('age', 17), ('hobby', '게임')])>>> sunnydale.values()dict_values(['잰더', 17, '게임'])>>> sunnydale.keys() dict_keys(['name', 'age', 'hobby'])>>> sunnydale_copy=sunnydale.items()>>> sunnydale_copy['address'] = "서울"Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: 'dict_items' object does not support item assignment>>> sunnydale['address'] = "서울">>> sunnydale{'name': '잰더', 'age': 17, 'hobby': '게임', 'address': '서울'}
pop(), popitem()
A.pop(key)는 딕셔너리 A의 key 항목을 제거한 후, 그 값을 반환한다.
A.popitem()은 딕셔너리 A에서 항목(키와 값)을 제거한 후, 그 키와 항목을 반환한다.
>>> sunnydale = dict(name="잰더", age=17, hobby='게임', address="서울") >>> sunnydale.pop("age")17>>> sunnydale{'name': '잰더', 'hobby': '게임', 'address': '서울'}>>> sunnydale.popitem()('address', '서울')>>> sunnydale{'name': '잰더', 'hobby': '게임'}
clear()
>>> sunnydale.clear()>>> sunnydale {}
3.2.2 딕셔너리 성능 측정
딕셔너리를 벤치마킹 테스트하여 성능을 측정해보자, 다음 코드는 리스트와 딕셔너리의 멤버십 연산을 테스트한다. 다음 코드는 리스트와 딕셔너리의 멤버십 연산을 테스트한다. 멤버십 연산에 대한 시간복잡도는 O(n)인 반면, 딕셔너리는 O(1)이다.
import timeitimport randomfor i in range(10000, 1000001, 20000): t = timeit.Timer("random.randrange(%d) in x" % i, "from __main__ import random, x") x = list(range(i)) #리스트 lst_time = t.timeit(number=1000) x = { j: None for j in range(i)} #딕셔너리 d_time = t.timeit(number=1000) print("%d,%10.3f,%10.3f" % (i, lst_time, d_time))
성능측정결과 i(10000부터 1000001까지 20000씩 더하면서 체크), lst_time(리스트 측정시간), d_time(딕셔너리 측정시간)
10000, 0.085, 0.00130000, 0.284, 0.00150000, 0.561, 0.00370000, 0.837, 0.00290000, 0.926, 0.002110000, 1.174, 0.002130000, 1.452, 0.002150000, 1.613, 0.002170000, 1.877, 0.002190000, 1.979, 0.002210000, 2.160, 0.002230000, 2.843, 0.002250000, 2.735, 0.002270000, 2.942, 0.002290000, 3.239, 0.002310000, 4.085, 0.002330000, 4.984, 0.002350000, 4.616, 0.002370000, 3.971, 0.002390000, 4.256, 0.002410000, 4.316, 0.002430000, 4.892, 0.002450000, 5.564, 0.003470000, 6.352, 0.003
딕셔너리 메서드의 시간복잡도는 다음과 같다.
연산시간복잡도
복사 |
O(n) |
항목 조회 |
O(1) |
항목 할당 |
O(1) |
항목 삭제 |
O(1) |
멤버십 테스트 in |
O(1) |
반복 |
O(n) |
3.2.3 딕셔너리 순회
반복문에서 딕셔너리를 순회할 때는 기본적으로 키를 사용한다. 딕셔너리의 키는 임의의 순서대로 나타나지만, sorted()함수를 사용하면 정렬된 상태로 순회할 수 있다. sorted()함수는 딕셔너리의 keys(), values(),items()에 대해 사용할 수 있다.
>>> d = dict(c="!", b="world", a="hello")>>> for key in sorted(d.keys()):... print(key, d[key])... a hellob worldc !
3.2.4 딕셔너리 분기
다음 두 함수를 조건에 따라 실행해야 한다고 가정해보자.
def hello(): print("hello") def world(): print("world")
이럴 때 우리는 보통 if문을 사용하여 다음과 같이 분기문을 작성한다.
action = "h"if action == "h": hello()elif action == "w": world()
하지만 딕셔너리를 사용하면 다음과 같이 더 효율적으로 분기할 수 있다.
functions = dict(h=hello, w=world)functions[action]()
3.3 파이썬 컬렉션 데이터 타입
파이썬의 collections 모듈은 다양한 딕셔너리 타입을 제공한다. 범용의 내장기능보다 더 강력한 성능을 보인다.
3.3.1 기본 딕셔너리
기본 딕셔너리(default dictionary)는 collections.defaultdict 모듈에서 제공하는 추가 딕셔너리 타입이다.
defaultdict는 내장 딕셔너리의 모든 연산자와 메서드를 사용할 수 있고, 추가로 다음 코드와 같이 누락된 키도 처리할 수 있다.
from collections import defaultdictdef defaultdict_example(): pairs = {("a",1), ("b",2), ("c",3)} # 일반 딕셔너리 d1 = {} for key, value in pairs: if key not in d1: d1[key] = [] d1[key] = [] d1[key].append(value) print(d1) #defaultdict d2 = defaultdict(list) for key, value in pairs: d2[key].append(value) print(d2)if __name__ == "__main__": defaultdict_example()
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\5_defaultdict_example.py{'c': [3], 'b': [2], 'a': [1]}defaultdict(<class 'list'>, {'c': [3], 'b': [2], 'a': [1]})
3.2.2 정렬된 딕셔너리
정렬된 딕셔너리(ordered dictionary)는 collections.OrderedDict 모듈에서 제공하는 정렬된 딕셔너리 타입이다. OrderedDict 역시 내장 딕셔너리의 모든 메서드와 속성을 가지고 있고, 추가로 삽입 순서대로 항목을 저장한다.
>>> from collections import OrderedDict>>> tasks = OrderedDict()>>> tasks[8031] = "백업">>> tasks[4027] = "이메일 스캔">>> tasks[5733] = "시스템 빌드">>> tasksOrderedDict([(8031, '백업'), (4027, '이메일 스캔'), (5733, '시스템 빌드')])
다음은 표준 딕셔너리와 OrderedDict 결과를 출력하는 예제다.(사용하는 파이썬 버전에따라 출력하는 결과가 다를 수 있다.)
6_orderedDict_example.py
from collections import OrderedDictdef orderedDict_example(): pairs = [("c",1),("b",2),("a",3)] #일반 딕셔너리 d1 = {} for key, value in pairs: if key not in d1: d1[key] = [] d1[key].append(value) for key in d1: print(key, d1[key]) #OrderedDict d2 = OrderedDict(pairs) for key in d2: print(key, d2[key])if __name__ == "__main__": orderedDict_example()
결과
c [1]b [2]a [3]c 1b 2a 3
키 값을 변경해도 순서는 변경되지 않는다. 항목을 맨 끝으로 저장하려면, 해당 항목을 삭제한 후 다시 삽입해야한다. 은 popitem() 메서드를 호출하여 딕셔너리의 마지막 키-값 항목을 제거한 후 반환할 수도 있다.
일반적으로, 정렬된 딕셔너리를 사용하는 것은 딕셔너리를 여러 번 순회할 것으로 예상되는 경우와 항목의 삽입을 거의 수행하지 않을 것으로 예상되는 경우에만 효율적이다.
3.3.3 카운터 딕셔너리
카운터(counter) 타입은 해시 가능한(hashable) 객체를 카운팅하는 특화된 서브클래스다. collections.Counter 모듈에서 제공한다.
from collections import Counterdef counter_example(): """항목의 발생 횟수를 매핑하는 딕셔너리를 생성한다.""" seq1=[1,2,3,5,1,2,5,5,2,5,1,4] seq_counts=Counter(seq1) print("seq_counts : Counter(seq1) ",seq_counts) """항목의 발생 횟수를 수동적으로 갱신하거나, update()메서드를 사용할 수 있다.""" seq2 = [1,2,3] seq_counts.update(seq2) print("seq_counts.update(seq2)",seq_counts) seq3 = [1,4,3] for key in seq3: seq_counts[key]+=1 print("seq_counts[key]+=1",seq_counts) """ a+b, a-b 같은 연산을 사용할 수 있다.""" seq_counts_2= Counter(seq3) print("seq_counts_2", seq_counts_2) print("seq_counts + seq_counts_2",seq_counts + seq_counts_2) print("seq_counts - seq_counts_2",seq_counts - seq_counts_2)if __name__ == "__main__": counter_example()
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\7_counterDict_example.pyseq_counts : Counter(seq1) Counter({5: 4, 1: 3, 2: 3, 3: 1, 4: 1})seq_counts.update(seq2) Counter({1: 4, 2: 4, 5: 4, 3: 2, 4: 1})seq_counts[key]+=1 Counter({1: 5, 2: 4, 5: 4, 3: 3, 4: 2})seq_counts_2 Counter({1: 1, 4: 1, 3: 1})seq_counts + seq_counts_2 Counter({1: 6, 2: 4, 3: 4, 5: 4, 4: 3})seq_counts - seq_counts_2 Counter({1: 4, 2: 4, 5: 4, 3: 2, 4: 1})
3.4 연습문제
3.4.1 단어횟수 세기
collections.Counters의 most_common() 메서드를 사용하면 문자열에서 가장 많이 나오는 단어와 그 횟수를 쉽게 찾을 수 있다.
from collections import Counterdef find_top_N_recurring_words(seq,N): dcounter = Counter() print("dcounter",dcounter) print("seq:",seq.split()) for word in seq.split(): print("word",word) dcounter[word] += 1 print("dcounter[word]",dcounter[word]) return dcounter.most_common(N)def test_find_top_N_recurring_words(): seq = "버피 에인절 몬스터 잰더 윌로 버피 몬스터 슈퍼 버피 에인절" N=3 assert(find_top_N_recurring_words(seq,N) == [("버피",3), ("에인절",2), ("몬스터",2)]) print("테스트 통과")if __name__ == "__main__": test_find_top_N_recurring_words()
출력 결과
seq: ['버피', '에인절', '몬스터', '잰더', '윌로', '버피', '몬스터', '슈퍼', '버피', '에인절']word 버피dcounter[word] 1word 에인절dcounter[word] 1word 몬스터dcounter[word] 1word 잰더dcounter[word] 1word 윌로dcounter[word] 1word 버피dcounter[word] 2word 몬스터dcounter[word] 2word 슈퍼dcounter[word] 1word 버피dcounter[word] 3word 에인절dcounter[word] 2테스트 통과
assert에 대하여
#assert는 뒤의 조건이 True가 아니면 AssertError를 발생한다.'''왜 assert가 필요한 것일까?어떤 함수는 성능을 높이기 위해 반드시 정수만을 입력받아 처리하도록 만들 수 있다. 이런 함수를 만들기 위해서는 반드시 함수에 정수만 들어오는지 확인할 필요가 있다. 이를 위해 if문을 사용할 수도 있고 '예외 처리'를 사용할 수도 있지만 '가정 설정문'을 사용하는 방법도 있다.'''assert 조건, '메시지''메시지'는 생략할 수 있다.assert는 개발자가 프로그램을 만드는 과정에 관여한다. 원하는 조건의 변수 값을 보증받을 때까지 assert로 테스트 할 수 있다.이는 단순히 에러를 찾는것이 아니라 값을 보증하기 위해 사용된다.예를 들어 함수의 입력 값이 어떤 조건의 참임을 보증하기 위해 사용할 수 있고 함수의 반환 값이 어떤 조건에 만족하도록 만들 수 있다. 혹은 변수 값이 변하는 과정에서 특정 부분은 반드시 어떤 영역에 속하는 것을 보증하기 위해 가정 설정문을 통해 확인 할 수도 있다.이처럼 실수를 가정해 값을 보증하는 방식으로 코딩 하기 때문에 이를 '방어적 프로그래밍'이라 부른다.
counter에 대하여 추가자료
dictionary를 이용한 카운팅아래 코드는 어떤 단어가 주어졌을 때 단어에 포함된 각 알파멧의 글자 수를 세어주는 간단한 함수입니다.def countLetters(word): counter = {} for letter in word: if letter not in counter: counter[letter] = 0 counter[letter] += 1 return countercountLetters('hello world'))# {'h': 1, 'e': 1, 'l': 3, 'o': 2, ' ': 1, 'w': 1, 'r': 1, 'd': 1}이처럼 데이터의 개수를 셀 때 파이썬의 내장 자료구조인 사전(dictionary)이 많이 사용되는데요.*******************************************************************************************************************************collections.Counter를 이용한 카운팅파이썬에서 제공하는 collections 모듈의 Counter 클래스를 사용하면 위 코드를 단 한 줄로 줄일 수가 있습니다.from collections import CounterCounter('hello world') # Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})collections.Counter 기본 사용법collections 모듈의 Counter 클래스는 별도 패키지 설치 없이 파이썬만 설치되어 있다면 다음과 같이 임포트해서 바로 사용할 수 있습니다.from collections import Countercollections 모듈의 Counter 클래스는 파이썬의 기본 자료구조인 사전(dictionary)를 확장하고 있기 때문에, 사전에서 제공하는 API를 그대로 다 시용할 수가 있습니다.예를 들어, 주어진 단어에서 가장 많이 등장하는 알페벳과 그 알파벳의 개수를 구하는 함수는 다음과 같이 마치 사전을 이용하듯이 작성할 수 있습니다.from collections import Counterdef find_max(word): counter = Counter(word) print("counter",counter) max_count = -1 for letter in counter: print("letter",letter) if counter[letter] > max_count: max_count = counter[letter] print("max_count",max_count) max_letter = letter print("max_letter",max_letter) return max_letter, max_countfind_max('hello world') # ('l', 3)
결과
counter Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})letter hmax_count 1max_letter hletter eletter lmax_count 3max_letter lletter oletterletter wletter rletter d
3.4.2 애너그램
애너그램( anagram)은 문장 또는 단어의 철자 순서를 바꾸는 놀이를 말한다. 두 문자열이 서로 애너그램인지 확인하고 싶다고 하자. 셋은 항목의 발생횟수를 계산하지 않고, 리스트의 항목을 정렬하는 시간복잡도는 최소 O(n log n)이다. 따라서 애너그램을 확인하는 데 딕셔너리를 사용하는 것이 가장 좋은 해결책이 될 수 있다.
다음 코드는 두 문자열이 애너그램인지 확인한다. 먼저 첫 번째 문자열의 문자 발생횟수를 더해서 딕셔너리에 저장한다. 두 번째 문자열의 문자 발생횟수를 빼어 딕셔너리에 저장한다. 마지막으로 딕셔너리의 모든 항목 값이 0이면 두 문자열은 애너그램이다.
from collections import Counterdef is_anagram(s1,s2): counter = Counter() print("counter",counter) for c in s1: counter[c] += 1 print("+=c : ",c) print("counter[c]1:",counter[c]) for c in s2: counter[c] -= 1 print("-=c : ", c) print("counter[c]2:",counter[c]) for i in counter.values(): if i: print("for i in counter.values",i) print("counter.values",counter.values()) return False return Truedef test_is_anagram(): s1 = "marina" s2 = "aniram" assert(is_anagram(s1,s2) is True) s1 = "google" s2 = "gouglo" assert(is_anagram(s1,s2) is False) print("테스트 통과")if __name__ == "__main__": test_is_anagram()
출력결과
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\9_is_anagram.pycounter Counter()+=c : mcounter[c]1: 1 +=c : acounter[c]1: 1 +=c : rcounter[c]1: 1 +=c : icounter[c]1: 1 +=c : ncounter[c]1: 1 +=c : acounter[c]1: 2 -=c : acounter[c]2: 1 -=c : ncounter[c]2: 0 -=c : icounter[c]2: 0 -=c : rcounter[c]2: 0 -=c : acounter[c]2: 0 -=c : mcounter[c]2: 0 counter Counter()+=c : gcounter[c]1: 1+=c : ocounter[c]1: 1+=c : ocounter[c]1: 2+=c : gcounter[c]1: 2+=c : lcounter[c]1: 1+=c : ecounter[c]1: 1-=c : gcounter[c]2: 1-=c : ocounter[c]2: 1-=c : ucounter[c]2: -1-=c : gcounter[c]2: 0-=c : lcounter[c]2: 0-=c : ocounter[c]2: 0for i in counter.values 1counter.values dict_values([0, 0, 0, 1, -1])테스트 통과
두 문자열이 애너그램인지 확인하는 또 다른 방법은 해시 함수의 속성을 이용하는 것이다.</br> ord()함수는 인수가 유니코드 객체일 때, 문자의 유니코드를 나타내는 정수를 반환한다. </br> 문자열에서 모든 문자의 ord() 함수 결과를 더했을 때 그 결과가 같으면 , 두 문자열은 애너그램이다.
import stringdef hash_func(astring): s = 0 for one in astring: if one in string.whitespace: continue print("astring",astring) print("one",one) s = s + ord(one) print("s=s+ord",s) return s def find_anagram_hash_function(word1,word2): return hash_func(word1) == hash_func(word2)def test_find_anagram_hash_function(): word1 = "buffy" word2 = "bffyu" word3 = "bffya" assert(find_anagram_hash_function(word1,word2) is True) assert(find_anagram_hash_function(word1,word3) is False) print("테스트 통과")if __name__ == "__main__": test_find_anagram_hash_function()
출력결과
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\10_is_anagram_using_ord.pyastring buffyone bs=s+ord 98astring buffyone us=s+ord 215astring buffyone fs=s+ord 317astring buffyone fs=s+ord 419astring buffyone ys=s+ord 540astring bffyuone bs=s+ord 98astring bffyuone fs=s+ord 200astring bffyuone fs=s+ord 302astring bffyuone ys=s+ord 423astring bffyuone us=s+ord 540astring buffyone bs=s+ord 98astring buffyone us=s+ord 215astring buffyone fs=s+ord 317astring buffyone fs=s+ord 419astring buffyone ys=s+ord 540astring bffyaone bs=s+ord 98astring bffyaone fs=s+ord 200astring bffyaone fs=s+ord 302astring bffyaone ys=s+ord 423astring bffyaone as=s+ord 520테스트 통과
3.4.3 주사위 합계 경로
주사위를 두 번 던져서 합계가 특정 수가 나오는 경우의 수와 경로를 구해보자.</br> 예를 들어 5가 나올 수 있는 모든 경로는 [1,4] , [2,3], [3,2], [4,1]이다. </br> 다음 코드에서는 두 개의 딕셔너리를 사용하여 주사위의 합계 경로를 구한다.
#주사위 합계경로from collections import Counter, defaultdictdef find_dice_probabilities(S,n_faces=6): if S > 2 * n_faces or S < 2: return None cdict = Counter() ddict = defaultdict(list) #두 주사위의 합을 모두 더해서 딕셔너리에 넣는다. for dice1 in range(1, n_faces+1): for dice2 in range(1,n_faces+1): t = [dice1, dice2 ] cdict[dice1+dice2] += 1 ddict[dice1+dice2].append(t) return [cdict[S], ddict[S]]def test_find_dice_probabilities(): n_faces=6 S=5 results = find_dice_probabilities(S,n_faces) print(results) assert(results[0] == len(results[1])) print("테스트 통과!")if __name__ == "__main__": test_find_dice_probabilities()
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\11_find_dice_probabilities.py[4, [[1, 4], [2, 3], [3, 2], [4, 1]]]테스트 통과!
3.4.4 단어의 중복 문자 제거
다음은 딕셔너리를 사용하여 단어에서 중복되는 문자를 모두 찾아서 제거하는 코드다.
def delete_unique_word(str1):
table_c = {key:0 for key in string.ascii_lowercase}
print("string.ascii_lowercase",string.ascii_lowercase)
for i in str1:
table_c[i] += 1
print("i",i)
print("table_c",table_c[i])
for key, value in table_c.items():
#print("table_c.items()",table_c.items())
if value >1:
str1 = str1.replace(key, "")
print("str1",str1)
return str1
def test_delete_unique_word():
str1 = "google"
assert(delete_unique_word(str1) == "le")
print("테스트 통과!")
if __name__ == "__main__":
test_delete_unique_word()
PS D:\Mastering-Python-Design-Patterns-Second-Edition\algo\collection_data_Structure> python .\12_delete_duplicate_char_str.py
string.ascii_lowercase abcdefghijklmnopqrstuvwxyz
i g
table_c 1
i o
table_c 1
i o
table_c 2
i g
table_c 2
i l
table_c 1
i e
table_c 1
str1 google
str1 google
str1 google
str1 google
str1 google
str1 google
str1 oole
str1 oole
str1 oole
str1 oole
str1 oole
str1 oole
str1 oole
str1 oole
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
str1 le
테스트 통과!