PEP 395 – 모듈의 정규화된 이름
- Author:
- Alyssa Coghlan <ncoghlan at gmail.com>
- Status:
- Withdrawn
- Type:
- Standards Track
- Created:
- 04-Mar-2011
- Python-Version:
- 3.4
- Post-History:
- 05-Mar-2011, 19-Nov-2011
Table of Contents
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
PEP 철회
이 PEP는 작성자가 2013년 12월에 철회했습니다. 작성된 이후 상당한 변화가 있었고, 그 결과 여러 부분이 더 이상 유효하지 않게 되었기 때문입니다. 특히 PEP 420 네임스페이스 패키지로 인해 패키지 탐지와 관련된 일부 제안은 실행할 수 없게 되었으며, PEP 451 모듈 명세로 다중 처리 문제가 해결되었고 pickle 호환성 문제를 해결할 수 있는 방법도 마련되었습니다.
남은 문제를 해결하기 위한 향후 PEP는 여전히 적절하겠지만, 이 PEP를 직접 기반으로 삼기보다는 업데이트된 맥락에서 남은 문제를 다시 서술하는 새로운 PEP로 그러한 작업을 시작하는 편이 가치 있습니다.
초록
이 PEP는 Python의 임포트 시스템과 함수 및 클래스의 직렬화와 인트로스펙션을 다룰 때 부주의한 사용자가 빠지기 쉬운 오래된 함정 일부를 제거하는 새로운 메커니즘을 제안합니다.
이는 PEP 3155에서 정의한 “Qualified Name” 개념을 기반으로 합니다.
다른 PEP와의 관계
무엇보다도 이 PEP는 현재 보류되어 있습니다. PEP 420에서 필수 __init__.py 파일을 제거한 것과 호환되도록 하려면 상당한 변경이 필요하기 때문입니다(이는 Python 3.3에서 구현되어 출시되었습니다).
이 PEP는 PEP 3155에서 소개한 “qualified name” 개념을 기반으로 하며, 임의의 함수와 클래스를 직렬화할 때 발생하는 몇 가지 난해한 예외 상황을 수정하려는 해당 PEP의 목표도 공유합니다.
또한 최소한 일부 상황에서 주 모듈에서의 명시적 상대 임포트가 올바르게 작동하도록 하기 위한 초기의 조심스러운 단계를 밟은 PEP 366을 기반으로 합니다.
마지막으로 PEP 328은 임포트된 모듈에서의 암시적 상대 임포트를 제거했습니다. 이 PEP는 sys.path[0]의 현재 초기화 동작이 제공하는 메인 모듈에서의 사실상 암시적 상대 임포트도 제거할 것을 제안합니다.
__name__에는 무엇이 들어 있습니까?
시간이 지나면서 모듈의 __name__ 속성은 여러 가지 작업을 처리하는 데 사용되어 왔습니다.
이 모듈 속성의 주요 사용 사례는 다음과 같습니다.
if __name__ == "__main__":관례를 사용하여 프로그램에서 메인 모듈임을 표시합니다.- 상대 임포트의 시작점으로 사용합니다.
- 실행 중인 애플리케이션에서 함수 및 클래스 정의의 위치를 식별합니다.
- 다른 인터프리터 인스턴스와 공유할 수 있는 pickle 객체로 직렬화할 클래스의 위치를 식별합니다.
부주의한 사용자가 빠지기 쉬운 함정
__name__의 의미를 여러 용도로 사용한 것과 sys.path[0] 초기화에 역사적으로 연관된 일부 동작으로 인해 부주의한 사용자가 빠지기 쉬운 몇 가지 함정이 생겼습니다. 이러한 함정은 매우 쉽게 알아차리기 어렵고(특히 초보자에게 그러합니다) 상당히 혼란스러운 동작을 일으킬 수 있으므로 실제로는 꽤 성가실 수 있습니다.
임포트가 왜 망가졌습니까?
sys.path를 수정할 때 적용되는 일반적인 원칙이 있습니다. 패키지 디렉터리를 sys.path에 직접 넣지 마십시오. 이것이 문제가 되는 이유는 해당 디렉터리의 모든 모듈이 이제 서로 다른 두 이름으로 접근 가능할 수 있기 때문입니다. 즉, 패키지 디렉터리가 sys.path에 있으므로 최상위 모듈로 접근할 수 있고, 패키지 자체를 포함하는 상위 디렉터리도 sys.path에 있다면 패키지의 서브모듈로도 접근할 수 있습니다.
예를 들어 Django는 버전 1.3 이하에서 사이트별 애플리케이션에 대해 정확히 이러한 상황을 설정했습니다. 애플리케이션이 모듈 네임스페이스에서 app과 site.app 모두로 접근 가능해지며, 실제로는 서로 다른 모듈 복사본입니다. 의미 있는 변경 가능 모듈 수준 상태가 있다면 이는 혼란을 일으키는 원인이 되므로, 버전 1.4에서는 이 동작을 기본 사이트 설정에서 제거하고 있습니다(사이트별 앱은 항상 사이트 이름으로 완전히 수식됩니다).
그러나 메인 모듈에서 __name__ = "__main__"을 설정하는 Python의 동일한 부분이 sys.path[0]의 값을 결정할 때 똑같은 오류를 저지르는 상황에서 Django만 탓하기는 어렵습니다.
Stack Overflow에서 “python” 및 “import” 태그를 따라가 보면 이러한 영향이 비교적 자주 나타나는 것을 볼 수 있습니다. 제가 직접 이를 따라갈 시간이 있었을 때는 다음과 같은 간단한 패키지 레이아웃의 동작을 이해하려고 애쓰는 사람들을 정기적으로 만났습니다(실제로 저는 제 프로젝트에서 이와 비슷한 패키지 레이아웃을 사용합니다).:
project/
setup.py
example/
__init__.py
foo.py
tests/
__init__.py
test_foo.py
처음에는 __init__.py 파일 없이 보는 경우가 많겠지만, 그것은 설명하기 쉬운 사소한 수정입니다. 설명하기 어려운 점은 다음의 모든 방식으로 test_foo.py를 호출하면 가져오기가 깨져 아마 작동하지 않을 것이라는 사실입니다(절대 가져오기의 경우 example을 찾지 못하거나, 명시적 상대 가져오기의 경우 패키지가 아닌 곳에서 또는 최상위 패키지를 넘어선 상대 가져오기에 대해 불평하거나, 직렬화를 처리하는 example.json 모듈이나 테스트 실행기인 example.tests.unittest처럼 다른 하위 모듈이 최상위 모듈의 이름을 가리는 경우에는 훨씬 더 난해한 오류를 내기도 합니다).:
# These commands will most likely *FAIL*, even if the code is correct
# working directory: project/example/tests
./test_foo.py
python test_foo.py
python -m package.tests.test_foo
python -c "from package.tests.test_foo import main; main()"
# working directory: project/package
tests/test_foo.py
python tests/test_foo.py
python -m package.tests.test_foo
python -c "from package.tests.test_foo import main; main()"
# working directory: project
example/tests/test_foo.py
python example/tests/test_foo.py
# working directory: project/..
project/example/tests/test_foo.py
python project/example/tests/test_foo.py
# The -m and -c approaches don't work from here either, but the failure
# to find 'package' correctly is easier to explain in this case
그렇습니다. 이 긴 목록은 시도하면 거의 확실히 실패할 모든 호출 방법의 목록이며, Python의 가져오기 시스템이 작동하는 방식뿐만 아니라 초기화되는 방식까지 이미 아주 잘 알고 있지 않다면 오류 메시지는 전혀 말이 되지 않습니다.
오랫동안 이런 설정에서 sys.path를 올바르게 설정하는 유일한 방법은 test_foo.py 자체에서 수동으로 설정하거나(초보자는 물론이고 많은 숙련된 Python 프로그래머도 그렇게 하는 방법을 알기 어려웠습니다), 아니면 모듈을 직접 실행하는 대신 가져오도록 하는 것이었습니다.:
# working directory: project
python -c "from package.tests.test_foo import main; main()"
패키지 내부의 모듈을 -m 스위치를 통해 실행할 때 상대 가져오기가 올바르게 작동하도록 하는 메커니즘을 정의한 PEP 366의 구현 이후에는 다음 방법도 제대로 작동합니다.:
# working directory: project
python -m package.tests.test_foo
패키지 내부에 있는 Python 코드를 명령줄에서 호출하는 대부분의 방법이 실패하고, 작동하는 두 방법도 현재 작업 디렉터리에 매우 민감하다는 사실은 초보자에게 매우 혼란스럽습니다. 저는 이것이 Python 패키지가 복잡하고 올바르게 구성하기 어렵다는 인식을 낳는 주요 요인 중 하나라고 개인적으로 생각합니다.
이 문제는 명령줄에만 국한되지 않습니다. test_foo.py가 Idle에서 열려 있을 때 F5를 눌러 실행하려고 하거나 그래픽 파일 브라우저에서 해당 파일을 클릭하여 실행하려고 해도, 명령줄에서 직접 실행할 때와 정확히 같은 방식으로 실패합니다.
일반적인 “sys.path에 패키지 디렉터리를 두지 않는다”라는 지침이 존재하는 데에는 이유가 있으며, sys.path[0]을 결정할 때 인터프리터 자체가 이 지침을 따르지 않는다는 사실이 온갖 문제의 근본 원인입니다.
과거에는 하위 호환성 문제 때문에 이 문제를 수정할 수 없었습니다. 그러나 이 문제의 영향을 받을 수 있는 스크립트는 Python 3.x로 포팅할 때 이미 수정이 필요합니다(모듈을 정상적으로 가져올 때 암시적 상대 임포트가 제거되기 때문입니다). 따라서 이는 sys.path[0]의 초기화 의미론에 상응하는 변경을 구현할 편리한 기회를 제공합니다.
메인 모듈을 두 번 가져오기
또 다른 오래된 함정은 __main__을 두 번 가져오는 문제입니다. 이는 메인 모듈이 본래 이름으로도 가져와질 때 발생하며, 결과적으로 서로 다른 이름 아래 동일한 모듈의 인스턴스가 두 개 만들어집니다.
__main__에 저장된 상태가 프로그램의 올바른 작동에 중요하거나 메인 모듈에 멱등적이지 않은 부작용을 일으키는 최상위 코드가 있다면, 이러한 중복으로 인해 난해하고 예상하기 어려운 오류가 발생할 수 있습니다.
피클에 빠진 상황
많은 사용자가 알지 못할 수 있는 점은 pickle모듈이 임의의 클래스 인스턴스를 직렬화할 때 때때로 __module__특성에 의존한다는 사실입니다. 따라서 __main__에서 정의된 클래스의 인스턴스는 그런 방식으로 피클링되며, 해당 모듈을 직접 실행하지 않고 가져오기만 한 다른 Python 인스턴스에서는 올바르게 언피클링되지 않습니다. 이러한 동작이 바로, 어떤 형태로든 객체 직렬화와 영속성을 사용하는 애플리케이션에서는 __main__모듈에서 가능한 한 적은 작업만 수행하도록 하라는 많은 Python 숙련자들의 조언이 나온 근본적인 이유입니다.
마찬가지로 의사 모듈(다음 단락 참조)을 만들 때 피클은 모듈 계층에서 해당 클래스에 대해 공식적으로 문서화된 위치가 아니라 클래스가 실제로 정의된 모듈의 이름에 의존합니다.
이 PEP의 목적상 “의사 모듈”은 Python 3.2의 unittest 및 concurrent.futures 패키지와 같은 방식으로 설계된 패키지입니다. 이러한 패키지는 단일 모듈인 것처럼 문서화되어 있지만, 실제로는 내부적으로 패키지로 구현되어 있습니다. 이는 사용자와 다른 구현이 신경 쓸 필요가 없는 구현 세부 사항이어야 하지만, pickle덕분에(그리고 일반적인 직렬화 덕분에) 세부 사항이 자주 노출되며 사실상 공개 API의 일부가 될 수 있습니다.
이 PEP는 표준 라이브러리의 주요 직렬화 방식인 pickle에 특히 초점을 맞추지만, 이 문제는 임의의 클래스 인스턴스 직렬화를 지원하고 역직렬화를 처리하는 방법을 결정하기 위해 __module__특성에 의존하는 다른 메커니즘에도 영향을 줄 수 있습니다.
소스는 어디에 있습니까?
위에서 설명한 의사 모듈 기법을 사용하는 일부 고급 사용자는 pickle모듈을 통해 구현 세부 사항이 노출되는 문제를 인식하고, 함수를 정의하거나 클래스를 정의하기 전에 __name__을 모듈의 공개 위치를 가리키도록 변경하여 이 문제를 해결하거나, 해당 객체가 정의된 후 그 객체의 __module__특성을 수정하기로 합니다.
이 접근 방식은 피클링을 통한 정보 노출을 제거하는 데 효과적이지만, 함수와 클래스의 인트로스펙션이 깨지는 대가를 치릅니다(__module__특성이 이제 잘못된 위치를 가리키기 때문입니다).
Fork 없는 Windows
Windows에 os.fork가 없다는 문제를 우회하기 위해 multiprocessing모듈은 동일한 메인 모듈로 Python을 다시 실행하려고 하지만, if __name__ == "__main__": 검사로 보호된 코드는 건너뜁니다. 이 모듈은 보유한 정보로 최선을 다하지만, 메인 모듈이 일반적인 직접 실행 스크립트나 최상위 모듈이 아닌 경우에는 타당하지 않은 가정을 할 수밖에 없습니다. -m스위치를 통해 실행된 패키지와 최상위가 아닌 모듈은 물론, 직접 실행된 zip 파일이나 디렉터리도 새 프로세스를 생성할 때 Windows의 multiprocessing이 잘못 동작하게 만들 가능성이 큽니다(애플리케이션 세부 사항에 따라 조용히 또는 오류를 드러내며 동작합니다).
이 문제는 현재 Windows에 직접적인 영향만 미치지만, 다른 플랫폼에서 multiprocessing 모듈을 통해 Windows 스타일의 “clean process” 호출을 제공하려는 모든 제안에도 영향을 미칩니다.
모듈의 정규화된 이름
이러한 문제를 완전히 해결할 수 있도록 새 모듈 수준 속성인 __qualname__을 추가할 것을 제안합니다. “qualified name”의 약어인 이 이름은 PEP 3155에서 가져온 것으로, 최상위 모듈을 기준으로 중첩 클래스 또는 함수 정의까지의 이름 지정 경로를 저장하는 데 사용됩니다.
모듈에서 __qualname__은 일반적으로 __name__과 동일하며, 이는 PEP 3155의 최상위 함수 및 클래스에서도 마찬가지입니다. 그러나 위의 문제를 해결할 수 있도록 일부 상황에서는 두 값이 달라집니다.
구체적으로, 메인 모듈을 나타내는 경우처럼 다른 목적으로 __name__이 수정될 때마다 __qualname__은 변경되지 않으므로, 이를 필요로 하는 코드가 원래의 수정되지 않은 값에 접근할 수 있습니다.
모듈 로더가 자체적으로 __qualname__을 초기화하지 않으면, 임포트 시스템이 자동으로 이를 추가하고 __name__과 동일한 값으로 설정합니다.
대체 이름
새 속성의 대체 이름으로 “full name” (__fullname__)과 “implementation name” (__implname__)도 고려되었습니다.
이 둘 중 어느 이름이든 이 PEP의 사용 사례에는 실제로 적합합니다. 그러나 메타 이슈로서, PEP 3155는 또한 새로운 특성을 추가하고 있습니다(함수와 클래스의 경우). 이 특성은 “__name__와 비슷하지만, __name__에 필요한 정보가 없는 일부 경우에는 다릅니다”라는 의미이며, 이러한 용어는 PEP 3155 함수 및 클래스 사용 사례에는 정확하지 않습니다.
PEP 3155는 의도적으로 모듈 정보를 생략하므로 “full name”이라는 용어는 사실이 아니며, “implementation name”은 __name__이 지정하는 것과 다른 객체를 지정할 수도 있음을 암시하지만, PEP 3155에서는 결코 그렇지 않습니다(해당 PEP에서 __name__과 __qualname__은 항상 동일한 함수 또는 클래스를 가리키며, 단지 __name__만으로는 중첩 함수와 클래스를 정확히 식별하기에 충분하지 않을 뿐입니다).
하위 호환성 문제로 인해 __name__ 자체의 동작을 변경할 수 없어서만 존재하는 속성에 새로운 용어를 두 개 추가하는 것은 불필요하게 일관성이 없어 보이므로, 이 PEP에서는 대신 PEP 3155의 용어를 채택했습니다.
“qualified name”과 __qualname__이 상대적으로 이해하기 어렵기 때문에, 관심 있는 개발자가 이름만 보고 의미를 안다고 가정하여 잘못 추측하는 대신 적어도 한 번은 찾아보게 된다면, 반드시 나쁜 결과라고 할 수는 없습니다.
게다가 Python 개발자의 99%는 이러한 추가 속성이 존재한다는 사실에 신경 쓸 필요조차 없습니다. 이는 임포트, 피클링 및 인트로스펙션에서 나타나는 몇 가지 문제가 있는 동작을 수정하기 위한 구현 세부 사항일 뿐이며, 사람들이 정기적으로 다룰 대상은 아닙니다.
함정 제거
다음 변경 사항은 서로 연관되어 있으며 함께 고려할 때 가장 타당합니다. 이러한 변경 사항은 앞서 언급한 부주의한 사용자를 위한 함정을 완전히 제거하거나, 그러한 함정에 대처할 수 있는 간단한 메커니즘을 제공합니다.
여기 제시된 개념 중 일부의 초안은 처음에 python-ideas 목록([1])에 게시되었지만, 해당 스레드에서 처음 논의된 이후 상당히 발전했습니다. 이후 import-sig 메일링 리스트([2]. [3])에서도 추가 논의가 이루어졌습니다.
패키지 내부의 메인 모듈 임포트 수정
이 함정을 제거하기 위해, sys.path[0]에 적합한 값을 결정할 때 추가 파일 시스템 검사를 수행할 것을 제안합니다. 이 검사는 Python의 명시적 패키지 디렉터리 표식을 찾고, 이를 사용하여 sys.path에 추가할 적절한 디렉터리를 찾습니다.
관련 사례에서 sys.path[0]을 설정하는 현재 알고리즘은 대략 다음과 같습니다:
# Interactive prompt, -m switch, -c switch
sys.path.insert(0, '')
# Valid sys.path entry execution (i.e. directory and zip execution)
sys.path.insert(0, sys.argv[0])
# Direct script execution
sys.path.insert(0, os.path.dirname(sys.argv[0]))
이 초기화 프로세스가 파일 시스템에 저장된 패키지 세부 정보를 고려하도록 수정될 것을 제안합니다:
# Interactive prompt, -m switch, -c switch
in_package, path_entry, _ignored = split_path_module(os.getcwd(), '')
if in_package:
sys.path.insert(0, path_entry)
else:
sys.path.insert(0, '')
# Start interactive prompt or run -c command as usual
# __main__.__qualname__ is set to "__main__"
# The -m switches uses the same sys.path[0] calculation, but:
# modname is the argument to the -m switch
# modname is passed to ``runpy._run_module_as_main()`` as usual
# __main__.__qualname__ is set to modname
# Valid sys.path entry execution (i.e. directory and zip execution)
modname = "__main__"
path_entry, modname = split_path_module(sys.argv[0], modname)
sys.path.insert(0, path_entry)
# modname (possibly adjusted) is passed to ``runpy._run_module_as_main()``
# __main__.__qualname__ is set to modname
# Direct script execution
in_package, path_entry, modname = split_path_module(sys.argv[0])
sys.path.insert(0, path_entry)
if in_package:
# Pass modname to ``runpy._run_module_as_main()``
else:
# Run script directly
# __main__.__qualname__ is set to modname
위의 의사 코드에서 사용된 split_path_module() 지원 함수는 다음과 같은 의미를 갖습니다:
def _splitmodname(fspath):
path_entry, fname = os.path.split(fspath)
modname = os.path.splitext(fname)[0]
return path_entry, modname
def _is_package_dir(fspath):
return any(os.exists("__init__" + info[0]) for info
in imp.get_suffixes())
def split_path_module(fspath, modname=None):
"""Given a filesystem path and a relative module name, determine an
appropriate sys.path entry and a fully qualified module name.
Returns a 3-tuple of (package_depth, fspath, modname). A reported
package depth of 0 indicates that this would be a top level import.
If no relative module name is given, it is derived from the final
component in the supplied path with the extension stripped.
"""
if modname is None:
fspath, modname = _splitmodname(fspath)
package_depth = 0
while _is_package_dir(fspath):
fspath, pkg = _splitmodname(fspath)
modname = pkg + '.' + modname
return package_depth, fspath, modname
또한 이 PEP에서는 split_path_module() 기능을 runpy 모듈을 통해 Python 사용자에게 직접 노출할 것을 제안합니다.
이 수정 사항이 적용되고 앞서 설명한 것과 동일한 단순한 패키지 구조를 사용하면, 다음 명령을 모두 테스트 모음을 올바르게 호출합니다:
# working directory: project/example/tests
./test_foo.py
python test_foo.py
python -m package.tests.test_foo
python -c "from .test_foo import main; main()"
python -c "from ..tests.test_foo import main; main()"
python -c "from package.tests.test_foo import main; main()"
# working directory: project/package
tests/test_foo.py
python tests/test_foo.py
python -m package.tests.test_foo
python -c "from .tests.test_foo import main; main()"
python -c "from package.tests.test_foo import main; main()"
# working directory: project
example/tests/test_foo.py
python example/tests/test_foo.py
python -m package.tests.test_foo
python -c "from package.tests.test_foo import main; main()"
# working directory: project/..
project/example/tests/test_foo.py
python project/example/tests/test_foo.py
# The -m and -c approaches still don't work from here, but the failure
# to find 'package' correctly is pretty easy to explain in this case
이러한 변경 사항을 적용하면 그래픽 파일 브라우저에서 Python 모듈을 클릭할 때 패키지 내부에 있더라도 항상 올바르게 실행됩니다. 스크립트를 호출하는 방식의 세부 사항에 따라 Idle도 Idle 전용 수정 없이 F5를 사용하여 test_foo.py를 올바르게 실행할 수 있을 가능성이 높습니다.
선택적 추가 사항: 명령줄 상대 임포트
위의 변경 사항이 적용되면 -m 스위치의 인자로 명시적 상대 임포트를 허용하는 것은 상당히 사소한 추가 사항이 됩니다:
# working directory: project/example/tests
python -m .test_foo
python -m ..tests.test_foo
# working directory: project/example/
python -m .tests.test_foo
이 추가 사항에 따라 -m 스위치의 시스템 초기화는 다음과 같이 변경됩니다:
# -m switch (permitting explicit relative imports)
in_package, path_entry, pkg_name = split_path_module(os.getcwd(), '')
qualname= <<arguments to -m switch>>
if qualname.startswith('.'):
modname = qualname
while modname.startswith('.'):
modname = modname[1:]
pkg_name, sep, _ignored = pkg_name.rpartition('.')
if not sep:
raise ImportError("Attempted relative import beyond top level package")
qualname = pkg_name + '.' modname
if in_package:
sys.path.insert(0, path_entry)
else:
sys.path.insert(0, '')
# qualname is passed to ``runpy._run_module_as_main()``
# _main__.__qualname__ is set to qualname
PEP 382와의 호환성
이 제안을 PEP 382네임스페이스 패키징 PEP와 호환되도록 만드는 것은 간단합니다. _is_package_dir()의 의미만 다음과 같이 변경됩니다:
def _is_package_dir(fspath):
return (fspath.endswith(".pyp") or
any(os.exists("__init__" + info[0]) for info
in imp.get_suffixes()))
PEP 402와의 비호환성
PEP 402는 Python 패키지에 대한 파일 시스템의 명시적 표식을 제거할 것을 제안합니다. 이는 파일 시스템 경로와 Python 모듈 이름을 가져와 Python 모듈 네임스페이스에 대한 모호하지 않은 매핑을 알아낼 수 있다는 제안된 개념을 근본적으로 무너뜨립니다. 대신 적절한 매핑은 sys.path의 현재 값에 따라 달라지므로, 인터프리터가 초기화될 때 sys.path[0]의 계산과 관련하여 위에서 설명한 문제를 결코 해결할 수 없게 됩니다.
관련 PEP 402가 채택된다면 이 PEP의 일부 측면은 아마도 되살릴 수 있겠지만, 메인 모듈과 다른 모듈의 임포트 의미론을 더 일관되게 만든다는 핵심 개념은 더 이상 실현 가능하지 않을 것입니다.
패키지에 저장된 스크립트와의 잠재적 비호환성
sys.path[0] 초기화에 제안된 변경 사항은 일부 기존 코드를 손상시킬 수 있습니다. 구체적으로, Python 3에서 올바르게 실행하기 위해 __main__에서의 암시적 상대 임포트에 의존하는 패키지 디렉터리에 저장된 스크립트가 손상됩니다.
이러한 스크립트는 암시적 상대 임포트로 인해 Python 2에서는 임포트할 수 있었지만, 모듈을 임포트할 때 암시적 상대 임포트가 더 이상 허용되지 않으므로 Python 3에서는 임포트할 수 없는 상태가 이미 되었습니다.
메인 모듈에서도 암시적 상대 임포트를 허용하지 않으면, 이러한 모듈은 이 PEP에 따라 스크립트로도 작동하지 않게 됩니다. 이들을 명시적 상대 임포트로 전환하면 실행 가능한 스크립트이면서 동시에 가져올 수 있는 모듈로서 다시 작동하게 됩니다.
이전 버전의 Python을 지원하려면 Python 버전에 따라 서로 다른 형식의 임포트를 사용하도록 스크립트를 작성할 수 있습니다:
if __name__ == "__main__" and sys.version_info < (3, 3):
import peer # Implicit relative import
else:
from . import peer # explicit relative import
메인 모듈의 이중 임포트 수정
메인 모듈에서 __qualname__을 일관되고 올바르게 설정하자는 위의 제안을 전제로, 메인 모듈의 이중 임포트 문제를 제거하기 위한 간단한 변경 사항으로 실제 이름으로 __main__을 임포트하려는 시도를 감지하고 원래 메인 모듈을 대신 반환하는 sys.metapath 훅을 추가할 것을 제안합니다:
class AliasImporter:
def __init__(self, module, alias):
self.module = module
self.alias = alias
def __repr__(self):
fmt = "{0.__class__.__name__}({0.module.__name__}, {0.alias})"
return fmt.format(self)
def find_module(self, fullname, path=None):
if path is None and fullname == self.alias:
return self
return None
def load_module(self, fullname):
if fullname != self.alias:
raise ImportError("{!r} cannot load {!r}".format(self, fullname))
return self.main_module
이 metapath 훅은 다음 논리에 따라 임포트 시스템 초기화 중 자동으로 추가됩니다:
main = sys.modules["__main__"]
if main.__name__ != main.__qualname__:
sys.metapath.append(AliasImporter(main, main.__qualname__))
이는 아마도 이 PEP에서 가장 중요하지 않은 제안일 것입니다. 인터프리터 시작 시 sys.path[0] 구성을 처리한 후 모듈 중복으로 이어질 가능성이 있는 마지막 메커니즘을 차단할 뿐입니다.
인트로스펙션을 손상시키지 않고 피클링 수정
이 문제를 해결하기 위해, 어떤 이유로든 __name__이 수정되었을 때 새로운 모듈 수준 __qualname__ 속성을 사용하여 실제 모듈 위치를 결정할 것을 제안합니다.
메인 모듈에서 __qualname__은 인터프리터에 의해 메인 모듈의 “실제” 이름(위에서 설명한 대로)으로 자동 설정됩니다.
__name__을 공용 네임스페이스를 가리키도록 조정하는 의사 모듈은 __qualname__을 변경하지 않으므로, 인트로스펙션을 위해 구현 위치에 쉽게 접근할 수 있습니다.
모듈의 최상위에서 __name__이 조정되면, 이후 해당 모듈에서 정의되는 모든 함수와 클래스의 __module__ 속성도 자동으로 조정됩니다.
여러 하위 모듈이 동일한 “공용” 네임스페이스를 사용하도록 설정될 수 있으므로, 함수와 클래스에는 해당 모듈의 __qualname__을 가리키는 새로운 __qualmodule__ 속성이 부여됩니다.
이는 함수에는 엄밀히 필요하지 않습니다(함수의 전역 딕셔너리를 조회하여 모듈의 정규화된 이름을 알아낼 수 있습니다). 그러나 클래스에는 필요합니다. 클래스는 자신을 정의한 모듈의 전역 변수에 대한 참조를 보유하지 않기 때문입니다. 클래스에 새 속성을 추가하면 API의 일관성을 유지하고 함수에도 새 속성을 추가하는 편이 더 편리합니다.
이러한 변경 사항은 __name__(및 직접 또는 간접적으로 이에 대응하는 함수와 클래스의 __module__ 속성)을 조정하는 것이, API를 여전히 단일 모듈인 것처럼 노출하면서 네임스페이스를 패키지로 구현하는 공식적으로 승인된 방법이 된다는 의미입니다.
현재 __name__ 및 __module__ 속성을 사용하는 모든 직렬화 코드는 기본적으로 구현 세부 정보를 노출하지 않게 됩니다.
메인 모듈에서 항목의 직렬화를 올바르게 처리하기 위해, 클래스 및 함수 정의 로직은 __name__ == "__main__"인 경우 __module__ 속성에도 __qualname__을 사용하도록 갱신됩니다.
__name__과 __module__이 사물의 공개 이름을 나타내는 데 사용되도록 공식적으로 인정됨에 따라, 표준 라이브러리의 인트로스펙션 도구들은 적절한 곳에서 __qualname__과 __qualmodule__을 사용하도록 갱신됩니다. 예를 들면 다음과 같습니다:
pydoc은 모듈에 대해 공개 이름과 정규화된 이름을 모두 보고합니다inspect.getsource()(및 이와 유사한 도구들)은 코드의 구현을 가리키는 정규화된 이름을 사용합니다- 주어진 공개
__name__을 가진 모든 모듈을 보고하는 추가적인pydoc및/또는inspectAPI가 제공될 수 있습니다.
Windows에서 multiprocessing 고치기
이제 __qualname__을 사용하여 메인 모듈의 실제 이름을 multiprocessing에 알려줄 수 있게 됨에 따라, 자식 프로세스로 전달되는 직렬화된 정보에 이를 단순히 포함시킬 수 있게 되어, __file__ 속성에 대한 현재의 의심스러운 인트로스펙션의 필요성이 사라집니다.
이전 Python 버전에서는, __file__ 속성을 기반으로 메인 모듈을 실행하는 방법을 알아내려 할 때 위에서 설명한 split_path_module() 알고리즘을 적용함으로써 multiprocessing을 개선할 수 있습니다.
명시적 상대 임포트
이 PEP는 __package__가 메인 모듈에서 무조건 __qualname__.rpartition('.')[0]로 정의되도록 제안합니다. 그 외에는, 명시적 상대 임포트의 동작을 그대로 두도록 제안합니다.
특히, 명시적 상대 임포트가 발생할 때 어느 모듈에 __package__가 설정되어 있지 않은 경우, 자동으로 캐시되는 값은 __qualname__이 아니라 계속해서 __name__으로부터 유도됩니다. 이는 __package__를 직접 설정하는 대신 __name__을 조정함으로써 상대 임포트를 의도적으로 조작하는 기존 코드와의 하위 호환성 문제를 최소화합니다.
이 PEP는 __package__를 폐지하자고 제안하는 것이 아닙니다. __qualname__의 도입에 따라 기술적으로는 중복되지만, Python 3.x의 수명 내에서 이를 폐지하는 수고를 들일 가치는 없습니다.
참조 구현
아직 없습니다.
참고 문헌
Copyright
This document has been placed in the public domain.