정확히 말하면 딕셔너리 자체는 시퀀스라고 할 수는 없지만 그냥 제목 편의상 그렇다고 치자.
1. Dictionary
사전을 쓰는 법은 우리 다 알 것 같다. 키워드를 열심히 뒤적거리면 옆에 설명이 나와 있는 식이다.
파이썬의 딕셔너리를 딕셔너리라고 부르는 이유는 이러한 방식을 채택하기 때문이다.
사전의 기본 구조는 키-값 페어 구조다.
ex={}
ex
#> {}
Brace(i. e. 중괄호)는 이 딕셔너리 타입을 지정한다.
항목을 추가하는 방법은 다음과 같다.
ex['key']='value'
ex
#> {'key': 'value'}
혹은 직접 입력하는 것도 가능하다.
ex={"Terrapin":"Handsome", "Hight":183, "Head":"Trash-garbage"}
ex
#> {'Terrapin': 'Handsome', 'Hight': 183, 'Head': 'Trash-garbage'}
물론 실전에서는 웬만해서 이렇게 하나씩 추가하는 경우는 잘 없을 것이다.
여기서 중요한 것, 딕셔너리는 리스트나 스트링처럼 정수로 인덱스되지 않는다. 즉, 각 키-밸류 페어의 순서가 지 맘대로라서 다른 pc에서 코드를 똑같이 돌려도 그 순서가 다르게 출력될 수도 있다. 그래서 시퀀스라고 하지 않는 것.
ex[1]
#> KeyError: 1
ex['Head']
#> 'Trash-garbage'
딕셔너리에서는 키를 입력해야 밸류 값을 찾을 수 있다.
딕셔너리의 len 함수는 페어의 개수를 return한다.
ex에 들어간 단어는 키 3개, value 3개 해서 총 6개이지만,
len(ex)
#> 3
in은 기본적으로 키에 대해서 작동한다.
value가 특정 딕셔너리 안에 있는지를 보려면 .values 메서드를 사용한다.
'Terrapin' in ex
#> True
'Handsome' in ex
#> False
'Handsome' in ex.values()
#> True
딕셔너리를 언제 쓰냐.
대충 가장 기본적인 응용으로 다음을 생각해보자.
어떤 단어를 넣으면 거기에 알파벳들이 몇 개나 들어 있는지를 return하는 함수를 짜보자.
def counting(word):
d={}
for letter in word:
if letter in d:
d[letter]+=1
else:
d[letter]=1
return d
cats="Jellicle cats come out tonight Jellicle cats come one come all"
counting(cats)
#> {'J': 2, 'e': 8, 'l': 8, 'i': 3, 'c': 7, ' ': 10, 'a': 3, 't': 5, 's': 2, 'o': 6, 'm': 3, 'u': 1, 'n': 2, 'g': 1, 'h': 1}
전직 세종문화회관 어셔로서 또 세종 대극장으로 오는 2023 캣츠 내한공연 놓칠 수가 없지.
1층 앞쪽 한가운데 근처 어딘가 예매해 두었다. 두달 뒤에 봅시다.
아무튼 요는 딕셔너리를 써서 이런 알고리즘을 만들 수 있다는 것. R에는 없는 자료 구조라 편리하다.
이 놈을 갖고 좀 더 응용을 해보자.
jellicle_ball=counting(cats)
앞서 말했듯 딕셔너리가 자료를 저장하는 순서는 지 맘대로다.
정렬하고 싶으면 앞에 뭔가 하나 붙여주면 된다.
sorted(jellicle_ball)
#> [' ', 'J', 'a', 'c', 'e', 'g', 'h', 'i', 'l', 'm', 'n', 'o', 's', 't', 'u']
for key in sorted(jellicle_ball):
print(key, jellicle_ball[key])
#> 10
#> J 2
#> a 3
#> c 7
#> e 8
#> g 1
#> h 1
#> i 3
#> l 8
#> m 3
#> n 2
#> o 6
#> s 2
#> t 5
#> u 1
함수 sorted(d)는 키들의 list를 return 한다.
이처럼 딕셔너리와 리스트는 서로 배타적인 게 아니라, 쓰다 보면 서로 왔다갔다 하게 된다.
1-1. 쓸만한 것들
먼저 .get 메서드가 있다.
Argument 로는 key와 기본값을 넣어준다. 입력한 key가 딕셔너리에 있다면 그 밸류를, 없다면 입력한 기본 값을 return한다.
jellicle_ball.get('l', '어차피 우승은 그리자벨라')
#> 8
jellicle_ball.get('Memory', '어차피 우승은 그리자벨라')
#> '어차피 우승은 그리자벨라'
이제야 말하지만, 자꾸 포스트에서 예시로 이런 10ㄷㅓㄱ같은 걸 쓰는 이유는, 개인적으로 참고 자료의 결과가 일반적이지 않을 수록 더 알아보기가 쉬워서다.
하나 또 연습해보자.
딕셔너리의 기본 구조는 "키"를 통해 그와 페어를 이루는 "밸류"를 부르는 것, 혹은 "키"의 정보를 그 페어의 "밸류"에 저자하는 것이다.
반대로 밸류에서 키를 찾을 수 있지 않을까? 만들어보자.
def FindKey(dictionary, value):
for key in dictionary :
if dictionary[key]==value :
return key
raise LookupError('딴데가서 찾아봐라 냥')
FindKey(jellicle_ball, 8)
#> 'e'
FindKey(jellicle_ball, 19480322)
#> LookupError: 딴데가서 찾아봐라 냥
raise LookupError line은 if 문이 만족하지 못했을 때 에러를 일으키라는 것이다.
그래서 아래의 라인에서 앤드류 로이드 웨버의 생년월일을 입력했을 때 에러 문구가 출력됐다. 해보면 일반적인 else문 과의 차이를 알 것이다.
1-2. 딕셔너리 안의 리스트
예시를 하나 보는 것으로 갈음하자.
딕셔너리의 아이템에도 당연히 리스트를 사용할 수 있다.
위의 jellicle_ball 딕셔너리의 키와 밸류를 뒤집는 함수를 만들어보자.
참고로, jellicle_ball 딕셔너리에는 같은 밸류를 갖는 key들이 있다 :
jellicle_ball.get('l', '어차피 우승은 그리자벨라')
#> 8
jellicle_ball.get('e', '어차피 우승은 그리자벨라')
#> 8
이 둘은 우리의 결과 딕셔너리에서는 같은 키 안에 들어가야 한다. {8 : ['l', 'e'] } 이런 식으로.
해보자.
def invert(dictionary):
inv={}
for key in dictionary:
value=dictionary[key]
if inv.get(value, 'default')=='default' :
inv[value]=[key]
else:
inv[value].append(key)
return inv
invert(jellicle_ball)
#> {2: ['J', 's', 'n'], 8: ['e', 'l'], 3: ['i', 'a', 'm'], 7: ['c'], 10: [' '], 5: ['t'], 6: ['o'], 1: ['u', 'g', 'h']}
어렵지 않게 만들어 낼 수 있다.
개인적으로, 딕셔너리와 리스트를 이용한 알고리즘을 짤 때는 로컬 변수 이름을 충분히 길게 지정하는 편이다. 헷갈린다.
2. Tuple
튜플은 또 다른 시퀀스인데, 리스트와 약간 차이가 있는 타입이다.
기본적으로 '콤마로 구분된 값의 시퀀스' 이고, 시퀀스이기 때문에 정수 인덱스를 갖지만, 리스트와는 다르게 불변이다.
sample1=1, 2, 3, 4, 5
sample2=(1, 2, 3, 4, 5)
sample3=[1, 2, 3, 4, 5]
sample4={1, 2, 3, 4, 5}
print(type(sample1), type(sample2), type(sample3), type(sample4))
#> <class 'tuple'> <class 'tuple'> <class 'list'> <class 'set'>
위처럼 괄호 "( )" 로 묶인 경우, 혹은 아무것도 쓰지 않은 경우에 튜플을 지정한다.
단, 반드시 콤마가 들어가야 한다.
sample1=1
sample2=1,
print(type(sample1), type(sample2))
#> <class 'int'> <class 'tuple'>
튜플도 역시 수정이 안되기 때문에 내용을 바꾸고 싶으면 새로 정의해야 한다.
앞서 스트링에서 비슷한 내용을 짚었기 때문에 넘긴다.
시퀀스를 나눠서 저장할 때 튜플을 이용할 수 있다.
즉, 여러개의 변수에 값을 지정해줄 때 튜플을 이용하면 한 방에 처리할 수 있다.
Terrapin='Trash&Garbage'
terra, pin=Terrapin.split('&')
terra
#> 'Trash'
pin
#> 'Garbage'
연산을 동시에 진행하는 것도 물론 좋다.
예시로 divmod 라는 내장 함수가 있다. (몫, 나머지) 튜플을 return한다.
divmod(20, 3)
#> (6, 2)
튜플에서 사용하는 특별한 연산자가 있다.
변수 앞에 *을 붙이면 변수 수집이 가능하다.
def gathering(*args):
print(args)
gathering(1,2,3)
#> (1, 2, 3)
gathering(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15)
#> (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
여기서 인풋 변수에 *을 붙였기 때문에, 몇 개의 변수가 들어오든 그대로 받을 수 있는 것이다.
참고로 이 때 수집하는 argument를 표시하는 말로 관습적으로 args를 쓴다.
2-1. 다른 type과 튜플
먼저 둘 이상의 시퀀스를 묶은 'zip'이라는 게 있다.
s='qwer'; l=[1,2,3,4]
z=zip(s, l)
type(z)
#> zip
print(z)
#> <zip object at 0x00000222224FA180>
for obj in z:
print(obj)
#> ('q', 1)
#> ('w', 2)
#> ('e', 3)
#> ('r', 4)
이처럼 zip 오브젝트는 주로 for 문 등에서 사용할 수 있는 페어링 객체다.
이걸 우리가 지금껏 봐온 타입으로 쓰려면 바꿔줘야 한다.
list(z)
#> [('q', 1), ('w', 2), ('e', 3), ('r', 4)]
tuple(z)
#> (('q', 1), ('w', 2), ('e', 3), ('r', 4))
dict(z)
#> {'q': 1, 'w': 2, 'e': 3, 'r': 4}
묶어주는 두 시퀀스의 길이가 다르다면 짧은 놈 기준으로 zip이 생성된다.
s='terrapin'
t=1,2,3,4,5
for z in zip(s, t):
print(z)
#> ('t', 1)
#> ('e', 2)
#> ('r', 3)
#> ('r', 4)
#> ('a', 5)
zip을 어따 써먹을 수 있냐. for 루프 등에서 동시에 둘 이상의 오브젝트를 루핑하도록 코드를 짤 수 있다.
t1=1,2,3,4,5,6,7,8,9
t2=9,8,7,6,5,4,3,2,1
def sameindex(sequence1, sequence2):
index=0
for s1, s2 in zip(sequence1, sequence2):
if s1==s2:
return index
else:
index+=1
raise LookupError('None')
sameindex(t1, t2)
#> 4
이런 식으로.
이 코드에서 zip을 통해 t1과 t2를 한 오브젝트로 묶고, 동시에 두 튜플을 탐색했다.
zip처럼 사용할 수 있는 또다른 오브젝트로는 dict_items 가 있다.
i=jellicle_ball.items()
i
#> dict_items([('J', 2), ('e', 8), ('l', 8), ('i', 3), ('c', 7), (' ', 10), ('a', 3), ('t', 5), ('s', 2), ('o', 6), ('m', 3), ('u', 1), ('n', 2), ('g', 1), ('h', 1)]
type(i)
#> dict_items
여기서 생성된 i도 역시 위 zip과 비슷하게 사용할 수 있다.
3. 시퀀스 몇 가지 더
시퀀스들에 대한 미세 팁.
- 딕셔너리의 밸류로는 리스트가 들어갈 수 있다. 하지만, 딕셔너리의 키로는 리스트 말고 튜플을 써야 한다. 밸류는 수정해도 상관 없지만, 키는 수정되면 안되니까.
- 튜플이 불변이라는 건, 오브젝트를 변경하지 않고서는 튜플 내의 원소 배열을 수정할 수 없다는 것이다. 즉, 튜플에는 sort 같은 메서드를 적용할 수 없다. 대신에, 새로운 오브젝트를 만드는 내장함수들, sorted(튜플) 나 reversed(튜플) 등이 준비되어 있다.
- 보통 함수를 정의할 때 아무 생각없이 쓰는 "a, b" 형태의 인풋 값들은 튜플로 인식이 된다. 또한 튜플의 표현 방식 때문에, 괄호를 묶든 안묶든 어차피 똑같다.
마지막이 뭔 소리냐면, 앞에서 봤던
def sameindex(sequence1, sequence2):
index=0
for s1, s2 in zip(sequence1, sequence2):
~
이거나
def sameindex(sequence1, sequence2):
index=0
for (s1, s2) in zip(sequence1, sequence2):
~
이거나 똑같다는 소리다.
뭔가 순서쌍을 맞춰줘야 할 것 같지만 굳이 그럴 필요가 없다는 것.
오늘은 크리스마스, 저녁에 일정이 있어서 대충 이만 줄인다!
'비단뱀과 알 > 튜토리얼' 카테고리의 다른 글
[Python] 기본적인 Sequence (1) : String(문자열)과 List(리스트) (0) | 2022.12.24 |
---|---|
[Python / R] 기본 연산자와 함수 (0) | 2022.11.24 |