본문 바로가기

진리는어디에/Python

[Python] 함수 #4 파라미터 패킹

이번 강의에서는 파이썬 함수 인자(parameter)의 개수를 유연하게 지정 할 수 있는(가변 인자) 파라미터 패킹(parameter packing)에 대해 살펴 보도록 하겠다.

목차

  1. 함수 소개
  2. 디폴트 파라미터 주의 사항
  3. 파라미터 언패킹
  4. >> 파라미터 패킹
  5. 함수 객체
  6. 일급 객체(first class object)
  7. 람다(lambda)

파라미터 패킹

파이썬에서 파라미터 패킹(parameter packing)이란 고정되지 않은 여러개의 인자를 하나의 시퀀스 객체로 묶어 함수에게 전달하는 것을 말한다.

고정되지 않은 여러개의 인자(parameter)를 묶은 하나의 시퀀스 인자

바꿔 말하면 파라미터 패킹을 통해 '가변 인자 함수[각주:1]'가 가능하다는 것이다.

우리가 자주 써왔던 print표준 함수가 좋은 파라미터 패킹과 가변 인자 함수의 예다.

print(*objects,
    sep=' ', end='\n',
    file=sys.stdout, flush=Flush
)

print 함수의 정의는 위와 같다. 가장 먼저 눈에 띄는 것이 첫번째 objects 인자 앞에 있는 * 기호다. 영어로는 아스테리스크(asterisk), 한글로는 별표라고 하는 이 기호가 함수의 인자 앞에 있다는 것의 의미는 몇 개의 인자가 넘어오든 하나로 묶은 '튜플(tuple)'시퀀스 객체로 변환하겠다는 의미다.

print(1, "hello", "world", "!!", ) # 1 hello world !!
print(2, "hi", "~", )              # 2 hi ~

위와 같이 다양한 개수의 인자로 print 함수를 호출하는 것이 가능한 이유가, 몇개의 인자라도 내부적으로 파라미터 패킹을 통해 튜플 시퀀스 객체 하나로 처리 하기 때문이다.

positional argument 패킹

[이전 강의]에서 함수의 인자를 전달하는 방법에는 위치(순서) 기반으로 인자를 전달하는 positional argument와 키워드 기반으로 전달하는 keyword argument방법이 있다고 했다. * 기호를 인자 앞에 사용한다는 것은 위치 기반의 모든 변수들을 하나의 튜플 객체로 만들어 전달하는 positional argument 패킹을 사용하겠다는 의미다.

아래의 foo() 함수와 파라미터 패킹을 이용하는 bar() 함수 예제를 살펴 보자.

# 일반 파라미터 함수 정의
def foo(a, b) :
    print(a, b)
    
foo(1, 2)      # ok
foo(1, 2, 3)   # error, 인자가 두개 뿐인데 3개를 넘겼다

# 패킹이 적용된 함수
def bar(a, *b) :
    print(a, b)
    
bar(1, 2, 3, 4) # 1 (2, 3, 4)
bar(1, 2, 3)    # 1 (2, 3)
bar(1, 2)       # 1 (2,)
bar(1)          # 1 ()

일반 함수 foo는 인자의 개수가 다를 경우 오류를 발생 시켰지만, bar의 경우 첫번째 인자 뒤에 오는 모든 인자들을 하나의 튜플(tuple)로 객체로 만들어 주는것을 볼 수 있다. 그렇다면 아래 처럼 패킹 인자 args 뒤에 다른 인자가 추가 된다면 어떻게 될까?

def foo(a, *args, c) :
    print(a, args, c)

foo(1, 2, 3, 4)    # error
foo(1, 2, 3, c=4)  # ok

결론은 4라인의 foo(1, 2, 3, 4)를 호출한 것은 에러가 발생했고, 5라인은 정상적으로 실행 된다.

여기서 부터 중요하다. 집중하기 바란다.

4라인의 foo(1, 2, 3, 4)를 호출하면 첫번째 인자 1을 제외하고 2, 3, 4가 패킹이 되어 foo(1, (2, 3, 4), ?) 와 같이 세번째 인자가 누락 되어 에러가 발생했을 거라고 생각 할 수 있을 것이다. 하지만 에러 내용은 다음과 같다.

TypeError: foo() missing 1 required keyword-only argument: 'c'

함수의 인자가 누락 되었다는 에러가 아니라, 인자 keyword-only 인자 'c'가 필요한데, 그것이 없다고 에러를 발생 시켰다. 파이썬에서 패킹된 파라미터( * 사용 이후) 뒤의 인자들은 자동으로 keyword-only 인자가 된다. keyword-only arguement가 무엇인지 궁금하신 분들은 [여기]를 참조 하도록한다.

그래서 5라인의 foo(1, 2, 3, c=4) 와 같은 경우 "c=4"와 같이 인자의 이름을 지정하여 전달 하였으므로 문제 없이 실행 될수 있다.

그렇다면 objects 인자 뒤의 sep, end 와 같은 변수들은 어떻게 구분하는지, 나머지 인자들도 같이 묶여 오면 어떻하냐는 질문이 있을 수 있다. 좋은 질문이다. 다시 print() 함수의 정의를 살펴 보자.

print(*objects,
    sep=' ', end='\n',
    file=sys.stdout, flush=Flush
)

print(1, 2, 3, sep="\t")

패킹된 파라미터 objects 뒤에 오는 sep, end, file, flush라는 이름을 가진 인자들이 있다. 그리고 이 인자들을 호출하기 위해서는 단순히 순서에 맞춰 값을 넘겨주는 것이 아니라 6라인과 같이 인자의 이름을 명시하여야만 한다.

축하한다 여러분은 이제 positional argument의 패킹을 완벽히 이해했다. 이 여세를 몰아 keyword argument의 패킹에 대해서도 살펴 보자.

NOTE - 파라미터와 아규먼트는 한글로 번역하면 '인자'로써 같은 의미다.

keyword argument 패킹

위에서 아스테리스크 기호 하나를 이용하여 입력되는 인자의 개수에 상관 없이 하나의 튜플 객체로 묶어주는 positional argument의 패킹을 알아보았다. 그렇다면 함수의 인자를 키워드를 이용해 넘겨 줄 때는 어떻게 파라미터 패킹을 적용 할 수 있을까?

파이썬에서는 keyword argument 패킹이라고 하여 **(별표 두개)를 이용하여 keyword arguement의 개수와 상관 없이 하나의 딕셔너리(dictionary) 시퀀스 객체로 묶어 넘겨주는 것을 지원한다.

def f1(*args) :
    print(args)

f1(1, 2, 3)         # f1( (1, 2, 3) )
f1(1, 2, 3, a = 10) # TypeError: f1() got an unexpected keyword argument 'a'

def f2(**kwargs) :
    print(kwargs)

f2(a=10, b=20)      # ok
f2(1, a=10, b=20)   # TypeError: f2() takes 0 positional arguments but 1 was given

f1의 경우에는 positional arguement만을 받을 수 있기 때문에 5라인의 keyword argument 에서는 에러를 발생 시킨다.

반면에 f2를 살펴보자. 위에서 말한것 처럼 ** 기호와 함께 kwargs인자를 선언했기 때문에 keyword-argument들은 개수에 상관 없이 받을 수 있다. 하지만 11라인에는 positional arguement가 첫번째 인자로 넘어오기 때문에 positional argument를 처리 할 수 없다는 에러를 발생 시킨다.

f(*args) : 모든 positional argument를 tuple로 받음
f(**kwargs) : 모든 keyword argument를 dictionary로 받음
모든 positional argument는 keyword argument 앞에 와야 한다.

Example

위에서 파라미터 패킹에 대한 개념을 배웠으니 실제 사용되는 예를 살펴 보도록 하자. 파이썬에서 파라미터 패킹을 이용해 주로 어떠한 인자도 받을 수 있는 함수를 만드는데 사용한다.

def foo() :
    print('function foo')
    
def bar(a, b, c) :
    print('function bar(' + str(a) + ',' + str(b) + ',' + str(c) + ')')
    
def caller(f) :
    f()
    
caller(foo)

위의 예제와 같이 caller라는 함수에 인자로 foo를 넘겨 주고 caller 내부에서 f(여기서는 foo)를 호출한다고 하면 아무런 문제 없이 잘 동작한다. 하지만 다음과 같이 bar를 caller의 인자로 넘겨 주게 되면 에러가 발생한다.

caller(bar) # TypeError: bar() missing 3 required positional arguments: 'a', 'b', and 'c'

bar 함수에는 인자가 세개가 필요한데 현재로써는 넘겨 줄수 있는 방법이 없다. 그렇다고 caller 함수의 인자 리스트를 bar함수와 동일하게 만들면 foo 함수를 호출 할 수 없다. 이럴 때 패킹을 이용하여 어떤한 인자 리스트라도 다 받을 수 있는 함수를 정의 해주면 된다. 위 예제의 caller 함수의 인자리스트를 아래와 같이 수정해보도록 하자

def foo() :
    print('function foo')
    
def bar(a, b, c) :
    print('function bar(' + str(a) + ',' + str(b) + ',' + str(c) + ')')
    
def caller(f, *args, **kwargs) :
    f(*args, **kwargs)
    
caller(foo)
caller(bar, 1, 2, 3)
caller(bar, 1, b=2, c=3)

caller 함수의 인자에 튜플(tuple)형태의 패킹 파라미터와 딕셔너리(dictionary) 형태의 패킹 파라미터를 추가 했다. 그리고 caller에서 호출하는 함수 f에는 파라미터 언팩킹을 통해 다시 튜플 또는 딕셔너리 형태의 인자를 풀어서 f에게 넘겨 준다.

caller는 어떤 형태의 인자도 다 받아서 자신이 호출하는 함수에게 넘겨 줄수 있다.

마치며

파라미터 패킹은 뒤에나오는 파이썬의 꽃이라 불리는 '데코레이터(Decorator)'에서 아주 중요하게 사용되므로 꼭 익혀 두도록 하자.

다음 장에서는 함수 객체에 대해 살펴 보도록 하겠다.

부록 1. 같이 읽으면 좋은 글

  1. 동일한 함수지만 필요에 따라 인자의 개수가 달라지는 함수 [본문으로]
유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!