언패킹은 반복을 사용해 list, tuple, dict 같은 컬렉션의 원소를 추출하는 작업을 말해요.
이렇게 꺼낸 값은 같은 문 안에서 변수에 할당할 수 있는데, 이를 보통 다중 할당이라고 해요.
특수 연산자 *와 **는 언패킹이나 다중 할당에서 자주 사용해요.
*<variable_name>과 **<variable_name>을 *와 **와 혼동하면 안 돼요. *와 **는 각각 곱셈과 거듭제곱에 사용되지만, *<variable_name>과 **<variable_name>은 패킹과 언패킹 연산자로 사용돼요.
다중 할당에서는 할당 연산자(=) 왼쪽에 있는 변수의 개수와 오른쪽에 있는 값의 개수가 같아야 해요.
값을 구분하려면 쉼표 ,를 사용해요:
>>> a, b = 1, 2
>>> a
1
주어진 값에 비해 변수의 개수가 맞지 않으면 ValueError가 발생해요:
>>> x, y, z = 1, 2
ValueError: too many values to unpack (expected 3, got 2)
다중 할당은 한 가지 데이터 타입에만 국한되지 않아요:
>>> x, y, z = 1, "Hello", True
>>> x
1
>>> y
'Hello'
>>> z
True
다중 할당으로 lists의 원소를 서로 바꿀 수 있어요.
이 방법은 정렬 알고리즘에서 꽤 흔히 쓰여요.
예를 들어:
>>> numbers = [1, 2]
>>> numbers[0], numbers[1] = numbers[1], numbers[0]
>>> numbers
[2, 1]
tuples는 불변이기 때문에 tuple의 원소는 바꿀 수 없어요.
아래 예제는 lists를 사용하지만, 같은 개념이 tuples에도 적용돼요.
Python에서는 list/tuple/dictionary의 원소를 각기 다른 변수로 언패킹할 수 있어요.
lists/tuples 안의 값은 정해진 순서대로 있기 때문에, 변수에도 같은 순서로 언패킹돼요:
>>> fruits = ["apple", "banana", "cherry"]
>>> x, y, z = fruits
>>> x
"apple"
필요 없는 값이 있다면 _로 표시할 수 있어요:
>>> fruits = ["apple", "banana", "cherry"]
>>> _, _, z = fruits
>>> z
"cherry"
list나 tuple 안에 들어 있는 list/tuple(_중첩 리스트/튜플_이라고도 해요)에서 값을 언패킹하고 할당하는 것은 얕은 언패킹과 같은 방식으로 동작해요. 다만 문맥이나 위치를 분명히 하기 위해 한정자를 붙여야 할 때가 많아요:
>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>>> [[a, b], [c, d]] = fruits_vegetables
>>> a
"apple"
>>> d
"potato"
중첩된 list/tuple의 일부분만 깊게 언패킹할 수도 있어요:
>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>>> [a, [c, d]] = fruits_vegetables
>>> a
["apple", "banana"]
>>> c
"carrot"
언패킹에서 변수의 위치가 잘못되었거나 값의 개수가 맞지 않으면 ValueError가 발생해요:
>>> fruits_vegetables = [["apple", "banana"], ["carrot", "potato"]]
>>> [[a, b], [d]] = fruits_vegetables
ValueError: too many values to unpack (expected 1)
*로 리스트/튜플 언패킹하기list/tuple을 언패킹할 때 * 연산자로 "나머지" 값을 담을 수 있어요.
이 방법은 list/tuple을 슬라이싱하는 것보다 더 명확해요(어떤 상황에서는 가독성이 떨어지기도 하죠).
예를 들어, 첫 번째 원소를 꺼낸 뒤 나머지 값을 첫 번째 원소가 없는 새로운 list에 담을 수 있어요:
>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>>> x, *last = fruits
>>> x
"apple"
>>> last
["banana", "cherry", "orange", "kiwi", "melon", "mango"]
list의 처음과 끝에 있는 값을 꺼내고 가운데 값을 모두 묶을 수도 있어요:
>>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>>> x, *middle, y, z = fruits
>>> y
"melon"
>>> middle
["banana", "cherry", "orange", "kiwi"]
깊은 언패킹에서도 *를 사용할 수 있어요:
>>> fruits_vegetables = [["apple", "banana", "melon"], ["carrot", "potato", "tomato"]]
>>> [[a, *rest], b] = fruits_vegetables
>>> a
"apple"
>>> rest
["banana", "melon"]
딕셔너리 언패킹은 list/tuple 언패킹과는 조금 달라요.
딕셔너리를 반복하면 기본적으로 키를 대상으로 해요.
따라서 dict를 언패킹할 때는 값이 아니라 키만 언패킹할 수 있어요:
>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> x, y, z = fruits_inventory
>>> x
"apple"
값을 언패킹하고 싶다면 <dict>.values() 메서드를 사용하면 돼요:
>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> x, y, z = fruits_inventory.values()
>>> x
6
키와 값이 모두 필요하다면 <dict>.items() 메서드를 사용해요.
<dict>.items()는 키-값 쌍을 담은 이터러블 뷰를 만들어요.
이 쌍들은 tuple로 언패킹할 수 있어요:
>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> x, y, z = fruits_inventory.items()
>>> x
("apple", 6)
패킹은 여러 값을 하나의 list로 묶어 변수에 할당하는 기능이에요.
값을 _언패킹_해서 변경한 뒤, 그 결과를 다시 변수로 _패킹_하고 싶을 때 유용해요.
또한 두 개 이상의 lists/tuples/dicts를 병합할 수도 있어요.
*로 리스트/튜플 패킹하기list/tuple은 * 연산자로 패킹할 수 있어요.
이렇게 하면 모든 값이 하나의 list/tuple로 패킹돼요.
>>> fruits = ("apple", "banana", "cherry")
>>> more_fruits = ["orange", "kiwi", "melon", "mango"]
# fruits and more_fruits are unpacked and then their elements are packed into combined_fruits
>>> combined_fruits = *fruits, *more_fruits
# If there is no * on to the left of the "=" the result is a tuple
>>> combined_fruits
("apple", "banana", "cherry", "orange", "kiwi", "melon", "mango")
# If the * operator is used on the left side of "=" the result is a list.
# Note the trailing comma.
>>> *combined_fruits_too, = *fruits, *more_fruits
>>> combined_fruits_too
['apple', 'banana', 'cherry', 'orange', 'kiwi', 'melon', 'mango']
*를 왼쪽에 사용하는 방법에 대한 더 자세한 배경은 PEP 3132를 참고해요.
**로 딕셔너리 패킹하기딕셔너리 패킹은 ** 연산자로 해요.
이렇게 하면 한 딕셔너리의 모든 키-값 쌍을 다른 딕셔너리로 패킹하거나, 두 딕셔너리를 하나로 합칠 수 있어요.
>>> fruits_inventory = {"apple": 6, "banana": 2, "cherry": 3}
>>> more_fruits_inventory = {"orange": 4, "kiwi": 1, "melon": 2, "mango": 3}
# fruits_inventory and more_fruits_inventory are unpacked into key-values pairs and combined.
>>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory}
# then the pairs are packed into combined_fruits_inventory
>>> combined_fruits_inventory
{"apple": 6, "banana": 2, "cherry": 3, "orange": 4, "kiwi": 1, "melon": 2, "mango": 3}
*와 ** 사용하기임의 개수의 인자를 받는 함수를 만들 때, 함수 정의에서 *args나 **kwargs를 사용할 수 있어요.
*args는 임의 개수의 위치(비키워드) 인자를 tuple로 패킹하고,
**kwargs는 임의 개수의 키워드 인자를 딕셔너리로 패킹할 때 사용해요.
*args 사용 예:
# This function is defined to take any number of positional arguments
>>> def my_function(*args):
... print(args)
# Arguments given to the function are packed into a tuple
>>> my_function(1, 2, 3)
(1, 2, 3)
>>> my_function("Hello")
("Hello")
>>> my_function(1, 2, 3, "Hello", "Mars")
(1, 2, 3, "Hello", "Mars")
**kwargs 사용 예:
# This function is defined to take any number of keyword arguments
>>> def my_function(**kwargs):
... print(kwargs)
# Arguments given to the function are packed into a dictionary
>>> my_function(a=1, b=2, c=3)
{"a": 1, "b": 2, "c": 3}
*args와 **kwargs는 함께 조합해서 사용할 수도 있어요:
>>> def my_function(*args, **kwargs):
... print(sum(args))
... for key, value in kwargs.items():
... print(str(key) + " = " + str(value))
>>> my_function(1, 2, 3, a=1, b=2, c=3)
6
a = 1
b = 2
c = 3
*args 앞에 매개변수를 써서 특정 위치 인자를 받도록 할 수도 있어요.
이때 개별 키워드 인자는 **kwargs 앞에 와야 해요.
def my_function(<positional_args>, *args, <key-word_args>, **kwargs)
이 순서를 지키지 않으면 오류가 발생해요.
>>> def my_function(a, b, *args):
... print(a)
... print(b)
... print(args)
>>> my_function(1, 2, 3, 4, 5)
1
2
(3, 4, 5)
인자를 잘못된 순서로 작성하면 오류가 발생해요:
>>>def my_function(*args, a, b):
... print(args)
>>>my_function(1, 2, 3, 4, 5)
Traceback (most recent call last):
File "c:\something.py", line 3, in <module>
my_function(1, 2, 3, 4, 5)
TypeError: my_function() missing 2 required keyword-only arguments: 'a' and 'b'
*를 사용해 인자로 이루어진 list/tuple을 함수 호출에 언패킹할 수 있어요.
iterable을 받지 않는 함수에 특히 유용해요:
>>> def my_function(a, b, c):
... print(c)
... print(b)
... print(a)
numbers = [1, 2, 3]
>>> my_function(*numbers)
3
2
1
내장 함수 zip()과 * 언패킹을 함께 사용하는 것도 흔한 활용법이에요.
zip() 함수는 여러 이터러블을 받아 각 iterable의 값을 묶은 tuples의 list를 반환해요:
>>> values = (['x', 'y', 'z'], [1, 2, 3], [True, False, True])
>>> a, *rest = zip(*values)
>>> rest
[('y', 2, False), ('z', 3, True)]
친구 리누스는 도시 사이를 오가는 화물 열차를 운전하는 기관사예요. 열차를 다루는 일에는 놀라울 만큼 능숙하지만, 물류나 컴퓨터를 다루는 일에는 그렇지 않아요. 그래서 열차 정보를 정리하고 경로 데이터의 실수를 바로잡는 일에 여러분의 프로그래밍 도움을 받고 싶어 해요.
이 연습 문제는 슬라이싱, 인덱싱, 그리고 다양한 dict 메서드를 사용하면 쉽게 풀 수 있어요.
하지만 아래 각 과제를 해결할 때는 패킹, 언패킹, 다중 할당을 연습해 보면 좋겠어요.
친구는 각 화차의 식별자(ID)를 계속 기록해 왔지만, 시스템이 어느 시점에 몇 대의 화차를 처리해야 할지 늘 확신이 없어요.
이 데이터를 하나의 list로 묶어 두면 나머지 물류 프로그램이 훨씬 수월해져요.
임의의 개수만큼 화차 ID를 받는 함수 get_list_of_wagons()를 구현해요.
각 ID는 양의 정수예요.
그런 다음 함수는 주어진 ID들을 하나의 list로 return해야 해요.
>>> get_list_of_wagons(1, 7, 12, 3, 14, 8, 5)
[1, 7, 12, 3, 14, 8, 5]
이쯤에서 데이터와 그것이 물류 프로그램에서 어떻게 쓰이는지 감이 잡히기 시작할 거예요. ID 시스템은 항상 기관차에 1이라는 ID를 부여하고, 열차의 나머지 화차에는 1보다 큰 임의의 ID를 부여해요.
친구가 새 화차 두 대를 열차에 연결하고는 시스템을 업데이트하는 걸 잊었어요!
이제 열차 list의 처음 두 화차를 끝으로 옮겨야 해요. 그렇지 않으면 모든 순서가 엉망이 돼요.
설상가상으로 친구가 방금 빠진 화차 ID가 들어 있는 듯한 두 번째 list를 발견했어요.
새 화차를 옮기고 나면 이 두 번째 list의 ID들을 지정된 기관차 바로 뒤에 놓아야 한다는 것만 기억해요.
리누스는 자기 실수를 바로잡고 데이터를 하나로 합쳐 주면 정말 고마워할 거예요.
화차 ID를 담은 두 개의 lists를 받는 함수 fix_list_of_wagons()를 구현해요.
첫 번째 list의 처음 두 항목을 끝으로 옮기고, 두 번째 list의 값들을 기관차 ID(1) 뒤(오른쪽)에 삽입해야 해요.
그런 다음 함수는 수정된 list를 return해야 해요.
>>> fix_list_of_wagons([2, 5, 1, 7, 4, 12, 6, 3, 13], [3, 17, 6, 15])
[1, 3, 17, 6, 15, 7, 4, 12, 6, 3, 13, 2, 5]
이제 모든 화차 데이터가 정확해졌으니, 리누스는 시스템의 경로 정보를 업데이트해 주기를 바라요.
운송 경로를 따라 열차는 화물을 싣거나 내리기 위해 여러 역에 정차할 수 있어요.
여정마다 이런 중간 배송 지점의 수가 다를 수 있어요.
친구는 빠졌거나 추가된 배송 정보를 시스템의 경로 dict에 업데이트해 주기를 바라요.
경로 dict와 그 뒤에 가변 개수의 키워드 인자를 받는 함수 add_missing_stops()를 구현해요.
이 인자들은 하나 이상의 정차역을 담은 dict 형태일 수도 있고, 임의의 개수만큼의 stop_number=city 키워드 쌍일 수도 있어요.
그런 다음 함수는 추가된 모든 정차역을 순서대로 담은 key 하나가 더해진 경로 dict를 반환해야 해요.
>>> add_missing_stops({"from": "New York", "to": "Miami"},
stop_1="Washington, DC", stop_2="Charlotte", stop_3="Atlanta",
stop_4="Jacksonville", stop_5="Orlando")
{"from": "New York", "to": "Miami", "stops": ["Washington, DC", "Charlotte", "Atlanta", "Jacksonville", "Orlando"]}
리누스는 경로 프로그램을 작업하다가 특정 경로에 중요한 세부 정보가 빠져 있다는 것을 알아챘어요.
초기 경로 정보는 dict로 만들어져 있고, 친구는 빠진 것이 있으면 그 dict를 업데이트해 주기를 바라요.
시스템의 모든 경로는 조금씩 다른 세부 정보를 필요로 하기 때문에, 리누스는 범용적인 해결책을 정말 선호해요.
두 개의 dicts를 받는 extend_route_information() 함수를 구현해요.
첫 번째 dict는 열차 경로가 오가는 출발지와 목적지 도시를 담고 있어요.
두 번째 dict는 열차 속도, 길이, 온도 같은 다른 경로 세부 정보를 담고 있어요.
함수는 모든 경로 정보를 하나로 합친 dict를 반환해야 해요.
두 번째 dict에는 예시에 나온 것과 다르거나 더 많은 속성이 들어 있을 수 있어요.
>>> extend_route_information({"from": "Berlin", "to": "Hamburg"}, {"length": "100", "speed": "50"})
{"from": "Berlin", "to": "Hamburg", "length": "100", "speed": "50"}
리누스는 화차 차량기지를 살펴보다가 화차들이 올바른 순서로 보관되지 않고 있다는 것을 알아챘어요. 각 화차에는 ID 외에도 운반하는 화물의 종류에 대응하는 색상이 있어요. 화차는 차량기지에 격자 형태로 보관되는데, 격자의 각 열에는 같은 색상의 화차가 들어가요.
그런데 물류 시스템에서는 차량기지에 보관할 화차 lists의 _행_이 색상별로 묶여 있어요.
하지만 보관 격자가 제대로 작동하려면 각 _행_에 서로 다른 세 가지 색상이 있어야 _열_이 색상별로 정렬돼요.
친구는 화차가 올바르게 보관되도록 차량기지 lists를 정리해 주기를 바라요.
세 개의 항목을 담은 list를 받는 fix_wagon_depot() 함수를 구현해요.
각 list 항목은 세 개의 tuples를 담은 하위 배열(또는 "행")이에요.
각 tuple은 (<wagon ID>, <wagon color>) 쌍이에요.
함수는 화차가 올바른 위치로 서로 바뀌도록 세 개의 "행" lists를 재정렬한 list를 반환해야 해요.
>>> fix_wagon_depot([
[(2, "red"), (4, "red"), (8, "red")],
[(5, "blue"), (9, "blue"), (13,"blue")],
[(3, "orange"), (7, "orange"), (11, "orange")],
])
[
[(2, "red"), (5, "blue"), (3, "orange")],
[(4, "red"), (9, "blue"), (7, "orange")],
[(8, "red"), (13,"blue"), (11, "orange")]
]
Exercism에 가입하고 Python 트랙을 개념 17개연습 문제 146개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.