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__ 함수를 지원할 것을 제안합니다.
근거
모듈 속성에 대한 접근을 사용자 정의하거나 그 밖의 방법으로 제어할 수 있으면 편리한 경우가 있습니다. 대표적인 예는 폐지 경고를 관리하는 것입니다. 일반적인 우회 방법은 모듈 객체의 __class__에 types.ModuleType의 사용자 정의 서브클래스를 할당하거나, sys.modules 항목을 사용자 정의 래퍼 인스턴스로 교체하는 것입니다. 모듈에 직접 정의된 __getattr__를 인식하여 이 절차를 단순화하면 편리할 것입니다. 이 __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가 이미 Python 스텁 파일에서 이 목적으로 모듈 __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]을 참조하십시오.) __getattr__는 존재하지 않는 속성에 대해서만 호출되므로 이 PEP가 성능에 미치는 영향은 미미합니다.
모듈 속성 탐색을 수행하는 일부 도구는 __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.