기본 unpacking
파이썬 unpacking(언패킹)에 대해서 잘 이해하고 있는가?
t1 = [1,2,3]
t2 = ['test']
t3 = [*t1,*t2]
print(t3)
a, *b, (c,*d) = t3
print(a)
print(b)
print(c)
print(d)
위 코드에서 a,b,c,d 변수에 어떤 값이 들어갈 것인지 예측이 가는가?
헷갈린다면 개념을 이번 포스팅을 확실하게 이해해보자.
a,b,c = [1,2,3]
print(a,b,c)
보이는 것처럼 오른쪽에 위치한 리스트의 각 원소들이 위치에 상응하는 a
,b
,c
변수에 들어가는 형태이다.
아웃풋은 a = 1
, b = 2
, c = 3
이 된다
unpacking은 리스트 뿐 아니라 튜플 , string , 딕셔너리 등 모든 iterable 형태의 데이터 타입들은 모두 가능하다.(* 데이터 타입이 iterable 형태인지 아닌지 궁금하다면 for 문을 돌렸을 때 출력이 되는 형태는 모두 iterable 형태라고 보면 된다 )
예시를 통해 정말 그런지 테스트 해보자
string 형태
a,b,c = 'WOW'
print(a,b,c)
# Output
W O W
Dictionary 형태
a,b,c = {'key1': 1, 'key2': 2, 'key3': 3}
print(a b c)
# Output
key1 key2 key3
딕셔너리는 unpacking을 했을 때 key 값만 나오는 특징이 있다.
이는 딕셔너리를 for문을 이용해 돌렸을 떄 key 값만 나오는 것과 같은 원리라는 것을 알 수 있는데, 그렇다면 딕셔너리 뒤에 .values(), .items() 함수를 추가하면 value값 또는 (key,value)의 unpacking이 가능하겠구나라는 걸 유추해볼 수 있겠다.
values()
a,b,c = {'key1': 1, 'key2': 2, 'key3': 3}.values()
print(a,b,c)
# Output
1 2 3
items()
a,b,c = {'key1': 1, 'key2': 2, 'key3': 3}.items()
print(a,b,c)
# Output
('key1', 1) ('key2', 2) ('key3', 3)
참고로 set 자료형또한 iterable 타입이기 때문에 unpacking이 가능하다. 하지만 set은 순서가 없는 데이터 타입이므로, unpacking을 하게되면 데이터가 뒤죽박죽이 되기 때문에 사용되지는 않는다
*을 통한 unpacking
*
는 iterable(리스트, 튜플 등)의 요소들을 개별적으로 풀어낸다.
- 함수 호출에서 사용: 함수 호출 시, 리스트나 튜플의 요소들을 개별 인자로 전달할 때 사용된다.
- 변수 할당에서 사용: 여러 값을 가진 객체의 요소들을 풀어서 변수에 할당할 때도 사용할 수 있다.
예제를 통해 알아보도록 하자.
변수 호출에서의 * 사용
- 우선 *가 어디에 위치하냐에 따라 전혀 다른 기능을 하니 잘 숙지해야 한다.
*가 왼쪽 변수에 있을 경우
a,b,c = [1,2,3]
기존에는 위와 같이 코드를 적으면 a,b,c 순서에 맞게 1,2,3이 잘 들어갔었다.
a,b,c = [1,2,3,4,5,6]
하지만 위와 같은 코드는 어떤가? 변수는 3개인데 리스트 안의 원소 6개나 존재하고있다. 이럴 때 쓸 수 있는 것이 *
이다.
아래 코드를 봐보자
a,*b,c = [1,2,3,4,5,6]
b 앞에 *
를 붙였다.
*
의 기능은 *가 없는 변수가 값을 할당받고 할당받지 못한 나머지 값들을 모아서 리스트로 만드는 기능을 한다고 보면 될 것 같다
# Output
1
[2,3,4,5]
6
보이는 것처럼 *가 없는 a,c 변수에 각각 1, 6이 할당 되었고 *가 있는 변수에 나머지 매칭안된 값을 모아서 리스트 형태로 반환하고 있다.
참고!
왼쪽 변수부분에는 *를 한 번만 사용가능하다.
함수 호출에서의 * 사용
위 예시에서는 리스트 값을 각 변수에 할당받는데, 리스트 안의 원소가 변수보다 더 많이 있는 경우에 사용했었다면 이번에는 리스트 자체를 분해 한다고 보면 될 것 같다.
아래 코드를 봐보자
t1 = [1,2,3]
print(*t1)
# Output
1 2 3
보이는 것처럼 리스트 값들이 하나씩 분해되었다.
그렇다면 리스트를 분리해서 어디다 쓰는 것일까?
1. 함수에 인자를 넣을 때
def func_arg(t1:int ,t2: int,t3: int) -> int:
return (t1 + t2 + t3)
nums :list[int] = [5,10,1]
result : int = func_arg(*nums) # *를 사용하여 리스트를 풀어서 함수의 개별 인자로 전달
print(result) # 결과는 16이 출력됨
위 코드를 봐보자. func_arg
함수에서 t1,t2,t3 라는 3개의 매개변수를 요구하고있다.
함수에 매개변수가 다수 존재할 때 우리는 위와 같이 리스트나 튜플 데이터를 언패킹(Unpacking)해서 함수에 전달할 수 있다
2. 원하는 컨테이너에 합칠 때
t1 = [1,2,3]
t2 = [4,5,6]
t3 = [*t1,*t2]
print(t3)
# Output
[1,2,3,4,5,6]
*를 통해서 각 리스트에 있는 값들을 분해하고 새로운 리스트에 담았다.
튜플로도 unpacking 하기
위에 예시에서 리스트로만 예시를 들었었는데 이번에는 튜플에도 담은 예시를 봐보자.
t2가 리스트가 아닌 string 형태이다.(위에서 말했듯 string 또한 iterable이니 각 글자들을 unpacking 할 수 있다)
t1 = [1,2,3]
t2 = 'WOW'
t3 = (*t1,*t2)
print(t3)
# Output
(1,2,3,'W','O','W')
이제 어느정도 이해가 되었는가?
*로 분해한다음 튜플로 쌓여있으면 최종 결과는 튜플로 묶이는 걸 알 수가 있다.
*그렇다면, 여기서 튜플로 감싸지 않고 로 바로 분해해버리면 어떻게 될까?
t1 = [1,2,3]
t2 = *t1
print(t2)
정답은 .. 땡! 오류가 난다
t2 = *t1
^^^
SyntaxError: can't use starred expression here
iterable 자체에 *를 넣는 경우는 값을 분리하는 것은 맞지만 그냥 그 자체로는 사용할 수 없다. 즉 튜플이든 리스트든 어떤 것으로 둘러쌓여야만 한다는 것이다
아래와 같이 튜플이나 리스트로 감싸줘야 위와 같은 에러가 발생하지 않는다
t1 = [1,2,3]
t2 = (*t1)
print(t2)
**을 통한 unpacking
**
는dictionary
의 키-값 쌍을 개별적으로 풀어낸다.
**는 딕셔너리를 unpacking 하는 목적으로 사용한다. 물론 아래 코드와 같은 기능은 제공하지 않고, 오로지 unpacking 기능만 제공한다.
a , **b , c = {'a':1 , 'b': 2 , 'c': 3}
코드를 통해 자세히 알아보자
t1 = {'a':1 , 'b': 2 }
t2 = {'c': 3, 'd': 4}
t3 = {'d': 5, 'f': 6, 'g': 7}
r = {*t1, *t2 , *t3}
print(r)
# Output
{'a', 'd', 'f', 'g', 'c', 'b'}
위 코드는 딕셔너리를 unpacking 한 후 Set 자료형 타입으로 값을 묶었다.
output을 살펴보면 몇가지 특징이 있는데
- 첫 번째로 *로 unpacking을 하다보니 키 값만 값으로 가져오게 되었다.
- 두 번째로 Set 형태로 값을 묶다보니 d가 두 개 인데도 불구하고 1개로 축소되었고, 순서가 없기 때문에 데이터 순서가 뒤죽박죽이다.
이럴 때 key와 value를 한꺼번에 보기 위해서 사용하는 것이 **
이다.
t1 = {'a':1 , 'b': 2 }
t2 = {'c': 3, 'd': 4}
t3 = {'d': 5, 'f': 6, 'g': 7}
r = {**t1, **t2 , **t3}
print(r)
# Output
{'a': 1, 'b': 2, 'c': 3, 'd': 5, 'f': 6, 'g': 7}
output을 통해 특징을 살펴보자.
보이는 것처럼 각각의 딕셔너리를 분해하여 새로운 딕셔너리에 차곡차곡 넣은 것을 알 수 있다.
이 때 자세하게 봐야할 부분은 중복되어있던 'd' 중 하나가 사라졌다. 왜냐하면 딕셔너리에선 key가 하나만 올 수 있기 때문이다.
더 구체적으로 설명하자면 순서상 늦게 정의된 'd':5 가 이전 값을 override 한 것이라고 보면 될 것 같다.
다른 예제를 풀어보면서 확실하게 이해해보자.
함수 호출에서의 ** 사용
def print_info(name, age):
print(f"Name: {name}, Age: {age}")
info = {'name': 'Alice', 'age': 25}
print_info(**info) # **를 사용하여 딕셔너리의 키-값을 풀어서 전달
# 출력: Name: Alice, Age: 25
nested unpacking
nested unpacking은 겹겹이 쌓여있는 리스트를 unpacking 하는 경우이다.
아래 코드와 같이 리스트로 둘러 쌓여있는데 그 안에 리스트가 또 있는 형태를 nested 리스트라고 지칭한다
t = [1,2,[3,4]]
a,b,c,d = t
print(a,b,c,d)
# Output
a,b,c,d = t
ValueError: not enough values to unpack (expected 4, got 3)
output을 확인해보니 not enough values to unpack
이라는 valueerror 가 발생하였다.
이 에러는 a는 1 b는 2 c는 [3,4] 리스트를 통째로 받고 d는 아무것도 받지 않아서 생긴 에러이다. 현재 내가 원하는건 [3,4] 리스트의 원소가 각각 c,d에 들어갔으면 하는데 이걸 어떻게 해야할까?
이 문제를 해결하기 위해선 아래와 같이 코드를 작성하면 된다
a,b,(c,d) = [1,2,[3,4]]
print(a,b,c,d)
# Output
1 2 3 4
즉 a는 1을 , b는 2을 , (c,d)는 두 개의 변수이지만 ()로 덮음으로써 가장 바깥쪽 리스트에선 하나의 변수 역할을 하게된다. (c,d)가 [3,4] 리스트를 받고 그 후에 c,d가 3,4를 각각 받게 되는 것이다!