
기본 언패킹(unpacking)에는 분명한 한계가 하나 있습니다. 왼쪽에 적은 변수 개수와 오른쪽 시퀀스의 길이가 정확히 맞아떨어져야 한다는 점입니다. 그래서 "가장 큰 값 둘만 따로 보고 나머지는 한 덩어리로 두겠다" 같은 요구가 들어오면 대부분 인덱스와 슬라이스로 손이 갑니다.
이 방식이 만드는 코드는 겉보기에는 멀쩡하지만 유지보수 관점에서는 지뢰밭입니다. [0], [1], [2:]처럼 서로 이어져야 할 숫자가 여러 줄에 흩어져 있으니, 나중에 항목을 하나 추가할 때 한 줄만 고치고 다른 줄을 놓치는 실수가 나옵니다. 게다가 이런 오류는 예외를 던지지 않고 그냥 잘못된 값을 반환합니다. 조용히 틀리는 코드가 가장 잡기 어려운 버그입니다.
car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
car_ages_descending = sorted(car_ages, reverse=True)
# 인덱스 경계값이 세 줄에 흩어져 있습니다
oldest = car_ages_descending[0]
second_oldest = car_ages_descending[1]
others = car_ages_descending[2:] # 앞에 변수를 하나 더 넣으면 여기도 3으로 고쳐야 합니다
print(oldest, second_oldest, others)
세 줄 모두가 "앞에서 두 개"라는 하나의 규칙을 각자 다른 방식으로 표현하고 있습니다. 규칙은 하나인데 고쳐야 할 곳이 셋이라면, 그 코드는 언젠가 어긋납니다.
car_ages = [0, 9, 4, 8, 7, 20, 19, 1, 6, 15]
car_ages_descending = sorted(car_ages, reverse=True)
# 별표가 붙은 식이 남는 원소를 전부 받아 갑니다
oldest, second_oldest, *others = car_ages_descending
print(oldest, second_oldest, others) # 20 19 [15, 9, 8, 7, 6, 4, 1, 0]
# 위치는 자유롭습니다. 맨 앞이나 가운데에도 놓을 수 있습니다
*others, second_youngest, youngest = car_ages_descending
# 남는 원소가 없으면 빈 리스트가 됩니다. None이 아닙니다
first, second, *rest = [1, 2]
print(rest) # []
한 줄이 규칙 전체를 담고 있으니 항목을 추가해도 고칠 곳이 그 줄 하나뿐입니다. 인덱스 경계값이 어긋날 여지가 구조적으로 사라진다는 점에서 가독성과 안전성을 동시에 얻는 방식입니다.
이터레이터(iterator)에도 그대로 쓸 수 있어서, 헤더와 본문을 분리하는 코드가 아주 짧아집니다.
def generate_csv():
yield ('날짜', '제조사', '모델')
yield ('2024-01-02', '현대', '아반떼')
yield ('2024-01-05', '기아', 'K5')
header, *rows = generate_csv() # 이터레이터도 한 줄로 분해됩니다
print(header) # ('날짜', '제조사', '모델')
print(len(rows)) # 2
다만 별표가 붙은 식은 언제나 리스트를 만들어 내므로, 남는 부분 전체가 메모리에 올라갑니다. 결과 크기를 가늠할 수 있을 때만 사용해야 합니다.
로그 한 줄을 파싱할 때 특히 잘 맞습니다. timestamp, level, *message_parts = line.split()처럼 쓰면 메시지에 공백이 몇 개 들어오든 앞의 두 필드는 안전하게 뽑히고, 나머지는 ' '.join(message_parts)로 복원하면 됩니다.
랭킹 API 응답을 가공할 때도 유용합니다. 상위 세 명만 강조하고 나머지는 목록으로 내려보내는 화면이라면 first, second, third, *rest = ranking 한 줄로 끝납니다. 다만 페이지네이션 없이 수십만 건이 내려오는 엔드포인트라면 itertools.islice로 먼저 잘라 내는 편이 안전합니다.