def twice(x):
return x*2
10 함수(functions
)
함수는 요청을 받았을 때 실행되는 문장(statements)의 집합이다.
함수 실행을 요청하는 것을 함수 호출(function call)이라고 한다. 함수를 호출할 때 arguments를 전달하고, 이렇게 전달된 값은 함수가 실행될 때 데이터로 사용된다. 파이썬에서 하나의 함수는 항상 하나의 결과를 반환한다: None
또는 하나의 값을 계산의 결과로 반환한다. 클래스 문 안에서 정의되는 함수는 메서드(method)라고 한다.
파이썬에서 함수는 객체(값)의 하나로, 다른 객체와 똑같이 취급된다. 그렇기 때문에 다음과 같은 것들이 가능하다. 파이썬에서 함수가 일반적인 객체라는 것을 함수는 일급 객체이다라고 표현하기도 한다.
- 함수는 다른 함수를 호출할 때 argument로 함수를 전달할 수 있다.
- 함수는 계산의 결과로 다른 함수를 반환할 수 있다.
- 다른 객체와 마찬가지로 하나의 함수를 변수에 바인딩할 수 있다.
- 컨테이너 안에 하나의 아이템으로 포함시킬 수 있다.
- 객체의 속성이 될 수 있다.
- 함수는 딕셔너리에서 키가 될 수 있다.
10.1 함수 정의, 호출, 반환값, parameters와 arguments
함수를 정의하는 방법은 다음과 같다. def
키워드를 쓰고, 다음에 함수의 이름, 그 다음에 괄호 안에 파리미터들을 코마로 구분하여 넣는다. 그 다음 콜론(:
)을 쓴다.
def function_name(parameters): statement(s)
파이썬 인터프리터가 def
를 만나면 함수의 body를 compile하여 함수 객체(function object)를 만들고, 이것을 function_name
에 바인딩한다. 다음은 twice
라는 함수를 정의하는 예이다. 파이썬 인터프리터가 이 def
문을 실행하면, twice
라는 함수 객체를 만들고, 이 객체 안에 여러 attributes에 함수와 관련된 정보를 저장한다.
파라미터(parameters)는 함수를 호출할 때 전달된 값들을 받아서 함수 안에서 로컬 변수로 사용되는 것들을 말한다.
함수는 하나의 값을 반환한다(None
또는 하나의 값), return
문을 사용하고, 뒤에 오는 값이 이 함수의 반환값이 된다. return
문이 없는 경우에는 함수의 body 끝에 가서 실행이 멈추고 None
을 반환한다.
함수를 실행하는 것을 함수 호출(function call) 또는 실행(function invocation)이라고 하는데, function_name(arguments)
과 같이 함수 이름 끝에 괄호를 붙여서 함수를 호출한다. 함수를 호출할 때 괄호 안에 넣는 값들을 arguments라고 하고, 이것들은 parameters에 매칭되어 함수 안으로 넘겨진다.
3) twice(
6
10.2 함수 객체의 attributes
앞에서 파이썬 함수는 일급 객체(first-class object)이기 때문에, 함수 역시도 다른 객체와 마찬가지로 attributes를 가질 수 있다.
파이썬 인터프리터가 def
문을 통해서 함수를 정의하면서 자동으로 만들어지는 함수 속성들이 있다. 다음과 같이 함수를 정의한다고 하자.
def example_function(a, b=2):
"""This is an example function."""
return a + b
__name__
은 함수의 이름이다.
__name__ example_function.
'example_function'
__doc__
은 함수의 docstring이다.
example_function.__doc__
'This is an example function.'
__defaults__
는 named parameters의 값들을 가진다. named parameters가 없으면 None이 된다.
example_function.__defaults__
(2,)
__code__
는 함수 body를 컴파일한code object
를 나타낸다1.
example_function.__code__
<code object example_function at 0x103e88370, file "/var/folders/6k/1d6wwd8x3057x_f57q9q7s1r0000gn/T/ipykernel_9661/2276525134.py", line 1>
10.3 Docstrings
Docstrings은 파이썬 함수와 메서드, 클래스, 모듈에 관한 정보를 지정하는 방법이다.
함수 docstring은 함수 body의 첫 번째 문장으로 triple quotes를 사용하여 만든다. 이 값은 function_name.__doc__
으로 접근할 수 있고, help()
함수에서도 사용된다.
def example_function(a, b=2):
"""This is an example function.
Args:
a (int): The first parameter.
b (int, optional): The second parameter. Defaults to 2.
Returns:
int: The sum of a and b.
"""
return a + b
help(example_function)
Help on function example_function in module __main__:
example_function(a, b=2)
This is an example function.
Args:
a (int): The first parameter.
b (int, optional): The second parameter. Defaults to 2.
Returns:
int: The sum of a and b.
10.4 함수의 반환값과 부수 효과(side effect)
파이썬 함수는 호출했을 때 하나의 값을 반환한다.
파이썬 함수를 호출했을 때 return
문을 만나면 함수가 종료되고, return
다음에 오는 표현식이 이 함수의 반환값이 된다. return
문이 없는 함수도 있는데, 이 경우에는 None
을 반환한다.
def my_fun():
print("Hello, World!")
= my_fun()
k print(k)
Hello, World!
None
위 함수와 그 결과들을 보자.
my_fun
이라는 함수는return
문을 가지고 있지 않다.- “Hello, World!”가 출력되는 이유는
k = my_fun()
코드가 실행되면서, 함수가 호출되기 때문이다. - 이 함수는
return
문이 없고 호출한 결과를 변수k
에 할당했는데, 그 값은None
이라는 것을 확인할 수 있다. return
문이 없는 함수는 함수의 body 마지막에 이르렀을 때 종료되고,None
값을 반환한다.
이와 같이 함수가 (None 값 말고) 값을 반환하지 않고, 함수 외부에 어떤 일을 하는 경우 부수 효과(side effect
)를 가진다고 말한다.
어떤 경우 함수가 하나의 값이 아니라 복수의 값을 반환하는 것처렴 보일 수도 있다. 대표적인 경우가 나눗셈의 몫과 나머지를 계산하는 divmod()
함수이다.
= divmod(19, 3)
q, r print(q)
print(r)
6
1
divmod()
함수는 그 결과를 2개의 값을 독립적으로 반환하지 않고, 그것을 하나의 튜플로 묶어서 반환하기 때문에 실제로 하나의 값을 반환하는 것이다. q, r = divmod(19, 3)
코드는 그렇게 반환되는 튜플에 대하여 unpacking을 하는 것이다.
divmod()
함수와 유사한 함수를 만들어 보면 다음과 같이 할 수 있다.
def my_divmod(x, y):
return x // y, x % y
19, 3) my_divmod(
(6, 1)
return x // y, x % y
를 볼 때, 이것을 2개의 값으로 보면 안 되고 return (x // y, x % y)
로 하나의 튜플인데 괄호가 생략된 것임을 알아 볼 수 있어야 한다.
10.5 Parameters
함수 정의에 사용되는 용어로, 함수가 호출될 때 arguments로 전달된 값을 받아서 함수 body 안에서 사용되는 변수로 사용되는 이름(name)을 parameters라고 한다.
- parameters: 함수를 정의할 때
- arguments: 함수를 호출할 때
파이썬 함수는 다양한 종류의 파라미터를 제공한다.
10.5.1 Positional Parameters와 Named Parameters
함수를 정의할 때
- positonal parameter는 이름만 단독으로 지정하는 것
- named parameters(keyword/optional/default parameters 라고도 함)은
이름 = 값
형태로 지정하는 것
def my_fun1(x):
print(x)
위 예에서 x
는 단독으로 존재하는 positional parameter이다. 이렇게 정의된 경우 다음과 같은 방법으로 호출할 수 있다.
3
만 전달하는 경우를 positional argment로 사용한 예x=2
인 경우는 named argument로 사용한 예
따라서 positional parameter로 정의된 경우는, positional argument 또는 named argument로 호출할 수 있다.
my_fun1(3)
my_fun1(x=2)
다음 예는 x
를 named parameter로 정의한 경우이다.
x=3
으로 값에 이름을 지정하여 named parameter라 한다.- 이렇게 하면
x
의 default 값을 정해서, 호출할 때 굳이 값을 지정하지 않아도 되고 지정해도 때문에optional parameter
라 불리기도 하고, default 값을 사용하여 함수가 호출되어default parameter
라고 부른다. - 관행적으로
keyword parameter
라는 용어가 사용되어 왔다.
def my_fun2(x=3):
print(x)
my_fun2()4)
my_fun2(=5) my_fun2(x
3
4
5
함수를 정의할 때, positional parameter와 names parameter를 둘 다 사용하는 경우에는 항상 positional parameter가 먼저 나오고, 그 다음 named parameters가 와야 한다.
def my_fun3(x, y=2):
return x + y
1) my_fun3(
3
그렇게 하지 않으면 오류가 발생한다.
def my_fun4(y=2, x):
return x + y
Cell In[15], line 1 def my_fun4(y=2, x): ^ SyntaxError: parameter without a default follows parameter with a default
다음 my_fun5()
함수는 다양한 방법으로 호출할 수 있다. Arguments가 Parameters에 제대로 매칭되는 경우 함수 호출이 제대로 이뤄진다. 이 과정에 대해서는 Python in a Nutshell를 읽는다(복잡하다).
def my_fun5(x, y=2):
return x, y
print("1: ", my_fun5(1))
print("2: ", my_fun5(1, 3))
print("3: ", my_fun5(y=2, x=1))
print("4: ", my_fun5(x=2))
print("5: ", my_fun5(1, y=3))
1: (1, 2)
2: (1, 3)
3: (1, 2)
4: (2, 2)
5: (1, 3)
10.5.2 임의의 개수의 인자를 받을 수 있게 하는 방법
내장 함수 print()
는 인자의 개수가 사전에 정해지지 않게 되어 있다. print(a)
, print(a, b, c)
등으로 모구 가능하다. 이와 같이 여러 인자들을 모을 수 있도록 정의하는 것을 positional argument collector라고 한다. 이 파라미터는 다음과 같이 정의하고 작동한다.
*
로 시작한다. 이름은 반드시args
라고 하지 않고 사용자가 임의로 정할 수 있다. 그런데 많은 사람들이 이렇게 사용한다.*args
는 positional argument collector로 positional arguments 들만 수집한다(named arguments는 수집하지 않는다).*args
는 argument-parameter 매칭후 남은 같들을 하나의 튜플(tuple)로 수집하고, 함수 body에서args
라는 이름의 튜플로 사용된다.
이 지식을 이용하여 임의의 값들을 받아서 그 합을 계산하는 sum2()
함수를 만들어 보자. 참고로 내장 함수 sum()
은 하나의 iterable을 받는다.
def sum2(*args):
return sum(args)
print(sum2(1, 3))
print(sum2(2, 4, 5))
print(sum2(3, 4, 5, 6, 8))
4
11
26
함수의 시그너처가 positional argument collector를 가지는 경우, 함수 호출할 때 뒤에 오는 named parameter는 positional argument로 함수를 호출할 수 없다. 반드시 named argument로 지정해 주어야 한다.
*args
는 positional arguments을 수집하는 반면 **kwargs
는 named arguments를 수집한다.
**
로 시작하고, 임의의 이름을 사용할 수 있다. 관행적으로**kwargs
으로 사용해 왔다.- named arguments을 수집하여,
kwargs
라는 이름의 파이썬 딕셔너리(dict
)에이름: 값
항목들로 만든다. - 함수 body 안에서
kwargs
라는 딕셔너리로 사용된다.
def print_details(**kwargs):
for key, value in kwargs.items():
print(f"{key}: {value}")
="Alice", age=30, city="New York")
print_details(nameprint("\n")
="Inception", director="Christopher Nolan", year=2010) print_details(title
name: Alice
age: 30
city: New York
title: Inception
director: Christopher Nolan
year: 2010
10.5.3 positional-only, keyword-only Parameters
Positinal-only paramenter
는 함수 정의에서 /
라는 기호를 사용하여 포함한다. 이 /
앞에서 명시된 파라미터들에 대한 값은 반드시 positional arguments로 전달해야 한다.
다음과 같이 position_only
함수가 정의되면, 이 함수를 호출할 때 x
, y
파라미터에 대응하는 arguments는 반드시 positional arguments로 주어야 한다.
def position_only(x, y, /):
return x + y
그래서 다음과 같은 코드는 이 함수를 호출할 수 있다.
1, 2) position_only(
3
그러나, 다음과 같이 호출할 수는 없다.
=1, 2) position_only(x
Cell In[21], line 1 position_only(x=1, 2) ^ SyntaxError: positional argument follows keyword argument
내장 함수 sum()
의 도움말을 보자.
help(sum)
Help on built-in function sum in module builtins:
sum(iterable, /, start=0)
Return the sum of a 'start' value (default: 0) plus an iterable of numbers
When the iterable is empty, return the start value.
This function is intended specifically for use with numeric values and may
reject non-numeric types.
따라서 이 함수를 호출할 때는 반드시 첫 번째 argument는 positional argument로 하나의 iterable로 전달해야 한다.
# 가능
sum([1, 2, 3])
6
# 불가능
sum(iterable=[1, 2, 3])
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[24], line 2 1 # 불가능 ----> 2 sum(iterable=[1, 2, 3]) TypeError: sum() takes at least 1 positional argument (0 given)
Positional-only parameter와 반대되는 개념이 keyword-only parameters이다. Positional argument collector *args
뒤에 정의되는 것들이 모두 keyword-only parameters이다. 여기에 Python 3.8부터는 *args
뿐만 아니라 *
단독으로 사용해도 된다. 따라서 *
또는 *args
뒤에 오는 것들은 모두 keyword-only parameters가 된다.
주의할 것은 함수를 정의할 때 keyword-only parameters를 반드시 keyword parameter로 정의해야 된다는 뜻은 아니다. Positional parameter, Named(keyword) parameter 모두 가능한다.
다음 함수를 보자. 이 함수는 b
를 positional parameter로 c
는 named parameter로 정의했다.
def f(a, *, b, c=56):
return a, b, c
이 함수는 다음과 같이 실행할 수 없다.
1, 34) f(
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[26], line 1 ----> 1 f(1, 34) TypeError: f() takes 1 positional argument but 2 were given
다음과 같이 하면 실행된다.
# b는 keyword-only argument이다.
1, b=34) f(
(1, 34, 56)
10.6 파라미터 지정 순서
- positional parameters
- named parameters,
- positional argument collectors
- named argument collectors
Positional-only marker(/
)는 아무 곳에나 넣을 수 있다.
10.7 Scoping, Namespaces
함수의 parameters와 함수 body 안에서 바인딩된 이름들(names)이 해당 함수의 local namespace(또는 local scope)를 구성한다. 이들 변수들을 함수의 local variables이라고 한다.
- 함수의 파라미터들
- 함수 body 안에서 바인딩된 이름들
local이 아닌 변수들(함수 밖의 변수들)을 global variables
이라고 하고, 이것은 모듈 객체의 attributes이다. 만약 함수 안의 변수가 밖의 변수와 이름이 같은 경우에는 안의 변수가 사용된다(로컬 변수가 글로벌 변수를 hiding한다고 말한다).
10.7.1 Global
다음 코드 블록이 42
를 출력하는 이유를 생각해 보자.
= 42
x def my_func():
= 13
x
my_func()print(x)
42
- 정수
42
와 바인된x
는 글로벌 변수이고,my_func
함수의 body에서 정수13
에 바인딩된x
는 로컬 변수이다. - 이 변수는 같은 이름을 가지고 있지만 있는 위치(scope)이 다르다.
- 따라서
my_func()
함수를 호출했을 때 이 함수의 로컬 스콥에서 x가13
과 바인딩하지만 함수가 종료되면 이 로컬 스콥은 사라진다. - global scope에 있는
x
는 영향을 받지 않기 때문에 원래의 값에 바인딩된 상태를 유지한다.
만약 함수 body 안에서 글로벌 변수의 값을 바꾸고 싶은 경우에는 global
문을 사용해서 변수가 글로벌 변수라는 것을 명시적으로 알려야 한다.
= 42
x def my_func():
global x
= 13
x
my_func()print(x)
13
다음은 이런 사실을 바탕으로 어떤 함수를 호출된 횟수를 저장하게 할 수 있다.
= 0
_count def counter():
global _count
+= 1
_count return _count
counter()
counter()
counter()print(_count)
3
전반적으로 global
을 선언하는 것은 함수가 외부에 영향을 주는 것으로 가급적 피해야 하는 것이므로 그 사용은 꼭 필요한 경우로 제한되어야 한다.
10.7.2 내포된 함수, Closure(클로저), nonlocal
파이썬 함수의 body 안에서 def
문을 사용하여 내포된 함수(nested function)을 정의할 수 있다. 이 경우에 다음과 같은 명칭을 사용한다.
outer function
: 바깥 함수, 안에def
문을 가지고 있는 함수nested fucntion
: 안쪽 함수
nested function
의 body에서는 outer function의 local variables에 접근할 수 있다. nested funciton
입장에서 이런 변수를 free variables
이라고 한다.
다음은 주어진 값을 2제곱, 3제곱, 4제곱 등을 계산하는 함수를 만들어 주는 함수이다. 함수가 반환하는 함수이다.
def my_raise_power(n):
def my_power(x):
return x ** n
return my_power
= my_raise_power(2)
p2 = my_raise_power(3)
p3 = my_raise_power(4)
p4 print(p2(3))
print(p3(3))
print(p4(3))
9
27
81
함수의 정의를 보면 outer function의 n
이 nested funtion인 my_power
에서 사용되고 있음을 볼 수 있다. my_power
함수에서는 x
는 local variable이고 이 n
은 free variable이다.
함수를 인자로 받거나, 함수를 반환하는 함수를 고수준 함수(high-order function)이라고 한다. 이 경우 my_raise_power()
함수는 함수를 반환하기 때문에 high-order function이라고 할 수 있다.
위에서 p2
, p3
, p4
는 모두 함수 my_raise_power()
에 의해서 반환된 함수들이다. 이렇게 필요에 따라 여러 가지 함수를 함수를 만들 수 있는 함수를 factory function
이라고 한다.
p2(3)
가 계산되는 과정을 보자. 먼저 p2
는 my_raise_power(2)
함수로 만들어진다. my_raise_power(2)
가 실행되면서 n = 2
값을 사용하여 my_power
함수가 반환된다. 이 반환된 함수는 n = 2
이라는 값을 기억하고 있다가 p2(3)
호출될 때 x=3
이라는 값을 사용하여 x ** n
을 계산한다. 이때 안에 함수는 이미 반환되었어도 free varibles의 값을 기억하고 있는 것이다. 이것을 클로저(closure)라고 한다.
다음도 클로저를 이용한 예이다. 다음 after()
함수는 함수를 인자로 받는 higher-order function이다. 이 함수는 주어진 초 동안 멈추었다가, 주어진 함수를 실행한다. main()
함수 안의 nested function인 greeting()
이 free variable인 name
변수의 값을 기억하고 있다. 그리고 greeting()
함수를 after()
함수로 전달될 때 , 이 기억된 정보가 함께 넘어간다.
import time
def after(seconds, func):
time.sleep(seconds)
func()
def main():
= 'Guido'
name def greeting():
print('Hello', name)
3, greeting)
after(
main()
Hello Guido
다음은 nonlocal
문에 대해서 알아보자. nonlocal
은 nested function에서, 함수 밖에 있는 변수에 접근하고자 할 때 사용한다.
def make_counter():
= 0
count def counter():
nonlocal count
+= 1
count return count
return counter
= make_counter()
c1 = make_counter()
c2 = make_counter()
c3
for n in range(1, 5):
print(c1())
for n in range(1, 6):
print(c2())
for n in range(1, 8):
print(c3())
1
2
3
4
1
2
3
4
5
1
2
3
4
5
6
7
10.8 Decorator 함수
Decorator 함수를 이해하려면 몇 가지 사전 지식이 필요한데 무엇보다, 파이썬에서 함수는 first-class object로 다른 객체와 똑같이 취급되기 때문에, 함수를 다른 함수의 인자로 쓸 수도 있고, 다른 함수의 반환값이 되기도 한다는 점을 이해할 수 있어야 한다.
이런 decorator는 유연하고 강력한 기능의 하나로 Python Framework 등에서 아주 자주 사용된다.
Decorator 함수는 다른 함수를 받아서, 받은 함수의 기능을 보충하고, 이후에도 그 함수의 이름의 이름을 그대로 사용할 수 있게 한다.
Decorator 함수를 d
라고 하고, 받은 함수를 ded
라고 정의되어 있을 때, 다음과 같은 문법으로 사용된다.
def d(f):
...
def ded(...): ...
이라고 정의된 경우, 다음과 같은 문법으로 사용한다.
@d
def ded(...): ...
이것은 다음과 같은 의미를 가진다.
ded = d(ded)
Decoration 되기 전에는 원래 정의된 대로 행동하다가, Decoration 된 다음부터는 기능이 보강된 대로 행동하는 것이다. 함수의 이름이 바뀌지는 않는다. 구체적인 예로 살펴보자.
다음 trace()
함수는 함수를 받아서, 받은 함수의 이름(func.__name
)을 출력한 다음 그대로 함수를 반환하도록 하는 함수이다.
def trace(func):
def call(*args, **kwargs):
print('호출한 함수:', func.__name__)
return func(*args, **kwargs)
return call
내부 함수 call()
의 파리미터가 *args
, **kwargs
로 지정했기 때문에, 이 함수는 거의 임의의 arguments를 받을 수 있다. 이렇게 해야 decoration 되는 함수의 파라미터에 어떤 형태로 주든 그것을 받을 수 있다. 또 그렇게 받은 인자들을 return func(*args, **kwargs)
코드를 통해 func
함수로 전달하고 있다.
이 함수를 decorator로 사용하고자 한다. 내부에 사용할 함수를 하나 만들어 보자.
def add(x, y):
"""
Add two values.
"""
return x + y
Decoration 되기 전에는 원래대로 행동한다.
3, 4) add(
7
이제 trace()
함수로 decoration 한다.
@trace
def add(x, y):
"""
Add two values.
"""
return x + y
이렇게 하고 난 다음에 다시 함수를 실행해 보자.
3, 4) add(
호출한 함수: add
7
이전과 다르게 호출한 함수: add
를 출력하고 결과를 반환하는 것을 볼 수 있다(함수의 기능이 보강되었다).
그런데 약간의 문제는 남아 있다. 함수를 출력해 보면 이름도 바뀌어 있고, 함수의 docstring도 원래의 것이 아님을 확인할 수 있다.
# 원래 함수가 아니라 decorator 이름을 반환
print(add)
<function trace.<locals>.call at 0x14800d260>
# 원래의 docstring이 보이지 않음
help(add)
Help on function call in module __main__:
call(*args, **kwargs)
이와 같이 되는 것은 전달된 함수가 call
함수로 바뀌어 반환되기 때문이다. 이와 같은 효과는 디버깅 등을 할 때 혼돈을 줄 수 있다.
그래서 원래 함수의 메타 데이터를 유지할 수 있게 만들어 놓은 도구가 functools.wraps
라는 decorator이고 다음과 같은 패턴으로 사용한다.
from functools import wraps
def trace(func):
@wraps(func)
def call(*args, **kwargs):
print('호출한 함수:', func.__name__)
return func(*args, **kwargs)
return call
이렇게 만들어진 decorator
를 다시 사용해 보자.
@trace
def add(x, y):
"""
Add two values.
"""
return x + y
그 결과 원래 함수의 메타 데이터들이 유지됨을 확인할 수 있다.
# 원래 함수의 이름
print(add)
<function add at 0x14800d440>
# 원래 함수의 docstring
help(add)
Help on function add in module __main__:
add(x, y)
Add two values.
더 구체적인 예는 『Effective Python』의 Item 38: Define Function Decorators with functools.wraps
을 참고한다.
다음은 numpy
패키지에 있는 np.vectorize()
함수를 decorator
로 사용한 예이다. 이 함수는 받은 함수가 작동하는 방식을 벡터화시킨다.
import numpy as np
@np.vectorize
def my_func(item):
return item ** 2
1, 2, 3]) my_func([
array([1, 4, 9])
10.9 Call By Sharing
함수를 호출할 때 arguments는 결국 parameters로 여러 가지 방식으로 매칭된다. 매칭 규칙이 어긋나면 함수가 실행되지 않는다. Arguments의 값들이 어떻게 parameters의 값들로 매칭되는지 잘 이해하고 있어야 한다.
파이썬은 call by sharing
방식으로 argument의 정보를 parameter로 넘긴다. call by sharing
이란 여기서 넘겨지는 정보가 객체의 값이 아니라 객체에 대한 레퍼펀스(또는 alias)라는 뜻이다. 맥락을 이해하기 위해서 다음 예를 보자. 파이썬의 Mutability와 관련된 내용이다.
= [1, 2, 3]
a = a
b 0] = 11
a[ b
[11, 2, 3]
위 코드에서 b = a
라는 코드 때문에 객체 b
는 [1, 2, 3]
을 가리키는 a
와 레퍼런스를 공유하게 된다. 따라서 a
를 업데이트 했을 때 b
도 자동으로 업데이트 되는 것이다. 이렇게 되는 것은 a
가 리스트로 mutable 객체이기 때문이기도 하다. 만약 tuple을 사용하는 경우에는 이런 현상이 일어나지 않는다.
= (1, 2, 3)
c = c
d = c + (4, )
c c, d
((1, 2, 3, 4), (1, 2, 3))
10.9.1 함수의 argument로 mutable 객체로 사용하는 경우
이 맥락이 함수의 arguments와 parameters 사이에도 그대로 적용된다. 우리는 함수가 함수 밖에 상태에 영향을 받지 않고, 주어진 값이 있다면 언제 호출하더라도 같은 값이 반환되는 함수를 원하는 경우가 많다(이런 성질을 idempotence라고 부른다).
만약, 어떤 함수를 호출할 때 mutable 객체를 argument로 지정하는 경우, 함수 안에서 이 객체가 바뀔 수 있음을 조심해야 한다.
다음과 같이 함수를 정의했다고 생각해 보자. 이 함수의 바디에서 a += b
라는 코드는 a
가 immutable인 경우에는 a = a + b
를, mutable
인 경우에는 a
가 in-place로 수정된다.
def my_f(a, b):
+= b
a return a
Immutable인 경우에는 아래 예와 같이 간단히 생각할 수 있고, 함수 밖에 있는 x
, y
가 바뀌지 않는다.
= 1
x = 2
y print(my_f(x, y))
x, y
3
(1, 2)
Muable 객체인 경우에는 생각이 필요하다.
= [1, 2]
l = [3, 4]
m print(my_f(l, m))
l, m
[1, 2, 3, 4]
([1, 2, 3, 4], [3, 4])
이 경우 l
이 함수 호출을 거치고 난 후에 값이 바뀌었다. 이렇게 되는 이유는 함수 밖에 있는 l
이 mutable 객체이고 함수의 argument로 사용될 때, 이 객체의 레퍼런스가 함수의 parameter a
와 공유되기 때문이다. 따라서 함수 안에서 파라미터 a
를 업데이트 하면 밖에 있는 l
이 바뀌게 된다.
10.9.2 파라미터 옵션 값으로 mutable 객체를 사용하는 경우
Python에서 파라미터를 주거나 주지 않아도 되는 함수(optional parameter를 가진 함수)는 중요하게 사용된다. 그런데 디폴트 값으로 mutable 객체를 사용하는 경우 주의를 요한다. 다음 함수를 가지고 생각해 보자.
def func(x, items=[]):
items.append(x)return items
파이썬 인터프리터는 def
문을 만났을 때(즉, 함수를 정의할 때), 새로운 함수 객체를 생성하고, 그 안에서 named parameter를 만나면 그 값을 계산하여 이 함수 객체의 __defaults__
라는 속성에 그 값을 저장한다. 그렇게 하기 때문에 이 함수를 처음 호출할 때도 이 값을 가져와 사용하기 때문에 사용자가 argument의 값을 생략할 수 있는 것이다. 위 함수의 경우 __defaults__
에 빈 리스트가 저장된다.
func.__defaults__
([],)
이 함수는 named parameter에 argument를 지정하지 않게 호출해 보자. 일부러 __defaults__
가 어떻게 바뀌는지 확인하게 했다.
1) func(
[1]
func.__defaults__
([1],)
2) func(
[1, 2]
func.__defaults__
([1, 2],)
3) func(
[1, 2, 3]
func.__defaults__
([1, 2, 3],)
이렇게 함수를 호출할 때, 이전 상태에 따라 반환되는 값이 다르는 것을 알 수 있다. 마치 함수가 이전 상태를 기억하는 듯하다. 실제로 그러하다. 하나씩 생각해 보자.
첫 번째 func(1)
은 당연히 이해가 될 것이다. func(2)
의 경우에는 다음과 진행된다.
함수
func
이 정의되면서 함수의__defaults__
의 속성으로 빈 리스트[]
가 만들어진다.func(1)
이 호출되면서 이 빈 리스트에1
이 추가되고 이것이 반환된다. 리스트는 mutable object이기 때문에 이 내용은__defaults__
에 있는 리스트에도 반영된다. 이 값이[1]
이 된다.func(2)
가 호출되면, 디폴트로 저장된[1]
을 가지고 여기에2
가 추가된다. 그리고[1, 2]
가 반환된다. 리스트는 mutable object이기 때문에 이 내용은__defaults__
의 리스트에도 반영되어 이 값이[1, 2]
가 된다.
이렇게 하지 않게(즉, 함수가 이전 호출과 상관없이 실행될 수 잇게 하려면) 다음과 같은 패턴을 사용한다.
None
값을 갖는 파라미터 사용하고,- 함수 body에서
if name is None:
을 사용하여, 값을 arugment로 주지 않은 경우와 지정해 주는 경우에 대응하는 코드를 작성한다.
def new_fun(x, items=None):
if items is None:
= []
items
items.append(x)return items
이와 같이 named parameter의 디폴트 값을 mutable object를 사용함으로서 생기는 문제를 피할 수 있게 된다. 어떻게 이것이 가능한지 살펴보자.
이렇게 정의되면 파이썬 인터프리터가 실행될 때 함수 객체의 __defaults__
attributes에 None
이 저장된다.
new_fun.__defaults__
(None,)
이제 함수를 호출해 보자.
1) new_fun(
[1]
new_fun.__defaults__
(None,)
위 결과에서 보듯이 new_fun(1)
을 호출할 때 items
는 저장된 None
을 가지고 함수를 시작하게 되고, 바디 안의 if
문에 의해서 items
라는 변수는 빈 리스트에 할당된다. 이 리스트에 1
이 추가되어, 이것이 반환된다. 이 리스트에 1
이 추가된다고 해도 이것은 디폴트의 None
과 전혀 상관이 없다.
다시 이 함수를 호출해 보자.
2) new_fun(
[2]
new_fun.__defaults__
(None,)
이 경우에도 new_fun(1)
을 호출할 때와 똑같이 작동한다. 그래서 결국은 함수가 호출될 때 파라미터의 디폴트에 영향을 받지 않는다.