Python의 마법 방법 II

2024. 4. 8. 19:54python/advanced

사용자 정의 클래스에서 연산자 오버로딩 지원

Python에는 특수 기호, 기호 조합 또는 특정 유형의 계산을 지정하는 키워드인 여러 유형의 연산자가 있습니다. 내부적으로 Python은 특수 메서드를 사용하여 연산자를 지원합니다. 예를 들어 특수 메서드 .add()는 이미 본 것처럼 더하기 연산자(+)를 지원합니다.

실제로는 연산자 오버로딩이라고 알려진 연산자 뒤에 있는 이러한 메서드를 활용하게 됩니다 .

연산자 오버로딩은 연산자에 추가 기능을 제공하는 것을 의미합니다. 대부분의 내장 유형과 특정 지원 연산자를 사용하여 이를 수행할 수 있습니다. 그러나 이것이 Python 연산자를 지원하는 특수 메서드로 수행할 수 있는 전부는 아닙니다. 또한 이러한 메서드를 사용하여 사용자 정의 클래스에서 일부 연산자를 지원할 수도 있습니다.

다음 섹션에서는 산술, 비교, 멤버십, 비트 연산자, 증대 연산자 등 Python 연산자를 지원하는 특정 특수 메서드에 대해 알아봅니다. 작업을 시작하려면 가장 일반적으로 사용되는 연산자인 산술 연산자부터 시작하겠습니다.

산술 연산자

산술 연산자는 숫자 값에 대한 산술 연산을 수행하는 데 사용하는 연산자입니다. 대부분의 경우 수학에서 나오므로 Python은 일반적인 수학 기호로 표현합니다.

다음 표에는 Python의 산술 연산자 목록과 이를 지원하는 매직 메서드가 나와 있습니다.

운영자지원방법
+ .add(self, other)
- .sub(self, other)
* .mul(self, other)
/ .truediv(self, other)
// .floordiv(self, other)
% .mod(self, other)
** .pow(self, other[, modulo])

이러한 모든 메소드는 other이라는 두 번째 인수를 취합니다. 대부분의 경우 이 인수는 동일한 유형dml self이거나 호환 가능한 유형이어야 합니다. 그렇지 않은 경우 오류가 발생할 수 있습니다.

이러한 연산자 중 일부를 오버로드하는 방법을 설명하려면 Storage class으로 돌아가세요. 이 클래스의 두 인스턴스를 더하거나 뺄 때 두 피연산자가 동일한 단위를 갖는지 확인하고 싶다고 가정해 보겠습니다.

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

In [ ]:
class Storage(float):
    def __new__(cls, value, unit):
        instance = super().__new__(cls, value)
        instance.unit = unit
        return instance

    def __add__(self, other):
        if not isinstance(other, type(self)):
            raise TypeError(
                "unsupported operand for +: "
                f"'{type(self).__name__}' and '{type(other).__name__}'"
            )
        if not self.unit == other.unit:
            raise TypeError(
                f"incompatible units: '{self.unit}' and '{other.unit}'"
            )

        return type(self)(super().__add__(other), self.unit)

.add() 메서드에서는 먼저 other가 Storage의 인스턴스인지 확인합니다. 그렇지 않은 경우 적절한 오류 메시지와 함께 TypeError 예외를 발생 시킵니다. 다음으로 두 개체가 동일한 단위를 가지고 있는지 확인합니다. 그렇지 않다면 TypeError를 다시 발생 시킵니다. 두 검사가 모두 통과되면 두 값을 추가하고 단위를 연결하여 생성한 새 인스턴스 Storage를 반환합니다.

이 클래스의 동작 방식은 다음과 같습니다.

In [ ]:
from storage import Storage

disk_1 = Storage(500, "GB")
disk_2 = Storage(1000, "GB")
disk_3 = Storage(1, "TB")

disk_1 + disk_2
In [ ]:
disk_2 + disk_3
In [ ]:
disk_1 + 100

이 예에서는 단위가 동일한 두 개체를 추가하면 올바른 값을 얻습니다. 단위가 다른 두 개체를 추가하면 단위가 호환되지 않는다는 TypeError 메시지가 표시됩니다. 마지막으로 내장된 숫자 값이 있는 Storage 인스턴스를 추가하려고 하면 내장된 유형이 Storage 인스턴스가 아니기 때문에 오류가 발생합니다.

연습으로 Storage 클래스에 .sub() 메소드를 구현하시겠습니까? 가능한 구ㅡ현을 보려면 아래의 축소 가능한 섹션을 확인하세요.

Storage 클래스에서 .sub()가 가능한 구현은 다음과 같습니다.

In [ ]:
class Storage(float):
    def __new__(cls, value, unit):
        instance = super().__new__(cls, value)
        instance.unit = unit
        return instance

    def __add__(self, other):
        if not isinstance(other, type(self)):
            raise TypeError(
                "unsupported operand for +: "
                f"'{type(self).__name__}' and '{type(other).__name__}'"
            )
        if not self.unit == other.unit:
            raise TypeError(
                f"incompatible units: '{self.unit}' and '{other.unit}'"
            )

        return type(self)(super().__add__(other), self.unit)

    def __sub__(self, other):
        if not isinstance(other, type(self)):
            raise TypeError(
                "unsupported operand for -: "
                f"'{type(self).__name__}' and '{type(other).__name__}'"
            )
        if not self.unit == other.unit:
            raise TypeError(
                f"incompatible units: '{self.unit}' and '{other.unit}'"
            )

        return type(self)(super().__sub__(other), self.unit)

.add()와 .sub()은 여러 줄이 공통되어 있어 반복적인 코드가 발생한다는 점에 유의하세요. 공통 논리를 추출하는 helper method를 사용하여 이 문제를 해결할 수 있습니다.

위의 예에서는 내장 float유형에 더하기 연산자(+)를 오버로드했습니다. 또한 .add() 메서드 및 기타 연산자 메서드를 사용하여 사용자 정의 클래스에서 해당 연산자를 지원할 수도 있습니다.

다음 Distance 클래스 예를 살펴보세요.

(참고) 다음 코드를 distance.py으로 저장해 주세요.

In [ ]:
class Distance:
    _multiples = {
        "mm": 0.001,
        "cm": 0.01,
        "m": 1,
        "km": 1_000,
    }

    def __init__(self, value, unit="m"):
        self.value = value
        self.unit = unit.lower()

    def to_meter(self):
        return self.value * type(self)._multiples[self.unit]

    def __add__(self, other):
        return self._compute(other, "+")

    def __sub__(self, other):
        return self._compute(other, "-")

    def _compute(self, other, operator):
        operation = eval(f"{self.to_meter()} {operator} {other.to_meter()}")
        cls = type(self)
        return cls(operation / cls._multiples[self.unit], self.unit)

Distance에는 ._multiples이라는 비공개 클래스 속성이 있습니다. 이 속성에는 길이 단위 사전과 해당 변환 요소가 포함되어 있습니다. .__init__() 메서드에서는 사용자 정의에 따라 .value 및 .unit 속성을 초기화합니다. 단위 문자열을 정규화하고 소문자 사용을 강제하기 위해 str.lower() 메소드를 사용했다는 점에 유의하세요.

.to_meter()에서는 현재 거리를 미터 단위로 표현하는 데 .multiples를 사용합니다. 이 메서드를 .\_add__() 및 .__sub__() 메서드의 도우미로 사용합니다. 이렇게 하면 class에서 다양한 단위의 거리를 올바르게 더하거나 뺄 수 있습니다.

.__add__() 및 .__sub__() 메서드는 각각 더하기 및 빼기 연산자를 지원합니다. 두 방법 모두 ._compute() 도우미 메서드를 사용하여 계산을 실행합니다.

._compute()에서는 other 및 operator 인수를 사용합니다. 그런 다음 내장 eval() 함수를 사용하여 표현식을 평가하고 현재 작업에 대해 의도한 결과를 얻습니다. 다음으로 내장 type() 함수를 사용하는 Distance 인스턴스를 만듭니다. 마지막으로 계산된 값을 현재 인스턴스 단위로 변환하여 클래스의 새 인스턴스를 반환합니다.

Distance class가 실제로 진행되는 방식은 다음과 같습니다 .

In [ ]:
from distance import Distance

distance_1 = Distance(200, "m")
distance_2 = Distance(1, "km")

total = distance_1 + distance_2
total.value
In [ ]:
total.unit
In [ ]:
>>> displacement = distance_2 - distance_1
>>> displacement.value
In [ ]:
displacement.unit

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

Python의 마법 방법 VI  (0) 2024.04.12
Python의 마법 방법 V  (0) 2024.04.11
Python의 마법 방법 IV  (0) 2024.04.11
Python의 마법 방법 III  (0) 2024.04.09
Python의 마법 방법 I  (0) 2024.04.07