네임스페이스와 스코프 그리고 심볼 테이블

Sawol·2021년 5월 4일
0

I Need Python

목록 보기
5/13
post-thumbnail

모듈패키지에 대해 공부하다가 여기까지 왔다. 모듈을 보고 있으니, 패키지가 나왔고, 그러다보니 심볼 테이블이 나왔고, 심볼 테이블에 대해 찾다가 네임스페이스스코프라는 키워드도 알게되었다.

심볼 테이블(Symbol Table)

Each module has its own private symbol table, which is used as the global symbol table by all functions defined in the module.

공식 문서를 보면서 모듈패키지를 공부하고 있는데 symbol table이라는 단어가 튀어나왔다. 나름 파이썬에 대한 자신감이 있었는데, 진짜 처음보는 단어였다. 대충 문맥상 변수나 클래스 등이 정의되면 심볼 테이블에 저장되는 것 같았다. 그러나 이렇게 대충 알고 넘어가면 내가 제일 싫어하는 수박에 겉핥기 식으로 공부하는 것이다. 바로 심볼 테이블에 대해 알아보기 시작했다.

심볼 테이블이 뭐야?

또 다시 공식문서를 찾아보기 시작했다. 항상 궁금증에 끝은 공식 레퍼런스라는 것을 난 알고 있다. 그러나 공식문서에 적힌 내용은 날 이해시키기에 미흡하다는 걸 알았다. 심볼 테이블을 만들고, 접근법에 대해 알려준다.😦 내가 궁금한건 이게 아니야!
열심히 구글링을 하다가 새로운 글을 발견했다.

A symbol table is a data structure maintained by a compiler which contains all necessary information about the program. These include variable names, methods, classes, etc.

심볼 테이블은 변수명과, 메서드, 클래스 등이 포함된 프로그램에 필요한 정보를 저장하는 곳이다. 좀 추상적인 정의이긴 한데, 일단 만족하고 계속 읽어봤다. 심볼 테이블은 크게 로컬 심볼 테이블글로벌 심볼 테이블, 이렇게 두 가지 종류로 나눌 수 있다고 한다. 로컬...? 글로벌...? 내가 아는 그건인가 싶어 계속 읽어봤다.

The local scope could be within a function, within a class, etc.
Likewise, a Global symbol table stores all information related to the global scope of the program, and is accessed in Python using globals() method.

그렇다. 우리가 흔히 말하는 지역 변수, 전역 변수를 말하는 것이 맞다! 근데 아직 뭔가 답답하다. 내 머리속에 퐉! 하고 꽂힐 정의가 필요하다.

The symbol table contains the declared types for all variables the compiler(or interpreter) encountered in the code.

이 문서에서는 컴파일러나 인터프리터가 코드에서 발견한 모든 변수가 심볼 테이블에 저장되어있다고 적혀있다. 인터프리터(또는 컴파일러)가 코드를 해석하면서 변수를 만나면 심볼 테이블에 값이 정의되어있나 살펴보고, 없다면 NameError를 일으킨다. 정의를 계속 찾아봐도 나를 100% 이해시켜주는 글이 없다. 이럴 때는 킵하고 다음으로 넘어간 뒤, 마지막에 다시 보았을 때 정의를 이해할 수도 있다.

심볼 테이블 보는 방법

정의만 자꾸 살펴보니 추상적으로만 개념이 잡힌다. 실제로 파이썬에서는 어떤 식으로 심볼 테이블이 구현되었는지 직접 확인해보고 싶어졌다. 로컬 심볼 테이블locals()를 통해, 글로벌 심볼 테이블globals()를 통해 확인 할 수 있다.

ff = 'hello world'

def outer(aa):
    cc = 10
    print("[outer] local symbol table : ", locals())
    print("[outer] global symbol table : ", globals())
    def inner():
        bb = 1
        print("[inner] local symbol table : ", locals())
        print("[inner] global symbol table : ", globals())
        return aa + bb + cc
        
    return inner()
    
    
outer(1)
print("[base] local symbol table : ", locals())
print("[base] global symbol table : ", globals())

>>> [outer] local symbol table :  {'aa': 1, 'cc': 10}
>>> [outer] global symbol table :  {'__name__': '__main__', '__doc__': None, '__package__': None, 
'__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000002AB2AD30880>, 
'__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 
'__file__': 'C:/Users/User/PycharmProjects/algo_solv/test2.py', '__cached__': None, 
'ff': 'hello world', 'outer': <function outer at 0x000002AB2AD171F0>}
>>> [inner] local symbol table :  {'bb': 1, 'aa': 1, 'cc': 10}
>>> [inner] global symbol table :  outer 와 동일
>>> [base] local symbol table :  outer 와 동일
>>> [base] global symbol table :  outer 와 동일

위 코드를 보면 심볼 테이블은 dictionary 타입이고, 여러 정보를 담고 있는 것을 볼 수 있다. 또한 global은 어디서 쓰든 제일 바깥쪽에 있는 base를 의미하고, local은 현재 local() 모듈을 호출한 위치를 토대로 출력된다.

심볼 테이블 수정하기

age = 23

globals()['age'] = 25
print('The age is:', age)
>>> The age is: 25

글로벌 심볼 테이블이 딕셔너리이기 때문에 간단히 변경 가능하다.

def localsPresent():
    present = True
    print(present)
    locals()['present'] = False;
    print(present)

localsPresent()
>>> True
>>> True

다만, 로컬 심볼 테이블은 내부 정보를 변경 불가능하다. 왜 로컬은 안되고 글로벌은 될까? 공식 문서를 살펴봤는데 따로 어떠한 설명이 없었다. 나와 같은 생각을 하는 사람들이 을 봤는데... 설계상 그렇게 되어있기 때문에 그 이상의 이유는 필요없다고 한다...흠.☹️

결국 심볼 테이블이란?

keyvalue로 구성되어 있는 dictionary 타입의 데이터 구조로 패키지함수, 클래스 정보를 포함한 코드 내에 정의된 여러 변수들에 대한 정보가 담긴 테이블이다.
함수를 실행하면 함수의 로컬 심볼 테이블이 생기고, 함수 내의 모든 변수 할당은 이 곳에 저장된다. 그래서 인터프리터가 코드를 해석할 때 변수의 실제 값을 찾기위해 로컬 심볼 테이블을 먼저 찾아보고 그 후로, 그 다음으로 함수를 둘러싸고 있는 로컬 심볼 테이블에서 찾아보고, 글로벌 심볼 테이블, 내장 심볼 테이블 순으로 찾는다. 여기를 참고하면 해당 내용을 볼 수 있다.
그리고 각 모듈 별로 글로벌 심볼 테이블을 갖고 있는데, 그래서 사용자의 전역 변수와 충돌이 발생하지 않고 모듈에서 전역 변수를 사용할 수 있다. 이는 모듈 레퍼런스에 적혀있다.

네임스페이스(NameSpace)

여기까지 글을 읽었다면 의문이 생길 것이다. 엇, 이 내용 어디서 봤는데? 네임스페이스의 정의 아닌가? 그래서 네임스페이스에 대해서도 다시 한번 찾아보기 시작했다.

네임스페이스는 뭐야?

The place where a variable is stored. Namespaces are implemented as dictionaries. There are the local, global and built-in namespaces as well as nested namespaces in objects (in methods).

공식 문서에서는 네임스페이스란 변수가 저장되는 공간으로 딕셔너리 형태를 갖고 있다고 한다. 또한, 메서드 내 외에도 local, global, built-in 에도 네임스페이스가 있다. 또 다른 문서를 찾아보면 네임스페이스와 스코프를 정의하고 있는데 일단 네임스페이스 부분만 살펴보자.

A namespace is a mapping from names to objects. Most namespaces are currently implemented as Python dictionaries, but that’s normally not noticeable in any way (except for performance), and it may change in the future.

네임스페이스란 어떠한 객체에 대한 이름(변수)의 매핑을 말한다.즉, 객체에 접근할 수 있는 방법이 이름 이라는 것이다. 또한 파이썬에서 대부분 딕셔너리로 구현되어 있으나 추후 바뀔 수 있다고 한다.
어쨌든 두 문서를 종합 해보자면 네임 스페이스란 파이썬에서는 객체를 변수에 매핑하여 해당 객체에 접근하는데 어떤 객체가 어떤 이름을 가지는지 저장하는 공간을 말한다.

a = 1 # namespace {a: 1}

# does `a` exist in the namespace? Yes!
# proceed without error
print(a) # => 1

# does `b` exist in the namespace? No.
# Throw a `NameError`
print(b) # => NameError: name `b` is not defined.

b = 16 # namespace {a: 1, b: 16}

# no more problems!
print(b)

네임스페이스는 서로 다른 네임스페이스가 공존 할 수 있지만 서로 완전히 격리되어있다. 내장 함수를 포함한 네임스페이스는 인터프리터가 시작할 때 생겨, 인터프리터가 실행되는 동안 존재한다. 그래서 프로그램 어디에서든 id(),print()등과 같은 내장 함수를 항상 사용할 수 있는 것이다.
모듈 또한, 자체 네임스페이스를 갖고 있기때문에 다른 모듈에 존재할 수 있는 이름이 충돌하지 않는다.

그럼 심볼 테이블과의 차이점은?

네임스페이스에 대해 다시 알아보아도 심볼 테이블과 완전히 동일하다. 어째, 공부할수록 모르는것만 느는걸까...😥
심볼 테이블과 네임스페이스의 차이점에 대해 의문을 품는 사람이 전세계에 나만 있을거라고 생각하지 않는다. 다시 구글링을 시작했다.
한참을 찾다가 나의 궁금증을 해결한 고마운 글을 발견했다. 역시 나와 같은 고민을 한 사람이 있었고, 아주 멋진 답글이 달렸다!

A symbol table is an implementation detail. Namespaces are implemented using symbol tables, but symbol tables are used for more than just namespaces. For example, functions have their own symbol table for local variables, but those variables do not exist in any namespace (that is, it is impossible to somehow access the local variables of a function using a fully-qualified name).

네임스페이스라는 추상적인 개념을 심볼 테이블로 구현한 것이다! 심볼 테이블은 네임스페이스 외에도 다양한 용도로 쓸 수 있는데, 지역 변수에 대한 심볼 테이블은 존재하지만, 네임스페이스는 존재하지 않는다는 것이 대표적인 예이다.
또한, 앞서 네임스페이스에 대한 공식 문서에서 현재는 딕셔너리로 구현 되어있지만, 추후 바뀔 수 있다.라고 언급했다. 즉, 딕셔러니 구현 = 심볼 테이블 구현으로 보면 된다.
이제야 왜 심볼 테이블과 네임스페이스의 개념이 겹치는지 알 수 있었다!

스코프(scope)

하지만 아직 끝난게 아니다. 네임스페이스를 살펴볼 때 계속 언급되었지만, 모르는 척 넘어갔던 스코프에 대해서 알아봐야한다. 물론 앞서 글에서 철저히 스코프라는 단어를 무시하여 언급하지 않았지만, 사실 내가 찾아본 거의 대부분의 문서에 스코프가 등장했다.😭

스코프가 뭐야?

앞서 글을 모두 이해했다면, 프로그램내에 다양한 네임스페이스가 있다는 것을 알 것이다. 그리고 또한 서로 격리되어 어떤 네임스페이스에서는 다른 네임스페이스에 접근을 못한다는 것도 알 것이다. 이때 접근 할 수 있는 네임스페이스는 현재 범위에 따라 결정된다. 여기서 말하는 범위공식 문서에 따르면 여러 네임스페이스 중 어떤 네임스페이스에 접근 할 수 있는지 결정하는 프로그램의 텍스트 영역이다. 여기서 말하는 텍스트 영역은 사용자가 작성하는 코드 영역을 말하는 것 같다.
즉, 현재의 스코프를 알아야 접근할 수 있는 네임스페이스를 알 수 있다.

이런 걸 왜 사용할까?

개발을 하다보면 아주 많은 모듈을 사용하고 변수를 사용하고 클래스를 사용한다. 이 모든 이름들을 다 다르게, 즉 모두 유니크하게 만들려면 개발자들의 머리가 다 빠져버릴거다. 네임스페이스 덕분에 변수를 반복해서 사용할 수 있어 개발자들의 편리를 제공한다. 사실 지금도 변수명을 짓는 건 어렵다.

번외

파이썬의 LEGB

파이썬에서 변수, 네임스페이스 또는 스코프를 지정할 때 LEGB라는 용어를 사용한다. 이는 4개의 스코프(범위)를 나타내며 아래와 같다.

  • L ocal : 함수 내에서 할당된 이름
  • E nclosing : 함수 내에 또 함수가 있을 때 바깥 함수에 할당되는 이름
  • G lobal : 모듈의 최상위 수준에 할당되는 이름
  • B uilt-in : open, import, print 등과 같은 내장 모듈

이러한 스코프에 필요한 몇 가지 규칙은 아래와 같다.

  • 가장 낮은 스코프는 local이므로 가장 높은 built-in의 이름에 접근 가능하다.
  • 낮은 스코프는 항상 높은 스코프의 이름에 접근 가능하다.
  • 높은 스코프는 낮은 스코프의 이름에 직접 엑세스 할 수 없다.
def outer_function():
    b = 20
    def inner_func():
        c = 30

a = 10

여기서 a는 글로벌 네임스페이스에 존재한다. bouter_function()의 로컬 네임스페이스에 존재한다. cinner_function()의 로컬 네임스페이스에 존재한다.
inner_function()를 실행할 때 우리에게 이 곳이 로컬이다. b는 로컬이 아니고, a는 글로벌이다. 그래서 couter_function()와 글로벌에서 읽을 순 없다. 그래서 만약 outer_function()에서 c를 바꾸려고 c = 10이라고 하면 outer_function()의 네임스페이스에 새로운 c가 할당되는 것이다.

def outer_function():
    a = 20

    def inner_function():
        a = 30
        print('a =', a)

    inner_function()
    print('a =', a)


a = 10
outer_function()
print('a =', a)
>>> a = 30 
>>> a = 20 
>>> a = 10

그렇다고 함수가 전역 변수를 변경할 수 있는 것은 아니다. 위 예제를 보면 전역 변수는 그대로 10으로 출력되었다. 즉, 함수에서 전역 변수에 접근할 때 읽기 전용으로 접근을 한다는 것이다. 즉, 함수에서 전역변수를 로컬 네임스페이스에 복사를 한 뒤 사용한다. 그러니 이 함수가 종료되면 로컬 네임스페이스도 사라지니, 전역 변수가 변하지 않는다.
다만, 개발을 하다보면 내부 함수에서 전역 변수를 변경하고 싶을 때가 있다. 이때, global를 사용하면 전역 변수가 로컬 네임스페이스에 생성되지 않고 글로벌 네임스페이스에 직접 접근할 수 있다.

def outer_function():
    global a
    a = 20

    def inner_function():
        global a
        a = 30
        print('a =', a)

    inner_function()
    print('a =', a)


a = 10
outer_function()
print('a =', a)
>>> a = 30 
>>> a = 30 
>>> a = 30

모듈에서 네임스페이스 살펴보기

모듈을 import하는 과정에서 네임스페이스에 대해 살펴보겠다.

my_string  = 'spam and eggs'
my_number  = 42

위 코드가 들어있는 spam.py라는 모듈이 있다. 이를 아래와 같이 import한다.

import spam

print(spam.my_string)  # spam with eggs
print(spam.my_number)  # 42

print(globals())	# {'__name__': '__main__', ... 'spam': <module 'spam' from 'C:\\Users\\spam.py'>}

보다시피 모듈 이름을 접두사로 사용해서 모듈의 네임스페이스에 접근할 수 있다. 다만, 모듈의 이름(변수)가 글로벌 네임스페이스에는 없다. 단지 spam 모듈 그 자체만 있을 뿐이다.
모듈의 모든 변수를 글로벌 네임스페이스에 갖고오고 싶으면 아래와 같이 하면 된다.

from spam import *

print(my_string)  # spam with eggs
print(my_number)  # 42

print(globals())	# {'__name__': '__main__', ... 'my_string': 'spam and eggs', 'my_number': 42}

더 깊이 알고싶어요!

Eli Bendersky의 블로그에 Cpython에서의 심볼 테이블 구조가 자세히 적혀있다. 글은 1편 ,2편 으로 나누어져있다.
또한, 이 문서이 문서에서는 네임스페이스와 스코프에 대해 예시와 함께 알려준다.

2개의 댓글

comment-user-thumbnail
2022년 2월 23일

좋은 글입니다. 컴파일러 관련 수업을 들어보면 symbol table에 대해서 꽤 자세하게 더 배울수 있습니다.

답글 달기
comment-user-thumbnail
2023년 1월 17일

좋은 글 잘 읽고 갑니다 홓홓

답글 달기