Post

[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.