트랙
/
Python
Python
/
학습 과정
/
언패킹과 다중 할당
언패

언패킹과 다중 할당 에서 Python

3개의 연습 문제

언패킹과 다중 할당 소개

언패킹은 반복을 이용해 list, tuple, dict 같은 컬렉션의 원소를 추출하는 걸 말해요. 이렇게 언패킹한 값은 같은 문 안에서 변수에 할당할 수 있어요. 이런 동작의 아주 흔한 예가 for item in list예요. 여기서 item은 반복하는 동안 list의 각 원소 값을 차례로 가지게 돼요.

다중 할당은 하나의 문 안에서 언패킹한 값들을 여러 변수에 할당할 수 있는 기능이에요. 이를 통해 코드를 더 간결하고 읽기 쉽게 만들 수 있어요. 할당할 변수들을 쉼표로 구분하면 되는데, first, second, third = (1,2,3)이나 for index, item in enumerate(iterable)처럼 써요.

특수 연산자 *와 **는 언패킹할 때 자주 쓰여요. *는 여러 list/tuple을 하나의 list/tuple로 합칠 때 쓸 수 있어요. 각각을 _언패킹_해서 새로운 공통 list/tuple로 만드는 거예요. **는 여러 딕셔너리를 하나의 딕셔너리로 합칠 때 쓸 수 있어요. 각각을 _언패킹_해서 새로운 공통 dict로 만드는 거예요.

* 연산자를 컬렉션 없이 사용하면 여러 값을 _패킹_해서 하나의 list로 만들어요. 다중 할당에서는 개별적으로 할당되지 않은 "나머지" 원소들을 하나의 변수로 묶을 때 이 방식을 자주 써요.

Python에서는 위치 인자나 키워드 인자를 임의의 개수만큼 받는 함수를 사용하거나 정의할 때 이 언패킹/패킹 동작을 활용하는 일도 흔해요. 이런 "특별한" 매개변수는 def some_function(*args, **kwargs)처럼 정의하고, "특별한" 인자는 some_function(*some_tuple, **some_dict)처럼 사용하는 걸 자주 보게 될 거예요.

Caution

*<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의 원소는 서로 바꿀 수 없어요.

언패킹

Note

아래 예제들은 lists를 사용하지만, 같은 개념이 tuples에도 적용돼요.

Python에서는 list/tuple/dict의 원소를 언패킹해서 각각 다른 변수에 담을 수 있어요. 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/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 패킹은 * 연산자로 할 수 있어요. 그러면 모든 값이 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']

# A list literal can be used instead, but might not be as readable.
>>> [*combined_fruits_too] = *fruits, *more_fruits
>>> combined_fruits_too
['apple', 'banana', 'cherry', 'orange', 'kiwi', 'melon', 'mango']

*와 ** 사용에 대한 자세한 내용은 PEP 3132와 PEP 448을 참고해요.

**로 딕셔너리 패킹하기

딕셔너리 패킹은 ** 연산자로 해요. 그러면 한 딕셔너리의 모든 키-값 쌍을 다른 딕셔너리로 패킹하거나, 두 딕셔너리를 하나로 합칠 수 있어요.

>>> 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는 임의의 개수만큼의 위치(비키워드) 인자를 패킹할 때 쓰고, **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 앞에 와야 해요.

Caution

인자는 이렇게 구조화해야 해요:

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)]
GitHub에서 편집 링크가 새 창이나 탭에서 열려요

언패킹과 다중 할당 배우기

연습이 잠겨 있어요

언패킹과 다중 할당 개념을 연습하려면 연습 문제 3개를 더 잠금 해제해요