PEP 726 – 모듈 __setattr__ 및 __delattr__
- Author:
- Sergey B Kirpichev <skirpichev at gmail.com>
- Sponsor:
- Adam Turner <adam at python.org>
- Discussions-To:
- Discourse thread
- Status:
- Rejected
- Type:
- Standards Track
- Created:
- 24-Aug-2023
- Python-Version:
- 3.13
- Post-History:
- 06-Apr-2023, 31-Aug-2023
- Resolution:
- Discourse message
번역·라이선스 안내
이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판
초록
이 PEP는 모듈에서 사용자 정의 __setattr__ 및 __delattr__ 메서드를 지원하여 PEP 562를 넘어 모듈 속성 접근의 사용자 지정을 확장할 것을 제안합니다.
동기
모듈 __setattr__에는 다음과 같은 여러 잠재적 용도가 있습니다.
- 속성 설정을 완전히 방지합니다(즉, 읽기 전용으로 만듭니다).
- 할당할 값을 검증합니다.
- 속성 설정을 가로채 다른 상태를 갱신합니다.
읽기 전용 속성을 제대로 지원하려면 해당 속성의 삭제를 방지하기 위해 __delattr__ 함수도 추가해야 합니다.
모듈에 정의된 __setattr__ 및 __delattr__ 메서드를 인식하여 이러한 사용자 지정을 직접 지원하면 편리합니다. 이 메서드들은 일반적인 object.__setattr__() 및 object.__delattr__() 메서드처럼 동작하지만, 모듈 인스턴스에 정의됩니다. 기존 __getattr__ 및 __dir__ 메서드와 함께 사용하면 모듈 속성 접근을 사용자 지정하는 모든 변형을 간소화할 수 있습니다.
예를 들면 다음과 같습니다.
# mplib.py
CONSTANT = 3.14
prec = 53
dps = 15
def dps_to_prec(n):
"""Return the number of bits required to represent n decimals accurately."""
return max(1, int(round((int(n)+1)*3.3219280948873626)))
def prec_to_dps(n):
"""Return the number of accurate decimals that can be represented with n bits."""
return max(1, int(round(int(n)/3.3219280948873626)-1))
def validate(n):
n = int(n)
if n <= 0:
raise ValueError('Positive integer expected')
return n
def __setattr__(name, value):
if name == 'CONSTANT':
raise AttributeError('Read-only attribute!')
if name == 'dps':
value = validate(value)
globals()['dps'] = value
globals()['prec'] = dps_to_prec(value)
return
if name == 'prec':
value = validate(value)
globals()['prec'] = value
globals()['dps'] = prec_to_dps(value)
return
globals()[name] = value
def __delattr__(name):
if name in ('CONSTANT', 'dps', 'prec'):
raise AttributeError('Read-only attribute!')
del globals()[name]
>>> import mplib
>>> mplib.foo = 'spam'
>>> mplib.CONSTANT = 42
Traceback (most recent call last):
...
AttributeError: Read-only attribute!
>>> del mplib.foo
>>> del mplib.CONSTANT
Traceback (most recent call last):
...
AttributeError: Read-only attribute!
>>> mplib.prec
53
>>> mplib.dps
15
>>> mplib.dps = 5
>>> mplib.prec
20
>>> mplib.dps = 0
Traceback (most recent call last):
...
ValueError: Positive integer expected
기존 옵션
현재의 우회 방법은 모듈 객체의 __class__를 사용자 지정 types.ModuleType 서브클래스에 할당하는 것입니다([1] 참조).
예를 들어, 속성의 수정 또는 삭제를 방지하려면 다음과 같이 할 수 있습니다.
# mod.py
import sys
from types import ModuleType
CONSTANT = 3.14
class ImmutableModule(ModuleType):
def __setattr__(name, value):
raise AttributeError('Read-only attribute!')
def __delattr__(name):
raise AttributeError('Read-only attribute!')
sys.modules[__name__].__class__ = ImmutableModule
그러나 이 방식은 제안된 해결책보다 느립니다(~2배). 더 중요한 점은 속성 접근에서도 눈에 띄는 속도 저하(~2~3배)를 초래한다는 것입니다.
사양
모듈 수준의 __setattr__ 함수는 두 인자, 즉 속성 이름과 할당할 값을 받아들이고, None을 반환하거나 AttributeError를 발생시켜야 합니다.
def __setattr__(name: str, value: typing.Any, /) -> None: ...
__delattr__ 함수는 하나의 인자, 즉 속성 이름을 받아들이고, None을 반환하거나 AttributeError를 발생시켜야 합니다.
def __delattr__(name: str, /) -> None: ...
__setattr__ 및 __delattr__ 함수는 모듈의 __dict__에서 조회됩니다. 함수가 존재하면 속성 설정 또는 삭제를 사용자 지정하기 위해 해당 함수가 호출되고, 그렇지 않으면 일반 메커니즘(모듈 딕셔너리에 값을 저장하거나 삭제하는 방식)이 작동합니다.
모듈 __setattr__ 또는 __delattr__의 정의는 속성 접근 구문을 사용하여 수행하는 조회에만 영향을 줍니다 — 모듈 전역에 직접 접근하는 경우(모듈 내부에서 globals()를 사용하거나 모듈의 전역 딕셔너리에 대한 참조를 사용하는 경우)는 영향을 받지 않습니다. 예를 들면 다음과 같습니다.
>>> import mod
>>> mod.__dict__['foo'] = 'spam' # bypasses __setattr__, defined in mod.py
또는 다음과 같습니다.
# mod.py
def __setattr__(name, value):
...
foo = 'spam' # bypasses __setattr__
globals()['bar'] = 'spam' # here too
def f():
global x
x = 123
f() # and here
모듈 전역을 사용하면서 __setattr__(또는 __delattr__)을 트리거하려면 모듈 코드 내에서 sys.modules[__name__]을 통해 접근할 수 있습니다.
# mod.py
sys.modules[__name__].foo = 'spam' # bypasses __setattr__
def __setattr__(name, value):
...
sys.modules[__name__].bar = 'spam' # triggers __setattr__
이 제한은 의도된 것입니다(PEP 562의 경우와 마찬가지입니다). 인터프리터는 모듈 전역에 대한 접근을 매우 고도로 최적화하므로, 이러한 최적화를 모두 비활성화하고 Python으로 작성된 특수 메서드를 거치게 하면 코드가 용납할 수 없을 정도로 느려지기 때문입니다.
이 내용을 가르치는 방법
문서의 “Customizing module attribute access” [1] 섹션이 새 함수를 포함하도록 확장됩니다.
참조 구현
이 PEP의 참조 구현은 CPython PR #108261에서 확인할 수 있습니다.
하위 호환성
이 PEP는 모듈 수준(전역) 이름 __setattr__ 및 __delattr__을 사용하는 코드를 손상시킬 수 있지만, 언어 레퍼런스는 문서화되지 않은 모든 더블 언더스코어 이름을 명시적으로 예약하며 “경고 없는 손상” [2]을 허용합니다.
추가적인 딕셔너리 조회는 딕셔너리에 값을 저장하거나 삭제하는 것보다 훨씬 저렴하므로, 이 PEP의 성능 영향은 작습니다. 또한 사용자가 성능 문제가 될 정도로 자주 속성을 설정하거나(및/또는 삭제할) 것으로 예상하는 모듈은 상상하기 어렵습니다. 반면 제안된 메커니즘을 사용하면 속성 액세스 속도에 영향을 주지 않고 속성 설정 및 삭제를 재정의할 수 있으며, 이는 성능 저하가 발생할 가능성이 훨씬 높은 시나리오입니다.
논의
Victor Stinner이 지적했듯이, 제안된 API는 이미 표준 라이브러리에서 유용하게 사용될 수 있습니다. 예를 들어 sys.modules의 타입이 항상 dict가 되도록 보장할 수 있습니다.
>>> import sys
>>> sys.modules = 123
>>> import asyncio
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<frozen importlib._bootstrap>", line 1260, in _find_and_load
AttributeError: 'int' object has no attribute 'get'
또는 중요한 sys속성의 삭제를 방지할 수 있으며, 이는 코드를 더 복잡하게 만듭니다. 예를 들어 sys.stderr를 사용하는 코드는 해당 속성이 존재하는지, 그리고 None이 아닌지를 확인해야 합니다. 현재는 함수를 포함하여 어떤 sys속성이든 제거할 수 있습니다.
>>> import sys
>>> del sys.excepthook
>>> 1+ # notice the next line
sys.excepthook is missing
File "<stdin>", line 1
1+
^
SyntaxError: invalid syntax
기타 세부 사항은 관련 이슈를 참조하십시오.
다른 표준 라이브러리 모듈에도 기능으로 재정의할 수 있는 속성이 포함되어 있으며, 여기에서 일부 입력 검사를 수행하면 유용할 수 있습니다. 예: threading.excepthook, warnings.showwarning, io.DEFAULT_BUFFER_SIZE 또는 os.SEEK_SET.
또한 모듈 속성 액세스를 사용자 지정하는 일반적인 사용 사례는 사용 중단 경고를 관리하는 것입니다. 그러나 PEP 562는 이 시나리오를 부분적으로만 해결합니다. 예를 들어 이름이 변경된 속성을 변경하려는 시도 중에는 경고를 발생시킬 수 없습니다.
각주
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.