파이썬 default arg 보는 방법

파이썬과 루비의 문법 차이로 인한 버그를 당한적은 자주 있었지만 글로 다룰 생각까진 (게을러서) 하지 않았다. 하지만 아래의 글을 읽고 파이썬와 루비의 차이로 인한 버그를 정리해보면 재밌을거라고 생각했다. 그래서 써보기로 했다.

루비와 파이썬에서 함수 호출과 함수 참조에 대한 차이

기본 매개변수를 이용해서 낚시를 해보자. (얼마나 낚일지는 모르지만) Default argument 를 남들이 기본 매개변수라고 번역하길래 기본 매개변수라고 적었지만 디폴트 매개변수, 기본 인자 등등으로도 불리는거같다. 용어를 하나로 합치려고 default argument 만을 사용하겠다.

문제1. 다음 코드의 실행 결과는?

간단한 파이썬, 루비 코드이다. Default parameter에는 현재 시간이 들어간다. 현재 시간을 찍는 함수를 호출하고 1초 쉬고 다시 현재 시간을 찍는 함수를 부른다. 어떤 실행 결과가 나올지 생각해보자.

#!/usr/bin/env python

import datetime
import time

def run(now = datetime.datetime.now()):
    print(now)

run()
time.sleep(1)
run()
#!/usr/bin/env ruby

require 'time'

def run(now = Time.now())
  puts now
end

run
sleep 1
run
$ python default_argument.py
2016-03-04 00:42:59.921858
2016-03-04 00:42:59.921858
$ ruby default_argument.rb
2016-03-04 00:43:01 +0900
2016-03-04 00:43:02 +0900

파이썬에서는 같은 시간이 2번 찍혔다. 루비에서는 1초 차이나는 시간이 찍혔다. 파이썬에서는 default argument는 미리 계산된 값을 사용하고 루비에서는 함수 호출할때 계산하는거같다.

문제2. 다음 코드의 실행 결과는?

함수 기본값은 언제 계산될까? 이를 확인하기 위해서 간단한 코드를 짰다.

“foo called” 를 출력하는 foo() 라는 함수를 만들었다. foo() 를 default argument로 사용하는 bar() 라는 함수를 만들었다. foo(), bar() 는 만들었을뿐 실제로 호출하진 않았다. 파이썬과 루비의 출력 결과는?

#!/usr/bin/env python

def foo():
    print("foo called")

def bar(a = foo()):
    print("bar called")
#!/usr/bin/env ruby

def foo
  puts "foo called"
end

def bar(a = foo())
  puts "bar called"
end
$ python initial.py
foo called
$ ruby initial.rb

함수를 호출한적이 없는데 파이썬에서는 “foo called” 가 찍혔다. 루비는 foo(), bar() 로 인한 출력이 없다. 파이썬에서는 소스를 읽으면서 default argument를 계산하나보다.

Language specification

“원래 파이썬과 루비는 이렇게 행동하는게 정상입니다” 라고 끝내면 심심하니 언어 스펙을 뒤져보자.

Python

파이썬 3.5 Language Reference 를 뒤져보면 함수 기본값과 관련된 정보를 찾을 수 있다. 8.6. Function definitions 에 나와있다. https://docs.python.org/3.5/reference/compound_stmts.html#function-definitions

Default parameter values are evaluated from left to right when the function definition is executed. This means that the expression is evaluated once, when the function is defined, and that the same “pre-computed” value is used for each call…

파이썬에서는 Default argument를 Default paramter value 라고 부른다. default paramter value는 함수가 정의될때 평가되서 정해지고 이후 함수 호출에서는 미리 계산된 값을 사용한다.

Ruby

Programming Languages — Ruby IPA Ruby Standardization WG Draft August 25, 2010 를 참고했다. https://www.ipa.go.jp/files/000011432.pdf 13.3.2 Method parameters , 13.3.3 Method invocation 에서 Default argument와 연관된 것을 찾을 수 있다.

13.3.2 Method parameters

Optional parameters: These parameters are represented by optional-parameters. Each optional parameter consists of a parameter name represented by optional-parameter-name and an expression represented by default-parameter-expression. For each optional parameter, when there is no corresponding argument in the list of arguments given to the method invocation, the value of the default-parameter-expression is used as the value of the argument.

13.3.3 Method invocation

7.i. For each optional parameter POi to which no argument corresponds, evaluate the default-parameter-expression of POi, and let X be the resulting value.

루비에서는 Default argument를 Optional parameter 라고 부른다. 함수 호출 시점에 optional parameter에 대응되는 argument가 넘어오지 않으면 (함수 호출시 기본값을 대신할 값을 넣지 않은 경우) default parameter expression을 평가해서 인자로 사용한다.

Python 함수에서 정의된 parameter의 순서와는 다르게 parameter의 이름을 명시해 준다면 순서가 반대로 되어도 값을 전달 해 주는 것이 가능하다

def test_function(my_parameter, your_parameter):
  print(f"{my_parameter} 그리고 {your_parameter}".format(my_parameter, your_parameter))

# 실행 결과 : 내꺼 and 니꺼 <- 이렇게 정상적으로 나온다
# parameter1 = value <- 이런 식으로 parameter 이름으로 맞춰서 값을 전해주는 것을 keyword arguments라고 한다. 
test_function(your_parameter = "니꺼", my_parameter = "니꺼")

keyword arguments를 명시하면서 함수를 실행하게 되면 가독성도 높아질 수 있고 만일 실수로 값이 바뀌었다 하더라도 어떤 parameter인지 명시적으로 알 수 있기 때문에 관련 오류가 발생했다 하더라도 바로 조치가 가능하다는 장점이 있다.

1-1. positional arguments와 같이 쓸 때 유의할 점

keyword arguments는 그 parameter가 확실히 명시되어 있기 때문에 순서가 바뀌어도 상관이 없지만 일반 arguments들(positional arguments)은 어떤 값인지 명시되어 있지 않기 때문에 그 순서가 중요하다. 당연하게도 엉뚱한 곳에 해당 값이 들어가게 되면 원치 않는 결과를 출력하게 되기 때문이다.

이 둘을 같이 사용할 때는 순서를 지켜줘야 하며 그렇게 하지 않으면 에러를 발생시키게 된다.

test_function(your_parameter = "니꺼", "내꺼")
  File "<stdin>", line 1
SyntaxError: positional argument follows keyword argument

에러가 나는 이유는 positional argument의 순서가 틀렸기 때문이다(첫번째에 와야 하는데 두번째에 왔다) 실제로 그렇게 되었다고 에러 메세지가 친절히 설명해 준다.

2. Parameter Default Value

함수에 parameter 그 자체에 default값을 정의해 줄 수도 있는데 이럴땐 default값이 정의되지 않은 parameter 보다 먼저 위치해서는 안된다. 실제로 syntax error가 발생한다.

def test_func(my_param = "hello", your_param):
...     print(f"{my_param} hello world {your_param}")
...
  File "<stdin>", line 1
SyntaxError: non-default argument follows default argument

2-1. 그렇다면 왜 에러가 나는 것일까 ?

아래와 같은 경우를 생각해보자.

test_func("world") # my_param에 들어갈지 your_param에 들어갈지 모호해진다
test_func("world", "hello") 

만약 test_func가 에러 없이 선언 되었다면 위의 함수들이 실행 되어야 한다. 그러나 해당 argument가 어떤 parameter에 해당 하는지 모호해져 버린다. 따라서 의도치 않는 함수 로직의 실행을 막고자 python interpreter에서 에러로 처리를 하게 되는 것이다. 이런 모호한 점을 방지하기 위해 아래와 같이 non-default value는 앞에서 정의 되어야 하며 parameter default value들은 non-default value 뒷 부분에 정의 되어야 한다.

* Reference

  1. 점프 투 파이썬 e-book : https://wikidocs.net/24#_9
  2. Stackoverflow link : https://stackoverflow.com/questions/16932825/why-cant-non-default-arguments-follow-default-arguments