파이썬 범위 이해하기

파이썬 범위 이해하기

파이썬에서 범위란, 함수나 반복문을 언급할 때 꼭 다뤄야 되는 부분 중 하나이다. 범위라는 개념이 프로그래밍에서 이해하기 어려운 부분 중 하나이지만, 매우 중요하니 잘 짚고 넘어가자.


범위란 무엇인가?

사용자가 변수에 값을 할당할 때, 사용자는 변수에 이름을 주게된다. 파이썬에서 이름은 고유하다. 예를들어, 두 개의 다른 숫자에 같은 이름을 할당할 수는 없다.

>>> x = 2
>>> x
2

>>> x = 3
>>> x
3

변수 x에 값 3이 할당되면, 더 이상 값 2는 변수 이름 x로 회수할 수 없다. 하지만, 다음 예제처럼 같은 변수 이름에 두 개의 다른 값을 할당할 수 있는 방법이 있다.

x = "Hello World"

def func():
	x = 2
    print(f"Inside 'func', x has the value {x}")

func()
print(f"Outside 'func', x has the value {x}")

위 예제에서, 변수 x에 두 개의 다른 값 들이 할당되었다. 처음 x는 "Hello World"가 할당되었고, func() 함수 안의 x에는 값 2가 할당되었다. 위 스크립트를 출력해보면 다음과 같다.

Inside 'func', x has the value 2
Outside 'func', x has the value Hello World

분명히 앞에서 같은 변수에 두 개의 다른 값을 할당할 수 없다고 했는데 어떻게 위 예제에서는 가능했을까? 정답은 함수 func()함수 밖에 존재하는 코드와 다른 범위(scope)를 가지고 있기에 가능한 것이다.

즉, 파이썬은 두 개의 분리된 상태를 유지한다. 함수 func() 안의 객체와 함수 외부에 동일한 이름을 가진 객체가 존재하는 것처럼 말이다.

함수 본문안에는 지역 범위(local scope)라고 하는 고유한 이름의 집합이 있다. 그리고 함수 본문 밖에 있는 코드는 전역 범위(global scope)에 있다.

파이썬에서 범위란 객체에 매핑된 이름의 집합이라고 생각할 수 있다. 코드 안에서 변수나 함수 같은 특정 이름을 사용할 때, 파이썬은 사용된 이름이 존재하는지를 결정하기 위해 이름이 속한 현재 범위를 체크한다.


범위 결정(Scope Resolution)

파이썬에서 범위는 계급을 가진다. 다음 예제를 참고해보자.

x = 5

def outer_func():
	y = 3

	def inner_func():
		z = x + y
		return z

	return inner_func()
inner_func() 함수는 내부 함수(inner function)이라고 불린다. 왜냐하면 다른 함수 안에 존재하는 함수이기 때문이다.

변수 zinner_func() 함수의 지역 변수이다. 파이썬이 z = x + y를 실행할 때, 파이썬은 변수 xy지역 변수에서 먼저 찾는다. 하지만, 위 예제에서는 지역 변수에서 존재하지 않으므로, 한 단계 위로 올라가 outer_function() 함수의 범위에서 계속하여 찾는다.

함수 outer_func()의 범위는 함수 inner_func()의 범위를 감싼다. outer_func()의 범위가 전역 변수는 아니지만, 또 inner_func()의 범위처럼 지역변수는 아니다. 그 사이 어딘가 존재한다고 생각하면 된다.

변수 youter_func()의 범위에서 정의가 되었고 값 3가 할당되었다. 하지만, 변수 x는 여전히 범위에 존재하지 않으므로, 파이썬은 다시 한번 전역 변수 위로 올라가, 변수 x를 찾고 값 5를 가진 것을 확인한다. 이제 변수 이름 xy가 모두 결정 되었으므로, 파이썬은 inner_func() 안의 z = x + y를 실행할 수 있게된다.


LEGB 법칙(LEGB Rule)

범위를 결정하는 방법을 기억하는 유용한 방법 중 하나는 LEGB 법칙을 아는 것이다. LEGB는 Local(지역), Enclosing(외부 함수 안, 내부 함수 밖), Global(전역), Built-in(내장)의 줄임말이다. 파이썬에서는 LEGB 목록에 있는 순서대로 범위를 결정한다.

지역(Local, L)

지역, 혹은 현재의 범위이다. 지역 범위는 함수의 본문이거나 스크립트의 최고 수준 범위이다. 지역 변수는 항상 파이썬 인터프리터가 현재 작업 중인 범위를 나타낸다.

외부 함수 안, 내부 함수 밖(Enclosing, E)

인클로징 범위는 지역 범위의 한 단계 위 범위이다. 만약 지역 범위가 내부 함수 안에 있다면, 인클로징 범위는 외부 함수의 범위이다. 만약 범위가 최고 수준의 함수라면, 인클로징 범위는 전역 범위와 같게 된다.

전역(Global, G)

전역 범위는 스크립트 최상단에 위치한 범위이다. 전역 범위에는 함수 본문에 포함되지 않은, 스크립트 안에 정의된 모든 이름들이 담겨있다.

내장(Built-in, B)

파이썬에서 키워드 같은 모든 이름들을 담고 있는 것이 내장 범위다. round() 함수나 abs() 함수 같은 것들이 내장 범위이다. 즉, 먼저 정의 하지않고 사용할 수 있는 모든 것들이 전역 범위에 담겨있다고 생각하면 된다.


규칙에 어긋난 코드

다음과 같은 스크립트가 있다고 가정해보자.

total = 0

def add_to_total(n):
	total = total + n

add_to_total(5)
print(total)

아무 생각없이 당연히 5가 출력될 것이라고 생각할 수도 있다. 하지만 실제로 코드를 실행시키면 다음과 같은 결과를 얻게된다.

Traceback (most recent call last):
  File "C:/Users/davea/stuff/python/scope.py", line 6, in <module>
    add_to_total(5)
  File "C:/Users/davea/stuff/python/scope.py", line 4, in add_to_total
    total = total + n
UnboundLocalError: local variable 'total' referenced before assignment

분명 앞에서 LEGB 규칙에 따라, 파이썬은 지역 범위의 없는 값을 한 단계 올라가 다음 범위(전역)에서 찾는다고 들었을 것이다. 사실, 이 스크립트의 문제는 변수 total에  total이란 값을 사용해서 할당하려고 하지만, 지역 변수에서는 total이란 값이 존재하지 않는 것이다.

자세히 보지 않으면 쉽게 일어날 수 있는 실수이다. 위 같은 문제는 다음과 같이 global 키워드를 사용해 해결할 수 있다.

total = 0

def add_to_total(n):
	global total
	total = total + n

add_to_total(5)
print(total)

global은 파이썬에게 이름을 전역 범위에서 찾으라고 명령하는 것이다. 즉 global total은 파이썬에게 전역 범위에 있는 변수 total을 찾으라고 한 것이다. 그래서 코드 total = total + n이 새로운 지역 변수를 새로 만들지 않고 실행될 수 있는 것이다.

global을 사용함으로써 위 문제가 해결되기는 하였지만, 보통 global 키워드를 사용하는 행위는 파이썬에서 좋지 않다고 여겨진다. 즉, global을 사용해야 할 상황이 온다면, 지금 코드를 더 잘 작성할 방법이 없는지 고민을 해야한다.

Reference