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는 PEP 562를 넘어 모듈 속성 접근의 사용자 정의를 확장하기 위해, 모듈에서 사용자 정의 __setattr__ 및 __delattr__ 메서드를 지원할 것을 제안합니다.

동기

모듈 __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으로 작성된 특수 메서드를 거치게 하면 코드가 용납할 수 없을 만큼 느려지기 때문입니다.

교육 방법

문서의 “모듈 속성 접근 사용자 정의” [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는 이 시나리오를 부분적으로만 달성합니다. 예를 들어, 이름이 바뀐 속성을 변경하려는 시도 중에 경고를 발생시키는 것은 불가능합니다.

각주