Argument
인자Argument
함수는 인자 전달 방식에 따라 크게 값에 의한 전달과 참조에 의한 전달로 나누어진다. 파이썬은 두 개념이 아닌 객체 참조에 의한 전달을 이용하므로 C++ 코드를 통해 알아보자.
값에 의한 전달 call by value
#include <iostream>
using namespace std;
void change_value(int x, int value)
{
x = value;
cout << "x : " << x << " in change_value" << endl;
}
int main(void)
{
int x = 10;
change_value(x, 20);
cout << "x : " << x << " in main" << endl;
return 0;
)
(main) : x : 20 in change_value
x : 10 in main
위 코드를 보면 함수 안에서는 값이 변경되었지만 함수를 호출한 쪽에서는 값이 변경되지 않았다. 그 이유는 함수에 x가 전달될 때 값에 의한 전달 방식으로 전달되었기 때문이다.
스택 프레임Stack Frame
스택 프레임은 함수의 메모리 공간(지역 변수)이 존재하는 영역이다.
# include <iostream>
using namespace std;
int test(int a, int b);
int main(void)
{
int a = 10, b = 5;
int res = test(a, b);
cout << "result of test : " << res << endl;
return 0;
}
int test(int a, int b)
{
int c = a + b;
int d = a - b;
return c + d;
}
스택 프레임은 스택 메모리 공간에 생기는데 스택 자료 구조의 작동 원리를 따른다. main() 함수가 먼저 실행되므로 스택 프레임이 먼저 쌓인 후 test() 함수의 스택 프레임은 그 위에 쌓인다. test() 함수가 모두 실행되면 test() 함수의 스택 프레임이 먼저 사라지고, 프로그램이 종료되면 main() 함수의 스택 프레임이 사라진다. 이때 각 함수의 스택프레임은 다음과 같다 [test([d], [c], [a], [b])] [main([res], [a], [b])] main(), test() 함수의 공간에도 a와 b가 모두 존재한다. 두 공간은 서로 독립된 공간이며 인자를 전달한 것이 아닌 test() 함수 스택 프레임의 지역 변수 a와 b의 값만 ‘복사’한 것이다.
스택 프레임의 작동 구조에 대해 알았으니 왜 x가 change_value 함수의 호출 후에도 main에서 변경된 값을 가지지 않는 이유에 대해 이해할 수 있다. 이처럼 인자를 전달할 때 값을 복사해 전달하는 경우를 값에 의한 전달이라고 한다.
참조에 의한 전달 call by reference
참조에 의한 전달 방식은 인자를 전달할 때 값을 전달하는 게 아닌 참조를 전달한다.
#include <iostream>
using namespace std;
void change_value(int *x, int value)
{
*x = value;
cout << "x : " << *x << " in change_value" << endl;
}
int main(void)
{
int x = 10;
change_value(&x, 20);
cout <<"x : " << x << " in main"" << endl;
return 0;
}
최상단의 코드와 비교해보면 int x와 x = value; 가 int*x 와 *x = value;로 바뀌었고, change_value(x, 20)이 change_value(&x, 20)으로 바뀌었다. int *는 int형 포인터를 의미한다. 이는 main() 함수 스택 프레임의 변수 x가 위치한 메모리 공간의 첫 번째 바이트 주소 값을 전달한다는 의미이다. int *x는 포인터 변수를 의미한다. 포인터 변수도 다른 변수처럼 데이터를 저장하지만, 그 데이터가 메모리 주소일 뿐이다. 포인터 변수가 주소 값을 저장한다는 것은 change_value 스택 프레임 안에 있는 int형 포인터 x가 main() 함수의 지역 변수 x를 가리키는 것과 같은 의미이다. 이때 가리킨다는 말은 다른 말로 참조이며, 인자로 변수의 참조를 전달하는 방식을 참조에 의한 전달이라 한다.
객체 참조에 의한 전달 call by object reference
파이썬은 함수를 호출할 때 인자로 전달된 객체를 일단 참조하며 이는 객체 참조에 의한 전달이라고 한다.
레퍼런스 카운트Reference Count
메모리 영역 중 ‘힙’에 할당한 메모리는 C에선 프로그래머가 직접 해체해야 한다. 하지만 자바, 파이썬 등에서는 해당 언어가 스스로 해체하며 그 개념을 Garbage collection라고 한다. Garbage collection을 구현하는 알고리즘은 다양하지만, 파이썬은 레퍼런스 카운팅으로 구현한다. 파이썬에서 변수는 값이 아닌 상수 객체를 가리키고 있다고 했는데, 이러한 개념을 참조라고 하며, 이 참조의 갯수를 레퍼런스 카운트라 한다.
>>> import sys
>>> a = "abcde"
>>> sys.getrefcount(a)
2
위 코드를 실행했을 때 ‘1이 출력되야 하는게 아닌가?’ 라고 예상할 수 있지만, getrefcount()함수도 ‘abcde’라는 문자열 상수를 참조하여 2가 출력되는 것이 정상이다. 따라서 실제 레퍼런스 카운트는 1을 뺀 값이라고 볼 수 있다.
변경 불가능 객체
변경 불가능 객체인 상수 객체를 인자로 전달해보자. 파이썬에선 함수를 호출할 때 인자로 전달된 객체를 일단 참조한다. 참조에 의한 전달과 유사하다. 다음 코드를 통해 살펴보자.
def chage_value(x, value): #3
x = value #4
print("x : {} in chage_value".format(x))
if __name__ == "__main__":
x = 10
change_value(x, 20)
print("x : {} in main".format(x)) #1
(main) : x : 20 in change value #2
x : 10 in main
\#2에서 change_value() 함수를 호출하면서 인자로 #1의 x를 전달한다. 이때 함수의 스택 프레임이 생성되면서 #3의 인자 x는 함수를 호출한 영역에 있는 #1의 x를 참조한다. \#4를 실행하기 전의 스택 프레임에서 두 개의 x는 10을, 함수 내부의 value는 20을 가리키고 있다. 여기서 알 수 있는 주목할 점은 파이썬의 변수는 변수라는 메모리 공간에 값을 직접 저장하지 않는다는 것이다. 변수 이름이 값 객체를 가리킬 뿐이다. 그렇다면 #4가 실행된 이후엔 어떻게 될까? 단순히 함수 안의 x가 20을 가르키고 - 좀 더 풀어서 설명하면, 변수 값이 바뀌는게 아닌 바꾸고자 하는 상수 객체를 참조하고 -, 함수의 호출이 완료되면 chage_value() 스택 프레임이 사라진다.
변경 가능 객체
변경 가능 객체인 리스트를 인자로 전달해 보자.
def funcc(li):
li[0] = 'hi' #1
if __name__ == "__main__"
li = [1, 2, 3, 4]
func(li)
print(li)
>>>['hi', 2, 3, 4]
def funcc(li):
`python
li = ['hi', 2, 3, 4] #2
if __name__ == "__main__"
li = [1, 2, 3, 4]
func(li)
print(li)
>>>[1, 2, 3, 4]
첫 번째와 두 번째의 출력 값이 다른 이유는 왜일까? #1의 코드는 참조한 리스트에 접근해 변경을 시도하였고, #2의 코드는 별개의 리스트를 새로 만든 다음 이를 참조해 리스트를 변경한 것이다. 변경 불가능 객체는 값을 바꾸려면 새로운 객체를 만든 다음 참조를 통해 새로 만든 객체를 가리키게 해야한다. 하지만 리스트는 변경 가능 객체이며, 값 객체만 새로운 공간에 만들어 참조하면 된다. 스택 프레임을 상상해보자면 main과 func 모두 각각의 리스트를 가지고 있고, func함수의 호출이 끝나면 해당 스택 프레임은 삭제되므로 main의 리스트가 출력되는 것이다.
Summary
- 함수 인자로 변경 불가능 객체를 전달해 값을 변경할 수 없다.
- 함수 내부에서 객체를 새롭게 할당해야만 값을 변경할 수 있는 객체는 상수, 문자열, 튜플뿐이다.
- 그 외 변경 가능 객체는 새로운 객체를 만들 경우 함수 호출이 끝나면서 객체는 사라진다.
- 변경 가능 객체를 인자로 전달할 때도 인자로 전달된 객체에 접근하여 변경해야만 함수를 호출한 쪽의 객체를 변경할 수 있다.
람다 함수Lambda
람다 함수란 이름이 없는 함수이다. 자주 사용하지 않는 간단한 함수는 람다 함수로 만들어 사용하는것이 좀 더 편리하게 사용할 수 있다.
>>> li = [i for i in range(1,11)]
>>> li
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> li.sort(key = lambda x: x % 2 == )
>>> li
[1, 3, 5, 7, 9, 2, 4, 6, 8, 10]
>>> f = lambda x: x**2
>>> f(2)
4
>>> f(5)
25
주의할 점은 람다 함수를 사용할 때에는 몸체에 반드시 식이 들어가야 한다는 것이다.
Enjoy Reading This Article?
Here are some more articles you might like to read next: