Python/내장함수&기타

[Python/내장함수] zip함수와 *args, **kwargs 란?

(Python) zip함수(내장함수)

[Python] zip함수와 파라미터 앞에 *, **는 어떤 의미인가?


zip함수는 동일한 개수로 이루어진 자료형을 묶어주는 역할을 한다.

동일한 위치에 있는 요소들을 가져와서 함께 묶어주는 것 같다.

나중에, 2차원 배열과 관련해서 열(=col)들을 가져올때 zip을 사용하면 유용할 것 같다.
[이유] 2차원 배열에서 각 행(=row)들은 인덱스를 통해서 가져오는 것이 쉽지만, 일반적인 방법으로 열(=col)을 가져오기 위해서는 for문이 필요하다.
  • ex) 서로 다른 2개의 자료형을 묶어보자.
In [1]:
A = [1,2,3]
B = [4,5,6]
In [2]:
for node in zip(A, B):
    print(node, type(node))
(1, 4) <class 'tuple'>
(2, 5) <class 'tuple'>
(3, 6) <class 'tuple'>
In [3]:
for i, j in zip(A, B):
    print(i, j, type(i))
1 4 <class 'int'>
2 5 <class 'int'>
3 6 <class 'int'>

알고리즘 문제를 풀 때, 동서남북과 같이 이동방향에 따른 변화량을 고려해줘야 할때가 있는데
zip함수를 사용하면 깔끔하게 표현할 수 있을 것 같다.

  • ex) 동서남북
In [4]:
dxs = (0, 0, 1, -1)
dys = (1, -1, 0, 0)

x, y = 0, 0                  # x, y : 처음 위치
for dx, dy in zip(dxs, dys): # dx, dy : 변화량
    nx, ny = x+dx, y+dy      # nx, ny : 변한 후의 위치
    print(nx, ny)
0 1
0 -1
1 0
-1 0

물론 굳이 zip을 이용할 필요는 없다.
좀 더 가독성이 좋아 보인다는 정도?

In [5]:
dxs = (0, 0, 1, -1)
dys = (1, -1, 0, 0)

x, y = 0, 0
for i in range(4):
    nx, ny = x+dxs[i], y+dys[i]
    print(nx, ny)
0 1
0 -1
1 0
-1 0
  • ex) 서로 다른 3개 이상의 자료형을 묶어보자.
In [6]:
A = [1,2,3]
B = [4,5,6]
C = [7,8,9]

for node in zip(A, B, C):
    print(node)
(1, 4, 7)
(2, 5, 8)
(3, 6, 9)
In [7]:
A = '1234'
B = '5678'
C = '4321'
D = '8765'

for node in zip(A, B, C, D):
    print(node)
('1', '5', '4', '8')
('2', '6', '3', '7')
('3', '7', '2', '6')
('4', '8', '1', '5')

리스트 외에도 가능할까?

문자열끼리도 가능하고
문자열&리스트 처럼, 서로 다른 타입이어도 엮어주는 것이 가능하다.

집합도 가능하지만, 사용할 일이 없을 것 같다.
[이유] 내가 원하는 순서로 엮어진단 보장이 없다.
  • ex) 다양한 자료형들을 엮어보자.
In [8]:
list(zip('abc', '123'))
Out[8]:
[('a', '1'), ('b', '2'), ('c', '3')]
In [9]:
list(zip('abc', [1,2,3]))
Out[9]:
[('a', 1), ('b', 2), ('c', 3)]
In [10]:
list(zip(set([1,2,3,4]), set([5,6,7,8])))
Out[10]:
[(1, 8), (2, 5), (3, 6), (4, 7)]

만약 A와 B의 길이가 서로 다르다면 어떻게 될까?

길이가 더 짧은 자료형에 맞춰서 엮어준다.
매칭이 되지 않는 나머지 부분은 고려해주지 않는 것 같다.
  • ex) 길이가 서로 다른 자료형을 엮어보자.
In [11]:
A = [1,2,3,4,5]
B = [6,7]

list(zip(A, B))
Out[11]:
[(1, 6), (2, 7)]
In [12]:
for node in zip(A, B):
    print(node)
(1, 6)
(2, 7)
In [13]:
for i, j in zip(A, B):
    print(i, j)
1 6
2 7

2차원 리스트에서 행(=row)이 아닌 열(=col)을 순서대로 가져오고 싶다면 어떻게 해야 할까?

결과적으로, zip함수를 다양한 상황에 맞춰 응용하고 싶다면,
*args, *kwargs에 대한 개념을 알아둘 필요가 있다.
  • ex) 2차원 리스트에서 열끼리 엮어보자.
In [14]:
alist = [[1,2,3], [4,5,6], [7,8,9]]

## 일단 2차원 리스트는 이렇게 생겼다.
for i in alist:
    print(i)
[1, 2, 3]
[4, 5, 6]
[7, 8, 9]

zip함수의 인수로 2차원 리스트를 그냥 넣을 경우, 원하는 결과는 나오지 않는다.

In [15]:
list(zip(alist))
Out[15]:
[([1, 2, 3],), ([4, 5, 6],), ([7, 8, 9],)]
In [16]:
for i in zip(alist):
    print(i)
([1, 2, 3],)
([4, 5, 6],)
([7, 8, 9],)
zip함수에 *args를 인수로 넣을 수 있다.  
리스트를 그냥 입력하지 말고, *를 붙여서 입력하면 col끼리 서로 엮어준다.
In [17]:
for i in zip(*alist):
    print(i)
(1, 4, 7)
(2, 5, 8)
(3, 6, 9)
  • ex) map과 zip함수를 사용해서 2차원 리스트를 transpose해주는것이 가능하지 않을까?
In [18]:
alist = [[1,1,1], [2,2,2], [3,3,3]]

## 일단 2차원 리스트 alist는 이렇게 생겼다.
for i in alist:
    print(i)
[1, 1, 1]
[2, 2, 2]
[3, 3, 3]

위의 2차원 리스트 (i, j)를 (j, i)로 간단하게 바꿔보자.

In [19]:
for i in map(list, zip(*alist)):
    print(i)
[1, 2, 3]
[1, 2, 3]
[1, 2, 3]

새로운 리스트에 transpose한 것을 할당해주고 싶다면?

In [20]:
blist = list(map(list, zip(*alist)))
blist
Out[20]:
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]

그렇다면, *args는 무엇인가?

*args : 
- 파라미터를 몇개 받을지 모르는 경우에 사용한다.
- 튜플 형태로 전달된다.
  • ex) 함수를 정의해서 알아보자
In [21]:
def func1(*args):
    
    print(args, type(args))
    print(*args, end='\n\n')
    
    for i in args:
        print(i)
In [22]:
## 변수 1개 입력받음
func1([1,2,3,4,5])
([1, 2, 3, 4, 5],) <class 'tuple'>
[1, 2, 3, 4, 5]

[1, 2, 3, 4, 5]
In [23]:
## 변수 여러개 입력 받는다면?
func1(1, 2, 3, 4)
(1, 2, 3, 4) <class 'tuple'>
1 2 3 4

1
2
3
4
In [24]:
func1('1', '2', '3', '4')
('1', '2', '3', '4') <class 'tuple'>
1 2 3 4

1
2
3
4
In [25]:
func1([1,2], [3,4,5], [6,7,8,9])
([1, 2], [3, 4, 5], [6, 7, 8, 9]) <class 'tuple'>
[1, 2] [3, 4, 5] [6, 7, 8, 9]

[1, 2]
[3, 4, 5]
[6, 7, 8, 9]

이 상태에서, 2차원 리스트를 인수값으로 넣어본다면?

In [26]:
func1([[1,2], [3,4]])
([[1, 2], [3, 4]],) <class 'tuple'>
[[1, 2], [3, 4]]

[[1, 2], [3, 4]]
*args를 입력값으로 받는 함수에서 리스트를 그냥 입력하는 경우, 변수 1개 받은것으로 판단하는 것 같다.  
리스트 앞에 *를 붙이면 어떻게 될까?
In [27]:
func1(*[[1,1], [2,2]])
([1, 1], [2, 2]) <class 'tuple'>
[1, 1] [2, 2]

[1, 1]
[2, 2]
In [28]:
func1(*[1,2,3,4,5])
(1, 2, 3, 4, 5) <class 'tuple'>
1 2 3 4 5

1
2
3
4
5
신기한 점이 *args를 인수로 받는 함수에 리스트를 변수로 넣어주는 경우  
리스트 앞에 *를 붙여서 보내주면  리스트 그 자체가 아니라,  
그 안의 요소값들을 각각의 변수로 인식하는 것 같다.
  • ex) *args를 인수로 받도록 설정하지 않은 함수여도 똑같이 적용될까?
In [29]:
def func2(xlist):
    print(xlist)
    
    for i in xlist:
        print(i)
In [30]:
func2(*[1,2,3,4,5])
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-30-562ff57aed2d> in <module>
----> 1 func2(*[1,2,3,4,5])

TypeError: func2() takes 1 positional argument but 5 were given
역시 안된다.  
*args의 의미를 다시 생각해보면, 변수를 몇개 받을지 알지 못하는 특수한 상황에 쓰인다는 점.  
이를 주의해서 사용하면 될 것 같다.

*args외에 특정한 다른 변수도 인수값으로 받고 싶다면 어떻게 해야 할까?

결과적으로 말하자면,
함수를 정의할 때, 입력받을 인수(=변수)의 순서를 조정해줘야 한다.
  • ex) 특정 변수 n과 임의 몇개를 받을 지 모르는 *args변수를 인수로 설정해보자.
만약 *args가 n보다 앞에 온다면?
In [31]:
def func3(*args, n):
    print(n)
    
    for i in args:
        print(i)
In [32]:
func3(1,2,3,4,5,6)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-32-bbd9362898ec> in <module>
----> 1 func3(1,2,3,4,5,6)

TypeError: func3() missing 1 required keyword-only argument: 'n'
변수 n을 입력받지 못했다는 TypeError가 발생하고 있다.  
이번엔 변수 n을 먼저 입력받도록 함수를 정의해보자.
In [33]:
def func4(n, *args):
    print(n)
    
    for i in args:
        print(i)
In [34]:
func4(100, 1,2,3,4,5)
100
1
2
3
4
5
n = 100  
*args = 1, 2, 3, 4, 5 라고 인식한 것 같다.  
특정 변수들을 먼저 할당해주고, 나머지들을 임의 변수로 몰아주는 방식을 사용해야 하는 것 같다.  
즉, 특정 변수를 *args보다 먼저 정의해줘야 한다.

**kwargs는 무엇인가?

**kwargs :
- 키워드를 함께 보낼 수 있다.  
- 딕셔너리 형태로 전달된다.
  • ex) 간단히 함수로 살펴보자.
In [35]:
def func5(**kwargs):
    print(kwargs)
    print()
    
    print(kwargs.keys())
    print()
    
    print(kwargs.values())
    print()
    
    for i, j in kwargs.items():
        print(i, j)
In [36]:
func5(name='juhee', sex='female', age=27)
{'name': 'juhee', 'sex': 'female', 'age': 27}

dict_keys(['name', 'sex', 'age'])

dict_values(['juhee', 'female', 27])

name juhee
sex female
age 27
주의할 점은, 문자열 키워드를 따로 str 변환해줄 필요가 없다는 점?  
그리고 맨 앞 자리가 숫자형태를 띄고 있으면 SyntaxError가 일어나는 것 같다.
In [37]:
func5('1'=100, '2'=200)
  File "<ipython-input-37-04ea9463e47a>", line 1
    func5('1'=100, '2'=200)
         ^
SyntaxError: keyword can't be an expression
In [38]:
func5(1=100, 2=200)
  File "<ipython-input-38-97d6a0e88226>", line 1
    func5(1=100, 2=200)
         ^
SyntaxError: keyword can't be an expression
In [39]:
func5(1x=100, 2x=200)
  File "<ipython-input-39-d898777031e3>", line 1
    func5(1x=100, 2x=200)
           ^
SyntaxError: invalid syntax
숫자를 앞에만 두지 않으면 되는 것 같다.
In [40]:
func5(x1 = 100, x2 = 200)
{'x1': 100, 'x2': 200}

dict_keys(['x1', 'x2'])

dict_values([100, 200])

x1 100
x2 200

*args와 **kwargs를 동시에 사용하면 어떻게 될까?

참고로, args와 kwargs는 arguments, keyword arguments 에서 따온 것이다.
In [41]:
def func6(*args, **kwargs):
    print(args)
    print(kwargs)
In [42]:
func6(1,2,3,4,5, name = 'juhee', age = 27)
(1, 2, 3, 4, 5)
{'name': 'juhee', 'age': 27}
특정 변수와 *args가 순서에 영향을 받았던 것 처럼,  
*args와 **kwargs도 서로 영향을 받을까?  
둘의 순서를 바꿔보자.
In [43]:
def func6(**kwargs, *args):
    print(args)
    print(kwargs)
  File "<ipython-input-43-9624b77c7c2c>", line 1
    def func6(**kwargs, *args):
                        ^
SyntaxError: invalid syntax
정의조차 불가능하다.
즉, 특정변수와 *args, **kwargs를 인수로 함께 정의하고 싶다면
순서를 주의해야 한다.