본문 바로가기

Computer/Python

파이썬의 매개변수 전달 방식

반응형

파이썬에서 함수의 매개변수 (arguments) 는 어떻게 전달될까요? C/C++ 언어에 익숙한 사람이라면 전달값이 복사되어 매개변수에 전달하는 pass by value (call by value), 존재하는 변수의 레퍼런스 (혹은 메모리 주소) 를 전달하는 pass by reference (call by reference) 를 생각하겠지만 파이썬에서는 pass by assignment 방식으로 함수의 매개변수에 값을 전달합니다.

Pass by value / reference in python

다음 코드를 살펴보겠습니다. 파이썬에서는 할당된 객체의 메모리 주소를 반환하는 id() 내장함수가 있고 이를 통해 1) 함수 매개변수가 원래의 변수와 같은 주소를 가지는지와, 2) 같은 이름으로 변수를 재할당 했을때의 주소 변화를 파악할 수 있습니다. 밑의 결과를 보면 n과 x는 정확히 같은 주소를 가지고 있습니다. 따라서 파이썬에서 함수의 매개변수에 원래 변수의 값을 복사에서 전달하는 pass by value 방식은 명확히 아닌 것을 알 수 있습니다. 

def main():
    n = 9001
    print(f"Initial address of n: {id(n)}")
    increment(n)
    print(f"  Final address of n: {id(n)}")

def increment(x):
    print(f"Initial address of x: {id(x)}")
    x += 1
    print(f"  Final address of x: {id(x)}")

main()


그렇다면 pass by reference 방식일까요? 파이썬의 함수 매개변수 전달방식이 pass by reference 라면 다음 코드에서 counter 변수가 증가해야할 것입니다. 하지만 밑의 결과를 보면 counter 변수의 값은 제자리입니다. 

def main():
    counter = 0
    print(greet("Alice", counter))
    print(f"Counter is {counter}")
    print(greet("Bob", counter))
    print(f"Counter is {counter}")

def greet(name, counter):
    return f"Hi, {name}!", counter + 1

main()

따라서 파이썬은 pass by reference 방식이라고도 엄밀히 말할 수 없습니다. 위의 코드가 원하는대로 동작하기 위해서는 다음과 같이 greet 함수의 출력값을 counter 변수에 재할당을 해주어야 합니다.

def main():
    counter = 0
    greeting, counter = greet("Alice", counter)
    print(f"{greeting}\nCounter is {counter}")
    greeting, counter = greet("Bob", counter)
    print(f"{greeting}\nCounter is {counter}")

def greet(name, counter):
    return f"Hi, {name}!", counter + 1

main()

 

Passing arguments in python

파이썬에서는 pass by assignment 방식으로 함수 매개변수에 값을 전달합니다. 즉, 함수가 호출될 떄, 함수의 각 매개변수는 전달된 값이 할당된 변수가 된다는 것이죠. 먼저 파이썬에서의 할당은 (=) 어떤 의미를 지닐까요? 예를 들어 "x=2" 라는 구문은 x라는 변수 이름을 2라는 객체에 할당한 것이고 "x=3" 을 선언할 경우 x라는 변수 이름이 3이라는 객체에 재할당된 것이죠. 특히, 모든 파이썬 객체는 reference counter 라는 것을 지니는데, reference counter는 특정 객체가 얼마나 많은 변수 이름에 할당됐는지를 추적하는 counter로 "x=2" 일때 2에 대한 reference counter는 증가하면서 현재 namespace에 x와 2가 연관되었다는 정보를 사전형태로 등록합니다.

파이썬의 reference counter는 sys 내장모듈의 getrefcount 함수를 통해 확인할 수 있습니다. 다음 코드를 보면 변수 할당에 따른 'value_1'과 'value_2' 객체가 얼마나 레퍼런스 되었는지를 볼 수 있습니다.

from sys import getrefcount

print("--- Before  assignment ---")
print(f"References to value_1: {getrefcount('value_1')}")
print(f"References to value_2: {getrefcount('value_2')}")
x = "value_1"
print("--- After   assignment ---")
print(f"References to value_1: {getrefcount('value_1')}")
print(f"References to value_2: {getrefcount('value_2')}")
x = "value_2"
print("--- After reassignment ---")
print(f"References to value_1: {getrefcount('value_1')}")
print(f"References to value_2: {getrefcount('value_2')}")

이를 통해 알 수 있는 점은 파이썬에서 특정 객체를 여러 변수가 동일하게 나타낼 때 객체의 reference counter를 증가시키고 현재의 namespace를 업데이트하면서 각 객체를 복사해 메모리 상에 중복되게 하지 않는다는 것입니다. 


함수가 호출될 떄, 함수의 각 매개변수는 전달된 값이 할당된 변수가 된다면 각 변수는 함수 안의 지역변수 (local variable) 로서 존재합니다. 지역이란 파이썬의 범위의 (scope) 하나로서 locals() 내장함수나 globals() 내장함수로 지역, 전역 범위의 사전형식으로 존재하는 namespace를 얻을 수 있습니다. 다음과 같이 my_arg 매개변수가 지역 범위의 namespace로 등록된 것을 볼 수 있습니다.

>>> def show_locals(my_arg):
...     my_local = True
...     print(locals())
...
>>> show_locals("arg_value")
{'my_arg': 'arg_value', 'my_local': True}

reference counter를 확인해보면 함수 매개변수가 객체의 reference counter를 어떻게 증가시키는지 볼 수 있습니다.

>>> from sys import getrefcount

>>> def show_refcount(my_arg):
...     return getrefcount(my_arg)
...
>>> getrefcount("my_value")
3
>>> show_refcount("my_value")
5

show_refcount 함수에 "my_value" 객체를 전달하면 show_refcount 함수 안의 my_arg 매개변수와 getrefcount 함수 안의 my_arg 매개변수를 통해 reference count가 2만큼 증가합니다. 즉, 정리하면 파이썬에서는 함수의 지역 namespace 상에 매개변수를 지역변수로 할당하며, 전달된 객체의 reference counter를 증가시키는 assignment 방식으로 동작합니다.

 

Replicating pass by reference with python

그렇다면 파이썬에서 변수를 매개변수로 전달하여 변화시키고 싶은 pass by reference를 어떻게 구현할 수 있을까요? 먼저 global 키워드를 통해 전역범위에 있는 변수를 지역범위로 전달함으로써 구현할 수 있습니다.

>>> def square():
...     # Not recommended!
...     global n
...     n *= n
...
>>> n = 4
>>> square()
>>> n
16

하지만 global 키워드는 파이썬의 thread safety 이슈와 더불어 함수의 매개변수가 명확히 정의되지 않으므로 일반적으로 사용될 수 없는 문제가 있어 사용이 굉장히 지양됩니다.


1. Return and reassign

가장 보편적인 방법으로 함수의 출력값을 정의하고 같은 이름의 변수에 재할당하는 방법이 있습니다. 또한, 출력값을 여러개 정의할 경우 출력 순서에 맞게 같은 이름의 변수를 재할당합니다.

def square(n):
    # Accept an argument, return a value.
    return n * n

x = 4
...
# Later, reassign the return value:
x = square(x)

 

2. Object attribute

다른 방법으로는 클래스 등의 속성을 통해 변화시키는 방법이 있습니다. 즉, 다음과 같이 속성을 가진 객체를 함수의 매개변수로 전달하면 함수 안에서 객체의 속성을 변화시킬 수 있습니다. 이때 주의할 점은 객체의 속성이 할당을 지원해야한다는 점입니다. 예를 들어 namedtuple과 같이 속성의 값을 변화시킬 수 없는 불변 객체에 대해서는 동작하지 않습니다.

>>> from types import SimpleNamespace

>>> ns = SimpleNamespace()

>>> # Define a function to operate on an object's attribute.
>>> def square(instance):
...     instance.n *= instance.n
...
>>> ns.n = 4
>>> square(ns)
>>> ns.n
16

 

3. Use mutable type

리스트나 사전과 같이 값이 변할 수 있는 mutable 타입을 사용하는 방법도 있습니다. 다음과 같이 매개변수로 사전 타입의 변수를 전달하고 사전의 특정 키의 값을 변경하면 수정된 값에 접근할 수 있습니다. 

>>> # Dictionaries are mapping types.
>>> mt = {"n": 4}
>>> # Define a function to operate on a key:
>>> def square(num_dict):
...     num_dict["n"] *= num_dict["n"]
...
>>> square(mt)
>>> mt
{'n': 16}

리스트도 인덱스로 접근 가능하고 (subscriptable) 값이 변경가능하므로 (mutable) 사전 타입과 마찬가지로 pass by reference 방식으로 사용할 수 있습니다.

>>> # Lists are both subscriptable and mutable.
>>> sm = [4]
>>> # Define a function to operate on an index:
>>> def square(num_list):
...     num_list[0] *= num_list[0]
...
>>> square(sm)
>>> sm
[16]
반응형

'Computer > Python' 카테고리의 다른 글

파이썬과 객체  (0) 2021.07.14
파이썬의 Namespace와 Scope  (0) 2021.07.13
Property, Setter, Getter  (0) 2021.06.28
concurrent.futures를 이용한 병렬화  (0) 2021.06.27
데코레이터와 functools.wrap  (0) 2021.06.27