Programming/Python

[Python] 언패킹 (Unpacking)

Jonghyuk Baek 2021. 4. 9. 01:33

Introduction

파이썬 언어에는 몇 가지 특징적인 기능들이 존재하는데, 그 중 하나가 바로 언패킹(Unpacking) 구문이다.

 

오늘은 이 언패킹에 대해 간단하게 정리해 보려 한다.


Iterable Unpacking

파이썬 문법 하에서는, 대입식의 좌측 항에 튜플 혹은 리스트를 가져다 놓는 것이 가능하다. 아래의 예제를 보자.

(var_a, var_b) = (5, 7)

print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")

대입식의 좌측과 우측이 모두 튜플로 이루어져 있고, 좌측 튜플의 변수들은 우측 튜플로부터 상대적인 위치를 고려해 값을 대입 받고 있다. 코드는 정상적으로 돌아가 5와 7을 출력한다.

 

물론 좌측 항이 리스트로 이루어진 경우에도 똑같이 잘 작동한다.

[var_a, var_b] = (5, 7)

print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")

하지만 아래처럼 대입할 변수의 갯수와 대입받을 변수의 갯수가 다른 경우엔 식이 작동하지 않는다. 

(var_a, var_b) = (5, 7, 9)
print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")

>>
Traceback (most recent call last):
File "<string>", line 18, in <module>
ValueError: too many values to unpack (expected 2)

또한, 아래와 같이 우측 항에는 어떠한 종류의 이터러블(iterable) 변수가 와도 된다.

(var_a, var_b) = [2*i+5 for i in range(2)]
print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")

(var_a, var_b) = "57"
print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")

(var_a, var_b) = range(5,8,2)
print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")

결과는 모두 같다!

 

이런 식으로, 어떤 이터러블의 값을 튜플이나 리스트의 element에 바로 대입하는 것을 이터러블 언패킹이라 부른다.

 

 

또한 파이썬에서는 변수들을 괄호로 묶지 않고 단순히 콤마로 구분해 나열해도, 암묵적으로 튜플로 만들어 주고 있기 때문에 다음과 같은 문장도 얼마든지 가능하다.

var_a, var_b, var_c = '321'
var_a, var_b, var_c = 3, 2, 1
var_a, var_b = var_b, var_a

특히 위 세 번째 문장의 경우, var_a와 var_b의 값을 교환해 저장하는 기능을 하고 있다. C++ 등의 언어에서 두 변수에 저장된 값을 교환하기 위해 temporary 변수를 정의해야 했던 것을 생각하면 굉장히 편리한 기능이라 할 수 있다.

def fun(a, b):
    return a, b
    
var_a, var_b = fun(5, 7)
print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")

함수의 출력에도 위와 같은 규칙이 적용되어, 콤마로 구분해 여러 값을 반환하는 함수의 출력을 위와 같이 바로 언패킹 구문을 이용해 변수에 대입할 수 있다. 

Iterable unpacking using starred expression

하지만 앞에서도 보았듯이, 이터러블의 변수에의 직접 대입은 대입식 양 쪽의 길이가 동일해야 한다는 제한이 있다. 하지만 코드를 작성하다 보면 함수의 출력 인자가 추가되어야 할 때도 있고, 더 긴 리스트를 받아와 처리해야 할 때도 있다.

 

이런 상황에 대응할 수 있게, 파이썬은 * (별표) 오퍼레이터를 통해 언패킹 되지 못 한 변수들을 다시 패킹할 수 있는 (묶어 주는) 기능을 제공하고 있다. 보통 이 * 오퍼레이터를 이용한 패킹은, 언패킹 이후 남는 나머지 값들을 리스트로 한 데 묶어주는 식으로 작동한다. 아래의 예시를 보자.

var_a, var_b, *others = (1,3,5,7,9)
print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")
print(f"Others: {others}")

>>
Variable A: 1
Variable B: 3
Others: [5, 7, 9]

우측 튜플의 값들이 하나하나 순차적으로 왼쪽의 변수들에 저장되고, 다 담지 못하는 나머지 값들은 *가 붙은 others에 리스트의 형태로 저장된다. 별표가 붙은 인자는 왼쪽 항의 어느 위치에나 올 수 있지만, 두 개 이상의 인자에 별표를 붙일 수는 없다.

 

아래의 예시를 참고하자.

var_a, *others, var_b = (1,3,5,7,9)
print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")
print(f"Others: {others}")

>>
Variable A: 1
Variable B: 9
Others: [3, 5, 7]

*others, var_a, var_b = (1,3,5,7,9)
print(f"Variable A: {var_a}")
print(f"Variable B: {var_b}")
print(f"Others: {others}")

>>
Variable A: 7
Variable B: 9
Others: [1, 3, 5]

상대적인 위치 관계를 유지한 채로 언패킹 시 남는 원소들이 별표 변수에 리스트의 형태로 저장되고 있다. 만약 남는 값이 없다면, 단순히 빈 리스트([ ])가 별표 인자에 들어가게 된다.

*variables, = (1,3,5,7,9)
print(f"Others: {variables}")

>>
Others: [1, 3, 5, 7, 9]

[*variables] = (1,3,5,7,9)
print(f"Others: {variables}")

>>
Others: [1, 3, 5, 7, 9]

이 * 오퍼레이터가 붙은 인자는 단독으로 좌항에 존재할 수는 없지만, 위와 같이 뒤가 빈 콤마나 대괄호를 이용해 좌항을 튜플 혹은 리스트 형태로 강제하면 우측 이터러블의 모든 값을 리스트 형태로 별표가 붙은 변수에 저장할 수 있다. 

 

이와 같이, 이터러블 언패킹 구문은 여러 변수를 한 번에 대입하는 데 굉장히 유용하게 쓰일 수 있다. 위에서 제시한 예시 이외에도 다양한 응용 방식이 존재한다. 

 

하지만 이터러블 언패킹 이외에도 파이썬에서 지원하는 언패킹 기능이 하나가 더 있는데, 바로 함수 인자 언패킹이다.

Function argument Unpacking

일단은 함수 인자 언패킹이라 적어놓긴 했지만, 사실 맨 처음의 이터러블 언패킹과 구분 없이 모두 언패킹이라는 이름 하나로 불리고 있는 것 같다.

 

기능적으로 연결되어 있는 것인지, 혹은 단순히 하는 작업이 비슷해서인지는 알 수 없지만 일단 여기서는 구분을 위해 함수 인자 언패킹이라 부르려 한다.

(혹시 뭔가 짚이시는 분이 있다면 피드백을 부탁드립니다.. 궁금하네요)

 

아무튼, 이 함수 인자 언패킹은 앞의 경우와 다르게 함수에서만 쓰이며, 함수의 인자 (argument)가 정확히 몇 개가 들어오는 지 확실하지 않을 때 유용하다. 

 

일단 예시를 한번 보자.

애견용품 매장에 사람과 개(들)이 방문한다고 하자. 방문하는 한 무리의 생물 중에 인간은 필수이지만, 개 혹은 개들은 같이 방문할 수도, 아닐 수도 있다. 이를 함수로 나타내 보자.

def ShopCustomers(human, dogs):
    if not dogs:
        print(f"{human} says hi.")
    else:
        print(f"Woof.")
        
ShopCustomers("Tomas", ["chiwawa"])

>>
Woof.

Function argument unpacking using starred expression

여기서 문제는, 멍멍이가 함께 방문하지 않는 경우에도 항상 함수의 인자로 빈 리스트를 하나 넣어줘야 한다는 점이다. 그렇다면 첫 번째 인자를 제외한 나머지 인자를 선택사항으로 남겨둘 순 없을까? 여기서 언패킹이 빛을 발한다.

아래 예시를 보자.

def ShopCustomers(human, *dogs):
    if not dogs:
        print(f"{human} says hi.")
    else:
        print(f"Woof.")
        
ShopCustomers("Tomas")
>>
Tomas says hi.

ShopCustomers("Tomas", "chiwawa")
>>
Woof.

dogs argument에 별표를 붙임으로써 dogs를 함수의 선택 인자로 사용할 수 있게 되었다. 이제는 dogs 인자가 없어도 함수가 정상적으로 작동할 수 있다.

 

인풋 인자가 더 많아져도, 명시된 필수 인자를 제외하고는 모두 dogs 변수에 리스트의 형태로 묶여 함수 내부에 전달된다.

def ShopCustomers(human, *dogs):
    if not dogs:
        print(f"{human} says hi.")
    else:
        for dog in dogs:
            print(f"{dog} says Woof.")

ShopCustomers("Tomas", "chiwawa", "dachshund", "puddle")
>>
chiwawa says Woof.
dachshund says Woof.
puddle says Woof.

위에서 설명한 이터러블 언패킹과 굉장히 유사한 방식으로 * 오퍼레이터가 작동하는 것을 눈치챌 수 있다!

 

참고로 말하자면, 보통은 위에서처럼 옵셔널 (혹은 가변) 인자를 *dogs 처럼 이름을 붙이기보단, *args 라 명명하는 것이 관례이다.

Starred expression for keyword argument

마지막으로 이런 방식의 언패킹은 함수의 keyword argument에도 적용될 수 있는데, 단순히 *를 붙이던 것을 ** (더블 별표)로 바꾸면 된다.

 

** 오퍼레이션은 인자를 단순히 리스트로 묶어오던 * 오퍼레이션과 달리, 인자 이름과 함께 딕셔너리의 형태로 keword 인자를 묶어 온다. 다음 예시를 참고하자.

def ShopCustomers(**kwargs):
    if "dogs" not in kwargs.keys():
        print(f"{kwargs.get('human')} says hi.")
    else:
        for dog in kwargs.get('dogs'):
            print(f"{dog} says Woof.")

ShopCustomers(human="Tomas", dogs=["chiwawa", "dachshund", "puddle"])
>>
chiwawa says Woof.
dachshund says Woof.
puddle says Woof.

예시가 literally 개판이 되어가는데 조금만 참아 달라..

List, Dict argument unpacking

이와 정확히 반대로, 함수를 call 할 때 인자로 리스트나 딕셔너리를 각각 positional 인자, 그리고 keyword 인자로 풀어서 넣어주는 것도 가능하다.

 

이 형태는 리스트나 딕셔너리 형태로 묶여 있는 변수들을 함수의 인자 하나하나로 제공해 주어야 하는 경우굉장히 유용하다. 하나씩 풀어다가 집어 넣을 필요 없이 리스트나 딕셔너리에 * 혹은 **만 붙여 주면 된다!

def ShopCustomers(**kwargs):
    if "dogs" not in kwargs.keys():
        print(f"{kwargs.get('human')} says hi.")
    else:
        for dog in kwargs.get('dogs'):
            print(f"{dog} says Woof.")

ShopCustomers(**{"human": "Tomas", "dogs": ["chiwawa", "dachshund", "puddle"]})
>>
chiwawa says Woof.
dachshund says Woof.
puddle says Woof.

위 예시에서는 딕셔너리를 풀어서, 함수에 value 값을 key 이름으로 제공하고 있다.

ShopCustomers(**{"human": "Tomas", "dogs": ["chiwawa", "dachshund", "puddle"]})
>>
chiwawa says Woof.
dachshund says Woof.
puddle says Woof.

ShopCustomers(human="Tomas", dogs=["chiwawa", "dachshund", "puddle"]})
>>
chiwawa says Woof.
dachshund says Woof.
puddle says Woof.

결과적으로 위의 두 라인은 완전히 equivalent한 기능을 수행한다. 리스트의 경우 각 element를 순서대로 positional argument의 형태로 함수에 제공하게 된다.

Starred expression for positional & keyword argument 

그리고 최종적으로, 필수 positional 인자와 가변 positional 인자, 그리고 가변 keword 인자는 함께 섞어서 쓰는 것이 가능하다.

def ShopCustomers(human, *dogs, **kwargs):
    print(f"{human} says hi.")
    
    for dog in dogs:
        print(f"{dog} says Woof.")
    for named_dog in kwargs.get('named_dogs'):
        print(f"{named_dog} says Woof.")

ShopCustomers("Tomas", "chiwawa", "dachshund", "puddle", named_dogs=["Bolt", "Spot"])
>>
Tomas says hi.
chiwawa says Woof.
dachshund says Woof.
puddle says Woof.
Bolt says Woof.
Spot says Woof.

 

......

위 예시에선 대충 이렇게도 쓸수 있다 정도만 알아주면 좋겠다.

 


 

이번에 설명하게 된 언패킹 기능은 파이썬에서 굉장히 유용하게 쓰이는 기능 중 하나이며 잘 숙지하고 응용할 수 있으면 전체적인 프로그램의 유연성과 강건성 개선, 그리고 코드 간결화에 큰 도움이 될 것이다.

 

아래는 참고할 만한 사이트이다.

References

Python documentation: Data Structures

https://docs.python.org/3/tutorial/datastructures.html

반응형