Python의 마법 방법 V

2024. 4. 11. 21:19python/advanced

증강 과제

산술 연산의 경우 변수의 현재 값을 사용하여 변수 자체를 업데이트하는 일반적인 표현식을 찾을 수 있습니다. 이 작업의 전형적인 예는 카운터나 누산기를 업데이트해야 하는 경우입니다.

In [ ]:
counter = 0
counter = counter + 1
counter = counter + 1
counter

이 코드 조각의 두 번째 및 세 번째 줄은 이전 값을 사용하여 카운터 값을 업데이트합니다. 이러한 유형의 연산은 프로그래밍에서 매우 일반적이므로 Python에는 증강 할당 연산자 라고 하는 단축어가 있습니다.

예를 들어, 추가를 위해 증가된 할당 연산자를 사용하여 위 코드를 단축할 수 있습니다.

In [ ]:
counter = 0
counter += 1
counter += 1
counter

이 코드는 더 간결해 보이고 더 파이썬적입니다. 또한 더 우아합니다. 이 연산자( +=)는 Python이 지원하는 유일한 연산자가 아닙니다. 다음은 수학 맥락에서 지원되는 연산자에 대한 요약입니다.

운영자지원방법
+= .__iadd__(self, other)
-= .__isub__(self, other)
*= .__imul__(self, other)
/= .__itruediv__(self, other)
//= .__ifloordiv__(self, other)
%= .__imod__(self, other)
**= .__ipow__(self, other[, modulo])

사용자 정의 클래스에서 이러한 메서드를 사용할 때 기본 작업을 in place에서 수행해야 합니다. 이는 변경 가능한 유형으로 작업할 때 가능합니다. 데이터 유형이 변경 불가능한 경우 업데이트된 값으로 새 객체를 반환합니다. 특정 증강 메서드를 정의하지 않으면 증강 할당은 일반 메서드로 대체됩니다.

예를 들어, Stack class로 돌아가세요:

In [ ]:
(참고) 아래 코드를 stack.py에 추가 하십시요.
In [ ]:
class Stack:
    # ...
    
    def __add__(self, other):
        return type(self)(self.items + other.items)

    def __iadd__(self, other):
        self.items.extend(other.items)
        return self

    def __repr__(self):
        return f"{type(self).__name__}({self.items!r})"

이 코드 조각에서는 먼저 일반 추가 연산자인 '+'를 사용하여 두 개의 스택 개체를 추가할 수 있는 .add()의 빠른 구현을 추가합니다. 그런 다음 현재 스택에 other 항목을 추가하기 위해 .extend()에 의존하는 .iadd() 메서드를 구현합니다.

.add() 메서드는 현재 인스턴스를 업데이트하는 대신 클래스의 새 인스턴스를 반환합니다. 이것이 두 방법의 가장 중요한 차이점입니다.

class 진행 방식은 다음과 같습니다.

In [ ]:
from stack import Stack

stack = Stack([1, 2, 3])

stack += Stack([4, 5, 6])
stack

이제 Stack 클래스에서 추가 할당 연산자 (+=)를 지원합니다. 이 예에서 .__iadd__() 메서드는 현재 개체의 끝에 새 항목을 추가합니다.

위의 예에서는 변경 가능한 데이터 유형에서 .iadd()을 구현했습니다. 불변 데이터 유형에서는 .iadd() 메소드를 구현할 필요가 없을 것입니다. 불변 데이터 유형을 내부에서 업데이트할 수 없기 때문에 이 .add() 메서드로 충분합니다.

마지막으로, 비트 연산자에도 확장된 변형이 있다는 점을 알아야 합니다.

|운영자|지원방법| |---|---| |&=|.__iand__(self, other)| ||=|.__ior__(self, other)| |^=|.__ixor__(self, other)| |<<=|.__ilshift__(self, other)| |>>=|.__irshift__(self, other)|

다시 말하지만, 이러한 메서드를 사용하여 자신의 클래스에서 해당하는 증강 비트 연산을 지원할 수 있습니다.

객체 조사하기

또한 일부 매직 메서드를 사용하여 사용자 정의 클래스에서 자체 검사를 지원할 수도 있습니다. 예를 들어 dir(), isinstance() 등의 내장 함수를 사용하여 개체를 검사할 때 개체의 동작 방식을 제어할 수 있습니다.

내부 조사에 유용한 몇 가지 특별한 방법은 다음과 같습니다.

방법설명
.__dir__() 객체의 속성 및 메소드 목록을 반환합니다.
.__hasattr__() 객체에 특정 속성이 있는지 확인합니다.
.__instancecheck__() 객체가 특정 클래스의 인스턴스인지 확인합니다.
.__subclasscheck__() 클래스가 특정 클래스의 하위 클래스인지 확인합니다.

클래스에 이러한 특수 메서드를 구현하면 사용자 정의 동작을 제공하고 객체와 상호 작용할 때 자체 검사 프로세스를 제어할 수 있습니다.

참고: 대부분의 경우 위 메서드의 기본 구현으로 충분합니다. 따라서 사용자 정의 클래스에서 이러한 메서드를 재정의할 필요가 없을 것입니다.

예를 들어, 이전에 Rectangle 클래스에 .__dir__() 메서드를 추가할 수 있습니다.

(참고) 아래 코드를 rectangle.py에 추가 하세요.

In [ ]:
class Rectangle:
    # ...

    def __dir__(self):
        print("__dir__ called")
        return sorted(self.__dict__.keys())

.__dir__() 사용자 정의 구현에서는 Rectangle 클래스의 현재 인스턴스 속성 이름만 반환합니다. 인스턴스 중 하나를 내장 dir() 함수에 대한 인수로 사용할 때 클래스가 동작하는 방식은 다음과 같습니다.

In [ ]:
from rectangle import Rectangle

dir(Rectangle(12, 24))

보시다시피, Rectangle 인스턴스를 dir() 함수에 전달하면 이 클래스의 모든 인스턴스 속성 이름이 포함된 목록을 얻게 됩니다. 이 동작은 모든 특성과 메서드를 반환하는 기본 동작과 다릅니다.

속성 액세스 제어

모든 속성 액세스, 할당 및 삭제 뒤에는 특정 작업을 지원하는 특수 메서드가 있습니다. 다음 표에는 관련 방법이 요약되어 있습니다.

방법설명
.__getattribute__(self, name) name이라는 속성에 액세스할 때 실행됩니다.
.__getattr__(self, name) 현재 객체에 존재하지 않는 속성에 액세스할 때 실행됩니다.
.__setattr__(self, name, value) name이라는 속성에 value가 할당하면 실행됩니다.
.__delattr__(self, name) name이라는 속성을 삭제할 때 실행됩니다.

무엇보다도 이러한 메서드를 사용하여 사용자 정의 클래스에서 속성에 액세스하고, 설정하고, 삭제하는 방법을 검사하고 사용자 정의할 수 있습니다. 다음 섹션에서는 클래스에서 이러한 메서드를 사용하는 방법을 알아봅니다.

속성 검색

Python에는 속성 액세스를 처리하는 두 가지 방법이 있습니다. .__getattribute__() 메서드는 모든 속성 액세스 시 실행되는 반면, .__getattr__() 메서드는 대상 속성이 현재 개체에 존재하지 않는 경우에만 실행됩니다.

다음은 두 가지 방법을 모두 보여주는 Circle 클래스입니다.

(참고) 아래 코드를 circle.py으로 저장 하세요.

In [ ]:
import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def __getattribute__(self, name):
        print(f"__getattribute__ called for {name}")
        return super().__getattribute__(name)

    def __getattr__(self, name):
        print(f"__getattr__ called for {name}")
        if name == "diameter":
            return self.radius * 2
        return super().__getattr__(name)

.__getattribute__() 메서드에서는 Python이 메서드를 호출한 시기를 식별하기 위한 메시지만 인쇄합니다. 그런 다음 내장 super() 함수를 사용하여 속성 액세스를 상위 클래스에 위임합니다. Python은 모든 속성 액세스에 대한 응답으로 이 메서드를 호출한다는 것을 기억하세요.

다음으로 .__getattr__() 메서드가 있습니다. 여기에서 메서드 호출을 식별하는 메시지를 인쇄합니다. 조건문에서 액세스된 속성이 "diameter"를 호출했는지 확인합니다. 그럴 겨우 직경을 계산하여 반환합니다. 그렇지 않으면 속성 액세스를 상위 클래스에 위임합니다.

참고: object 클래스는 사용자 정의 클래스를 포함하여 모든 Python 클래스의 상위 또는 기본 클래스입니다. 이 클래스는 여러 특수 메서드의 기본 구현을 제공합니다.

Circle class가 실제로 어떻게 진행되는지 고려하세요.

In [ ]:
from circle import Circle

circle = Circle(10)

circle.radius
In [ ]:
circle.diameter

점 표기법을 사용하여 .radius를 액세스하면 Python은 암시적으로 .__getattribute__()를 호출합니다. 따라서 해당 메시지와 속성 값을 얻습니다. Python은 모든 속성 액세스에서 이 메서드를 호출합니다.

다음으로 .diameter에 액세스합니다. 이 속성은 Circle에 존재하지 않습니다. Python이 .__getattribute__()를 먼저 호출합니다. 이 메서드는 .diameter 속성을 찾지 못하기 때문에 Python은 .getattr() 호출을 계속합니다. 직경 값을 계산하려면 다시 .radius를 액세스하십시오. 이것이 최종 메시지가 화면에 나타나는 이유입니다.

속성 설정

속성에 값을 설정하는 것은 값에 액세스하는 보완적인 작업입니다. 일반적으로 할당 연산자를 사용하여 특정 속성에 새 값을 설정합니다. Python이 할당을 감지하면 .setattr() 매직 메서드를 호출합니다.

이 방법을 사용하면 할당 프로세스의 특정 측면을 사용자 정의할 수 있습니다. 반경을 양수로 확인하고 싶다고 가정해 보겠습니다. 이 상황에서는 다음과 같은 작업을 수행할 수 있습니다.

(참고) 아래 코드를 circle.py에 추가 하십시요.

In [ ]:
import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    # ...

    def __setattr__(self, name, value):
        if name == "radius":
            if not isinstance(value, int | float):
                raise TypeError("radius must be a number")
            if value <= 0:
                raise ValueError("radius must be positive")
        super().__setattr__(name, value)

이 .setattr() 메서드를 사용하면 과제의 맥락에서 클래스의 동작을 사용자 정의할 수 있습니다.

이 예에서는 대상 속성의 이름이 "radius"인지 확인합니다. 그럴 경우 제공된 값이 정수인지 부동 소수점 숫자인지 확인합니다. 다음으로 숫자가 '0' 보다 큰지 확인합니다. 두 경우 모두 적절한 예외를 발생시킵니다. 마지막으로 상위 클래스에 과제를 위임합니다.

REPL 세션을 다시 시작하세요. 그런 다음 다음 코드를 실행합니다.

(참고) Kernel에 들어 가서 Restart Kernel을 실행하세요.

In [ ]:
from circle import Circle

circle = Circle(10)
In [ ]:
circle.radius = 20
In [ ]:
circle.radius = "42"
In [ ]:
circle.diameter = 42
In [ ]:
circle.diameter

클래스 생성자 Circle()를 호출하면 .__setattr__()가 호출되었음을 나타내는 메시지가 표시됩니다. 이는 .__init__() 메서드가 할당을 실행하기 때문입니다.

그런 다음에 새 값을 .radius에 할당하고 Python이 .__setattr__() 다시 호출합니다. .radius에 문자열을 할당하려고 하면 입력 값이 유효한 숫자가 아니기 때문에 TypeError이 표시됩니다.

참고: .__setattr__()에서 사용자 정의 논리를 구현하는 것보다 속성을 사용하여 읽기 전용 속성을 제공하는 것이 더 일반적입니다.

마지막으로, 사용자 정의 .__setattr__() 메서드를 구현하지 않는 클래스의 인스턴스와 마찬가지로 Circle 인스턴스에 .diameter 같은 새 속성을 연결할 수 있습니다. 이는 구현이 끝날 때 부모 .__setattr__() 메서드에 할당을 위임했기 때문에 가능합니다.

속성 삭제

때로는 특정 속성을 사용한 후 또는 어떤 일이 발생했을 때 객체에서 해당 속성을 삭제해야 하는 경우가 있습니다. 클래스가 del 명령문을 사용하여 속성 삭제에 응답하는 방법을 세밀하게 제어하려면 .__delattr__() 매직 메소드를 사용할 수 있습니다.

예를 들어, 속성 삭제를 금지하는 클래스를 생성한다고 가정해 보겠습니다.

(참고) 아래 코드를 non_deletable.py으로 저장 하십시요.

In [ ]:
class NonDeletable:
    def __init__(self, value):
        self.value = value

    def __delattr__(self, name):
        raise AttributeError(
            f"{type(self).__name__} doesn't support attribute deletion"
        )

이 장난감 클래스에는 .value 이라는 속성이 있습니다. 누군가가 클래스에서 속성을 제거하려고 할 때마다 .__delattr__() 메서드를 재정의 하고 예외를 발생시킵니다.

class가 실제로 어떻게 진행되는지 확인하세요.

In [ ]:
from non_deletable import NonDeletable

one = NonDeletable(1)
one.value
In [ ]:
del one.value

del 명령문을 사용하여 .value 속성을 제거하려고 하면 AttributeError 예외가 발생합니다. 이 .__delattr__() 메소드는 클래스의 인스턴스 속성을 삭제하려는 모든 시도를 포착합니다.

'python > advanced' 카테고리의 다른 글

Python의 마법 방법 VII  (0) 2024.04.13
Python의 마법 방법 VI  (0) 2024.04.12
Python의 마법 방법 IV  (0) 2024.04.11
Python의 마법 방법 III  (0) 2024.04.09
Python의 마법 방법 II  (0) 2024.04.08