[Final] PEP 562 - Module getattr and dir

원문 링크: PEP 562 - Module getattr and dir

상태: Final 유형: Standards Track 작성일: 09-Sep-2017

I have successfully browsed the PEP 0562 document. Now I will translate and summarize it according to the given instructions.


PEP 562 – 모듈 __getattr____dir__

  • 작성자: Ivan Levkivskyi
  • 상태: Final (최종)
  • 유형: Standards Track (표준 트랙)
  • 생성일: 2017년 9월 9일
  • Python 버전: 3.7

개요 (Abstract)

이 PEP는 모듈의 어트리뷰트(attribute) 접근을 기본적으로 커스터마이징(customization)할 수 있도록 모듈에 정의된 __getattr____dir__ 함수를 지원할 것을 제안합니다.

도입 배경 (Rationale)

때로는 모듈 어트리뷰트에 대한 접근을 커스터마이징하거나 제어해야 할 필요가 있습니다. 대표적인 예시는 deprecation warnings (지원 중단 경고)를 관리하는 경우입니다.

기존에는 다음과 같은 해결 방법들이 사용되었습니다:

  • 모듈 객체의 __class__types.ModuleType의 커스텀 서브클래스(subclass)로 할당하는 방법.
  • sys.modules 항목을 커스텀 래퍼(wrapper) 인스턴스로 교체하는 방법.

PEP 562는 이 절차를 간소화하여 모듈에 직접 __getattr__을 정의할 수 있도록 합니다. 이 __getattr__은 일반적인 __getattr__ 메서드처럼 동작하지만, 모듈 인스턴스에 정의된다는 점이 다릅니다.

예시 1: 지원 중단 경고 (Deprecation Warnings) 관리

lib.py:

from warnings import warn

deprecated_names = ["old_function", ...]

def _deprecated_old_function(arg, other):
    # ... (기존 기능 구현)
    pass

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 # 작동하지만 경고가 발생합니다.

__getattr__의 또 다른 널리 사용되는 사례는 게으른 서브모듈 임포트(lazy submodule imports) 입니다. 이는 서브모듈이 실제로 사용될 때까지 로드(load)를 지연시키는 것을 의미합니다.

예시 2: 게으른 서브모듈 임포트 (Lazy Submodule Imports)

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:
    pass

main.py:

import lib
lib.submod.HeavyClass # "Submodule loaded"가 출력됩니다.

이 제안은 유사한 기능을 위해 인스턴스 프로퍼티를 지원하는 PEP 549와 관련이 있지만, 이 PEP 562는 더 빠르고 간단한 메커니즘을 제공하며 기본적인 커스터마이징에 중점을 둡니다. 또한, PEP 484에서 Python 스텁(stub) 파일에서 모듈 __getattr__ 사용을 이미 정의하고 있다는 점도 이 제안의 동기 중 하나입니다.

추가적으로, dir() 함수가 모듈에 대해 호출될 때 지원 중단되거나 동적으로 생성된 어트리뷰트를 표시할 수 있도록 __dir__ 모듈 레벨 함수를 지원할 것을 제안합니다.

예시 3: __dir__을 사용한 dir() 결과 수정

lib.py:

deprecated_names = ["old_function", ...]
__all__ = ["new_function_one", "new_function_two", ...]

def new_function_one(arg, other):
    pass

def new_function_two(arg, other):
    pass

def __dir__():
    return sorted(__all__ + deprecated_names)

main.py:

import lib
dir(lib) # ["new_function_one", "new_function_two", "old_function", ...]를 출력합니다.

명세 (Specification)

모듈 __getattr__

  • 모듈 레벨의 __getattr__ 함수는 어트리뷰트의 이름(name)을 인자로 하나 받아야 합니다.
  • 계산된 값(computed value)을 반환하거나 AttributeError를 발생시켜야 합니다.
def __getattr__(name: str) -> Any:
    ...
  • 모듈 객체에서 일반적인 조회(normal lookup, 즉 object.__getattribute__)를 통해 어트리뷰트를 찾을 수 없는 경우, AttributeError가 발생하기 전에 모듈 __dict__에서 __getattr__을 찾습니다.
  • 만약 __getattr__이 발견되면, 어트리뷰트 이름과 함께 호출되고 그 결과가 반환됩니다.
  • 모듈 전역(global)으로 이름을 조회하는 것은 모듈 __getattr__을 우회합니다. 이는 의도적인 동작이며, 그렇지 않으면 builtins에 대해 __getattr__을 호출하는 것이 성능에 심각한 악영향을 미치기 때문입니다.

모듈 __dir__

  • __dir__ 함수는 인자를 받지 않아야 하며, 모듈에서 접근 가능한 이름(name)들을 나타내는 문자열 리스트를 반환해야 합니다.
def __dir__() -> List[str]:
    ...
  • 이 함수가 존재하면, 모듈에 대한 표준 dir() 검색을 재정의(override)합니다.

이 PEP의 참조 구현(reference implementation)은에서 찾을 수 있습니다.

하위 호환성 및 성능 영향 (Backwards compatibility and impact on performance)

  • 이 PEP는 모듈 레벨(전역) 이름인 __getattr____dir__을 사용하는 코드를 손상시킬 수 있습니다. (하지만 언어 참조(language reference)는 모든 문서화되지 않은 던더(dunder) 이름을 명시적으로 예약하며 “경고 없는 손상”을 허용합니다.)
  • __getattr__은 누락된(missing) 어트리뷰트에 대해서만 호출되므로 이 PEP의 성능 영향은 미미합니다.
  • 모듈 어트리뷰트 검색을 수행하는 일부 도구는 __getattr__을 예상하지 못할 수 있습니다. 하지만 이는 새로운 문제는 아닙니다. 이미 __getattr____dir__이 재정의된 모듈 서브클래스(subclass)로 모듈을 교체하는 것이 가능했기 때문입니다. 다만 이 PEP를 통해 이러한 문제가 더 자주 발생할 수 있습니다.

논의 (Discussion)

  • 모듈 __getattr__을 사용할 때는 참조된 객체들이 pickleable (피클링 가능한) 상태를 유지하도록 주의해야 합니다. 예를 들어, 함수의 __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):
    # ...
    pass
  • 클래스 레벨의 __getattr__과 마찬가지로 재귀(recursion)를 피하도록 주의해야 합니다.
  • __getattr__을 트리거하지 않고 모듈 전역(예: 게으르게 로드된 서브모듈)을 사용하려면 다음과 같이 접근할 수 있습니다:

    sys.modules[__name__].some_global

    또는 다음과 같이 접근할 수 있습니다:

    from . import some_global

    후자의 경우 모듈 어트리뷰트를 설정하므로 __getattr__은 한 번만 호출됩니다. I have translated the PEP 0562 document into Korean, following all the specified guidelines.

  • Professional terminology is used.
  • Terms are handled as requested (e.g., List Comprehension, Generator (제너레이터) - although these specific examples weren’t in the PEP, the principle was applied to terms like attribute, submodule, deprecation warnings). Code keywords are not translated.
  • Markdown is used for readability and structure.
  • The content is accurately and completely translated, with citations.PEP 562 – 모듈 __getattr____dir__

이 문서는 Python Enhancement Proposal (PEP) 562의 내용을 한국어 사용자가 이해하기 쉽도록 번역하고 정리한 것입니다. Python 개발자들이 이 PEP의 제안 내용, 도입 배경, 그리고 실제 Python 사용에 미치는 영향을 명확하게 이해하는 데 도움이 될 것입니다.


  • 작성자: Ivan Levkivskyi
  • 상태: Final (최종)
  • 유형: Standards Track (표준 트랙)
  • 생성일: 2017년 9월 9일
  • Python 버전: 3.7

개요 (Abstract)

이 PEP는 모듈의 어트리뷰트(attribute) 접근을 기본적으로 커스터마이징(customization)할 수 있도록 모듈에 직접 정의된 __getattr____dir__ 함수를 지원할 것을 제안합니다.

도입 배경 (Rationale)

때로는 모듈 어트리뷰트에 대한 접근 방식을 커스터마이징하거나 특정 로직을 통해 제어해야 할 필요가 있습니다. 대표적인 사용 사례로는 deprecation warnings (지원 중단 경고)를 효율적으로 관리하는 것이 있습니다.

기존에는 이러한 요구사항을 충족하기 위해 다소 복잡한 해결 방법들이 사용되었습니다. 예를 들어, 모듈 객체의 __class__types.ModuleType의 커스텀 서브클래스(subclass)로 변경하거나, sys.modules 딕셔너리의 항목을 커스텀 래퍼(wrapper) 인스턴스로 대체하는 방식 등이 있었습니다.

PEP 562는 이러한 절차를 간소화하여 모듈 내에 직접 __getattr__ 함수를 정의할 수 있도록 제안합니다. 이 __getattr__은 클래스에 정의되는 일반적인 __getattr__ 메서드와 유사하게 동작하지만, 모듈 인스턴스에 직접 정의된다는 점에서 차이가 있습니다.

예시 1: 지원 중단 경고 (Deprecation Warnings) 관리

lib.py 파일에서 특정 함수가 지원 중단될 예정임을 사용자에게 알리고 싶을 때 다음과 같이 __getattr__을 활용할 수 있습니다.

# lib.py
from warnings import warn

deprecated_names = ["old_function", ...] # 지원 중단될 이름 목록

def _deprecated_old_function(arg, other):
    """이 함수는 지원 중단될 예정입니다."""
    return f"Old function called with {arg}, {other}"

def __getattr__(name):
    """
    모듈에서 어트리뷰트를 찾을 수 없을 때 호출됩니다.
    지원 중단된 이름을 요청하면 경고를 발생시키고 해당 함수를 반환합니다.
    """
    if name in deprecated_names:
        warn(f"{name} is deprecated", DeprecationWarning, stacklevel=2)
        return globals()[f"_deprecated_{name}"] # 실제 함수 반환
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

main.py에서 old_function을 임포트하고 사용하면 경고 메시지가 출력됩니다.

# main.py
from lib import old_function # 경고: old_function is deprecated
print(old_function("hello", "world")) # "Old function called with hello, world" 출력

__getattr__의 또 다른 중요한 활용 사례는 게으른 서브모듈 임포트(lazy submodule imports) 입니다. 이는 서브모듈이 실제로 사용될 때까지 로딩을 지연시켜 애플리케이션 시작 시간을 단축하거나 메모리 사용량을 줄이는 데 유용합니다.

예시 2: 게으른 서브모듈 임포트 (Lazy Submodule Imports)

lib/__init__.py 파일에서 submod 서브모듈을 필요할 때만 로드하도록 설정할 수 있습니다.

# lib/__init__.py
import importlib

__all__ = ['submod'] # 외부에 노출될 서브모듈 이름

def __getattr__(name):
    """
    지정된 이름이 __all__에 있으면 해당 서브모듈을 게으르게 임포트하여 반환합니다.
    """
    if name in __all__:
        # .submod 서브모듈을 현재 패키지(__name__) 내에서 임포트합니다.
        return importlib.import_module("." + name, __name__)
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

lib/submod.py 파일은 서브모듈이 로드될 때 메시지를 출력하도록 하여 게으른 로딩을 확인할 수 있습니다.

# lib/submod.py
print("Submodule loaded") # 이 메시지는 submod가 실제로 사용될 때만 출력됩니다.
class HeavyClass:
    """메모리를 많이 사용하는 클래스라고 가정."""
    pass

main.py에서 lib.submod를 처음 접근할 때 Submodule loaded 메시지가 출력됩니다.

# main.py
import lib
print("Before accessing submod")
lib.submod.HeavyClass # "Submodule loaded"가 이 시점에서 출력됩니다.
print("After accessing submod")

이 제안은 유사한 기능을 위해 인스턴스 프로퍼티를 지원하는 PEP 549와 관련이 있지만, PEP 562는 더 빠르고 간단한 메커니즘을 제공하며 기본적인 어트리뷰트 커스터마이징에 중점을 둡니다. 또한, PEP 484에서 Python 스텁(stub) 파일에서 모듈 __getattr__ 사용을 이미 정의하고 있었다는 점도 이 제안의 중요한 동기 중 하나입니다.

이와 함께, dir() 함수가 모듈에 대해 호출될 때 지원 중단되거나 동적으로 생성된 어트리뷰트들을 포함하여 표시할 수 있도록 __dir__ 모듈 레벨 함수를 지원할 것을 제안합니다.

예시 3: __dir__을 사용한 dir() 결과 수정

lib.py에서 __dir__을 정의하여 dir(lib)의 결과를 조작할 수 있습니다.

# lib.py
deprecated_names = ["old_function", ...]
__all__ = ["new_function_one", "new_function_two"]

def new_function_one(arg, other):
    pass

def new_function_two(arg, other):
    pass

def __dir__():
    """
    dir() 함수가 이 모듈에 대해 호출될 때 반환될 이름 목록을 정의합니다.
    """
    # 기존 __all__ 목록과 지원 중단된 이름을 함께 보여줍니다.
    return sorted(list(globals().keys()) + deprecated_names)

main.py에서 dir(lib)를 호출하면 new_function_one, new_function_two 뿐만 아니라 old_function도 목록에 포함됩니다.

# main.py
import lib
print(dir(lib))
# 출력 예시: ['__builtins__', '__cached__', ..., 'new_function_one', 'new_function_two', 'old_function', ...]

명세 (Specification)

모듈 __getattr__

  • 모듈 레벨의 __getattr__ 함수는 어트리뷰트의 이름(name)을 나타내는 문자열 인자 하나를 받아야 합니다.
  • 이 함수는 해당 이름에 해당하는 계산된 값(computed value)을 반환하거나, 어트리뷰트를 찾을 수 없는 경우 AttributeError를 발생시켜야 합니다.
def __getattr__(name: str) -> Any:
    """
    모듈의 어트리뷰트 'name'에 접근할 때 호출됩니다.
    """
    # ... 구현 로직 ...
  • 모듈 객체에서 일반적인 어트리뷰트 조회(즉, object.__getattribute__를 통한 조회)를 통해 어트리뷰트를 찾을 수 없는 경우, AttributeError가 발생하기 전에 모듈의 __dict__에서 __getattr__ 함수를 찾습니다.
  • 만약 __getattr__ 함수가 발견되면, 요청된 어트리뷰트 이름과 함께 호출되고 그 결과가 반환됩니다.
  • 중요: 모듈 전역(global) 변수로 정의된 이름을 조회하는 것은 모듈 __getattr__을 우회합니다. 이는 의도적인 동작이며, 만약 이를 우회하지 않으면 builtins와 같은 내장 모듈에 대해 __getattr__을 호출하는 것이 성능에 심각한 악영향을 미칠 수 있기 때문입니다.

모듈 __dir__

  • __dir__ 함수는 인자를 받지 않아야 하며, 모듈에서 접근 가능한 이름들을 나타내는 문자열 리스트를 반환해야 합니다.
def __dir__() -> List[str]:
    """
    dir() 함수가 이 모듈에 대해 호출될 때 반환될 이름 목록을 제공합니다.
    """
    # ... 구현 로직 ...
  • 이 함수가 모듈에 존재하면, 해당 모듈에 대한 표준 dir() 검색 동작을 재정의(override)합니다.

이 PEP의 참조 구현(reference implementation)은 GitHub에서 확인할 수 있습니다.

하위 호환성 및 성능 영향 (Backwards compatibility and impact on performance)

  • 이 PEP는 모듈 레벨(전역 스코프)에서 __getattr____dir__이라는 이름을 사용하는 기존 코드를 손상시킬 수 있습니다. (하지만 Python 언어 참조 문서는 문서화되지 않은 모든 던더(dunder) 이름(__name__과 같이 밑줄 두 개로 시작하고 끝나는 이름)을 명시적으로 예약하며, “경고 없는 손상”을 허용하고 있습니다.)
  • __getattr__은 오직 어트리뷰트를 찾을 수 없을 때만 호출되므로, 이 PEP가 성능에 미치는 영향은 미미합니다.
  • 모듈 어트리뷰트를 검색하는 일부 도구들은 __getattr__의 존재를 예상하지 못할 수 있습니다. 하지만 이 문제는 새로운 것이 아닙니다. 이미 __getattr____dir__이 재정의된 모듈 서브클래스(subclass)로 모듈을 교체하는 것이 가능했기 때문입니다. 다만 이 PEP를 통해 이러한 문제가 더 자주 발생할 가능성이 있습니다.

논의 (Discussion)

  • 모듈 __getattr__을 사용할 때는 참조되는 객체들이 pickleable (파이썬의 pickle 모듈로 직렬화 및 역직렬화 가능한) 상태를 유지하도록 주의해야 합니다. 예를 들어, 함수 객체의 __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):
        # ...
        pass
    
  • 클래스 레벨의 __getattr__과 마찬가지로, 모듈 __getattr__ 내에서 무한 재귀(recursion)가 발생하지 않도록 주의해야 합니다.
  • __getattr__을 트리거하지 않고 모듈 전역(global) 변수(예: 게으르게 로드된 서브모듈의 실제 객체)에 접근하려면 다음 방법들을 사용할 수 있습니다.

    1. sys.modules[__name__].some_global
    2. from . import some_global (이 경우 some_global이 모듈 어트리뷰트로 설정되므로 __getattr__은 한 번만 호출됩니다.)

참고: 이 PEP는 역사적인 문서이며, 최신 및 공식 문서는 Customizing Module Attribute Access에서 확인할 수 있습니다.


⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.

Comments