Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 451 – 임포트 시스템을 위한 ModuleSpec 타입

Author:
Eric Snow <ericsnowcurrently at gmail.com>
BDFL-Delegate:
Brett Cannon <brett at python.org>, Alyssa Coghlan <ncoghlan at gmail.com>
Discussions-To:
Import-SIG list
Status:
Final
Type:
Standards Track
Created:
08-Aug-2013
Python-Version:
3.4
Post-History:
08-Aug-2013, 28-Aug-2013, 18-Sep-2013, 24-Sep-2013, 04-Oct-2013
Resolution:
Python-Dev message

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 importlib.machinery에 “ModuleSpec”이라는 새 클래스를 추가할 것을 제안합니다. 이 클래스는 모듈을 로드하는 데 사용되는 임포트 관련 정보를 모두 제공하며, 모듈을 먼저 로드하지 않아도 사용할 수 있습니다. 파인더는 로더 대신 모듈의 스펙을 직접 제공하게 됩니다(로더는 계속 간접적으로 제공합니다). 임포트 절차는 모듈 스펙을 활용하도록 조정되며, 모듈을 로드할 때도 모듈 스펙을 사용하게 됩니다.

용어와 개념

이 제안의 변경 사항은 현재 (안타깝게도) 모호한 기존 용어와 개념 몇 가지를 더 명확하게 만들 기회가 됩니다. 이 제안에서는 새로운 개념도 도입합니다. 마지막으로, 사람들에게 그다지 익숙하지 않을 수 있는 다른 기존 용어 몇 가지도 설명할 필요가 있습니다. 맥락을 제공하기 위해 세 가지 용어 및 개념 그룹을 모두 간략하게 요약하면 다음과 같습니다. 임포트 시스템에 대한 더 자세한 설명은 [2]에서 찾을 수 있습니다.

이름

이 제안에서 모듈의 “이름”은 완전히 정규화된 이름(fully-qualified name)을 가리킵니다. 즉, 모듈의 부모(있는 경우)의 완전히 정규화된 이름과 모듈의 단순 이름을 마침표로 연결한 것입니다.

파인더

“파인더”는 임포트 시스템이 모듈을 로드하는 데 사용해야 할 로더를 식별하는 객체입니다. 현재 이 작업은 파인더의 find_module() 메서드를 호출하여 이루어지며, 이 메서드는 로더를 반환합니다.

파인더는 오직 로더를 제공하는 일만 책임지며, find_module() 메서드를 통해 로더를 제공합니다. 그러면 임포트 시스템은 그 로더를 사용하여 모듈을 로드합니다.

로더

“로더”는 임포트 중에 모듈을 로드하는 데 사용되는 객체입니다. 현재 이 작업은 로더의 load_module() 메서드를 호출하여 이루어집니다. 로더는 자신이 로드할 수 있는 모듈에 대한 정보와 그러한 모듈과 연관된 소스의 데이터를 가져오는 API도 제공할 수 있습니다.

현재 로더는 (load_module()을 통해) 임포트와 관련된 특정 상투적인 작업을 책임집니다. 그 작업은 다음과 같습니다.

  1. (모듈 관련) 검증 일부 수행
  2. 모듈 객체 생성
  3. 모듈에 임포트 관련 속성을 설정합니다
  4. 모듈을 sys.modules에 “등록”
  5. 모듈 실행
  6. 모듈 로드 중 실패 시 정리

이 모든 작업은 임포트 시스템이 Loader.load_module()을 호출하는 동안 이루어집니다.

origin

이것은 새로운 용어이자 개념입니다. 이 개념은 임포트 시스템에 이미 암묵적으로 존재하지만, 이 제안은 그 개념을 명시적으로 만듭니다.

임포트 맥락에서 “origin”은 모듈이 유래한 시스템(또는 시스템 내의 리소스)을 의미합니다. 이 제안에서 “origin”은 그러한 리소스나 시스템을 식별하는 문자열이기도 합니다. “origin”은 모든 모듈에 적용됩니다.

예를 들어, 내장 모듈과 동결(frozen) 모듈의 origin은 인터프리터 자체입니다. 임포트 시스템은 이미 이 출처를 각각 “built-in”(내장)과 “frozen”으로 식별합니다. 이는 다음 모듈 repr에서 볼 수 있습니다: “<module ‘sys’ (built-in)>”(내장).

사실 모듈 repr은 암묵적이기는 하지만 이미 모듈의 origin을 비교적 신뢰성 있게 나타내는 지표입니다. 다른 모듈들도 “location” 항목에서 설명하는 것처럼 다른 수단을 통해 자신의 origin을 나타냅니다.

모듈의 origin을 어떻게 해석하고 사용할지, 그리고 애초에 사용할지 여부는 로더가 결정합니다.

location

이것은 새로운 용어입니다. 그러나 이 개념은 모듈의 __file__ 및 __path__ 속성, 그리고 다른 곳에서 쓰이는 “path”라는 이름/용어와 연관되어 임포트 시스템에 이미 분명히 존재합니다.

“location”은 시스템 전체가 아니라 모듈이 로드되는 리소스 또는 “장소”입니다. location은 “origin”의 일종에 해당합니다. location의 예로는 파일 시스템 경로와 URL이 있습니다. location은 리소스의 이름으로 식별되지만, 그 리소스가 속한 시스템까지 반드시 식별하는 것은 아닙니다. 그런 경우에는 로더가 직접 시스템을 식별해야 합니다.

다른 종류의 모듈 origin과 달리, location은 로더가 모듈 이름만으로 추론할 수 없습니다. 대신 로더는 location을 식별하는 문자열을 전달받아야 하며, 이 문자열은 보통 로더를 생성하는 파인더가 제공합니다. 그런 다음 로더는 이 정보를 사용하여 모듈을 로드할 리소스를 찾습니다. 이론적으로는 주어진 location에 있는 모듈을 여러 이름으로 로드할 수 있습니다.

임포트 시스템에서 location의 가장 흔한 예는 소스 모듈과 확장 모듈이 로드되는 파일입니다. 이러한 모듈의 위치는 __file__ 속성에 담긴 문자열로 식별됩니다. __file__은 일부 모듈(예: zip으로 압축된 모듈)에 대해서는 그다지 정확하지 않지만, 현재로서는 임포트 시스템이 모듈에 location이 있음을 나타내는 유일한 방법입니다.

location이 있는 모듈은 “locatable”하다고 부를 수 있습니다.

캐시

임포트 시스템은 최적화를 위해 컴파일된 모듈을 __pycache__ 디렉터리에 저장합니다. 현재 사용하는 이 모듈 캐시는 PEP 3147에서 도입되었습니다. 이 제안과 관련된 모듈 캐싱 API는 모듈의 __cache__ 속성과 importlib.util의 cache_from_source() 함수입니다. 로더는 모듈을 캐시에 넣는 일(그리고 캐시에서 불러오는 일)을 책임집니다. 현재 캐시는 컴파일된 소스 모듈에만 사용됩니다. 그러나 로더는 다른 종류의 모듈에도 모듈 캐시를 활용할 수 있습니다.

패키지

개념도 용어도 바뀌지 않습니다. 그러나 모듈과 패키지의 구분은 대부분 피상적입니다. 패키지는 모듈입니다. 패키지는 단지 __path__ 속성을 가지고 있을 뿐이며, 임포트 과정에서 하위 모듈에 바인딩된 속성이 추가될 수 있습니다. 흔히 인식되는 이 차이는 혼란을 일으킵니다. 이 제안은 타당한 경우 패키지와 모듈의 구분을 명시적으로 덜 강조합니다.

동기

임포트 시스템은 Python의 역사 전반에 걸쳐 발전해 왔습니다. 2002년 말 PEP 302는 파인더와 로더, 그리고 sys.meta_path를 통해 표준화된 임포트 훅을 도입했습니다. Python 3.1에서 도입된 importlib 모듈은 이제 PEP 302에 기술된 API뿐 아니라 임포트 시스템 전체의 순수 Python 구현을 제공합니다. 이제 임포트 시스템을 이해하고 확장하기가 훨씬 쉬워졌습니다. 이렇게 접근성이 높아진 것은 Python 커뮤니티에 이점이지만, 동시에 과제를 안겨 주기도 합니다.

임포트 시스템을 이해하고 커스터마이즈하는 개발자가 늘어날수록, 파인더와 로더 API에 있는 약점이 미치는 영향도 커질 것입니다. 따라서 임포트 시스템의 그러한 약점은 빨리 해결할수록 좋으며, 이 제안으로 해결하고자 하는 약점이 몇 가지 있습니다.

첫째, 임포트 시스템이 모듈에 대한 정보를 저장해야 할 때마다 모듈 객체에는 일반적으로 임포트 시스템에만 의미가 있는 속성이 늘어나게 됩니다. 앞으로 생길 임포트 관련 정보를 담아 두고 임포트 시스템 안에서 주고받을 수 있는 모듈별 네임스페이스가 있다면 좋을 것입니다. 둘째, 파인더와 로더 사이에는 API 공백이 있어서, 이 공백에 부딪히면 불필요한 복잡성이 생깁니다. PEP 420 (네임스페이스 패키지) 구현은 이 문제를 우회해야 했습니다. 이 복잡성은 최근 별도의 제안을 진행하는 과정에서도 다시 드러났습니다. [1]

위의 finder 및 loader 섹션에서 둘의 현재 책임을 자세히 설명합니다. 특히 로더는 load_module() 메서드가 제공하는 기능을 다른 메서드를 통해서도 제공해야 할 의무가 전혀 없습니다. 따라서 모듈에 관한 임포트 관련 정보는 모듈을 로드하지 않고도 얻을 수 있을 가능성이 높지만, 다른 방법으로는 노출되지 않습니다.

게다가 load_module()과 관련된 요구 사항은 모든 로더에 공통되며 대부분 정확히 같은 방식으로 구현됩니다. 즉, 모든 로더가 같은 상용구 코드를 중복해서 작성해야 합니다. importlib.util이 이를 돕는 몇 가지 도구를 제공하지만, 임포트 시스템이 이러한 책임을 직접 맡는다면 더 도움이 될 것입니다. 문제는 이렇게 하면 load_module()이 앞으로도 손쉽게 지원할 수 있는 커스터마이즈의 폭이 제한된다는 점입니다.

더 중요한 점은, 파인더가 로더의 load_module()에 필요한 정보를 제공할 수는 있지만, 현재로서는 그 정보를 로더에 전달할 일관된 방법이 없다는 것입니다. 이것이 바로 이 제안이 메우고자 하는 파인더와 로더 사이의 간극입니다.

마지막으로, 임포트 시스템이 파인더의 find_module()을 호출할 때(시스템 호출) 파인더는 모듈에 관한 다양한 정보를 활용하는데, 이 정보는 메서드의 맥락 밖에서도 유용합니다. 이 메서드는 로더만 반환하므로, 현재로서는 메서드 호출 이후까지 그 모듈별 정보를 유지할 수 있는 선택지가 제한적입니다. 이 제약에 대처하는 흔한 방법은 파인더 자체의 어딘가에 모듈-정보 매핑으로 정보를 저장하거나, 로더에 저장하는 것입니다.

안타깝게도 로더가 모듈별로 존재해야 한다는 요구 사항은 없습니다. 게다가 파인더가 제공할 수 있는 유용한 정보 중 일부는 모든 파인더에 공통적이므로, 이상적으로는 임포트 시스템이 그러한 세부 사항을 처리할 수 있어야 합니다. 이것은 앞서 말한 파인더와 로더 사이의 간극과 같은 것입니다.

이 결함 때문에 생긴 복잡성의 예로, Python 3.3의 네임스페이스 패키지 구현(PEP 420 참조)에서는 find_module()이 네임스페이스 검색 위치를 제공할 좋은 방법이 없었기 때문에 FileFinder.find_loader()를 추가했습니다.

이 간극에 대한 해답은 모듈별 정보를 담고 모듈 로딩에 수반되는 상용구 기능을 처리하는 ModuleSpec 객체입니다.

명세

목표는 파인더와 로더의 의미를 가능한 한 적게 바꾸면서 둘 사이의 간극을 해소하는 것입니다. 일부 기능과 정보가 새로운 ModuleSpec 타입으로 옮겨지기는 하지만, 그 동작은 동일하게 유지되어야 합니다. 다만 명확성을 위해 파인더와 로더의 의미를 명시적으로 밝히겠습니다.

다음은 이 PEP에서 설명하는 변경 사항의 개략적인 요약입니다. 더 자세한 내용은 뒤의 절에서 확인할 수 있습니다.

importlib.machinery.ModuleSpec (신규)

임포트 중 모듈의 임포트 시스템 관련 상태를 캡슐화한 것입니다. 더 자세한 설명은 아래의 ModuleSpec 절을 참조하십시오.

  • ModuleSpec(name, loader, *, origin=None, loader_state=None, is_package=None) (로더)

속성:

  • name - 모듈의 완전한 정규화된 이름을 나타내는 문자열입니다.
  • loader - 로딩에 사용할 로더입니다.
  • origin - 모듈을 로드해 오는 장소의 이름입니다. 예를 들어 내장 모듈의 경우 “builtin”이고, 소스에서 로드되는 모듈의 경우 파일 이름입니다.
  • submodule_search_locations - 패키지인 경우 하위 모듈을 찾을 위치를 나타내는 문자열의 리스트입니다(그렇지 않으면 None입니다).
  • loader_state - 로딩 중에 사용할 모듈별 추가 데이터를 담는 컨테이너입니다.
  • cached (프로퍼티) - 컴파일된 모듈을 저장할 위치를 나타내는 문자열입니다.
  • parent (읽기 전용 프로퍼티) - 모듈이 하위 모듈로서 속한 패키지의 완전한 정규화된 이름입니다(또는 None입니다).
  • has_location (읽기 전용 프로퍼티) - 모듈의 “origin” 속성이 위치를 가리키는지 여부를 나타내는 플래그입니다.

importlib.util 추가 사항

이것들은 파인더의 편의를 위한 ModuleSpec 팩토리 함수입니다. 더 자세한 내용은 아래의 Factory Functions 절을 참조하십시오.

  • spec_from_file_location(name, location, *, loader=None, submodule_search_locations=None) - 파일 기반 정보와 로더 API로 스펙을 만듭니다.
  • spec_from_loader(name, loader, *, origin=None, is_package=None) - 로더 API로 누락된 정보를 채워 스펙을 만듭니다.

기타 API 추가 사항

  • importlib.find_spec(name, path=None, target=None)은 importlib.find_loader()(이 함수를 대체함)와 똑같이 동작하지만, 로더 대신 스펙을 반환합니다.

파인더의 경우:

  • importlib.abc.MetaPathFinder.find_spec(name, path, target)과 importlib.abc.PathEntryFinder.find_spec(name, target)은 임포트 중에 사용할 모듈 스펙을 반환합니다.

로더의 경우:

  • importlib.abc.Loader.exec_module(module)은 모듈을 그 모듈 자신의 네임스페이스에서 실행합니다. 이 메서드는 importlib.abc.Loader.load_module()을 대체하며, 그 메서드의 모듈 실행 기능을 넘겨받습니다.
  • importlib.abc.Loader.create_module(spec)(선택 사항)은 로딩에 사용할 모듈을 반환합니다.

모듈의 경우:

  • 모듈 객체에는 새 속성이 생깁니다: __spec__.

API 변경 사항

  • InspectLoader.is_package()는 선택 사항이 됩니다.

폐지 예정 사항

  • importlib.abc.MetaPathFinder.find_module()
  • importlib.abc.PathEntryFinder.find_module()
  • importlib.abc.PathEntryFinder.find_loader()
  • importlib.abc.Loader.load_module()
  • importlib.abc.Loader.module_repr()
  • importlib.util.set_package()
  • importlib.util.set_loader()
  • importlib.find_loader()

제거 사항

이것들은 Python 3.4 릴리스 전에 도입되었으므로 그냥 제거하면 됩니다.

  • importlib.abc.Loader.init_module_attrs()
  • importlib.util.module_to_load()

기타 변경 사항

  • importlib의 임포트 시스템 구현은 ModuleSpec을 사용하도록 변경됩니다.
  • importlib.reload()는 ModuleSpec을 사용합니다.
  • 모듈의 임포트 관련 속성(__spec__ 이외의 속성)은 더 이상 해당 모듈을 임포트하는 동안 임포트 시스템에서 직접 사용되지 않습니다. 그러나 이는 다른 모듈(예: 하위 모듈)을 로드할 때 해당 속성(예: __path__)을 사용하는 데에는 영향을 주지 않습니다.
  • 임포트 관련 속성은 임포트 시스템을 제외하고는 더 이상 모듈에 직접 추가해서는 안 됩니다.
  • 모듈 타입의 __repr__()는 ModuleSpec을 활용하는 순수 Python 구현을 감싸는 얇은 래퍼가 됩니다.
  • __main__ 모듈의 스펙에는 적절한 이름과 출처가 반영됩니다.

하위 호환성

  • 파인더가 find_spec()을 정의하지 않으면 find_module()이 반환한 로더로부터 스펙을 만들어 냅니다.
  • PathEntryFinder.find_loader()는 여전히 find_module()보다 우선합니다.
  • exec_module()이 정의되지 않았으면 Loader.load_module()을 사용합니다.

무엇이 바뀌지 않습니까?

  • import 문의 문법과 의미입니다.
  • 기존 파인더와 로더는 계속 정상적으로 동작합니다.
  • 임포트 관련 모듈 속성은 여전히 동일한 정보로 초기화됩니다.
  • 파인더는 여전히 로더를 생성합니다(이제는 로더를 스펙에 저장합니다).
  • 모듈이 Loader.load_module()을 정의한다면, 이 메서드에는 이전과 같은 요구 사항이 모두 적용되며 여전히 직접 호출할 수 있습니다.
  • 모듈 데이터 API는 여전히 로더가 담당합니다.
  • importlib.reload()는 여전히 임포트 관련 속성을 덮어씁니다.

책임

이 PEP 이후 각 책임이 어디에 있는지 간단히 정리하면 다음과 같습니다.

파인더:

  • 모듈을 로드할 수 있는 로더를 생성하거나 식별합니다.
  • 모듈의 스펙을 생성합니다.

로더:

  • 모듈을 생성합니다(선택 사항).
  • 모듈을 실행합니다.

ModuleSpec:

  • 모듈 로딩을 총괄합니다
  • sys.modules 관리와 임포트 관련 속성 설정을 포함한 모듈 로딩용 상용구 코드
  • 로더가 모듈을 생성하지 않으면 모듈을 생성합니다
  • 실행 대상 모듈을 전달하여 loader.exec_module()을 호출합니다
  • 로더가 모듈을 실행하는 데 필요한 모든 정보를 담습니다
  • 모듈의 repr을 제공합니다

기존 파인더와 로더는 무엇을 다르게 해야 합니까?

당장은 무엇을 해야 합니까? 아무것도 하지 않습니다. 현재 방식은 폐지 예정(deprecated)이 되지만 계속 동작합니다. 다만 파인더와 로더 작성자가 이 PEP에 맞추어 변경해야 할 사항은 다음과 같습니다:

  • 파인더에 find_spec()을 구현하십시오.
  • 가능하다면 로더에 exec_module()을 구현하십시오.

importlib.util에 있는 ModuleSpec 팩토리 함수들은 기존 파인더를 변환하는 데 도움이 되도록 만들어졌습니다. spec_from_loader()와 spec_from_file_location()은 모두 이 용도로 쓰기 쉬운 유틸리티입니다.

기존 로더의 경우 exec_module()은 load_module()에서 상용구(boilerplate)가 아닌 부분을 비교적 그대로 옮긴 것이어야 합니다. 흔치 않은 일부 경우에는 로더가 create_module()도 구현해야 합니다.

ModuleSpec 사용자

ModuleSpec 객체의 대상 사용자는 서로 다른 세 부류로, Python 자체, 임포트 훅, 그리고 일반 Python 사용자입니다.

Python은 임포트 절차, 인터프리터 시작 과정, 그리고 여러 표준 라이브러리 모듈에서 스펙을 사용할 것입니다. pkgutil처럼 임포트 중심인 모듈도 있고, pickle과 pydoc처럼 그렇지 않은 모듈도 있습니다. 어느 경우든 ModuleSpec API 전체가 사용될 것입니다.

임포트 훅(파인더와 로더)은 특정한 방식으로 스펙을 활용할 것입니다. 먼저, 파인더는 importlib.util의 스펙 팩토리 함수를 사용하여 스펙 객체를 생성할 수 있습니다. 파인더는 스펙이 생성된 후 스펙 속성을 직접 조정할 수도 있습니다. 둘째로, 파인더는 로더가 모듈 생성/실행 중에 사용할 수 있도록 추가 정보를 (finder_extras에) 스펙에 연결할 수 있습니다. 마지막으로, 로더는 모듈을 생성하거나 실행할 때 스펙의 속성을 활용합니다.

Python 사용자는 모듈의 __spec__을 살펴보아 해당 객체의 임포트 관련 정보를 얻을 수 있게 됩니다. 일반적으로 Python 애플리케이션과 대화형 사용자는 ModuleSpec 팩토리 함수나 인스턴스 메서드를 사용하지 않을 것입니다.

로딩 동작 방식

다음은 모듈의 스펙과 새 로더 API를 활용하도록 조정했을 때 임포트 절차가 로딩 중에 수행하는 작업의 개요입니다:

module = None
if spec.loader is not None and hasattr(spec.loader, 'create_module'):
    module = spec.loader.create_module(spec)
if module is None:
    module = ModuleType(spec.name)
# The import-related module attributes get set here:
_init_module_attrs(spec, module)

if spec.loader is None and spec.submodule_search_locations is not None:
    # Namespace package
    sys.modules[spec.name] = module
elif not hasattr(spec.loader, 'exec_module'):
    spec.loader.load_module(spec.name)
    # __loader__ and __package__ would be explicitly set here for
    # backwards-compatibility.
else:
    sys.modules[spec.name] = module
    try:
        spec.loader.exec_module(module)
    except BaseException:
        try:
            del sys.modules[spec.name]
        except KeyError:
            pass
        raise
module_to_return = sys.modules[spec.name]

이 단계들은 Loader.load_module()이 이미 수행하도록 기대되는 작업과 정확히 같습니다. 따라서 로더는 exec_module()만 구현하면 되므로 더 단순해집니다.

모듈은 반드시 sys.modules에서 가져와 반환해야 한다는 점에 유의하십시오. 로딩 도중 모듈이 sys.modules에 있는 자기 자신을 다른 객체로 교체했을 수 있습니다. 이 사용 사례를 수용할 임포트 후 훅(임포트 훅) API가 없으므로 이를 직접 처리해야 합니다. 그러나 교체된 경우에는 해당 객체에 임포트 관련 모듈 속성을 설정하는 것을 신경 쓰지 않습니다. 모듈 작성자가 이렇게 한다면 그에 따른 처리는 스스로 책임져야 합니다.

재로딩 동작 방식

다음은 reload()에 해당하는 개요입니다:

_RELOADING = {}

def reload(module):
    try:
        name = module.__spec__.name
    except AttributeError:
        name = module.__name__
    spec = find_spec(name, target=module)

    if sys.modules.get(name) is not module:
        raise ImportError
    if spec in _RELOADING:
        return _RELOADING[name]
    _RELOADING[name] = module
    try:
        if spec.loader is None:
            # Namespace loader
            _init_module_attrs(spec, module)
            return module
        if spec.parent and spec.parent not in sys.modules:
            raise ImportError

        _init_module_attrs(spec, module)
        # Ignoring backwards-compatibility call to load_module()
        # for simplicity.
        spec.loader.exec_module(module)
        return sys.modules[name]
    finally:
        del _RELOADING[name]

여기서 핵심은 Loader.exec_module()로 전환하면 로더가 실행 시점에 리로드인지 아닌지를 더 이상 쉽게 알 수 없게 된다는 점입니다. 이 제안 이전에는 로더가 모듈이 이미 sys.modules에 있는지 확인하기만 하면 되었습니다. 이제는 (리로드가 아닌) 로드 중에 exec_module()이 호출될 때쯤이면 임포트 메커니즘이 이미 모듈을 sys.modules에 넣어 두었을 것입니다. 이것이 find_spec()에 “target” 매개변수가 있는 이유 중 하나입니다.

리로드의 의미 체계는 기존과 본질적으로 동일하게 유지됩니다 [5]. 이 PEP가 일부 종류의 지연 로딩 모듈에 미치는 영향이 논의의 대상이 되었습니다. [4]

ModuleSpec

속성

다음 각 이름은 ModuleSpec 객체의 속성입니다. None 값은 “설정되지 않음”을 나타냅니다. 이는 속성이 아예 존재하지 않는 모듈 객체와 대조됩니다. 대부분의 속성은 모듈의 임포트 관련 속성에 대응합니다. 그 매핑은 다음과 같습니다. 이 매핑을 역으로 적용하면 임포트 메커니즘이 exec_module()을 호출하기 직전에 모듈 속성을 설정하는 방식이 됩니다.

ModuleSpec에서 모듈에서
name __name__
로더 __loader__
parent __package__
origin __file__*
cached __cached__*,**
submodule_search_locations __path__**
loader_state -
has_location -
* Set on the module only if spec.has_location is true.
** Set on the module only if the spec attribute is not None.

parent와 has_location은 읽기 전용 프로퍼티이지만, 나머지 속성은 모듈 스펙이 생성된 후는 물론 임포트가 완료된 후에도 교체할 수 있습니다. 이를 통해 스펙을 직접 수정하는 것이 최선인 특이한 경우에 대응할 수 있습니다. 그러나 일반적인 사용에서는 모듈 스펙의 상태를 변경하지 않아야 합니다.

origin

“origin”은 모듈이 유래한 장소의 이름을 나타내는 문자열입니다. 위의 origin을 참조하십시오. 정보로서의 가치 외에도, 이 값은 모듈의 repr에서도 사용됩니다. “has_location”이 참인 스펙의 경우, __file__은 “origin”의 값으로 설정됩니다. 내장 모듈의 경우 “origin”은 “built-in”으로 설정됩니다.

has_location

위의 location 절에서 설명한 것처럼, 많은 모듈은 “위치를 지정할 수 있습니다(locatable)”. 이는 모듈을 로드할 대응 리소스가 있고 그 리소스를 문자열로 기술할 수 있다는 뜻입니다. 반면, 위치를 지정할 수 없는 모듈은 이런 방식으로 로드할 수 없습니다. 내장 모듈이나 코드에서 동적으로 생성된 모듈이 그 예입니다. 이러한 모듈에 접근하는 유일한 방법은 이름이므로, 이 모듈들에는 “origin”은 있지만 “location”은 없습니다.

모듈의 위치를 지정할 수 있으면 “has_location”은 참입니다. 이 경우 스펙의 origin이 위치로 사용되며 __file__은 spec.origin으로 설정됩니다. 추가적인 위치 정보가 필요한 경우(예: zipimport) 해당 정보는 spec.loader_state에 저장할 수 있습니다.

“has_location”은 로더에 load_data() 메서드가 존재하는지로 추론할 수 있습니다.

덧붙여, 위치를 지정할 수 있는 모듈이 모두 캐시 가능한 것은 아니지만 대부분은 캐시 가능합니다.

submodule_search_locations

하위 모듈을 검색할 위치 문자열(일반적으로 디렉터리 경로)의 목록입니다. 모듈이 패키지이면 이 값은 리스트(빈 리스트일 수도 있음)로 설정됩니다. 그렇지 않으면 None입니다.

대응하는 모듈 속성의 이름인 __path__는 비교적 모호합니다. 그 이름을 그대로 따르는 대신, 목적을 분명히 드러내는 더 명시적인 속성 이름을 사용합니다.

loader_state

파인더는 로더가 로딩 중에 사용할 추가 데이터를 제공하기 위해 loader_state를 임의의 값으로 설정할 수 있습니다. 기본값은 None이며, 이는 추가 데이터가 없음을 나타냅니다. 그렇지 않으면 관련 추가 정보를 담은 dict, list, types.SimpleNamespace 등 임의의 객체로 설정할 수 있습니다.

예를 들어, zipimporter는 origin에서 zip 아카이브 이름을 도출하거나 find 작업마다 사용자 정의 로더를 만들 필요 없이, 이를 사용해 그 이름을 로더에 직접 전달할 수 있습니다.

loader_state는 파인더와 그에 대응하는 로더가 사용하기 위한 것입니다. 그 밖의 용도에 대해서는 안정적인 리소스임이 보장되지 않습니다.

팩토리 함수

spec_from_file_location(name, location, *, loader=None, submodule_search_locations=None)

파일 중심 정보와 로더 API로부터 스펙을 만듭니다.

  • “origin”은 location으로 설정됩니다.
  • “has_location”은 True로 설정됩니다.
  • “cached”는 cache_from_source()를 호출한 결과로 설정됩니다.
  • “origin”은 loader.get_filename()으로부터 유추할 수 있습니다(“location”이 전달되지 않은 경우).
  • 위치가 파일 이름이면 접미사로부터 “loader”(로더)를 추론할 수 있습니다.
  • location이 파일 이름이면 “submodule_search_locations”는 loader.is_package()와 os.path.dirname(location)으로부터 유추할 수 있습니다.

spec_from_loader(name, 로더, *, origin=None, is_package=None)

로더 API를 사용하여 누락된 정보를 채운 스펙을 만듭니다.

  • “has_location”은 loader.get_data로부터 유추할 수 있습니다.
  • “origin”은 loader.get_filename()으로부터 유추할 수 있습니다.
  • location이 파일 이름이면 “submodule_search_locations”는 loader.is_package()와 os.path.dirname(location)으로부터 유추할 수 있습니다.

하위 호환성

ModuleSpec에는 하위 호환성 문제가 없습니다. Finder.find_module()이 로더 대신 모듈 스펙을 반환한다면 이야기가 달라질 것입니다. 그 경우 스펙은 원래 반환되었을 로더처럼 동작해야 할 것입니다. 그렇게 하는 것은 비교적 간단하지만 불필요한 복잡성을 더합니다. 이는 이 PEP의 이전 버전에 포함되어 있었습니다.

서브클래싱

ModuleSpec의 서브클래스는 허용되지만, 필요하지는 않을 것입니다. 단순히 loader_state를 설정하거나 사용자 정의 파인더 또는 로더에 기능을 추가하는 편이 더 적합할 가능성이 높으므로, 이를 먼저 시도해야 합니다. 그러나 서브클래스가 임포트 시스템의 요구 사항을 계속 충족하는 한, 그 타입의 객체를 Finder.find_spec()의 반환값으로 사용해도 전혀 문제가 없습니다. 덕 타이핑에도 같은 점이 적용됩니다.

기존 타입

모듈 객체

__spec__을 추가하는 것 외에는 임포트 관련 모듈 속성 중 어느 것도 변경되거나 폐지되지 않습니다. 일부는 그렇게 할 수도 있지만, 그러한 폐지는 Python 4까지 미룰 수 있습니다.

모듈의 spec은 대응하는 임포트 관련 속성과 동기화된 상태로 유지되지 않습니다. 둘은 서로 다를 수 있지만, 실제로는 대개 같습니다.

한 가지 주목할 만한 예외는 -m 플래그를 사용하여 모듈을 스크립트로 실행하는 경우입니다. 그 경우 module.__spec__.name은 실제 모듈 이름을 반영하지만 module.__name__은 __main__이 됩니다.

이름이 같은 두 모듈이라도 모듈의 spec이 동일하다는 보장은 없습니다. 마찬가지로 importlib.find_spec()을 연속으로 호출했을 때 같은 객체가 반환된다는 보장은 없으며, 동등한 객체가 반환된다는 보장조차 없습니다. 다만 적어도 동등한 객체가 반환될 가능성은 높습니다.

파인더

파인더는 여전히 모듈을 로드하는 데 사용할 로더를 식별하고, 일반적으로 생성하는 역할을 맡습니다. 이제 그 로더는 직접 반환되는 대신 find_spec()이 반환하는 모듈 spec에 저장됩니다. 이 PEP가 없는 현재와 마찬가지로, 로더를 생성하는 비용이 크다면 그 비용을 나중으로 미루도록 로더를 설계할 수 있습니다.

MetaPathFinder.find_spec(name, path=None, target=None)

PathEntryFinder.find_spec(name, target=None)

파인더는 find_spec()이 호출되면 ModuleSpec 객체를 반환해야 합니다. 이 새 메서드는 find_module()과 (PathEntryFinder의 경우) find_loader()를 대체합니다. 로더에 find_spec()이 없으면 하위 호환성을 위해 find_module()과 find_loader()가 대신 사용됩니다.

로더에 비슷한 메서드를 또 하나 추가하는 것은 실용성을 고려한 결정입니다. find_module()이 로더 대신 spec을 반환하도록 변경할 수도 있었습니다. 임포트 API가 이미 충분히 많은 변화를 겪었고, 특히 PathEntryFinder.find_loader()가 Python 3.3에서 막 추가되었다는 점을 고려하면 이 방법은 매력적입니다. 그러나 그로 인해 늘어나는 복잡성과 명시적이지 못한 메서드 이름을 감수할 만한 가치는 없습니다.

find_spec()의 “target” 매개변수

find_spec() 호출에는 선택적으로 “target” 인자를 포함할 수 있습니다. 이는 이후 로딩 대상으로 사용될 모듈 객체입니다. 일반적인 임포트 중에는 (그리고 기본적으로) “target”이 None이며, 이는 대상 모듈이 아직 생성되지 않았음을 의미합니다. 리로드 중에는 reload()에 전달된 모듈이 대상으로서 find_spec()에 그대로 전달됩니다. 이 인자 덕분에 파인더는 다른 방법으로는 얻을 수 없는 더 많은 정보를 바탕으로 모듈 spec을 만들 수 있습니다. 이는 특히 사용할 로더를 식별하는 데 중요합니다.

파인더는 find_spec()을 통해 spec에 담아 반환할 로더를 항상 식별합니다(또는 None을 반환합니다). “target”이 전달된 경우, 파인더는 로더를 식별하는 시점에 그 로더가 대상 모듈로의 로딩을 지원하는지도 결정해야 합니다. 이 결정을 위해 로더에 확인해야 할 수도 있습니다.

로더가 대상 모듈로의 로딩을 지원하지 않는다고 판단하면, 파인더는 다른 로더를 찾거나 ImportError를 발생시켜야 합니다(이 경우 모듈 임포트가 완전히 중단됩니다). How Reloading Will Work에서 언급했듯이 로더는 더 이상 리로드 상황을 스스로 쉽게 식별할 수 없으므로, 이 판단은 리로드 중에 특히 중요합니다.

“target” 매개변수의 대안으로 두 가지가 제시되었습니다. Loader.supports_reload()와, “target”을 find_spec() 대신 Loader.exec_module()에 추가하는 방안입니다. supports_reload()는 리로드 상황에 대한 초기 접근 방식이었습니다. [6] 그러나 로더에 특화되고 리로드 중심적인 이 접근 방식에는 일부 반대가 있었습니다. [7]

exec_module()의 “target”에 관해 말하자면, 로더는 리로드 중에 대상 모듈(또는 spec)로부터 단순히 “이 로더가 이 모듈의 리로드를 지원하는가” 이상의 다른 정보가 필요할 수 있는데, load_module()에서 벗어나면서 그런 정보는 더 이상 얻을 수 없습니다. 논의 중이던 제안 하나는 exec_module()에 “target”과 비슷한 것을 추가하는 것이었습니다. [8] 그러나 “target”을 그 대신 find_spec()에 두는 편이 이 PEP의 목표에 더 부합합니다. 게다가 그렇게 하면 supports_reload()가 필요하지 않게 됩니다.

네임스페이스 패키지

현재 경로 엔트리 파인더는 네임스페이스 패키지가 될 수 있는 것의 일부를 찾았음을 나타내기 위해 find_loader()에서 (None, portions)를 반환할 수 있습니다. 같은 효과를 얻으려면 find_spec()은 “loader”(로더)가 None으로 설정되고(즉, 설정되지 않고) submodule_search_locations가 find_loader()에서 제공했을 것과 같은 부분들로 설정된 스펙을 반환해야 합니다. 그러한 스펙을 어떻게 처리할지는 PathFinder에 달려 있습니다.

로더

Loader.exec_module(모듈)

로더에는 새로운 메서드인 exec_module()이 추가됩니다. 이 메서드의 유일한 역할은 모듈을 “exec”하여 결과적으로 모듈의 네임스페이스를 채우는 것입니다. 이 메서드는 모듈 객체를 생성하거나 준비할 책임이 없으며, 이후의 정리 작업을 할 책임도 없습니다. 이 메서드에는 반환값이 없습니다. exec_module()은 로딩과 리로딩 모두에 사용됩니다.

exec_module()은 두 번 이상 호출되는 경우를 올바르게 처리해야 합니다. 어떤 종류의 모듈에서는 이것이 메서드가 처음 호출된 이후로는 매번 ImportError를 발생시키는 것을 의미할 수 있습니다. 이는 특히 리로딩과 관련이 있는데, 일부 종류의 모듈은 제자리 리로딩을 지원하지 않기 때문입니다.

Loader.create_module(spec)

로더는 exec할 새 모듈을 반환하는 create_module()도 구현할 수 있습니다. 이 메서드는 기본 모듈 생성 코드를 사용해야 함을 나타내기 위해 None을 반환할 수 있습니다. 일반적이지는 않지만, create_module()의 한 가지 사용 사례는 내장 모듈 타입의 서브클래스인 모듈을 제공하는 것입니다. 대부분의 로더는 create_module()을 구현할 필요가 없습니다,

create_module()은 같은 스펙/모듈에 대해 두 번 이상 호출되는 경우를 올바르게 처리해야 합니다. 여기에는 None을 반환하거나 ImportError를 발생시키는 것이 포함될 수 있습니다.

Note

exec_module()과 create_module()은 임포트 관련 모듈 속성을 설정해서는 안 됩니다. load_module()이 그렇게 한다는 사실은 이 제안이 바로잡고자 하는 설계 결함입니다.

기타 변경 사항:

PEP 420은 모듈 타입의 __repr__()에서 특수 처리하는 부분을 줄이기 위해 선택적인 module_repr() 로더 메서드를 도입했습니다. 이 메서드는 ModuleSpec의 일부이므로 로더에서는 폐지 예정이 됩니다. 그러나 로더에 이 메서드가 존재하면 그것만 사용됩니다.

Python 3.4 릴리스 이전에 추가된 Loader.init_module_attr() 메서드는 제거되며, 대신 ModuleSpec의 같은 메서드가 사용됩니다.

그러나 InspectLoader.is_package()는 같은 정보를 ModuleSpec에서 찾을 수 있음에도 폐지 예정이 되지 않습니다. ModuleSpec은 다른 방법으로 해당 정보를 얻을 수 없는 경우 이 메서드를 사용하여 자신의 is_package를 채울 수 있습니다. 그렇지만 이 메서드는 선택 사항이 됩니다.

로딩 중에 모듈을 실행하는 것 외에도, 로더는 여전히 모듈 관련 데이터에 관한 API를 제공할 직접적인 책임을 집니다.

기타 변경 사항

  • importlib이 제공하는 다양한 파인더와 로더는 이 제안을 따르도록 갱신됩니다.
  • 표준 라이브러리에 있는 임포트 관련 API(특히 파인더와 로더)의 다른 모든 구현이나 이 API에 대한 의존성도 마찬가지로 이 PEP에 맞게 조정됩니다. 이들은 계속 동작해야 하지만, 이러한 변경 중 누락된 것이 있다면 Python 3.4.x 시리즈의 버그로 간주해야 합니다.
  • __main__ 모듈의 스펙은 인터프리터가 어떻게 시작되었는지를 반영합니다. 예를 들어 -m을 사용하면 스펙의 이름은 사용된 모듈의 이름이 되지만, __main__.__name__은 여전히 “__main__”입니다.
  • importlib.find_loader()(이제 폐지 예정이 됩니다)에 대응하는 importlib.find_spec()을 추가합니다.
  • importlib.reload()는 ModuleSpec을 사용하도록 변경됩니다.
  • 이제 importlib.reload()는 모듈별 임포트 잠금을 사용합니다.

참조 구현

참조 구현은 http://bugs.python.org/issue18864 에서 확인할 수 있습니다.

구현 참고 사항

* 이 PEP를 구현할 때는 pkgutil(및 setuptools)에 미치는 영향을 염두에 두어야 합니다. pkgutil에는 PEP 302에 대한 몇 가지 제네릭 함수 기반 확장이 있는데, importlib가 이 도구들이 모르는 사이에 로더를 감싸기 시작하면 이 확장들이 망가질 수 있습니다.

* 살펴보아야 할 다른 모듈로는 runpy(및 pythonrun.c), pickle, pydoc, inspect가 있습니다.

예를 들어 pickle은 __main__의 경우 module.__spec__.name을 참조하도록 갱신되어야 합니다.

PEP에서 거부된 추가 사항

이 제안에 몇 가지 추가 사항이 제안되었지만, 제안의 범위에 충분히 맞지 않았습니다.

has_location, cached, submodule_search_locations를 분리하는 ModuleSpec의 “PathModuleSpec” 서브클래스는 없습니다. 그렇게 하면 분리가 더 깔끔해질 수는 있지만, 모듈 객체에는 그러한 구분이 없습니다. ModuleSpec은 두 경우를 똑같이 잘 지원합니다.

“ModuleSpec.is_package”는 단순한 추가 속성(self.submodule_search_locations is not None의 별칭)이 되겠지만, 모듈과 패키지 사이의 인위적인(그리고 대부분 잘못된) 구분을 지속시킵니다.

모듈 스펙 Factory Functions는 ModuleSpec의 클래스 메서드가 될 수도 있습니다. 그러나 그렇게 하면 __spec__을 통해 모든 모듈에 이 함수들이 노출되며, 이는 고급 사용자가 아닌 Python 사용자에게 불필요한 혼란을 줄 가능성이 있습니다. 팩토리 함수에는 파인더 작성자를 지원한다는 특정한 사용 사례가 있습니다. ModuleSpec Users를 참조하십시오.

마찬가지로 임포트 메커니즘이 모듈 스펙을 사용하는 특정 방식을 노출하는 몇 가지 다른 메서드를 ModuleSpec에 추가할 수도 있습니다.

  • create() - Loader.create_module()을 감싸는 래퍼입니다.
  • exec(module) - 모듈에 대해 Loader.exec_module()을 감싸는 래퍼입니다.
  • load() - 폐지 예정인 Loader.load_module()에 대응하는 메서드입니다.

팩토리 함수와 마찬가지로 이러한 메서드를 module.__spec__을 통해 모듈에 노출하는 것은 그다지 바람직하지 않습니다. 이 메서드들은 (이 PEP의 이전 버전에서처럼) “비공개” 속성으로만 노출하더라도 결국 매력적인 골칫거리가 될 것입니다. 나중에 누군가 이 메서드들이 필요하다고 판단하면, 그 시점에 (ModuleSpec과는 별개인) 적절한 API를 통해 노출할 수 있으며, 아마도 PEP 406 (임포트 엔진)과 관련된 방식이 될 것입니다.

생각해 보면, load() 메서드가 sys.modules 대신 상호 작용할 모듈 목록을 선택적으로 받을 수도 있습니다. 또한 load()를 활용하여 다중 버전 임포트를 구현할 수도 있습니다. 둘 다 흥미로운 아이디어이지만, 분명히 이 제안의 범위를 벗어납니다.

제외된 그 밖의 사항:

  • ModuleSpec.submodules(읽기 전용 프로퍼티)를 추가합니다. 이 프로퍼티는 spec을 기준으로 가능한 하위 모듈을 반환합니다.
  • ModuleSpec.loaded 추가 (읽기 전용 프로퍼티) - sys.module에 해당 모듈이 있다면 그 모듈입니다.
  • ModuleSpec.data 추가 - 스펙의 로더가 제공하는 데이터 API를 감싸는 디스크립터입니다.
  • [3]도 참고하십시오.

참고 문헌