PEP 562 – 모듈 __getattr__과 __dir__
- Author:
- Ivan Levkivskyi <levkivskyi at gmail.com>
- Status:
- Final
- Type:
- Standards Track
- Created:
- 09-Sep-2017
- Python-Version:
- 3.7
- Post-History:
- 09-Sep-2017
- Resolution:
- Python-Dev message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
개요
모듈 속성 접근에 대한 기본적인 사용자 정의를 제공하기 위해, 모듈에 정의된 __getattr__과 __dir__ 함수를 지원하는 것을 제안합니다.
근거
모듈 속성에 대한 접근을 사용자 정의하거나 그 밖의 방법으로 제어할 수 있으면 편리한 경우가 있습니다. 대표적인 예로 폐지 경고(deprecation warning)를 관리하는 경우가 있습니다. 일반적인 임시방편은 모듈 객체의 __class__를 types.ModuleType의 사용자 정의 서브클래스로 대입하거나, sys.modules 항목을 사용자 정의 래퍼 인스턴스로 교체하는 것입니다. 모듈에 직접 정의된 __getattr__을 인식하여, 이것이 일반적인 __getattr__ 메서드처럼 동작하되 모듈 인스턴스에 정의된다는 점만 다르게 함으로써 이 절차를 단순화하면 편리할 것입니다. 예를 들면:
# lib.py
from warnings import warn
deprecated_names = ["old_function", ...]
def _deprecated_old_function(arg, other):
...
def __getattr__(name):
if name in deprecated_names:
warn(f"{name} is deprecated", DeprecationWarning)
return globals()[f"_deprecated_{name}"]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# main.py
from lib import old_function # Works, but emits the warning
__getattr__의 또 다른 널리 쓰이는 사용 사례는 지연된 서브모듈 임포트일 것입니다. 간단한 예를 살펴봅시다.:
# lib/__init__.py
import importlib
__all__ = ['submod', ...]
def __getattr__(name):
if name in __all__:
return importlib.import_module("." + name, __name__)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
# lib/submod.py
print("Submodule loaded")
class HeavyClass:
...
# main.py
import lib
lib.submod.HeavyClass # prints "Submodule loaded"
유사한 기능을 위해 인스턴스 프로퍼티를 지원하는 것을 제안하는 관련 제안 PEP 549가 있습니다. 차이점은 이 PEP가 더 빠르고 단순한 메커니즘을 제안하지만, 더 기본적인 수준의 사용자 정의만 제공한다는 것입니다. 이 제안의 추가적인 동기는 PEP 484가 이미 파이썬 스텁 파일에서 이러한 목적으로 모듈 __getattr__을 사용하는 것을 정의하고 있다는 점입니다. PEP 484를 참고하십시오.
또한, 모듈에 대한 dir() 호출 결과를 수정하여 폐지된 속성이나 그 밖의 동적으로 생성된 속성을 보여줄 수 있도록, 모듈 수준의 __dir__ 함수를 지원하는 것을 제안합니다. 예를 들면:
# lib.py
deprecated_names = ["old_function", ...]
__all__ = ["new_function_one", "new_function_two", ...]
def new_function_one(arg, other):
...
def new_function_two(arg, other):
...
def __dir__():
return sorted(__all__ + deprecated_names)
# main.py
import lib
dir(lib) # prints ["new_function_one", "new_function_two", "old_function", ...]
명세
모듈 수준의 __getattr__ 함수는 어트리뷰트의 이름인 하나의 인자를 받아들여야 하며, 계산된 값을 반환하거나 AttributeError를 발생시켜야 합니다.:
def __getattr__(name: str) -> Any: ...
정상적인 조회(즉, object.__getattribute__)를 통해 모듈 객체에서 어트리뷰트를 찾지 못하면, AttributeError를 발생시키기 전에 모듈 __dict__에서 __getattr__을 찾습니다. 찾으면, 어트리뷰트 이름과 함께 호출되고 그 결과가 반환됩니다. 이름을 모듈 전역으로 조회하면 모듈 __getattr__을 우회합니다. 이는 의도된 것으로, 그렇지 않으면 내장 객체에 대해 __getattr__을 호출하는 것이 성능을 크게 해칠 것입니다.
__dir__ 함수는 인자를 받지 않아야 하며, 모듈에서 접근 가능한 이름들을 나타내는 문자열 리스트를 반환해야 합니다.:
def __dir__() -> List[str]: ...
존재하는 경우, 이 함수는 모듈에 대한 표준 dir() 검색을 재정의합니다.
이 PEP의 참조 구현은 [2]에서 찾을 수 있습니다.
하위 호환성과 성능에 미치는 영향
이 PEP는 모듈 수준(전역) 이름 __getattr__과 __dir__을 사용하는 코드를 깨뜨릴 수 있습니다. (하지만 언어 참조는 문서화되지 않은 모든 던더 이름을 명시적으로 예약해 두고 있으며, “경고 없는 손상”을 허용합니다. [3]을 참조하십시오.) 이 PEP의 성능상 영향은 미미한데, __getattr__은 어트리뷰트가 없을 때만 호출되기 때문입니다.
모듈 어트리뷰트 검색을 수행하는 일부 도구는 __getattr__을 예상하지 못할 수 있습니다. 그러나 이 문제는 새로운 것이 아닌데, 재정의된 __getattr__과 __dir__을 가진 모듈 서브클래스로 모듈을 대체하는 것이 이미 가능하기 때문입니다. 다만 이 PEP로 인해 그러한 문제가 더 자주 발생할 수 있습니다.
논의
모듈 __getattr__의 사용은 참조된 객체가 피클 가능한 상태를 유지하도록 주의가 필요하다는 점에 유의하십시오. 예를 들어, 함수의 __name__ 어트리뷰트는 __getattr__을 통해 접근 가능한 이름과 일치해야 합니다.:
def keep_pickleable(func):
func.__name__ = func.__name__.replace('_deprecated_', '')
func.__qualname__ = func.__qualname__.replace('_deprecated_', '')
return func
@keep_pickleable
def _deprecated_old_function(arg, other):
...
클래스 수준 __getattr__에서 하듯이 재귀를 피하도록 주의해야 합니다.
__getattr__ 트리거 없이 모듈 전역을 사용하려면(예를 들어 지연 로드되는 서브모듈을 사용하고자 하는 경우) 다음과 같이 접근할 수 있습니다:
sys.modules[__name__].some_global
또는 다음과 같이:
from . import some_global
후자는 모듈 속성을 설정하므로 __getattr__이 한 번만 호출된다는 점에 유의하십시오.
참고 자료
Copyright
This document has been placed in the public domain.