프로그램에서는 값을 하나씩 저장하는 것보다 여러 데이터를 어떤 구조로 묶고 다룰지가 더 중요해지는 순간이 많다.
사용자 목록, 주문 내역, API 응답, 로그, 태그처럼 실제 데이터는 대부분 여러 값의 집합으로 표현된다.
Python에서 가장 자주 사용하는 기본 자료구조는 다음 네 가지다.
listtupledictset이 글에서는 각 자료구조의 특징과 함께 인덱싱, 슬라이싱, 참조, 복사, 문자열 메서드, 컴프리헨션까지 정리한다.
| 자료구조 | 순서 | 수정 | 중복 | 대표 용도 |
|---|---|---|---|---|
list | 있음 | 가능 | 가능 | 사용자 목록, 로그 |
tuple | 있음 | 불가 | 가능 | 좌표, 고정 값 묶음 |
dict | 삽입 순서 유지 | 가능 | 키 중복 불가 | API 데이터, 객체 정보 |
set | 순서를 전제로 하지 않음 | 가능 | 불가 | 태그, 권한, 중복 제거 |
처음에는 이 정도 기준만 알아도 대부분의 상황에서 적절한 자료구조를 고를 수 있다.
list리스트는 여러 값을 순서대로 저장하는 자료구조다.
users = ["kim", "lee", "park"]
인덱스는 0부터 시작한다.
print(users[0])
print(users[1])
kim
lee
users = ["kim", "lee"]
users.append("park")
print(users)
여러 값을 추가할 때는 extend()를 사용할 수 있다.
users.extend(["choi", "jung"])
특정 위치에 삽입:
users.insert(1, "han")
값으로 삭제:
users.remove("lee")
인덱스로 삭제:
del users[0]
삭제하면서 값을 가져오기:
removed_user = users.pop()
원본 리스트 자체를 정렬하는 sort():
scores = [80, 95, 70, 88]
scores.sort()
print(scores)
새 리스트를 반환하는 sorted():
scores = [80, 95, 70, 88]
sorted_scores = sorted(scores)
print(scores)
print(sorted_scores)
원본을 보존해야 한다면 두 방식의 차이를 이해해야 한다.
tuple튜플은 리스트처럼 순서를 가지지만 생성 후 항목을 수정할 수 없다.
position = (37.5, 127.0)
latitude = position[0]
longitude = position[1]
position = (37.5, 127.0)
latitude, longitude = position
함수에서 여러 값을 반환할 때도 자주 사용된다.
def get_min_max():
return 10, 100
minimum, maximum = get_min_max()
괄호보다 쉼표가 중요하다.
value = (10,)
다음은 튜플이 아니라 정수다.
value = (10)
dict딕셔너리는 키와 값의 연결 관계를 저장한다.
user = {
"id": 101,
"name": "kim",
"role": "admin",
}
값 조회:
print(user["name"])
값 추가:
user["age"] = 30
값 수정:
user["role"] = "editor"
get()다음 코드는 키가 없으면 KeyError를 발생시킨다.
user["email"]
값이 없을 가능성이 있다면 get()이 편리하다.
email = user.get("email")
print(email)
기본값도 지정할 수 있다.
email = user.get(
"email",
"등록되지 않음",
)
키와 값을 함께 순회:
for key, value in user.items():
print(key, value)
키만:
for key in user.keys():
print(key)
값만:
for value in user.values():
print(value)
웹 API에서 받은 JSON 객체는 Python에서 보통 딕셔너리와 리스트의 조합으로 변환된다.
response_data = {
"status": "success",
"users": [
{
"id": 1,
"name": "kim",
},
{
"id": 2,
"name": "lee",
},
],
}
사용자 이름 출력:
for user in response_data["users"]:
print(user["name"])
데이터 분석이나 API 개발을 공부한다면 list와 dict 조합에 익숙해지는 것이 특히 중요하다.
set집합은 중복되지 않는 값을 저장한다.
tags = {
"python",
"api",
"python",
"backend",
}
print(tags)
"python"은 한 번만 남는다.
user_ids = [1, 1, 2, 3, 3, 4]
unique_ids = set(user_ids)
print(unique_ids)
단, 집합을 사용할 때는 원래 순서를 보존하기 위한 자료구조로 생각하면 안 된다.
admin_permissions = {
"read",
"write",
"delete",
}
editor_permissions = {
"read",
"write",
}
교집합:
print(
admin_permissions
& editor_permissions
)
합집합:
print(
admin_permissions
| editor_permissions
)
차집합:
print(
admin_permissions
- editor_permissions
)
부분집합 검사:
print(
editor_permissions.issubset(
admin_permissions
)
)
권한, 태그, 카테고리처럼 포함 관계가 중요한 데이터에 잘 맞는다.
리스트, 튜플, 문자열처럼 순서가 있는 객체는 인덱스로 접근할 수 있다.
users = ["kim", "lee", "park"]
print(users[0])
print(users[-1])
-1은 마지막 항목이다.
print(users[-2])
뒤에서 두 번째 항목을 의미한다.
일부분을 잘라낼 때 사용한다.
numbers = [0, 1, 2, 3, 4, 5]
print(numbers[1:4])
[1, 2, 3]
끝 인덱스는 포함되지 않는다.
numbers[:3]
numbers[3:]
스텝:
numbers[::2]
역순:
numbers[::-1]
문자열에도 동일하게 적용된다.
text = "python"
print(text[::-1])
nohtyp
in과 not inroles = ["user", "editor", "admin"]
print("admin" in roles)
print("guest" not in roles)
문자열에서도 사용할 수 있다.
url = "/api/users"
if "/api/" in url:
print("API 요청")
다음 코드를 보자.
original = [
"python",
"sql",
]
copied = original
copied.append("ai")
print(original)
print(copied)
결과:
['python', 'sql', 'ai']
['python', 'sql', 'ai']
copied = original은 새로운 리스트를 생성한 것이 아니다.
두 변수가 같은 리스트 객체를 가리킨다.
별도의 리스트를 만들고 싶다면 복사가 필요하다.
original = [
"python",
"sql",
]
copied = original.copy()
copied.append("ai")
print(original)
print(copied)
결과:
['python', 'sql']
['python', 'sql', 'ai']
슬라이싱으로도 얕은 복사를 만들 수 있다.
copied = original[:]
얕은 복사는 바깥 컨테이너만 새로 만든다.
original = [
{"name": "kim"},
{"name": "lee"},
]
copied = original.copy()
copied[0]["name"] = "park"
print(original)
원본 내부 딕셔너리도 변경된다.
완전히 독립된 중첩 구조가 필요하다면 deepcopy()를 사용할 수 있다.
import copy
copied = copy.deepcopy(original)
문자열도 객체이기 때문에 다양한 메서드를 제공한다.
text = " Python API Tutorial "
앞뒤 공백 제거:
text.strip()
대문자:
text.upper()
소문자:
text.lower()
시작 문자열 검사:
text.strip().startswith("Python")
find()와 in문자열에 특정 값이 있는지만 확인하려면 in이 읽기 쉬운 경우가 많다.
url = "/api/users"
if "/api/" in url:
print("API 경로")
위치를 찾아야 한다면 find()를 사용할 수 있다.
index = url.find("users")
찾지 못하면 -1을 반환한다.
join()과 split()여러 문자열 연결:
skills = [
"python",
"sql",
"fastapi",
]
text = ", ".join(skills)
print(text)
python, sql, fastapi
문자열 분리:
skills = text.split(", ")
반복문으로 새 리스트를 만드는 패턴을 간결하게 표현할 수 있다.
일반 반복문:
numbers = [1, 2, 3, 4]
squares = []
for number in numbers:
squares.append(
number ** 2
)
컴프리헨션:
squares = [
number ** 2
for number in numbers
]
짝수만 제곱:
numbers = [
1,
2,
3,
4,
5,
6,
]
result = [
number ** 2
for number in numbers
if number % 2 == 0
]
print(result)
[4, 16, 36]
조건과 변환이 너무 복잡해지면 일반 반복문이 더 읽기 쉽다.
users = [
"kim",
"lee",
"park",
]
length_by_user = {
user: len(user)
for user in users
}
values = [
1,
1,
2,
3,
3,
]
squares = {
value ** 2
for value in values
}
중복은 자동 제거된다.
logs = []
→ list
rgb = (
255,
120,
80,
)
→ tuple
user = {
"id": 1,
"name": "kim",
}
→ dict
permissions = {
"read",
"write",
}
→ set
logs = [
{
"user_id": 1,
"action": "login",
},
{
"user_id": 2,
"action": "view",
},
{
"user_id": 1,
"action": "purchase",
},
{
"user_id": 3,
"action": "login",
},
{
"user_id": 2,
"action": "view",
},
]
활동 사용자와 행동별 횟수를 집계해보자.
active_users = set()
action_count = {}
for log in logs:
active_users.add(
log["user_id"]
)
action = log["action"]
action_count[action] = (
action_count.get(
action,
0,
)
+ 1
)
print(active_users)
print(action_count)
여기서는 자료구조의 역할이 분명하다.
listdictsetdict자료구조는 결국 데이터를 앞으로 어떻게 사용할지에 따라 선택한다.
list는 순서가 있고 수정 가능한 데이터 묶음이다.tuple은 순서가 있지만 수정할 수 없어 고정된 값을 표현하기 좋다.dict는 키를 이용해 값을 조회하는 구조에 적합하다.set은 중복 제거와 집합 연산에 강하다.0부터 시작하며 음수 인덱스로 뒤에서 접근할 수 있다.a = b는 컬렉션을 복사하는 것이 아니라 같은 객체를 참조하게 만들 수 있다.