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는 PEP 562를 넘어 모듈 속성 접근의 사용자 정의를 확장하기 위해, 모듈에서 사용자 정의 __setattr__ 및 __delattr__ 메서드를 지원할 것을 제안합니다.
동기
모듈 __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으로 작성된 특수 메서드를 거치게 하면 코드가 용납할 수 없을 만큼 느려지기 때문입니다.
교육 방법
문서의 “모듈 속성 접근 사용자 정의” [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.