본문 바로가기

Python

Python Scoping Rule ( by LEGB rule )

파이썬에서 Scope 즉 범위의 개념은 코드에서 변수와 name을 찾는 방법을 결정한다. 이러한 변수와 name의 범위는 일반적으로 해당 변수 혹은 name을 생성하는 코드상의 위치에 따라 나뉘게 된다. Python에서의 Scoping rule은 LEGB 규칙이라고 부르는 rule에 의해서 정의된다. 그렇다면 먼저 파이썬에서 범위 Scope란 무엇이며, 파이썬에서 어떻게 동작할지에 대해서 알아보자.

Scope

프로그래밍에서 name의 scope는 변수, 함수, 객체 (파이썬에서는 모두 객체이지만) 등과 같이 해당 name에 명확하게 접근할 수 있는 프로그램 영역으로 정의된다. 이러한 Scoping rule을 사용하는 이유는 name의 충돌 및 혼란을 피하기 위해서이다. 우리가 흔히 알 듯 범위는 일반적으로 크게 두가지 Global, Local로 나뉜다. Global scope에서 정의한 name을 모든 코드에서 사용할 수 있고, Local scope에서 정의한 name은 범위 내의 코드에서만 사용가능하다. 이렇게 사실 프로그래밍 언어는 프로그램 코드에서 특정 name에 접근할 수 있을 때 그 name의 scope에 있다고 정의하고, 접근할 수 없다면 해당 scope를 벗어난다고 한다. 

결국 우리는 파이썬에서 name이 할당 및 정의가 되는 위치를 통해서 특정 범위와 연결한다. 즉, 코드상에서 이름을 할당하거나 정의하는 위치에 따라서 해당 name의 scope또는 가시성이 결정된다는 것이다. 일반적으로 name을 할당 혹은 정의하는 방법으로는 무엇들이 있을까?

OperationStatement

Assignments x = value
Import operations import module or from module import name
Function definitions def my_func(): ...
Argument definitions in the context of functions def my_func(arg1, arg2,... argN): ...
Class definitions class MyClass: ...

예를 들자면 이렇게 함수를 정의한 후 그 내부에서 어떤 변수name에 값을 할당하면 해당 name은 local 한 범위를 가지는 것으로 해당 함수 내에서만 접근이 가능하다. 비유를 들어보면 좋다. 다른 scope를 가졌다는 것은 다른 세계인 것이다. def myhouse() 즉 myhouse라는 함수에 즉 내집에 어떤 숟가락과 수저가 있고, 내가 거기에 어떤 이름을 붙였다고 하자. 이것을 def friendhouse() friendhouse라는 함수에 즉 내 친구집에서 내 집에 어떤 숟가락과 수저가 있고, 어떤 이름을 붙였는지 알 수 있을까? 전혀아니다. 거꾸로 나도 내 친구집에 어떤게 있고, 어떻게 이름지었는지 알 수 없다는 것이다.

NameSpace

이러한 scope의 개념은 namespace의 개념과 밀접하게 연결되어 있다. 앞서 계속 name에 대해 이야기했는데 이 name이란 무엇일까? 프로그래밍 언어에서는 특정한 객체를 name에 따라서 구분한다. 그리고 파이썬에서 모든 것은 객체로 되어있어, 각각의 객체의 특정 이름과 그 객체가 매칭관계를 가지게 되는데 이러한 mapping을 포함하고 있는 전체 공간을 namespace라고 한다. 이러한 namespace는 파이썬에서 dictionaty형태로 구성되어있다. 그래서 각 객체의 name의 경우 string으로 되어있고 해당 네임스페이스의 범위에서 실제 특정 객체를 가리킨다. 런타임내에서 이러한 namespace는 가변적이지만, built-in namespace같은 경우 함부로 변경할 수 없다. 이는 파이썬에서 special attribute 혹은 magic method라고 불리는 것들 중에 __dict__를 통해서 확인할 수 있다.

>>> import sys
>>> sys.__dict__.keys()
dict_keys(['__name__', '__doc__', '__package__',..., 'argv', 'ps1', 'ps2'])
>>> sys.ps1
'>>> '
>>> sys.__dict__['ps1']
'>>> '

https://blog.confirm.ch/python-namespaces/

 

이러한 namespace가 필요한 이유는 프로그래밍과정에서 모든 name을 겹치지않게 하기도 어렵고, 만약 겹쳤을 때 우리의 의도에 맞게 동작하게 하기 위해서는 헷갈리지 않도록 각 namespace를 정의한 후 특정 namespace의 name을 활용하도록 해야하는 것이다.

LEGB Rule

이러한 네임스페이스의 접근성을 위해서 고안된 개념이 Scope다. 즉 파이썬에서 변수나 함수이름 같은 name을 사용할 때마다 python은 다른 범위 수준의 namespace를 검색하여 해당 name이 존재하는지의 여부를 결정한다. Python에서 사용하는 이러한 검색 메커니즘을 LEGB Rule이라고 한다. LEGB 는 Local, Enclosing, Global, Built-in이다. LEGB 규칙은 파이썬이 name을 검색하는 순서를 결정하는 메커니즘으로 L -> E -> G -> B 순서대로 순차적으로 name을 검색한다. 만약 name이 현재의 level에서 검색되었다면 즉 존재한다면 바로 가져오고, 그렇지 않다면 그 다음 level에서 검색을 하고 모두 검색 후 없다면 Error를 반환한다. 물론 이 경우는 현재 local 범위에 있을 경우이고, 그 외의 다른 파이썬 상의 범위에서 어떤 name에 접근하려고할 때 없다면 해당 scope부터 상위의 scope를 순차적으로 탐색하게 된다.

https://velog.io/@idnnbi/Python-Variable-Scope

그렇다면 각 L E G B 의 scope이 어떻게 정의되는지를 확인해보자.

  • Local : def로 정의되는 함수 혹은 lambda를 통해 정의되는 코드 블럭이 local 범위이다. 이 범위내에서 정의 혹은 할당된 name은 해당 코드 블럭 내에서만 볼 수 있다. 이 loacl 범주의 경우 함수를 정의할 때가 아닌 함수를 호출할 때 생성되므로 함수 호출만큼 다양한 local 범위를 가지게 된다. 즉, 우리가 def func(a)라는 함수를 정의하고 이 함수를 func(1) ,func(2) 와 같이 두번 호출하게 될 경우 각 호출만큼 local 범위가 새롭게 생성되는 것이다. 아래와 같이 local범위에서 정의된 result라는 name의 경우 local 범위가 끝날 때 종료되므로, local 범위 밖에서 접근할 수 없는 것이다.
    >>> def square(base):
    ...     result = base ** 2
    ...     print(f'The square of {base} is: {result}')
    ...
    >>> square(10)
    The square of 10 is: 100
    >>> result  # Isn't accessible from outside square()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        result
    NameError: name 'result' is not defined
    
    # https://realpython.com/python-scope-legb-rule/​​
  • Enclosed : 중첩 함수에만 존재하는 특수 범위로 중첩함수에서 안쪽의 함수의 코드블럭을 local 그 바깥쪽의 코드블럭을 enclosed라고 한다. 따라서 enclosed한 범위에 있는 변수는 그 바깥쪽 코드블록에서만 접근할 수 있는 것이다.(물론 이것도 함수 호출마다 새롭게 범위가 생성된다.) 아래와 같이 outer_func 안에서 정의된 name인 inner_func같은 경우 해당 범위에서는 접근이 가능하지만, 더 상단의 범위에서 호출하고자 하면 name error를 발생하는 것을 볼 수 있다.
    >>> def outer_func():
    ...     # This block is the Local scope of outer_func()
    ...     var = 100  # A nonlocal var
    ...     # It's also the enclosing scope of inner_func()
    ...     def inner_func():
    ...         # This block is the Local scope of inner_func()
    ...         print(f"Printing var from inner_func(): {var}")
    ...
    ...     inner_func()
    ...     print(f"Printing var from outer_func(): {var}")
    ...
    >>> outer_func()
    Printing var from inner_func(): 100
    Printing var from outer_func(): 100
    >>> inner_func()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    NameError: name 'inner_func' is not defined
    
    # https://realpython.com/python-scope-legb-rule/
  • Global : 파이썬 프로그램, 스크립트 혹은 모듈의 최상위 범위로, 최상위 수준에서 정의하는 모든 name이 이 범위에 포함된다. 따라서 global하게 정의된 name은 코드의 모든 위치에서 볼 수 있다.
    우리가 어떤 파이썬 프로그램을 시작하려는 순간 우리가 있는 곳이 파이썬의 global scope다. 즉, 파이썬은 파이썬의 메인 스크립트를 __main__이라고 불리는 모듈로 바꾸고, 그 메인 프로그램을 실행시킨다. 이 __main__ 모듈의 namespace가 main global scope인 것이다. 파이썬의 special attribute인 __name__을 실행하면 현재 모듈의 이름을 반환해준다. 따라서, 우리가 현재 실행하는 모듈인 경우 __name__이 __main__을 가지게 되고, 직접 실행하지 않는 import되는 모듈의 경우 모듈의 이름(파이썬에서는 파일명)을 가진다.
    # module.py
    def hello():
    	print("Hello!)
       
    print(__name__)
    # main.py
    
    import module
    print(__name__)
    
    >>> module
    >>> __main__​​
    따라서 우리가 종종 보는 if __name__ == "__main__"의 경우 해당 모듈을 직접 실행시켰을 때만 실행되길 원하는 코드들을 넣어주는 것이다. 추가적으로 dir() 이라는 built in function을 사용해서 현재 scope에서 사용할 수 있는 name들의 리스트를 확인해볼 수도 있다. 이러한 scope는 프로그램이 종료되거나 이 names가 지워지기 전까지 존재하게 된다.
     그런데 주의해야할 부분이 있다. 파이썬은 함수 내부에서 전역 name에 값을 할당하려고 하면 함수의 로컬범위에서 해당 이름을 만들고, 전역 name을 숨기거나 재정의하게 된다. 즉, 함수 내부에서는 함수 외부에서 정의된 대부분의 변수를 변경할 수 없다는 것이다. 이렇기에 아래와 같은 코드들이 에러를 반환하게 된다.
    >>> var = 100  # A global variable
    >>> def increment():
    ...     var = var + 1  # Try to update a global variable
    ...
    >>> increment()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        increment()
      File "<stdin>", line 2, in increment
        var = var + 1
    UnboundLocalError: local variable 'var' referenced before assignment​
    이렇게 전역변수를 증가시키려고 할 때, var가 내부에 선언되지 않았기 때문에 파이썬은 함수 내부에 동일한 이름의 새 지역변수를 생성하게 되고, 이 과정에서 파이썬은 첫번째 할당전에 (함수 호출되어서 진짜 할당되기 전에) 로컬을 사용하려한다는 것으로 인지해서, var을 local 변수로 해석하는데 우리는 local에서 var이라는 변수에 대해 정의 한적이 없기에, 이러한 bound error가 발생하게 된다.( LEGB규칙에 따라 접근은 되지만, 이러한 수정, 변경을 하려한다면 새롭게 정의되는 방식을 파이썬이 가지고 있다는 것을 알아야한다.) 이러한 관점에서 아래의 코드또한 마찬가지로 에러를 반환한다. 왜냐하면 함수 내에서 var을 재할당하는 코드가 있기에 파이썬은 이 var을 지역변수로 인지하게 되고, 실제로 이 함수가 호출되어서 var에 접근하게 될 때 local variable var을 찾는데, 없기에 해당 에러가 발생하는 것이다.
    >>> var = 100  # A global variable
    >>> def func():
    ...     print(var)  # Reference the global variable, var
    ...     var = 200   # Define a new local variable using the same name, var
    ...
    >>> func()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        func()
      File "<stdin>", line 2, in func
        print(var)
    UnboundLocalError: local variable 'var' referenced before assignment​
     
  • >>> dir() ['__annotations__', '__builtins__',..., '__package__', '__spec__'] >>> var = 100 # Assign var at the top level of __main__ >>> dir() ['__annotations__', '__builtins__',..., '__package__', '__spec__', 'var']​

지금까지 다룬 python scope에 대한 내용을 하나의 예시를 통해 총정리해보자. 

>>> # This area is the global or module scope
>>> number = 100
>>> def outer_func():
...     # This block is the local scope of outer_func()
...     # It's also the enclosing scope of inner_func()
...     def inner_func():
...         # This block is the local scope of inner_func()
...         print(number)
...
...     inner_func()
...
>>> outer_func()
100

outer_func()가 실행될 때 LEGB 규칙에 따라서 number라는 name을 가진 변수에 다음과 같은 순서로 접근하게 된다. 먼저 local scope인 inner_func() 에서 local scope에서 정의된 number가 존재하지 않는다. 따라서 enclosed scope로 가서 number를 탐색하지만, 여기도 존재하지 않는다. 따라서 global scope에서 number를 탐색하고 이 코드의 경우 찾을 수 있어서, 해당 number 100을 출력하게 되는 것이다. 

  • Built-in : 스크립트를 실행할 때마다 생성되거나 로드되는 특수한 범위로, keyword, function, exception 및 파이썬에 내장된 기타속성과 같은 것들이 포함된다. 이 범위또한 global과 마찬가지로 코드의 모든 위치에서 사용할 수 있다. 이 경우 프로그램을 실행할 때 파이썬에 의해 자동으로 로드된다.
    사실 이 built-in이라는 scope는 builtins라는 파이썬 표준 라이브러리 모듈로 구현되는 특수한 범위이다. 파이썬의 모든 내장 객체는 이 builtins라는 모듈안에 있고, python 인터프리터를 실행할 때 자동으로로드되게 된다. 따라서 우리는 LEGB 규칙에 따라서 모듈을 가져오지 않고도 이름을 통해서 이 builtins안의 객체들에 대해서 접근할 수 있는 것이다.
    >>> dir()
    ['__annotations__', '__builtins__',..., '__package__', '__spec__']
    >>> dir(__builtins__)
    ['ArithmeticError', 'AssertionError',..., 'tuple', 'type', 'vars', 'zip']​​

     

지금까지 우리는 파이썬의 scoping rule에 대해서 그리고 이러한 scope가 어떤식으로 동작하는지에 대해 배웠고, 코드의 모든 위치에서 global name에 접근할 수 있고, global scope에서 이러한 global name을 수정할 수 있다는 것을 알았다. 추가적으로 local name이 생성된 local scope에서만(enclosed scope도) local name에 접근할 수 있고, 다른 local scope나 global scope에서는 접근할 수 없으며, 이러한 enclosed scope나 local scope에서 상위 level의 name에 접근할 수 있지만, 수정하거나 업데이트할 수는 없다는 것을 배웠다.

결국 자신의 scope에서 다른 상위 level의 scope에 대해서 접근 읽기는 가능하지만, 그 scope에 뭔가를 수정 변경하는 쓰기는 제한적인 것이다. 그리고 함수는 자신의 제어권을 가장 많이 확보하고 있는 가장 가까운 scope부터 찾아나가는 것이다.(LEGB rule)

이렇게 파이썬의 scoping rule을 알아봤다. 이것이 일반적인 규칙이다. 하지만, 언제나 예외가 있는 것처럼 파이썬에서도 이러한 규칙의 동작을 수정할 수 있는 방법이 있다.


여기서부터는 추후에 업데이트할 예정입니다.

( 내용 : galobal, nonlaocal, 일급객체, closure, class 및 인스턴스의 속성범위)

 

 

References

https://realpython.com/python-scope-legb-rule/

 

Python Scope & the LEGB Rule: Resolving Names in Your Code – Real Python

In this step-by-step tutorial, you'll learn what scopes are, how they work, and how to use them effectively to avoid name collisions in your code. Additionally, you'll learn how to take advantage of a Python scope to write more maintainable and less buggy

realpython.com

https://shoark7.github.io/programming/python/closure-in-python

 

Python의 Closure에 대해 알아보자

Python에서 유용한 Closure에 대해 살펴봅니다.

shoark7.github.io

https://hyoje420.tistory.com/45

 

[Python]if __name__ == "__main__"

파이썬의 모듈에 아래와 같은 코드가 존재할 때가 있다. <코드> if __name__=="__main__" 그대로 해석해보면 '__name__이라는 변수의 값이 __main__이라면 아래의 코드를 실행하라.'라는 뜻이다. 위 글을 이

hyoje420.tistory.com

 

'Python' 카테고리의 다른 글

Python - Variable Length Arguments  (0) 2022.02.02
Python - Iterable / Iterator / Generator  (0) 2022.02.01
Python - Call by Object Reference  (0) 2022.02.01
Python Dynamic Typing  (0) 2022.01.28
Python이라는 언어  (0) 2022.01.24