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 모듈 명세가 multiprocessing 문제를 해결했으며 pickle 호환성 문제를 다룰 수 있는 방법도 제공한다는 것입니다.
남은 문제를 해결하기 위한 향후 PEP는 여전히 적절하겠지만, 그러한 작업은 이 PEP를 직접 발전시키려 하기보다는 갱신된 맥락에서 남은 문제를 다시 기술하는 새로운 PEP로 시작하는 것이 좋습니다.
초록
이 PEP는 Python의 임포트 시스템을 다룰 때, 그리고 함수와 클래스의 직렬화 및 인트로스펙션을 다룰 때 부주의한 사용자가 빠지기 쉬운 오래된 함정 몇 가지를 제거하는 새로운 메커니즘을 제안합니다.
이 PEP는 PEP 3155에서 정의한 “정규화된 이름(Qualified Name)” 개념을 기반으로 합니다.
다른 PEP와의 관계
무엇보다도, 이 PEP는 PEP 420에서 필수 __init__.py 파일이 제거된 것(Python 3.3에서 구현 및 릴리스되었습니다)과 호환되도록 하려면 상당한 변경이 필요하므로 현재 보류되었습니다.
이 PEP는 PEP 3155에서 도입한 “정규화된 이름” 개념을 기반으로 하며, 임의의 함수와 클래스를 직렬화할 때 발생하는 몇 가지 골치 아픈 예외 상황을 해결한다는 그 PEP의 목표도 공유합니다.
또한 이 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()"
PEP 366이 구현된 이후로(이 PEP는 패키지 안의 모듈이 -m 스위치로 실행될 때 상대 임포트가 올바르게 동작하도록 하는 메커니즘을 정의했습니다), 다음 방법도 제대로 동작합니다:
# 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로 인한 곤경
많은 사용자가 깨닫지 못할 수도 있는 점은 pickle 모듈이 임의 클래스의 인스턴스를 직렬화할 때 때때로 __module__ 속성에 의존한다는 것입니다. 따라서 __main__에 정의된 클래스의 인스턴스는 그런 방식으로 피클링되며, 해당 모듈을 직접 실행하지 않고 임포트만 한 다른 Python 인스턴스에서는 올바르게 언피클링되지 않습니다. 이 동작은 어떤 형태로든 객체 직렬화와 영속성이 관련된 애플리케이션에서는 __main__ 모듈에서 가능한 한 적은 일을 하라는 많은 Python 베테랑의 조언의 근본적인 이유입니다.
마찬가지로, 의사 모듈(다음 문단 참고)을 만들 때 피클은 모듈 계층 구조에서 해당 클래스의 공식 문서화된 위치가 아니라 클래스가 실제로 정의된 모듈의 이름에 의존합니다.
이 PEP에서 “의사 모듈(pseudo-module)”이란 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에서 가져온 것으로, 해당 PEP에서는 최상위 모듈을 기준으로 중첩된 클래스나 함수 정의까지의 이름 경로를 저장하는 데 사용됩니다.
모듈의 경우 __qualname__은 일반적으로 __name__과 같으며, 이는 PEP 3155에서 최상위 함수와 클래스의 경우와 마찬가지입니다. 그러나 위의 문제들을 해결할 수 있도록 일부 상황에서는 두 값이 달라집니다.
구체적으로, __name__이 다른 목적(예를 들어 메인 모듈을 나타내는 것)으로 수정될 때마다 __qualname__은 변경되지 않은 채로 유지되므로, 원래의 수정되지 않은 값이 필요한 코드가 그 값에 접근할 수 있습니다.
모듈 로더가 __qualname__을 직접 초기화하지 않으면, 임포트 시스템이 이를 자동으로 추가합니다(__name__과 같은 값으로 설정합니다).
대체 이름
새 속성의 이름으로 두 가지 대안도 고려되었습니다. “전체 이름”(__fullname__)과 “구현 이름”(__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에 메인 모듈의 실제 이름을 알려 줄 수 있으므로, multiprocessing은 자식 프로세스에 전달되는 직렬화된 정보에 그 이름을 간단히 포함할 수 있게 되어, 현재의 미심쩍은 __file__ 속성 인트로스펙션이 필요 없어집니다.
이전 Python 버전에서는, __file__ 속성을 바탕으로 메인 모듈을 실행하는 방법을 알아내려 할 때 위에서 설명한 split_path_module() 알고리즘을 적용하여 multiprocessing을 개선할 수 있습니다.
명시적 상대 임포트
이 PEP는 메인 모듈에서 __package__를 무조건 __qualname__.rpartition('.')[0]으로 정의할 것을 제안합니다. 그 외에는 명시적 상대 임포트의 동작을 그대로 둘 것을 제안합니다.
특히, 명시적 상대 임포트가 일어날 때 모듈에 __package__가 설정되어 있지 않으면, 자동으로 캐시되는 값은 계속해서 __qualname__이 아니라 __name__으로부터 유도됩니다. 이렇게 하면 __package__를 직접 설정하는 대신 __name__을 조정하여 의도적으로 상대 임포트를 조작하는 기존 코드와의 하위 호환성 문제를 최소화할 수 있습니다.
이 PEP는 __package__를 폐지(deprecate)할 것을 제안하지 않습니다. __qualname__이 도입된 이후로는 기술적으로 중복이지만, Python 3.x의 수명 동안 이를 폐지하는 수고를 들일 만한 가치는 없습니다.
참조 구현
아직 없습니다.
참고 문헌
Copyright
This document has been placed in the public domain.