programming ⌨/파이썬 Python

Python : import system

Kortsec1 2023. 11. 16. 23:49

최근, Python을 이용해 간단한 웹 서버를 구축하고 이를 시험하면서 구조적인 이해의 필요성을 느꼈습니다.
개인적으로 호기심이 생긴 기본적인 개념부터 차근차근 뜯어보려 합니다.
 
Python 코드는 import를 통해 여러 모듈과 연결을 할 수 있습니다.
이번 포스팅 에선 import system의 작동 방식을 세세하게 알아볼 예정입니다.
 
 
 

1. 기본적인 import

>>> import math
>>> math.pi
3.141592653589793

import 구문은 호출 시 기본적으로 다음 두 가지의 과정을 거칩니다.

  1. 해당하는 모듈을 찾고, 필요하다면 불러옵니다.
  2. local namespace에 1번의 결과에 해당하는 이름들을 모읍니다.
    • If the module name is followed by as, then the name following as is bound directly to the imported module.
    • If no other name is specified, and the module being imported is a top level module, the module’s name is bound in the local namespace as a reference to the imported module
    • If the module being imported is not a top level module, then the name of the top level package that contains the module is bound in the local namespace as a reference to the top level package. The imported module must be accessed using its full qualified name rather than directly

 
1번의 과정에서 Python은 적절한 인자를 포함하여 built-in 함수인, __import__()를 호출합니다.

import 구문의 활용 없이 직접적인 __import__()의 호출은 상위 패키지 불러오기, 다양한 캐시에 정보를 업데이트시키기 등의 정상적인 실행이 이루어 지지 않을 수 있습니다.
이어서 __import__() 함수의 결과를 이용하여 2번 과정을 수행합니다.
 

>>> from math import pi
>>> pi
3.141592653589793

from이 포함된 import 구문은 더욱 복잡한 단계를 거치지만, 위와 비슷한 과정이기에 상세한 설명은 제외하겠습니다.
 
 
 

2. 패키지와 import

Python 에는 앞서 시작부분에서 언급한 모듈들을 활용하기 위해 파일과 디렉터리로 구성된 package가 존재합니다.
아래 img_1에서 알 수 있듯 패키지는 모듈의 범주에 포함되어 있습니다.
참고로 모든 모듈이 패키지는 아닌데, 이들을 구분할 수 있는 패키지의 attribute로 __path__가 있습니다.

Specifically, any module that contains a __path__ attribute is considered a package
- https://docs.python.org/3/reference/import.html#packages

 
Package에 관한 포스팅에서 추후 다룰 내용이니, 간단하게만 짚고 넘어가자면
__path__는 부모패키지 속 서브패키지들을 효율적으로 불러오기 위해 사용되는 속성입니다.
 

img_1 모듈과 패키지

 
 
패키지 속에는 __init__.py 파일이 존재하는데, 이 파일은 해당 패키지 임포트 시 자동으로 실행됩니다.
이후 __init__.py의 내용에 따라 다른 모듈을 추가할 수도 있고 추가적인 속성을 설정할 수도 있습니다.
 

import 구문 발생 시, __import__() 함수는 불러오고자 하는 모듈이 패키지인지, 해당 패키지 속 submodule인지 확인합니다. 구체적인 이해를 위해 아래 img_2를 그려봤습니다.

img_2 __import__함수의 탐색순서

 


 

3. import 프로세스 : Searching

앞서 <1. 기본적인 형태의 import>에서 짧게 소개되었던 단계를 기억하시나요?
 
모듈을 찾기 위해 여러 가지 방법의 import 구문을 통해 이름을 받습니다.
이름은 점(.)을 통해 submodule을 구분할 수 있습니다.

import A.B.C

위의 경우 A → B → C 의 순서로 import를 시도하게 됩니다.
구체적으로 어떻게 import를 시도할까요?
 
우선, 해당하는 모듈이 sys.modules에 있는지 확인하는 과정을 거칩니다.
sys.modules는 이전에 import 되었던 모듈들이 저장되어 있는 dirtionary입니다.

sys.modules를 가장 먼저 탐색하는 것은 추후 모듈을 불러오는 과정에서 발생할 수 있는 무한 반복(unbounded recursion) 예방과도 관련이 있습니다.  자세한 내용은 <4. import 프로세스 : Loading>에서 살펴보겠습니다.
 
모듈 이름의 key가 있다면 연결된 값인 module object를 반환하며 import 과정을 끝마치고,
key와 연결된 값이 None이라면, ModuleNotFoundError를 발생시킵니다.

img_3 foods.meat의 import 오류 모습

 
 
두 번째로 sys.modules에 찾는 모듈이 없다면 finder objects를 이용하여 찾는 과정에 들어갑니다.

finder는 각각 정해진 탐색 범위에 따라 다양한 object들이 존재합니다.

  • built-in modules
  • forzen modules
  • file system paths
  • zip files
  • URLs

 또한, find_spec()에 새로운 방식으로 도달하는 finder를 추가하여 더욱 넓고 다양한 범위에서 모듈을 찾을 수 있습니다.

find_spec()이라는 메서드는 주어진 모듈이 사용가능한지를 알려줍니다. 구조는 아래와 같습니다.

find_spec(fullname, path, target=None)
    fullname : 임포트 될 모듈의 fully qualified name
    path : submodules, subpackages를 위한 parent package의 __path__속성
        * 그 외 top-level 모듈을 찾기 위함이라면 None
    target : 추후 불러올 이미 존재하는 module object. 기본값은 None

 

이렇게 다양한 탐색과정을 거친 후 모듈을 발견하면 module spec을 반환합니다.

module spec object에는 해당 모듈의 속성과 모듈을 실행시키는 데 중요한 역할을 하는 loader가 들어있습니다.

Python 3.4 이후 버전부터는 loader를 직접 리턴 시키는 대신, loader가 포함된 module spec을 반환합니다.

 

 

 

4. import 프로세스 : Loading

module spec이 반환된다면, 마지막 단계인 loader를 통해 모듈을 불러오는 과정이 시작됩니다.

모듈에 대한 다양한 정보를 담고 있는 module spec을 finder가 만들었다면, loader는 이를 실행하는 역할을 합니다.

loading 과정을 아래 그림을 참고하여 살펴보겠습니다.

img_4 loading module

 

sys.modules 확인

loading 과정으로 돌아와서, 가장 먼저 <3. import 프로세스 : Searching>과 동일하게 주어진 모듈명이 sys.modules에 존재하는지 확인합니다.

존재한다면 기존 모듈을 반환하고, 존재하지 않는다면 불러오는 절차를 거친 뒤 코드 실행 전 sys.modules에 등록하게 됩니다.

불러오고 실행하는 과정에서 오류가 발생한다면 sys.modules에서 삭제됩니다.

 

attribute check

loader objectload_module()이란 메서드를 호출하는데, 이때 필요한 요소들이 있습니다.

__file__ loader가 모듈을 load한 위치
- built-in 모듈의 경우 설정하지 않음
__name__ 모듈의 fully qualified name
__path__ 패키지 속 submodules의 위치가 담긴 list
- import 대상이 패키지일 경우 사용
__loader__ 사용된 loader 객체
__package__ 해당 모듈이 속해있는 패키지의 이름

모두 갖춰졌다면 loader는 다음 단계로 이끕니다.

 

execute code

해당 모듈이 Python module이라면 모듈 속 global namespace인 module.__dict__안에서 코드를 실행시킵니다.

Python module이 아닌 경우라면 built-in 모듈, C로 구현된 확장 모듈의 동적 로드 등이 있습니다.

 

return module object

load 된 모듈 object를 반환하면서 loading 과정을 끝마치게 됩니다.

 

추가적으로, 처음 언급되었던 sys.modules에 등록되는 시점을 알아야 합니다. 3번 단계에서 loader가 모듈의 코드를 실행시키지 전에 등록이 되어야 하는데, 이는 모듈이 자신을 import 할 시 무한 루프에 빠질 위험에서 벗어나게 해 줍니다.

 

 


 

Python import system에 대해 알아보았습니다.

추가적인 import hooks에 관한 내용과 확장된 import의 사용에 관해서는 다른 포스팅에서 다뤄볼 예정입니다.