본문 바로가기

Computer/Python

파이썬의 Namespace와 Scope

반응형

Namespace

파이썬에서 할당은 (assignment, =) 특정 객체에 변수 이름을 부여하는 행위를 말합니다. 예를 들어 "x=3" 이라면 3이라는 객체를 x라는 변수 이름으로 할당하고 x라는 이름은 3이라는 객체의 레퍼런스가 됩니다. Namespace (이름공간)는 여러 객체와 그것의 레퍼런스가 (이름) 사전 타입으로 (키는 객체 레퍼런스의 이름, 값은 객체) 묶인 콜렉션으로 다음과 같이 4가지 타입이 존재합니다.

Built-in namespace

Built-in namespace 에는 파이썬의 built-in 객체의 이름이 담긴 것으로 파이썬이 구동되고 있다면 언제 어디서든 사용할 수 있는 것들로 구성되어 있습니다. 파이썬에서 다루는 사전예약된 키워드 (return, local, id, dict, list, ...)와 에러나 exception 들로 구성되어 있습니다. 파이썬 인터프리터는 코드가 수행되면 built-in namespace를 구성하고 끝날 때까지 존재합니다.

Global namespace

Global namespace는 파이썬의 메인 프로그램이 실행될 때 생성되는 namespace로 일반적으로 파이썬 메일 파일에 정의되어 있는 모든 객체를 담고 있습니다. 또한 import 키워드를 통해서 다른 파일에 정의되어 있는 객체를 global namespace에 담을 수 있습니다.

Local and enclosing namespace

파이썬에서 함수를 실행하면 인터프리터는 해당 함수의 local namespace를 생성합니다. Local namespace는 함수가 종료될 때까지 남아있습니다. 특히, 다음과 같이 함수 안에 함수를 정의할 수 있는데, 프로그램이 f()를 호출하면 f()의 namespace를 생성하고 f()가 g()를 호출하면 g()의 namespace를 생성합니다. 이때 g()의 namespace를 local namespace, f()의 namespace를 enclosing namespace라 합니다. 각 namespace는 해당하는 함수의 실행이 종료될 때까지 남아있습니다.

>>> def f():
...     print('Start f()')
...
...     def g():
...         print('Start g()')
...         print('End g()')
...         return
...
...     g()
...
...     print('End f()')
...     return
...

>>> f()
Start f()
Start g()
End g()
End f()

 

Variable scope

이와 같이 다양한 namespace가 존재한다는 것은 같은 이름이 여러 namespace에 동시에 존재할 수 있다는 것이고 서로 개별적으로 유지된다는 것을 암시합니다. 그렇다면 파이썬에서는 x라는 이름을 찾고 싶고 x가 여러 namespace에 존재한다면 어떠한 우선순위를 가지고 x에 접근할까요? 여기에서 scope (범위)의 개념을 사용할 수 있습니다. Scope란 레퍼런스하고자 하는 해당 이름이 존재하는 프로그램 내의 지역을 말하고 파이썬 인터프리터는 해당 이름이 어디에서 정의되었고 어디에서 호출되었는지를 기반으로 이를 런타임 기반으로 다음의 LEGB 순서로 결정합니다.

1. Local: x를 함수 안에서 참조한다면 인터프리터는 해당 함수의 local namespace에서 x를 찾습니다.

2. Enclosing: Local namespace에서 x를 찾지 못한다면 해당 함수를 감싸고 있는 enclosing 함수의 namespace에서 x를 찾습니다.

3. Global: 1, 2 과정에서 찾지 못했을 경우 global namespace를 탐색합니다.

4. Built-in: 전 지역에서 찾지 못할 경우 파이썬의 built-in scope를 탐색합니다.

Examples

그렇다면 x가 정의된 위치에 따라 어떠한 x가 출력되는지 살펴보겠습니다. 가장 안쪽의 g() 함수에서 print(x)를 했을 때 결과를 살펴보겠습니다. 먼저 x가 함수 밖에 프로그램 상에서 선언된 경우입니다. 이 경우는 x가 global namespace에서만 존재하므로 'global'이 출력됩니다.

>>> x = 'global'

>>> def f():
...
...     def g():
...         print(x)
...     g()

>>> f()
global

다음과 같이 x가 global, enclosing namespace에 정의될 경우에는 LEGB 룰에 따라 'enclosing'이 출력됩니다.

>>> x = 'global'

>>> def f():
...     x = 'enclosing'
...
...     def g():
...         print(x)
...     g()

>>> f()
enclosing

다음과 같이 x가 global, enclosing, local namespace에 정의될 경우에는 LEGB 룰에 따라 'local'이 출력됩니다.

>>> x = 'global'

>>> def f():
...     x = 'enclosing'
...
...     def g():
...         x = 'local'
...         print(x)
...     g()

>>> f()
local

 

Python namespace dictionaries

파이썬에서는 globals(), locals() 라는 내장함수를 통해 global namespace, local namespace에 접근할 수 있고 위에서 언급했다시피 각 namespace는 사전 타입으로 구성되어 있습니다. (Built-in namespace 같은 경우 dir(__builtin__) 을 통해서 볼 수 있듯이 사전 타입이 아닌 모듈로 구성됩니다.)

globals()

globals() 내장함수를 통해 global namespace에 접근할 수 있습니다. 프로그램 상에 x라는 변수를 선언하면 global namespace에 'x': 'foo' 가 추가된 것을 확인할 수 있습니다. 사전 타입이므로 globals()['x'] 를 통해 x변수의 객체를 참조할 수 있고 새로운 변수를 추가하거나 값을 변경할 수도 있습니다. 

>>> type(globals())
<class 'dict'>

>>> x = 'foo'

>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'x': 'foo'}

>>> globals()['y'] = 100

>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'x': 'foo', 'y': 100}

>>> y
100

>>> globals()['y'] = 3.14159

>>> y
3.14159

 

locals()

locals() 내장함수는 호출된 지역의 namespace를 출력합니다. 다음과 같이 함수 안에서 locals() 함수를 호출할 경우 함수 안의 s 변수 뿐만 아니라 매개변수인 x, y 또한 local namespace에 존재하는 것을 확인할 수 있습니다. 만약 메인 프로그램 상에서 locals() 함수가 호출된다면 globals() 함수와 동일한 결과를 출력합니다.

>>> def f(x, y):
...     s = 'foo'
...     print(locals())
...

>>> f(10, 0.5)
{'s': 'foo', 'y': 0.5, 'x': 10}

globals()와 locals()의 차이점은 globals() 함수는 global namespace 객체의 레퍼런스를 리턴하는 반면 locals()는 local namespace 객체의 현재시점의 복사본을 리턴한다는 점입니다. 따라서 다음과 같이 globals() 함수의 리턴값을 g라는 변수에 담은 이후에 전역 변수를 추가하면 그대로 반영되지만 locals() 함수는 리턴값이 정의된 이후의 변경은 적용되지 않습니다.

>>> g = globals()
>>> g
{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'g': {...}}

>>> x = 'foo'
>>> y = 29
>>> g
{'__name__': '__main__', '__doc__': None, '__package__': None,
'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None,
'__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,
'g': {...}, 'x': 'foo', 'y': 29}

>>> def f():
...     s = 'foo'
...     loc = locals()
...     print(loc)
...
...     x = 20
...     print(loc)
...
...     loc['s'] = 'bar'
...     print(s)
...

>>> f()
{'s': 'foo'}
{'s': 'foo'}
foo

 

Modify variables out of scope

그렇다면 함수 안에서 함수 밖의 변수 값을 어떻게 변경할 수 있을까요? 파이썬에서 함수의 매개변수는 다른 언어와 달리 특정한 타입이 지정되지 않습니다. 특히, 리스트나 사전 타입과 같은 변경가능한 타입 (mutable)은 함수 안에서 변경이 가능합니다. 먼저 다음과 같이 변경이 불가능한 타입에 대해서는 함수 안에서 지역적으로 값 변경이 불가능합니다. 함수 밖의 x값을 변경하는 것이 아니라 f()안의 namespace 상에서 새로운 x가 정의되는 것이므로 f()를 실행하면 40이 나오고 global namespace의 x값은 변경되지 않습니다.

>>> x = 20
>>> def f():
...     x = 40
...     print(x)
...

>>> f()
40
>>> x
20

하지만 다음과 같이 리스트와 같은 mutable type 객체는 함수 안에서 값 변경이 가능합니다. 하지만 함수 안에서 같은 이름의 변수로 새로운 리스트를 생성할 경우 위와 마찬가지로 local namespace 상에 같은 이름이 정의되기 때문에 global namespace 상의 이름에 접근할 수 없습니다.

>>> my_list = ['foo', 'bar', 'baz']
>>> def f():
...     my_list[1] = 'quux'
...
>>> f()
>>> my_list
['foo', 'quux', 'baz']

>>> my_list = ['foo', 'bar', 'baz']
>>> def f():
...     my_list = ['qux', 'quux']
...
>>> f()
>>> my_list
['foo', 'bar', 'baz']

 

global

파이썬에서는 global 키워드를 통해 함수 안에서 함수 밖의 변수에 접근할 수 있습니다. 다음과 같이 함수 안에서 "global x"를 통해 global namespace의 x를 가리키는 것임을 인터프리터를 통해 전달할 수 있습니다. 따라서 이후의 "x=40"은 local 변수를 생성하는 것이 아니라 함수 밖의 x 변수의 객체를 변경합니다.

>>> x = 20
>>> def f():
...     global x
...     x = 40
...     print(x)
...

>>> f()
40
>>> x
40

특히, global namespace 상의 해당 변수이름이 존재하지 않더라도 global 키워드를 통해 global namespace에 해당 변수를 선언할 수 있습니다.

>>> y
Traceback (most recent call last):
  File "<pyshell#79>", line 1, in <module>
    y
NameError: name 'y' is not defined

>>> def g():
...     global y
...     y = 20
...

>>> g()
>>> y
20

 

nonlocal

그렇다면 함수 안에 함수가 존재하는 중첩함수에 대해서도 global 키워드를 적용할 수 있을까요? 다음과 같이 g() 함수 안에서 "global x"를 선언한다 하더라도 x 변수는 enclosing namespace 상에서 존재하기 때문에 f()의 x 변수의 값은 그대로 20으로 남아있습니다. 오히려 global namespace 상에 x 변수가 40으로 생성된 것을 볼 수 있습니다.

>>> def f():
...     x = 20
...
...     def g():
...         global x
...         x = 40
...
...     g()
...     print(x)
...

>>> f()
20
>>> x
40

이떄 활용할 수 있는 것이 nonlocal 키워드입니다. nonlocal 키워드는 global과 비슷하게 동작하면서 해당 함수를 감싸는 가장 가까운 함수의 namespace에 접근합니다. 다음과 같이 f() 함수의 x값이 40으로 변경되었습니다.

>>> def f():
...     x = 20
...
...     def g():
...         nonlocal x
...         x = 40
...
...     g()
...     print(x)
...

>>> f()
40
반응형

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

파이썬의 memory management  (3) 2021.07.14
파이썬과 객체  (0) 2021.07.14
파이썬의 매개변수 전달 방식  (0) 2021.07.13
Property, Setter, Getter  (0) 2021.06.28
concurrent.futures를 이용한 병렬화  (0) 2021.06.27