저번에는 시퀀스의 기본 개념인 컨테이너/플랫, 가변/불변, 그리고 리스트 컴프리헨션과 얕은 복사 & 깊은 복사까지 정리했었다.
오늘(2월 28일)은 그 연장선상에서 튜플과 언패킹의 고급 활용법에 대해 공부했다.
파이썬이 편하다는 말을 항상 들어왔는데, 솔직히 언패킹을 알기전과 알고난 후의 차이가 좀 크다. 언패킹이라는 기능이 파이썬이 편하다고 말하는데 가장 대표적인 예시라고 생각한다.
C나 Java에서 두 변수의 값을 서로 바꾸려면 임시 변수 temp가 반드시 필요했다. 파이썬에서는 그냥 이렇게 하면 된다.
a, b = b, a
한 줄 끝. 이게 가능한 이유는 파이썬이 오른쪽 식을 먼저 튜플로 묶어서 평가한 뒤 왼쪽에 순서대로 언패킹해주기 때문이다.
* (Asterisk-에스터리스크)를 통한 시퀀스 해제* 기호는 시퀀스를 풀어헤치는 역할을 한다. 크게 두 가지 상황에서 활용한다.
1. 함수 인자 전달 시
print(divmod(100, 9)) # 그냥 호출
print(divmod(*(100, 9))) # 튜플을 풀어서 각각의 인자로 전달
print(*(divmod(100, 9))) # 결과값 자체를 풀어서 출력: 11 1
# divmod: 숫자 두 개를 인자로 받아서, (몫, 나머지) 형태의 튜플(Tuple)로 반환
divmod(*(100, 9))에서 *는 튜플 (100, 9) 라는 상자를 열어서 각 요소를 독립적인 인자로 전달한다. 마지막 줄은 반환된 결과값(튜플)을 다시 풀어서 출력하는 것이다.
2. 변수 할당 시
x, y, *rest = range(10)
print(x, y, rest)
# 결과: 0 1 [2, 3, 4, 5, 6, 7, 8, 9]
이게 진짜 편하다. 왼쪽 변수의 수와 오른쪽 값의 수가 딱 맞아떨어지지 않아도 *rest가 나머지를 전부 리스트로 받아버린다.
x, y, *rest = range(2)
print(x, y, rest)
# 결과: 0 1 [] <- 남는 게 없으면 빈 리스트로
x, y, *rest = 1, 2, 3, 4, 5
print(x, y, rest)
# 결과: 1 2 [3, 4, 5]
심지어 나머지가 없어도 에러가 발생하지 않고 빈 리스트로 처리된다. 이게 유연한 할당이다.
+=, *= 연산자 동작 차이같은 *= 2 연산을 해도 내부적으로 전혀 다르게 동작한다는 걸 오늘 처음 알았다.
l = (15, 20, 25) # 튜플 (불변)
m = [15, 20, 25] # 리스트 (가변)
print(id(l), id(m))
# 결과 (예시)
# 100, 200
l *= 2
m *= 2
print(id(l)) # 주소값이 바뀜!
# 결과 (예시)
# 324
print(id(m)) # 주소값이 그대로!
# 결과 (예시)
# 200
print(l)
# 결과
# (15, 20, 25, 15, 20, 25)
print(m)
# 결과
# [15, 20, 25, 15, 20, 25]
id()로 주소값을 찍어보면 결과가 눈에 보인다. 리스트는 같은 주소를 유지하고, 튜플은 완전히 새로운 주소가 생긴다.
이 차이가 발생하는 이유가 바로 뒤에서 호출되는 매직 메서드가 다르기 때문이다.
리스트 (가변형): m *= 2는 __imul__을 호출한다. imul의 i는 In-place, 즉 제자리에서 수정을 의미한다. 기존 메모리 주소를 유지한 채 데이터만 뒤에 이어 붙여서 연장한다.
튜플 (불변형): 불변형이기 때문에 기존 데이터를 수정하는 게 아예 불가능하다. 그래서 __mul__이 호출되어 연산 결과를 담은 완전히 새로운 튜플 객체를 생성하고 변수에 재할당한다. 그래서 주소값이 바뀐다.
반면 m = m * 2처럼 작성하면 리스트도 __mul__이 호출되어 새 객체를 만든다. 즉, m *= 2와 m = m * 2는 겉보기엔 같아도 내부 동작이 다르다.
sort() vs sorted()대학교 입학하고 파이썬을 배우면서 거의 처음 사용해봤던 내장 함수인데 그 내부를 파본거는 이번이 처음이다. 정렬을 할 때 sort()와 sorted() 둘 다 사용할 수 있는데, 이 둘은 명확하게 다르다.
f_list = ['orange', 'apple', 'mango', 'papaya', 'lemon', 'strawberry', 'coconut']
| 비교 항목 | list.sort() | sorted(list) |
|---|---|---|
| 원본 수정 여부 | 원본 자체를 정렬 (수정됨) | 원본은 유지됨 |
| 반환값 | None | 정렬된 새로운 리스트 반환 |
# sorted: 원본 유지, 새 리스트 반환
print(sorted(f_list)) # 오름차순
print(sorted(f_list, reverse=True)) # 내림차순
print(sorted(f_list, key=len)) # 길이 기준
print(sorted(f_list, key=lambda x: x[-1])) # 마지막 글자 기준
# sort: 반환값이 None이라는 점이 핵심
print(f_list.sort()) # None이 출력됨
sort()는 반환값이 None이기 때문에 result = f_list.sort() 처럼 변수에 담으면 result는 None이 된다. 이게 흔한 실수 중 하나다.
솔직히 아직도 헷갈린다.
sorted() 함수의 시그니처(내부 구조들어가서 어떻게 정의)를 보면 이렇게 생겼다.
sorted(iterable, /, *, key=None, reverse=False)
/ 앞의 인자 (Positional-only): iterable은 반드시 위치 인자로만 전달해야 한다. 즉, sorted(iterable=f_list) 처럼 이름을 명시하면 에러가 발생한다.
* 뒤의 인자: key와 reverse는 값을 전달할 때 반드시 이름을 명시해야 한다. sorted(f_list, len) 은 에러지만 sorted(f_list, key=len) 은 정상이다.
이 규칙을 사용자 정의 함수에도 그대로 적용할 수 있다.
def my_sort(data, /, *, opt=True):
pass
오늘 공부한 내용을 한마디로 정리하면, 파이썬의 편의 기능들이 그냥 그냥 되니까 작동하는 게 아니라 내부적으로 명확한 원리가 있다는 거다. 언패킹 하나만 제대로 알아도 코드가 훨씬 깔끔해지고, 가변/불변의 차이를 이해하면 예상치 못한 버그를 사전에 막을 수 있다.
그리고 이런 문법들을 공부하는게 내가
다른 사람들의 코드를 읽을 수 있고, 더 나아가서LLM이 이제 코드를 더 많이 짜는 시대가 되었는데, 걔네들이 짠 코드를 내가 읽을 수 없으면 내가 병목구간이 될 것 같다는 생각이 들었다.