Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
04-25 19:32
관리 메뉴

ulismoon

[Python] Function Argument 본문

Development

[Python] Function Argument

ulismoon 2016. 6. 22. 02:47

def func(arg, *args, **kwargs):

 파이썬의 함수는 매우 잘 조직되어 있다. 아마도 이는 모든 것을 1급 객체로 만들어놓은 노고로 인한 보상이 아닐까 생각하는데, 덕분에 우리는 함수를 아주 다양하고 직관적으로 사용할 수 있다. 개인적으로 파이썬 함수를 공부하면서 가장 놀라웠던 것은 그 인자의 자유도였다. 함수를 쓰면서 인자값에 그렇게 신경을 안 써도 된다는 것이 개인적으로는 아주 큰 매력이었다. 파이썬의 함수 인자는 종류도 많고 이들이 우선순위를 가지며, 상당히 치밀하게 설계되어 있음에도, 내가 지금까지 본 파이썬 관련 책에서는 이를 깔끔하게 정리해준 것을 아직 발견하지 못했다. 그래서 이번에는 파이썬 함수 인자에 대해 적어보기로 한다.


Function Arguments 개요

 파이썬 함수 정의에 사용할 수 있는 인자의 종류는 3가지로 나뉜다. Positional, Arbitrary, Keyword가 그것인데, 이들은 서로 판단의 우선순위가 존재하며, 엄격한 순서에 의해서만 선언되고 사용될 수 있다. 기술한 순서대로 Positional, Arbitrary, Keyword 순으로 우선순위를 가지며, 이 순서는 절대적이다. 함수가 인자를 받아서 처리할 때 이 순서를 토대로 인자를 배정한다. 우선순위가 높은 것부터 살펴보자.

1. Positional Arguments

 한글로 뭐라 바꾸기가 참 애매해 계속 영어를 쓰고 있는데, 굳이 말하자면 위치 인자정도로 말할 수 있을 것 같다. 우리가 함수를 사용하면서 가장 많이 쓰는 형식으로, def func(a, b): 에서의 a, b를 말한다. 우리가 이 함수를 사용하면서 func(1, 2)라고 하면 a=1, b=2가 되어 함수 내에서 사용된다. 이게 뭐 당연한건데 이걸 이렇게 열심히 설명하냐 할 수 있지만, 사실 이것부터 설명해야 뒤에 나올 녀석들이 유연하게 설명되므로, 다시 한번 설명하겠다. 파이썬은 함수 정의에서 선언된 인자가 이름만 단독으로 존재할 경우 이를 positional argument로 간주하고, 함수 사용 시에 전달된 인수를 배정하는 데 가장 높은 우선순위를 준다. 물론 같은 우선순위를 가진 positional arguments 사이에서는 선언 순서가 배정 순서가 된다. 그래서 우리가 두 개의 값을 전달하면 순서대로 배정이 되는 것이다. 
 우리가 모두 알듯이, 함수를 사용할 때, 최소한 positional argument가 선언된 갯수만큼의 인자를 전달해야 한다. 이는, 다른 두 인자 타입과 달리 유일하게 "필수적"으로 값의 존재를 요구하는 특성이 있다. 따라서 함수 정의에서 선언된 positional arguments와 갯수와 동일한 갯수의 인자는 필수적으로 전달해주어야 하며, 이를 어기면 파이썬에서는 인자에 값을 배정하지 못해 에러를 발생시킨다. 
위같은 에러메세지는 꽤 자주 볼 수 있다. 필수적이며, 위치에 따라 값이 배정되는 것이 positional argument의 특징이라 정리하면 될 것 같다.

2. Arbitrary Arguments

 실제로 사용하는 경우는 가장 적으나, 때에 따라 매우 유용하게 쓸 수 있다. 개인적으로는 optional 또는 가변 갯수 인자라고 부른다. 실제로 그런 기능을 하기 때문인데, *을 앞에 붙여 Arbitrary라는 것을 선언한다. 이 형태의 인자는 0개 이상의 값을 전달받아 하나의 튜플로 묶어 함수 내에서 사용한다. 0개 이상이기 때문에 없어도 되고, 몇 개를 받아도 하나로 묶어서 함수 안에서 자유롭게 사용할 수 있다. 함수 내에 확인을 위한 변수를 여럿 전달해야 하는데 상황에 따라 갯수를 달리해 함수에 전달하게 되는 경우 유용하다. def func(*args): 의 형태로 선언할 수 있으며, arguments라는 이름을 줄인 args라는 이름을 관용적으로 사용한다. 우선순위는 positional argument보다 낮으며, 따라서 positional arguments를 모두 배정하고 남은 변수들이 이 자리에 들어온다. 남는 값이 없다면? 빈 튜플이 args가 된다. 

 여러 개의 값을 한방에 전달할 수 있다고? 그럼 아예 리스트나 튜플을 넘겨버릴 수도 있나? 당연히 있다. 함수를 사용할 때 적절한 위치에 리스트나 튜플을 던져주면 되는데, 이를 그냥 던져주면 1개의 리스트 객체/튜플 객체로 인식하기 때문에, 그 안에 들어있는 여러 원소를 전달한다는 의미를 주기 위해 앞에 *을 같이 붙여 사용한다. func(*[1, 2, 3]) 같은 형태가 된다.

연관성 있는 다양한 갯수의 값들을 묶어서 한번에 전달하고 싶을 때 유용하게 쓸 수 있다.


3. Keyword Arguments

 이 방식은 크게 둘로 나눌 수 있다. 하나는 positional argument와 같은 방식으로 선언하면서 기본값을 주는 형식이고, 하나는 key-value map을 전달받아 사용하는 방식이다. 먼저 기본값을 지정하는 방식을 알아보자면 별로 어려울 것 없이 함수 선언부에서 인자 뒤에 =[default]만 붙여주면 된다. def func(a=0, b=0): 같은 형식이 된다. 그러면 해당 이름을 가진 인자가 전달되지 않은 경우 자동으로 기본값을 사용한다. 

 매번 같은 기본값을 보내주는 귀찮음을 해결함과 동시에, 1번에서 보이는 형태의 에러도 미연에 방지할 수 있어 매우 편리하다. 함수를 사용할 때에도 똑같이 func(a=1, b=2)와 같이 key=value 형식으로 값을 전달해주면 된다. 이 방식의 가장 큰 특징은 key-value mapping으로 인자에 값을 배정하기 때문에 함수 사용 시에 인자들의 순서를 고려하지 않아도 된다는 것이다. 그래서 순서의 혼동으로 인한 잘못된 인자 전달을 막을 수 있고, 함수를 사용하는 입장에서도 훨씬 더 명시적으로 전달값의 의미를 확인할 수 있다. 사실 1번의 방식은 기본값 없는 keyword arguments 방식처럼 작동한다.

이를 보면 positional argument같은건 존재하지 않고 keyword argument 방식만 존재하는게 아닌가 하는 생각이 들지만, 파이썬 C 코드를 보면 그것은 아닌 것 같다. positional argument 배정 시에는 이 형식으로 전달받은 값이 없으므로 지나가고, keyword argument를 배정하는 단계에서 해당하는 이름이 있으니 값을 배정해준다. 결과적으로 모든 필수적인 인자가 값을 배정받았으니 이는 성공으로 판단한다고 생각하면 될 것 같다.

 다음 방식인 key-value map을 전달받는 방식은 말 그대로 key-value 매핑이 된 인자들을 가변 갯수로 받는 것을 말한다. 

def func(**kwargs): 와 같이 선언하며, keword arguments의 줄임인 kwargs를 관용적으로 사용한다. 흡사 기본값이 있는 인자를 Arbitrary arguments 전달하듯이 사용한다. 사용할 때에는 0개 이상의 key=value 매핑을 전달하면 된다.

 dict 형식으로 매핑이 전달되는 것을 확인할 수 있다. 음? 그럼 사전을 던져보자. 역시 arbitrary 방식에 Sequential 자료구조를 던졌던 것처럼 여기에 당연히 dict를 통째로 던질 수 있다. 사전형 인자 1개가 아닌 매핑이 전달된다는 것을 알리기 위해 **을 사전 앞에 붙여 전달하면 된다.

상당히 손쉽게 매핑을 전달해 사용할 수 있다. 위치를 고민하지 않아도 되는데, 갯수도 걱정할 필요가 없으니 이쯤되면 신기할 정도이다.


4. Combination

 위에서 본 3가지 형식의 인자는 물론 하나의 함수 정의 시에 섞어서 사용할 수 있다. 처음에 우선순위가 있다고 말했는데, 우선순위는 여기에서 중요해진다. 무조건 함수 정의 내의 인자 배치 순서는 positional - arbitrary - keyword 형태가 되어야 한다. 그도 그럴 것이, 일단 위치를 신경쓰지 않아도 되는 keyword는 맨 뒤로 몰아놓아야 이름 없는 변수들을 모두 제자리에 배치할 수 있을 것이며, 이들 중에서는 positional로 필수적인 인자들이 앞에 있어야 전달받은 값을 꼭 필요한 곳부터 배정해줄 수 있기 때문일 것이다. 그러한 논리로 위와 같은 순서를 지켜 인자를 선언해주면 된다. def func(arg, *args, **kwargs): 의 형식은 이렇게 완성된다. 이에 관해 꽤 재밌는 게 있는데, 위에서 positional argument가 기본값 없는 keyword argument처럼 작동한다고 했는데, 역으로 keyword argument들은 기본값 있는 positional arguments처럼 작동하기도 한다. 다음을 보자.

분명 선언하기는 keyword로 선언했는데 positional을 이어받아 저장한다. 그러면 내가 인자를 전달할 때, positional에 갯수가 틀린건지, keyword에 기본값이 아닌 값을 전달한건지 판단하기가 애매하다. 이는 파이썬이 값을 배정할 때 positional을 먼저 배정하기 때문인데, positional argument에 값을 배정하는 과정에서는 기본값 같은것에 상관없이 값을 배정받을 변수가 있고 전달할 값이 있다면 그냥 넣기 때문에 이와 같은 현상이 발생한다. 그럼 이러한 혼돈을 방지하고 keyword는 keyword로만 값을 전달하게 할 수 있을까? 인자 배정의 우선순위를 생각하면 쉽게 이를 해결할 수 있다. 인자 선언의 우선순위 상, arbitrary argument 뒤에 선언된 keyword argument는 죽었다 깨나도 positional argument에서 남은 값을 배정받을 수 없다. 몇 개가 더 들어와도 arbitrary에서 먹을 것이므로. 이를 이용해 값을 받아 쓰지는 않지만 두 종류를 구분하기 위해 "bare *"를 이용한다. 그럼 아래와 같은 것을 볼 수 있다.

실수로 positional argument의 갯수를 초과해 이것이 문제가 되는 상황을 막을 수 있다. 역으로 positional argument를 keyword argument처럼은 사용할 수 있는데, 이는 위에서 positional이 기본값 없는 keyword처럼 작동하는 것을 참고하면, 또 논리적으로 내가 어디에 어떤 값을 주겠다고 명시적으로 선언한 마당에 못 줄 게 없으므로 정상 작동하는 것이 합리적이다.


또 쓰다보니 길고긴 글이 돼버렸다. 아직 핵심을 간결하게 정리해 설명하는 재주는 부족한가보다. 나름 꽤 열심히 설명했다고 생각하는데, 어딘가 의문이 있거나 허술한 부분이 있을 지도 모르겠다. 이 글을 읽은 분들이 함수를 좀 더 즐겁게 사용할 수 있으면 좋겠다.


tl;dr;

  • 파이썬 함수를 정의하면서 인자를 선언하는 방식은 positional, arbitrary, keyword 형식의 3가지가 있으며, 순서대로의 우선순위를 가진다. 
  • positional argument는 함수를 사용할 때 필수적으로 값을 전달해야 한다. 
  • positional arguments를 모두 배정하고 남은 것 또는 unpacking된 sequential 자료구조는 arbitrary argument가 하나의 튜플로 만들어 저장한다. arbitrary argument는 앞에 *을 하나 붙여 선언한다. 
  • key=value 매핑 또는 unpacking된 dict 자료구조는 keyword argument가 해당 자리에 값을 배정한다. keyword argument는 positional argument 뒤에 =[default] 를 붙이는 방식과 * 두개(**)를 붙여 하나의 사전으로 모아서 받는 방식이 있다. 
  • 이 특성을 이용해 positional 변수가 갯수를 초과했을 때 keyword argument의 영역을 침범하지 않게 할 수 있다.


  1. SyntaxError가 아닌 TypeError가 나는 게 의아해서 조금 찾아봤는데, https://github.com/python/cpython/blob/58b4ad791dde049f635e0cd477ba94bd102b4527/Python/ceval.c#L3651-L3711 이곳에서 최종적으로 에러를 생성하고 있었다. 이를 더듬어 올라가보니 function_call()을 시작으로 https://github.com/python/cpython/blob/58b4ad791dde049f635e0cd477ba94bd102b4527/Python/ceval.c#L3922-L3943 에서 에러를 체크하고 있었다. 이게 TypeError인 이유는 아마도 파이썬 자체의 문법에 의한 에러(뭐 파싱 중에 에러가 생겼다거나 하는)가 아니기 때문이며, positional argument, keyword argument 같은 타입을 제대로 지켜서 인자를 넘겨주지 않아 생기는 에러라는 의미로 TypeError를 발생시킨 게 아닌가 생각한다. 애초에 SyntaxError가 나려면 선언조차 못해야 되는거니까.. 갑자기 생긴 궁금증에 파이썬 코드를 좀 까봤다. [본문으로]
Comments