[Python] 함수
컴퓨터 사이언스 부트캠프 with 파이썬을 보고 정리한 내용입니다.
전역변수와 지역변수
- 전역변수: 전체 영역에서 접근할 수 있는 변수
- 함수 안에서도 접근할 수 있음
- 단, 함수 내에서 값을 변경하려고 하면 되지 않음(똑같은 이름으로 호출해도 지역변수로 인식하기 때문)
- 만약 함수 내에서 전역변수를 변경하고 싶다면
global var_a
와 같이 전역변수라고 명시해주면 됨! - 함수가 2개 이상 중첩되어 있다면? A 함수 안에 B 함수가 있다면 A의 변수들은 B 함수 입장에서는 전역도 지역도 아님. 이때는
nonlocal var_a
를 사용함
- 만약 함수 내에서 전역변수를 변경하고 싶다면
- 지역변수: 특정 지역 = 함수 내에서만 접근 가능한 변수, 함수 내에서 선언되며 호출이 끝나면 사라짐
인자 전달 방식에 따른 분류
값에 의한 전달(call by value)
- 함수가 호출될 때 메모리에는 ‘스택 프레임’이 생김: 스택 프레임은 함수의 메모리 공간, 즉 지역 변수가 존재하는 영역
- 만약 a와 b를 받아 합을 c, 차를 d로 선언한 다음 c+d를 돌려주는 함수 test가 있고, a, b 값을 정한 다음 이 함수의 결과를 res로 하는 main 이 있다면
- main()이 먼저 실행되므로, 먼저 스택프레임이 생기고 그 위에 test()의 스택 프레임이 쌓임
- 스택의 원칙 = 마지막에 들어온 데이터가 가장 먼저 나감
- test()의 스택 프레임이 먼저 사라지고 그 다음에 프로그램이 종료되면 main()의 스택 프레임이 사라짐
- 둘 다에 a, b가 있는데 둘은 서로 독립된 공간(서로 다른 메모리 공간에 존재하는 서로 다른 변수)
- 인자를 전달할 때 값에 의한 전달을 하게 되면, main의 지역변수인 a와 b를 전달한 것이 아니라 test()의 스택 프레임의 그 값만 복사한 것임
- 따라서 test() 내에서 값이 변경되어도 main()의 a,b 값이 변경되지 않는 이유
참조에 의한 전달 (call by reference)
- 참조에 의해 전달한다는 것은 값을 복사하는 게 아니라, main() 함수 스택 프레임의 변수 x가 위치한 첫번째 바이트 주소 값(4바이트 공간 중 첫번째)을 전달하는 것
- 이렇게 하면, 그 함수 내의 변수가 main()의 지역변수 x를 가리키게 됨
- 이렇게 하면 change_value() 함수 내에서 main()의 x값이 변경될 수 있음
- 파이썬은 함수에 인자를 전달할 때 값에 의한 전달 방식이나 참조에 의한 전달 방식을 사용하지 않고 객체 참조에 의한 전달 방식을 사용함
객체 참조에 의한 전달 (call by object reference)
변경 불가능한 객체를 전달할 때
- 파이썬에서는
- 실행전
- 실행후
- 파이썬의 변수는 C처럼 변수라는 메모리 공간에 값을 직접 저장하지 않는다는 게 포인트, 즉 변수 이름 값 객체를 가리키고 있음
- 함수 스택 프레임 안에서는 x값이 변경됐지만 호출한 쪽에서는 x값이 변경되지 않음
- 상수 객체는 변경 불가능 객체이기 때문. 변수 값을 바꾼다는 의미는 변수 이름이 가리키는 메모리 공간의 값을 직접 바꾸는 게 아니라, 바꾸고자 하는 상수 객체(20)를 참조하는 것
- 만약 change_value() 호출이 완료되면, change_value 스택 프레임이 사라지면서 지역변수 x와 value가 사라지고 x → 10 만 남음, 레퍼런스 카운트가 0이 된 20은 사라질 것
- 레퍼런스 카운트란?
- 메모리 영역 중 힙(heap)이라는 공간이 있음. C/C++에서는 힙에 할당한 메모리는 프로그래머가 직접 해제해야 하나, 자바/C#/파이썬에서는 메모리를 해당 언어가 스스로 해제 (= garbage collection)
- 파이썬이 가비지 컬렉션을 구현하는 방법이 바로 레퍼런스 카운트임. 위 예시에서 변수가 상수 객체를 가리키는 것이 바로 레퍼런스(참조)임.
- 만약 변수 a가 10이라는 상수 객체를 가리키면, 레퍼런스 카운트는 1
- b=a 라는 코드로 b도 10d이라는 상수 객체를 가리키면, 레퍼런스 카운트는 2
- 다음과 같은 코드로 직접 확인 가능
1
2
3
4
5
import sys
a = "abcde"
sys.getrefcount(a)
## 2가 나옴-> 왜지? -> getrefcount도 'abdcde'를 참조하니깐.
# 다만 함수 호출이 끝나면 그 1은 사라지는 값이므로, getrefcount()에서 나온 값에서 1개 빼야 맞음
- 만약 a와 b가 다른 객체를 가리키도록 코드를 수정하면 상수 객체 10은 레퍼런스 카운트가 0이 되고 메모리에서 해제됨
변경 가능 객체를 전달할 때
- 변경가능 객체 중 리스트를 전달해보자.
- 예시1 : 참조한 리스트에 접근해 변경을 시도
1
2
3
4
5
6
7
8
9
def func(li):
li[0] = "I am your father!"
if __name = = "__main__":
li = [1,2,3,4]
func(li)
print(li)
# ['I am your father!', 2,3,4]
1
2
- func 스택 프레임의 li와 main 스택 프레임의 li가 모두 같은 메모리 공간을 참조함
- 이렇게 되는 이유는 상수처럼 변경 불가능한 객체는 값을 바꾸려면 그냥 다른 메모리 공간에 새로운 객체를 만든 다음 그 객체를 참조하도록 만드는 경우밖에 없는 반면, 리스트는 그냥 새로운 공간에 그 값( `'I am your father!'` )을 만들고 참조하면 됨. 리스트 자체는 원래 메모리 공간에 둬도 됨. - 예시2: 아예 다른 리스트를 메모리 공간에 새로 만든 다음 이를 참조해 리스트를 변경
1
2
3
4
5
6
7
8
9
10
def func(li):
li = ["I am your father!",2,3,4]
if __name = = "__main__":
li = [1,2,3,4]
func(li)
print(li)
# [1, 2,3,4]
# 새로 할당한 게 반영이 안됨!
1
2
- 이 코드의 경우 다른 메모리 공간에 새롭게 리스트를 만들어 li로 참조하게 함
- 요소가 아니라 리스트 자체를 새로 지정한 것이고, 함수 호출이 끝나면 이 li는 func 스택 프레임이 사라지면서 삭제되기 때문에 기존 main 스택 프레임의 li는 그대로 변경되지 않고 남음
정리
- 함수 안에서 새 객체를 만든 다음 참조하여 바꾸려 하면 함수 호출이 끝난 후 사라진다. 그래서 원래 값은 변경되지 않는다.
- 변경 불가능 객체는 새 객체를 만들어 참조하여 바꾸는 것밖에 할 수 없기 때문에(상수, 문자열, 튜플)
- 반면 변경 가능 객체일지라도 새 객체를 만들어 참조하는 식으로 하면 마찬가지로 함수 호출이 끝난 후 사라진다.
변경 불가능 객체는 인자로 전달해 바꿀 수 없는 걸까?
파이썬 공식 문서에서 제시하는 몇 가지 방법 중 가장 추천하는 방법
1
2
3
4
5
6
7
8
def change_value(tu):
tu = ('I am your father', 2,3,4)
return tu
if __name__ = = "__main__":
tu = (1,2,3,4)
tu = change_value(tu) #함수 안에서 할당된 객체를 main()의 tu가 참조하도록 하기!
print(tu)
This post is licensed under CC BY 4.0 by the author.