Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

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

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain or CC0-1.0, whichever is more permissive 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 모듈에서 사용자 정의 __setattr____delattr__ 메서드를 지원하여 PEP 562를 넘어 모듈 속성 접근의 사용자 지정을 확장할 것을 제안합니다.

동기

모듈 __setattr__에는 다음과 같은 여러 잠재적 용도가 있습니다.

  1. 속성 설정을 완전히 방지합니다(즉, 읽기 전용으로 만듭니다).
  2. 할당할 값을 검증합니다.
  3. 속성 설정을 가로채 다른 상태를 갱신합니다.

읽기 전용 속성을 제대로 지원하려면 해당 속성의 삭제를 방지하기 위해 __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는 이 시나리오를 부분적으로만 해결합니다. 예를 들어 이름이 변경된 속성을 변경하려는 시도 중에는 경고를 발생시킬 수 없습니다.

각주