- 프롤로그
- 개발 첫걸음
- 파이썬 기초
- 파이썬 중급
- 파이썬 고급
- 내장 함수 톺아보기
- 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
- 변수의 범위 – 이름 검색의 범위
- 파이썬 심화
- 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
- 데코레이터 – 함수의 기능을 강화하기
- 프로퍼티
- 제너레이터
- async와 await
- 객체로서의 클래스 – 클래스를 동적으로 정의하기
- 파이썬 프로젝트 실습
- 원카드 게임 만들기 (1)
- 원카드 게임 만들기 (2)
- 원카드 게임 만들기 (3) (작성중)
- 턴제 자동 전투 게임 만들기 (작성중)
- 실전 (파이썬 외적인 것들)
- 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
- 유니코드 – 컴퓨터에서 문자를 표기하는 방법
- html, css, 인터넷 – 자동화 첫 걸음 내딛기
- 네트워크 – 인터넷으로 통신하는 방법
- 문서 – 문맥을 읽어보기
이번 절에서는 namedtuple
에 대해서 다룹니다. namedtuple
은 간단히 말하자면 각 항목에 이름을 붙일 수 있는 튜플입니다. 이는 꼭 알 필요는 없지만 적지 않은 곳에 유용하게 쓰일 수 있습니다. 개발 속도와 편리함이 최고 장점인 파이썬인만큼, 유용한 도구를 많이 알아두면 더더욱 편리해지겠지요. 하지만 지금까지 배웠던 클래스나 각종 기본 컨테이너들, 이를테면 리스트, 딕셔너리, 튜플 등에 익숙하지 않다면 그것부터 먼저 확실하게 익히고 와주시길 바랍니다!
개요
우리는 데이터를 묶어서 관리하고자 할 때 편하게 쓸 수 있는 것이 튜플과 클래스입니다. 클래스는 어떤 객체의 틀이라고 했습니다. 붕어빵을 찍어낼 때 쓰는 그 틀 말입니다. 틀을 통해 붕어빵(객체)을 찍어냅니다. 객체는 저마다의 속성과 메소드가 있고, 이는 언제든지 수정이 가능합니다. 그런데 메소드까지 정의하려니 머리가 아픕니다.
하지만 우리에게는 언제든지 간단하게 사용할 수 있는 튜플이 있습니다. 하지만 튜플은 내부 데이터에 접근하려면 인덱스를 사용하는 수 밖에 없습니다. 이런 상황에서 어떻게 더 손쉽게 데이터를 묶어서 관리할까요?
구현 목표 설정
붕어빵의 정보를 관리한다고 생각해봅시다. 그냥 뭐 붕어빵 게임에서 손님들의 취향에 맞는 붕어빵을 잘 만들어야 낸다고 가정해보자구요. 그렇다면 붕어빵에 관련된 정보는 아래와 같이 설정하도록 하겠습니다.
붕어빵에 관련하여 저장해야 할 정보는 속재료(팥, 바닐라)와 바삭함 정도(1~10). 붕어빵을 여러 개 저장할 수 있어야 함.
또한, 우리에게는 다음과 같은 요구사항이 있다고 가정합시다.
- 다음 다섯 개의 붕어빵을 데이터로 가지고 있어주세요 >
(팥, 1)
,(바닐라, 5)
,(바닐라, 4)
,(팥, 8)
,(팥, 9)
- 입력한 순서에서 세번째 붕어빵에 대한 정보를 출력하세요.
- 모든 붕어빵을 출력하세요.
- 바삭함이
5
미만인 붕어빵을 모두 삭제하고 다시 모든 붕어빵을 출력해보세요.
쌩짜 리스트로 구현하기
가장 간단한 구현은 붕어빵의 속재료와 바삭함에 해당하는 정보를 각각 리스트로 만들어 인덱스 위주로 관리를 하는 것이죠. 예를 들어 source
리스트와 crispy
리스트를 만들고, 같은 인덱스를 가지고 있는 것끼리 같은 데이터를 저장한다는 개념을 우리가 기억하도록 합시다. 즉 source[0]
과 crispy[0]
을 붕어빵 하나에 대응시키고, source[1]
과 crispy[1]
을 또 다른 붕어빵 하나에 대응시키는 식으로 반복하는 것입니다.
어쨌거나 이런 방법으로, 한번 구현해보도록 하겠습니다.
source = []
crispy = []
source.extend(["팥", "바닐라", "바닐라", "팥", "팥"])
crispy.extend([1, 5, 4, 8, 9])
print(f"세번째 붕어빵에 대한 정보 - 재료:{source[2]}, 바삭함:{crispy[2]}") ##a_1##
print(f"--- 모든 붕어빵을 출력 --- ")
for i in range(len(source)): ##a_2##
print(f"{i+1}번째 붕어빵 - 재료:{source[i]}, 바삭함:{crispy[i]}")
print(f"--- 바삭함이 5 미만인 붕어빵들을 삭제합니다. ---") ##a_3##
remove_idx = [i for i in range(len(crispy)) if crispy[i] < 5]
remove_idx.sort(reverse=True)
for i in remove_idx:
source.pop(i)
crispy.pop(i)
print(f"--- 모든 붕어빵을 출력 ---")
for i in range(len(source)):
print(f"{i+1}번째 붕어빵 - 재료:{source[i]}, 바삭함:{crispy[i]}")
세번째 붕어빵에 대한 정보 - 재료:바닐라, 바삭함:4
--- 모든 붕어빵을 출력 ---
1번째 붕어빵 - 재료:팥, 바삭함:1
2번째 붕어빵 - 재료:바닐라, 바삭함:5
3번째 붕어빵 - 재료:바닐라, 바삭함:4
4번째 붕어빵 - 재료:팥, 바삭함:8
5번째 붕어빵 - 재료:팥, 바삭함:9
--- 바삭함이 5 미만인 붕어빵들을 삭제합니다. ---
--- 모든 붕어빵을 출력 ---
1번째 붕어빵 - 재료:바닐라, 바삭함:5
2번째 붕어빵 - 재료:팥, 바삭함:8
3번째 붕어빵 - 재료:팥, 바삭함:9
이렇게 작성하게 된다면 걷잡을 수 없는 개발의 늪으로 한 발자국 내딛는 것이나 마찬가지 입니다. 아래에서 각종 문제를 설명하도록 합니다.
a_1(1.) 이름 설정의 문제
print(f"세번째 붕어빵에 대한 정보 - 재료:{source[2]}, 바삭함:{crispy[2]}")
단순한 리스트로 만들게 되면 이름부터 고민입니다. 한 눈에 보아도 source
가 붕어빵의 재료를 나타내는 말이라는 것을 유츄해내기란 결코 쉬운 일이 아닙니다. 굳이 정확히 표현하자면 fish_shaped_bun_sources
라고 표기해야 올바른 표현이겠지만, 변수명이 너무 길어지네요.
또한 우리의 프로그램은 늘 간단하지만, 언제나 복잡해질 수 있는 가능성을 지니고 있습니다. 처음에는 간단하게 붕어빵만 취급하지만 국화빵까지 다루게 된다면 어떻게 될까요? 재료와 바삭함은 국화빵에도 동일하게 적용할 수 있는 부분이기 때문에 붕어빵과 국화빵을 명확하게 구별하기 위해 더 정확한 변수명을 사용하여야 겠습니다.
사실 이름 설정 문제는 후술할 문제에 비하면 그다지 까다롭진 않습니다.
a_2(2.) 리스트 순회 문제
for i in range(len(source)):
리스트의 인덱스에 대응하는 항목을 한꺼번에 가져오기 위해 인덱스를 직접 순회하여야 하므로 range(len(source))
가 계속 쓰이고 있습니다. 이는 가독성을 떨어뜨립니다. 또한 source
의 길이를 잴 건지, crispy
의 길이를 잴 건지 결정해야 하는데요, 둘 중 아무거나 선택해도 상관이 없지만, 아무 상관 없는 결정을 내려야 하는데에 더 피로감을 느낄 수도 있습니다!
a_3(3.) 리스트 관리 문제
가장 큰 문제입니다. 우리는 데이터들을 업데이트하고 삭제할 때마다 난관에 봉착합니다. 가장 큰 관건은 리스트의 각 인덱스가 어떤 같은 붕어빵을 가리킨다라는 전제 조건을 항상 만족하도록 하는 것입니다. 만약 붕어빵 하나를 삭제하고 싶은데, 두 리스트 중 하나라도 삭제가 되지 않는다면 큰 문제입니다. 모든 인덱스가 뒤틀려져 데이터가 엉망이 되어버리고 말겠지요! 아래는 만약 crispy
에서만 하나의 데이터가 삭제되었을 때의 데이터를 보여줍니다.
리스트 인덱스 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
source | 팥 | 바닐라 | 바닐라 | 팥 | 팥 |
crispy | 1 | 5 | 8 | 9 | – |
crispy
에서만 인덱스 2
가 삭제되었을 때의 데이터 뒤틀림이러한 상황을 보여주는 이유는, 우리는 언제든지 실수할 수 있는 사람이기 때문입니다. 프로그램이 의도대로 동작하지 않을 때 우리의 실수를 찾아들어가는 과정은 때로는 매우 고되기 때문에, 실수할 가능성 자체를 줄이는 노력도 필요합니다. 그러한 관점에서는 두 개의 리스트로 관리하는 건 썩 좋은 방법이 아닌듯 싶습니다.
print(f"--- 바삭함이 5 미만인 붕어빵들을 삭제합니다. ---") ##a_3##
remove_idx = [i for i in range(len(crispy)) if crispy[i] < 5]
remove_idx.sort(reverse=True)
for i in remove_idx:
source.pop(i)
crispy.pop(i)
어쨌든 이러한 상황을 잘 다스리기 위해 조건에 맞는 붕어빵을 삭제하는 데만도 다음 세 가지 단계를 거쳐야 합니다. 아래는 위 코드를 순서에 맞게 설명한 것입니다.
- 우선 조건에 해당하는 항목의 인덱스를 뽑아내어 리스트에 보관합니다. (
remove_idx
) 여기서는 지능형 리스트를 이용했습니다. - 삭제해야 할 인덱스를 크기가 큰 순으로 정렬합니다. 인덱스가 작은 것부터 삭제하게 되면 뒤의 인덱스가 모두 -1이 되어버리므로 추가적으로 보정해야 올바른 항목을 삭제할 수 있기 때문에 더 힘들어집니다.
- 비로소 삭제합니다.
튜플로 구현하기
튜플은 여러 개의 데이터를 한꺼번에 묶을 때 아주 편리합니다. 그렇다면 한번 튜플로 작성해보도록 할까요? (아무래도 클래스보단 쉬우니깐요) 결과는 완전히 동일하므로 계속 생략하도록 하겠습니다.
fishes = [("팥", 1), ("바닐라", 5), ("바닐라", 4), ("팥", 8), ("팥", 9)] ##c_1##
def printFishes(fishes): ##c_2##
print(f"--- 모든 붕어빵을 출력 --- ")
for i, fish in enumerate(fishes):
print(f"{i+1}번째 붕어빵 - 재료:{fish[0]}, 바삭함:{fish[1]}")
print(f"세번째 붕어빵에 대한 정보 - 재료:{fishes[2][0]}, 바삭함:{fishes[2][1]}") ##c_3##
printFishes(fishes)
print(f"--- 바삭함이 5 미만인 붕어빵들을 제외합니다. "
"(5 이상만 가져옵니다.) -- ")
crispy_fishes = [f for f in fishes if f[1] >= 5] ##c_4##
printFishes(crispy_fishes)
결과는 전과 거의 동일합니다. 바뀌어진 부분 위주로 코드를 살펴보도록 합시다.
c_1(1.) 데이터 정의
fishes = [("팥", 1), ("바닐라", 5), ("바닐라", 4), ("팥", 8), ("팥", 9)]
튜플의 첫 번째 항목에는 재료를, 두번째 항목에는 바삭함 정도를 넣도록 변경했습니다.
c_2(2.) 편의 함수 정의
def printFishes(fishes):
print(f"--- 모든 붕어빵을 출력 --- ")
for i, fish in enumerate(fishes):
print(f"{i+1}번째 붕어빵 - 재료:{fish[0]}, 바삭함:{fish[1]}")
붕어빵을 담고 있는 배열을 받아 출력하는 편의 함수를 따로 정의했습니다. 함수 내부에서 for
와 enumerate
(추가 예정)를 통해 접근하고 있습니다. 첫번째 요소가 재료를 나타내고, 두번째 요소가 바삭함을 나타내므로 fish[0]
, fish[1]
과 같이 표현합니다.
여기에서 튜플을 썼을 때의 단점이 드러나지요. 0
과 1
은 단지 숫자일 뿐인데, 각각의 위치에 어떤 값이 대응되는지 아는 사람은 오로지 코딩을 직접 하는 우리밖에 없습니다. 아까 작성한 source
와 crispy
는 글자 자체가 사라졌다구요! 1년 뒤 이 프로그램을 본다고 상상해보세요. fish[0]
과 fish[1]
에 어떤 값을 넣었는지 저조차 기억하기 힘들 겁니다. 주석을 적어놓으면 되기야 하지만 쓸데없는 노력이 들어가게 된다는 기분이 드네요.
c_3(3.) 값끼리 묶여있음이 보장됨
print(f"세번째 붕어빵에 대한 정보 - 재료:{fishes[2][0]}, 바삭함:{fishes[2][1]}")
리스트 두 개를 쓰는 것보다 더 좋은 점입니다. 세 번째 붕어빵에 접근하기 위해 우리는 첫 번째 예시에서는 source[2]
와 crispy[2]
이렇게 분리하여 접근하고 관리도 따로따로 해주어야 했습니다. 우리는 같은 인덱스 값에 대해서 올바르게 엮여있어야 한다는 것을 기억할 뿐 프로그램상으로 어떤 제약사항을 만들 수 없었습니다. 언제든 source
와 cripsy
의 길이가 달라질 위험이 있었으니까요. 반면 여기서는 fishes[2]
라는 중간 과정이 있어, fishes[2][0]
과 fishes[2][1]
이 프로그램 상에서 하나의 튜플로 강하게 엮여있음을 보장해줍니다. 그래서 아무리 심한 실수를 해도 데이터가 뒤틀려있다는 걱정을 하지 않아도 됩니다.
하지만 여전히 0
과 1
이라는 의미를 기억해야 하는 숫자를 사용하고 있습니다.
c_4(4.) 수월한 관리
print(f"--- 바삭함이 5 미만인 붕어빵들을 제외합니다. "
"(5 이상만 가져옵니다.) -- ")
crispy_fishes = [f for f in fishes if f[1] >= 5] ##c_4##
바삭함이 5
미만인 항목을 삭제하는 대신, 새로운 리스트를 만들어 바삭함이 5
이상이 되는 붕어빵들을 긁어왔습니다. 만약 source
와 crispy
두 개의 리스트로 관리해야 한다면, 새로운 리스트를 만들 때에도 두 개를 만들어서 따로 관리를 해주어야겠지만, 여기서는 그럴 필요가 없습니다! 한층 더 편리해졌다는 것을 느끼실 수 있을 겁니다.
클래스로 구현하기
0
과 1
이라는 것을 쓰지 말고 source
, crispy
와 같이 의미가 있는 단어를 사용하기 위해 클래스를 만들어 구현해보도록 합시다.
class FishShapedBun: ##d_1##
def __init__(self, source, crispy):
self.source = source
self.crispy = crispy
def printFishes(fishes):
print(f"--- 모든 붕어빵을 출력 --- ")
for i, fish in enumerate(fishes):
print(f"{i+1}번째 붕어빵 - 재료:{fish.source}, 바삭함:{fish.crispy}") ##d_2##
fishes_data = [("팥", 1), ("바닐라", 5), ("바닐라", 4), ("팥", 8), ("팥", 9)]
fishes = [FishShapedBun(*data) for data in fishes_data] ##d_3##
printFishes(fishes)
print(f"--- 바삭함이 5 미만인 붕어빵들을 제외합니다. "
"(5 이상만 가져옵니다.) -- ")
crispy_fishes = [fish for fish in fishes if fish.crispy >= 5]
printFishes(crispy_fishes)
d_1(1.) 클래스 정의
클래스 정의에 관한 부분입니다. 딱히 힘들지 않습니다. 내부적으로 특별 메소드를 두 개 정의했는데요, 속성을 초기화하기 위한 __init__
과 print
할 때 예쁘게 보이게 하기 위한 __repr__
입니다. 여기선 source
와 crispy
가 다시 등장하게 되었습니다! 의미있는 단어를 쓸 수 있어서 기쁨이 앞서군요.
살짝 아니꼬운 자세로 클래스를 보게 되면, 그냥 단순히 데이터 두 개를 엮는데 클래스까지 만들어야 하나 싶을 수 있습니다. 맞습니다. 데이터 두 개를 엮기 위해 클래스를 사용하는 것은 과분해보이기도 합니다. 유연함에 있어서 클래스는 속성과 메소드를 더 자유롭게 추가할 수 있으나, 사실 그렇게까지 많은 기능이 필요하지 않습니다! 이것이 클래스로 만들 때의 유일한 단점인데, 이를 극복하기 위해 후술할 namedtuple
이 사용됩니다.
d_2(2.) 속성에 접근하기
객체의 속성에 접근하기 위해 fish[0]
, fish[1]
대신 fish.source
, fish.crispy
를 사용했습니다. 기존에는 0
과 1
이 어떤 의미인지 기억해두고 있거나 주석으로 표기를 해놓거나 했어야 했는데, 이제는 한 눈에 무슨 의미인지 알아볼 수 있게 되었습니다. 가독성 향상은 커다란 성과입니다.
d_3(3.) 인스턴스 만들기
fishes = [FishShapedBun(*data) for data in fishes_data]
여기에서는 지능형 리스트와 함께 언패킹(추가 예정)을 이용했습니다. FishShapedBun(*data)
는 FishShapedBun(data[0], data[1])
과 같습니다. 그러니까 data
의 모든 순회할수 있는 요소를 뽑아내어 생성자로 그대로 전달하는 것이지요. 지금은 data
내부에 팥과 바삭함이라는 두 항목밖에 없지만 만약에 항목이 5개라면 5개로 언패킹이 되겠습니다.
namedtuple
로 구현하기
드디어 namedtuple
입니다! 클래스를 정의하는 대신 namedtuple
을 구현하고, 나머지는 코드는 모두 같습니다.
from collections import namedtuple ##e_1##
FishShapedBun = namedtuple('FishShapedBun', "source crispy") ##e_2##
def printFishes(fishes):
print(f"--- 모든 붕어빵을 출력 --- ")
for i, fish in enumerate(fishes):
print(f"{i+1}번째 붕어빵 - 재료:{fish.source}, 바삭함:{fish.crispy}") ##e_3##
fishes_data = [("팥", 1), ("바닐라", 5), ("바닐라", 4), ("팥", 8), ("팥", 9)]
fishes = [FishShapedBun(*data) for data in fishes_data]
printFishes(fishes)
print(f"--- 바삭함이 5 미만인 붕어빵들을 제외합니다. "
"(5 이상만 가져옵니다.) -- ")
crispy_fishes = [fish for fish in fishes if fish.crispy >= 5]
printFishes(crispy_fishes)
namedtuple
은 튜플과 행동 방식이 아주 비슷하지만 내부 항목을 접근할 때 0
, 1
, 2
와 같은 인덱스 기반이 아니라 속성명을 주입하여 접근합니다. 직접 만든 클래스 기반의 객체와 동일한 사용 방법이지요. namedtuple
은 튜플이 가진 단순함에 속성명으로 항목을 접근할 수 있는 기능만 덧댄 타입입니다. 클래스의 방대한 기능 앞에 쩔쩔매지 않고도 손쉽게 타입을 만들어서 이용할 수 있습니다.
e_1(1.) import
from collections import namedtuple
일단은 namedtuple
을 사용하기 위해 collections
모듈에 있는 namedtuple
을 임포트 합니다. 기본적으로 제공해주는 타입이 아니기 때문에 임포트가 필요합니다.
e_2(2.) 새로운 타입 정의
FishShapedBun = namedtuple('FishShapedBun', "source crispy")
class
키워드를 쓰지 않고 클래스(타입)을 형성하는 과정입니다. 클래스는 메소드나 속성같은 요소와 비슷하게 하나의 객체로 인식이 가능하기 때문에 함수의 리턴값으로도 받아올 수 있습니다. 좀더 기술적인 용어로 정확하게 말하자면, 위 코드에서 namedtuple
은 클래스 팩토리로서, tuple
을 상속받아, 이름으로 항목에 접근할 수 있는 기능이 추가된 클래스를 형성하여 리턴합니다. namedtuple
을 호출하려면 두 가지 인수가 필요한데, 하나는 사용할 타입의 이름이고, 하나는 사용할 속성들을 띄어쓰기로 나열한 문자열입니다. 사용할 타입의 이름은 사용할 변수명과 동일하게 설정해주면 됩니다.
아래 내용과 같은 구체적인 동작 방식 및 호출 방법에 대해서는, 이 글에서는 다루지 않으므로 공식 문서(영어)나 다른 문서를 참조해주시기 바랍니다! 우리는 핵심 개념만 이해하고 넘어가도록 합니다.
namedtuple
호출 시 사용할 타입의 이름과 변수명을 동일하게 설정하지 않았을 때 벌어지는 일namedtuple
호출 시 사용할 속성들을 나열하는 여러 방법namedtuple
호출 시rename
,default
와 같은 다른 파라미터의 역할_make
,_asdict
와 같은 편리 메소드
e_3(3.) 클래스 기반 객체와 동일하게 항목 접근
클래스를 썼을 때와 동일하게 fish.source
와 fish.crispy
로 접근할 수 있습니다. 아주 편리하지요!
프로그래밍 문제
사실 아래의 프로그래밍 문제는 namedtuple
과는 큰 연관성은 없습니다.
- 생짜 리스트로 구현하기에서,
for-in
루프를len(source)-1
부터0
까지i
를 계속 작아지도록 순회하여,for
문 내에서 즉시 삭제하여도 인덱스에 문제가 유발되지 않도록 프로그램을 수정해보세요. - 생짜 리스트로 구현하기에서, 바삭함이 5 미만인 붕어빵들을 직접 삭제하는 대신, 새로운 리스트를 만들어 바삭함이 5 이상만 되는 붕어빵만 따로 저장한 다음 원래의 리스트를 대체하는 방식으로 구현해보세요.
프로그래밍 문제 정답
- 코드입니다.
print(f"--- 바삭함이 5 미만인 붕어빵들을 삭제합니다. ---")
for i in range(len(source)-1, -1, -1):
if crispy[i] < 5:
source.pop(i)
crispy.pop(i)
- 코드입니다.
print(f"--- 바삭함이 5 미만인 붕어빵들을 삭제합니다. ---")
new_source = []
new_crispy = []
for i in range(len(source)):
if crispy[i] >= 5:
new_source.append(source[i])
new_crispy.append(crispy[i])
source = new_source
crispy = new_crispy
- 프롤로그
- 개발 첫걸음
- 파이썬 기초
- 파이썬 중급
- 파이썬 고급
- 내장 함수 톺아보기
- 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
- 변수의 범위 – 이름 검색의 범위
- 파이썬 심화
- 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
- 데코레이터 – 함수의 기능을 강화하기
- 프로퍼티
- 제너레이터
- async와 await
- 객체로서의 클래스 – 클래스를 동적으로 정의하기
- 파이썬 프로젝트 실습
- 원카드 게임 만들기 (1)
- 원카드 게임 만들기 (2)
- 원카드 게임 만들기 (3) (작성중)
- 턴제 자동 전투 게임 만들기 (작성중)
- 실전 (파이썬 외적인 것들)
- 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
- 유니코드 – 컴퓨터에서 문자를 표기하는 방법
- html, css, 인터넷 – 자동화 첫 걸음 내딛기
- 네트워크 – 인터넷으로 통신하는 방법
- 문서 – 문맥을 읽어보기