Python 강좌 : 제 36강 - 매직 메서드 (2)
매직 메서드(Magic Method)
매직 메서드(Magic Method)는 미리 정의되어 있는 메서드들을 재정의하여 클래스를 활용할 수 있도록 변경합니다.
내장 함수들이 처리하는 연산을 변경해 사용자 정의 클래스나 함수 등을 효율적으로 사용할 수 있습니다.
메서드 이름의 앞뒤에 밑줄(Underscore, _)을 두 개씩 붙여(__이름__) 매직 메서드를 정의할 수 있습니다.
제 35강에서는 인스턴스의 생성과 문자열 표현을 담당하는 매직 메서드를 다뤘습니다. 이번 강좌에서는 +, -, <, & 같은 연산자와 int(), bool() 같은 형식 변환을 담당하는 매직 메서드를 다룹니다.
3 + 5나 "a" + "b"처럼 같은 + 연산자라도 형식에 따라 덧셈이 되기도 하고 연결이 되기도 합니다. 이는 int와 str이 각자 다른 __add__를 정의하고 있기 때문입니다.
마찬가지로 직접 만든 클래스에 연산자 매직 메서드를 정의하면, 벡터의 덧셈이나 버전 비교처럼 그 클래스에 맞는 의미의 연산을 연산자로 표현할 수 있습니다. 이를 연산자 오버로딩(Operator Overloading)이라 부릅니다.
- Tip : 매직 메서드는
이중 밑줄(Double Underscore)을 사용해 정의하므로던더 메서드(Dunder Method)라고도 부릅니다.
산술 연산 정의
산술 연산자는 하나의 연산마다 세 가지 매직 메서드를 가집니다.
x + y를 위한 정방향 __add__, y + x처럼 인스턴스가 오른쪽에 있을 때를 위한 역방향 __radd__, x += y를 위한 복합 대입 __iadd__입니다.
다음 예제는 세 메서드가 각각 어떤 문법에서 호출되는지 확인하기 위해, 일부러 서로 다른 방식으로 계산하도록 정의했습니다.
class Daeheeyun:
def __init__(self, value):
self.value = value
def __add__(self, other):
return self.value + int(other)
def __radd__(self, other):
return self.value + other
def __iadd__(self, other):
return self.value + abs(other)
instance = Daeheeyun(5)
add = instance + 3.076923
radd = 3.076923 + instance
instance += -3.076923
print(add)
print(radd)
print(instance)- 결과
- 8
8.076923
8.076923
정방향 연산자
정방향 연산자는 x + y에 대한 연산을 정의합니다.
인스턴스의 값이 x가 되며, 외부 값이 y가 됩니다.
예제는 y를 int로 변환 후, 연산합니다.
instance + 3.076923은 instance.__add__(3.076923)으로 호출되어 5 + 3의 결과인 8이 됩니다.
정방향 연산자의 self는 연산자의 왼쪽 피연산자이고, other는 오른쪽 피연산자입니다. Python은 항상 왼쪽 피연산자의 정방향 연산자를 먼저 시도합니다.
역방향 연산자
역방향 연산자는 y + x에 대한 연산을 정의합니다.
인스턴스의 값이 x가 되며, 외부 값이 y가 됩니다.
3.076923 + instance는 먼저 float의 __add__를 호출하지만, float은 Daeheeyun과의 연산 방법을 모르므로 NotImplemented를 반환합니다.
이 경우 Python은 오른쪽 피연산자의 역방향 연산자인 instance.__radd__(3.076923)를 호출합니다.
예제는 y를 변환 없이 연산합니다. 그러므로 5 + 3.076923의 결과인 8.076923이 됩니다.
역방향 연산자의 self는 연산자의 오른쪽 피연산자이고, other는 왼쪽 피연산자입니다. 덧셈이나 곱셈처럼 순서가 상관없는 연산이 아니라면, __rsub__나 __rtruediv__에서는 other - self처럼 순서를 뒤집어 계산해야 합니다.
- Tip : 직접 정의한 정방향 연산자에서 처리할 수 없는 형식이 전달되면, 오류를 발생시키는 대신
return NotImplemented를 반환해 상대 객체의 역방향 연산자가 시도되도록 할 수 있습니다.
복합 대입 연산자
복합 대입 연산자는 x += y에 대한 연산을 정의합니다.
인스턴스의 값이 x가 되며, 외부 값이 y가 됩니다.
예제는 y를 절댓값으로 변환하고 연산합니다.
복합 대입 연산자는 반환값이 x에 다시 할당됩니다. 예제는 숫자를 반환하므로 instance는 더 이상 Daeheeyun 인스턴스가 아닌 float 값 8.076923이 됩니다.
일반적으로 복합 대입 연산자는 자기 자신을 수정한 뒤 self를 반환하도록 정의합니다.
__iadd__가 정의되지 않았다면, x += y는 x = x + y처럼 __add__를 사용해 처리됩니다.
class Daeheeyun:
def __init__(self, value):
self.value = value
def __iadd__(self, other):
self.value += other
instance = Daeheeyun(5)
instance += 3
print(instance)- 결과
- None
복합 대입 연산자에서 가장 흔한 실수는 return self를 빠뜨리는 것입니다.
instance += 3은 instance = instance.__iadd__(3)처럼 동작하므로, return 문이 없으면 None이 instance에 다시 할당됩니다. 값은 정상적으로 더해졌지만, 인스턴스 자체를 잃어버리게 됩니다.
NotImplemented 활용
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Vector({self.x}, {self.y})"
def __add__(self, other):
if isinstance(other, Vector):
return Vector(self.x + other.x, self.y + other.y)
return NotImplemented
def __mul__(self, other):
if isinstance(other, (int, float)):
return Vector(self.x * other, self.y * other)
return NotImplemented
def __rmul__(self, other):
return self.__mul__(other)
a = Vector(1, 2)
b = Vector(3, 4)
print(a + b)
print(a * 3)
print(3 * a)
try:
a + 1
except TypeError as e:
print("TypeError:", e)- 결과
- Vector(4, 6)
Vector(3, 6)
Vector(3, 6)
TypeError: unsupported operand type(s) for +: ‘Vector’ and ‘int’
Vector는 실제로 사용할 수 있는 형태로 연산자를 정의한 예제입니다. 연산 결과로 새로운 Vector 인스턴스를 반환하므로, 원본인 a와 b는 변경되지 않습니다.
a + b는 a.__add__(b)가 호출되어 각 성분을 더한 Vector(4, 6)이 반환됩니다.
a * 3은 a.__mul__(3)이, 3 * a는 int의 __mul__이 NotImplemented를 반환한 뒤 a.__rmul__(3)이 호출됩니다. 곱셈은 순서가 상관없으므로 __rmul__은 __mul__을 그대로 호출합니다.
a + 1은 __add__가 NotImplemented를 반환하고, int에도 Vector를 처리하는 __radd__가 없으므로 Python이 TypeError를 발생시킵니다.
NotImplemented는 오류가 아닌 “이 형식은 처리할 수 없다”는 신호 값입니다. 직접 raise TypeError를 발생시키면 상대 객체의 역방향 연산자가 시도될 기회가 사라지므로, 처리할 수 없는 형식은 NotImplemented를 반환하는 것이 좋습니다.
- Tip :
NotImplemented는 값이므로return으로 반환합니다. 이름이 비슷한NotImplementedError는 아직 구현되지 않은 메서드에서raise로 발생시키는 예외이므로 혼동하지 않아야 합니다.
가변 객체와 불변 객체의 복합 대입
a = [1, 2]
alias = a
a += [3]
print(a, alias)
t = (1, 2)
alias = t
t += (3,)
print(t, alias)- 결과
- [1, 2, 3] [1, 2, 3]
(1, 2, 3) (1, 2)
같은 +=라도 객체가 __iadd__를 정의했는지에 따라 결과가 달라집니다.
list는 __iadd__를 정의하고 있어 원본 리스트를 직접 수정한 뒤 자기 자신을 반환합니다. 그러므로 같은 리스트를 가리키는 alias에도 3이 추가됩니다.
tuple은 불변 형식이므로 __iadd__가 없습니다. 그러므로 t += (3,)은 t = t + (3,)으로 처리되어 새로운 튜플이 만들어지고, alias는 원래 튜플을 그대로 가리킵니다.
직접 만든 클래스도 같은 규칙을 따릅니다. 원본을 수정하는 동작이 필요하면 __iadd__를 정의하고, 항상 새 객체를 만들어야 하는 불변 객체라면 __add__만 정의합니다.
| 산술 연산자 | 의미 | 역방향 | 복합 대입 |
|---|---|---|---|
| object.__add__(self, other) | object + other 연산 정의 | __radd__ | __iadd__ |
| object.__sub__(self, other) | object - other 연산 정의 | __rsub__ | __isub__ |
| object.__mul__(self, other) | object * other 연산 정의 | __rmul__ | __imul__ |
| object.__matmul__(self, other) | object @ other 연산 정의 | __rmatmul__ | __imatmul__ |
| object.__truediv__(self, other) | object / other 연산 정의 | __rtruediv__ | __itruediv__ |
| object.__floordiv__(self, other) | object // other 연산 정의 | __rfloordiv__ | __ifloordiv__ |
| object.__mod__(self, other) | object % other 연산 정의 | __rmod__ | __imod__ |
| object.__pow__(self, other[, modulo]) | pow() 연산 정의 | __rpow__ | __ipow__ |
| object.__divmod__(self, other) | divmod() 연산 정의 | __rdivmod__ | 없음 |
| object.__round__(self[, ndigits]) | round() 연산 정의 | 없음 | 없음 |
| object.__trunc__(self) | math.trunc() 연산 정의 | 없음 | 없음 |
| object.__floor__(self) | math.floor() 연산 정의 | 없음 | 없음 |
| object.__ceil__(self) | math.ceil() 연산 정의 | 없음 | 없음 |
-
Tip :
divmod()는 복합 대입 연산자가 없으므로__idivmod__는 존재하지 않습니다. -
Tip :
Python 3.14이상부터pow(x, y, mod)처럼 인자가 세 개인pow()도y의__rpow__를 호출할 수 있습니다. -
Tip :
@연산자는Python 3.5에서 행렬 곱셈을 위해 추가되었으며, 내장 형식 중에는 이 연산자를 지원하는 형식이 없습니다. 주로numpy배열에서 사용합니다.
단항 연산 정의
단항 연산자는 피연산자가 하나인 연산자입니다. -x, +x, ~x, abs(x)가 해당하며, 상대 피연산자가 없으므로 self 외의 매개변수를 받지 않습니다.
다음 예제는 각 메서드가 호출되는 것을 확인하기 위해, 일부러 원래의 의미와 다르게 계산하도록 정의했습니다.
class Daeheeyun:
def __init__(self, value):
self.value = value
def __pos__(self):
return self.value + 2
def __neg__(self):
return self.value - 2
def __abs__(self):
return -abs(self.value)
def __invert__(self):
return ~self.value
instance = Daeheeyun(5)
print(+instance)
print(-instance)
print(abs(instance))
print(~instance)- 결과
- 7
3
-5
-6
__pos__ : Positive
+object 연산자는 +x에 대한 연산을 정의합니다.
예제는 x를 양수를 곱하지 않고, +2 연산으로 변경합니다. 그러므로 +instance는 5 + 2인 7이 됩니다.
__neg__ : Negative
-object 연산자는 -x에 대한 연산을 정의합니다.
예제는 x를 음수를 곱하지 않고, -2 연산으로 변경합니다. 그러므로 -instance는 5 - 2인 3이 됩니다.
__abs__ : Absolute
abs() 연산자는 abs(x)에 대한 연산을 정의합니다.
예제는 x를 절대값으로 취하지 않고, 음수로 변경합니다. 그러므로 abs(instance)는 -5가 됩니다.
__invert__ : Invert
~object 연산자는 ~x에 대한 연산을 정의합니다.
예제는 ~x를 변환 없이 연산합니다. 정수의 ~x는 모든 비트를 반전하며, 결과는 -(x + 1)과 같으므로 ~5는 -6이 됩니다.
class Money:
def __init__(self, amount):
self.amount = amount
def __neg__(self):
return Money(-self.amount)
def __abs__(self):
return Money(abs(self.amount))
def __repr__(self):
return f"Money({self.amount})"
debt = Money(-500)
print(-debt)
print(abs(debt))
print(debt)- 결과
- Money(500)
Money(500)
Money(-500)
실제로 단항 연산자를 정의할 때는 원래의 의미를 지키고, 예제처럼 같은 클래스의 새 인스턴스를 반환하는 것이 일반적입니다.
-debt와 abs(debt)는 각각 새로운 Money(500)을 반환하며, 마지막 줄처럼 원본인 debt는 -500으로 그대로 유지됩니다.
단항 연산의 결과로 정수를 반환하면 -(-debt)처럼 연산을 이어서 적용할 때 Money의 다른 기능을 사용할 수 없으므로, 같은 형식을 반환해야 연산을 자연스럽게 연결할 수 있습니다.
| 단항 연산자 | 의미 |
|---|---|
| object.__pos__(self) | +object 연산 정의 |
| object.__neg__(self) | -object 연산 정의 |
| object.__abs__(self) | abs() 연산 정의 |
| object.__invert__(self) | ~object 연산 정의 |
비트 연산 정의
비트 연산자는 정수의 비트 단위 연산인 <<, >>, &, |, ^를 정의합니다.
and, or, not은 비트 연산자가 아닌 논리 연산자이며, 매직 메서드로 직접 재정의할 수 없습니다.
not x의 결과는 __bool__(정의되지 않았다면 __len__)의 결과에 따라 결정되므로, __bool__을 재정의해 간접적으로 변경할 수 있습니다.
비트 반전 연산자인 ~object는 단항 연산자이므로 __invert__로 정의합니다.
비트 연산자도 산술 연산자와 마찬가지로 정방향, 역방향, 복합 대입 메서드를 가집니다. 정수가 아닌 클래스에서도 집합의 |(합집합), &(교집합)처럼 “합치기”와 “공통 부분”의 의미로 자주 재정의됩니다.
class Permission:
def __init__(self, bits):
self.bits = bits
def __or__(self, other):
return Permission(self.bits | other.bits)
def __and__(self, other):
return Permission(self.bits & other.bits)
def __repr__(self):
return f"Permission({self.bits:03b})"
READ = Permission(0b100)
WRITE = Permission(0b010)
user = READ | WRITE
print(user)
print(user & WRITE)
print(user & Permission(0b001))- 결과
- Permission(110)
Permission(010)
Permission(000)
Permission은 읽기, 쓰기, 실행 권한을 각각 하나의 비트로 표현하는 클래스입니다.
READ | WRITE는 READ.__or__(WRITE)가 호출되어 두 비트를 합친 110이 됩니다.
user & WRITE는 __and__가 호출되어 공통으로 켜진 비트인 010만 남으므로, 사용자에게 쓰기 권한이 있음을 확인할 수 있습니다. 실행 권한 001과의 & 결과는 000이므로 실행 권한은 없습니다.
__repr__의 {self.bits:03b}는 정수를 세 자리 2진수로 표현하는 형식입니다.
- Tip : 실제 프로그램에서 이러한 권한 조합은
enum.Flag로 간단하게 정의할 수 있습니다. 열거형은 제 29강에서 확인할 수 있습니다.
| 비트 연산자 | 의미 | 역방향 | 복합 대입 |
|---|---|---|---|
| object.__lshift__(self, other) | object « other 연산 정의 | __rlshift__ | __ilshift__ |
| object.__rshift__(self, other) | object » other 연산 정의 | __rrshift__ | __irshift__ |
| object.__and__(self, other) | object & other 연산 정의 | __rand__ | __iand__ |
| object.__or__(self, other) | object | other 연산 정의 | __ror__ | __ior__ |
| object.__xor__(self, other) | object ^ other 연산 정의 | __rxor__ | __ixor__ |
비교 연산 정의
비교 연산자는 __radd__ 같은 별도의 역방향 연산자와 복합 대입 연산자가 존재하지 않습니다.
대신, 서로 반대가 되는 비교 연산자가 역방향 연산자의 역할을 합니다.
비교 연산자를 정의하면 ==, < 같은 연산자뿐만 아니라, 비교를 내부적으로 사용하는 sorted(), max(), min(), in 연산 등에서도 직접 만든 클래스를 사용할 수 있습니다.
class Daeheeyun:
def __init__(self, value):
self.value = value
def __lt__(self, other):
print("__lt__ 호출")
return self.value < other
def __gt__(self, other):
print("__gt__ 호출")
return self.value > other
instance = Daeheeyun(5)
print(instance < 10)
print(10 < instance)- 결과
- __lt__ 호출
True
__gt__ 호출
False
instance < 10은 instance.__lt__(10)으로 호출됩니다.
10 < instance는 int가 Daeheeyun과의 비교 방법을 모르므로, 반대 연산자인 instance.__gt__(10)으로 호출됩니다.
10 < instance와 instance > 10은 같은 의미이므로, __gt__가 5 > 10을 계산해 False를 반환합니다.
이처럼 __lt__와 __gt__, __le__와 __ge__가 서로의 역방향 역할을 하며, __eq__와 __ne__는 자기 자신이 역방향 역할을 합니다.
- Tip :
__eq__만 정의하면__ne__는__eq__의 결과를 반전해 자동으로 처리됩니다. 나머지 비교 연산자를 한 번에 채우려면functools.total_ordering데코레이터를 사용할 수 있습니다.
__eq__ 와 __hash__
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
print(Point(1, 2) == Point(1, 2))
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
print(Point(1, 2) == Point(1, 2))
print(Point(1, 2) != Point(3, 4))
print(Point.__hash__)
try:
{Point(1, 2)}
except TypeError as e:
print("TypeError:", e)- 결과
- False
True
True
None
TypeError: cannot use ‘Point’ as a set element (unhashable type: ‘Point’)
__eq__를 정의하지 않으면, ==는 두 객체가 같은 객체인지(is)를 비교합니다. 그러므로 값이 같아도 서로 다른 인스턴스인 첫 번째 비교는 False입니다.
__eq__를 정의하면 x, y 값을 비교하므로 True가 되며, __ne__를 정의하지 않았지만 !=도 __eq__를 반전해 True를 반환합니다.
주의할 점은 __eq__만 정의하면 __hash__가 자동으로 None이 된다는 것입니다. 해시값이 없는 객체는 집합의 원소나 사전의 키로 사용할 수 없으므로 TypeError가 발생합니다.
같다고 판단되는 두 객체는 반드시 같은 해시값을 가져야 집합과 사전이 올바르게 동작합니다. 기존 __hash__는 객체마다 다른 값을 반환하므로, __eq__를 바꾸면 Python이 일관성을 위해 __hash__를 제거합니다.
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
if not isinstance(other, Point):
return NotImplemented
return (self.x, self.y) == (other.x, other.y)
def __hash__(self):
return hash((self.x, self.y))
points = {Point(1, 2), Point(1, 2), Point(3, 4)}
print(len(points))
print(Point(1, 2) in points)- 결과
- 2
True
__hash__에서 __eq__가 비교하는 값과 같은 값들로 해시값을 계산하면, 집합과 사전에서도 사용할 수 있습니다.
Point(1, 2) 두 개는 해시값과 __eq__ 결과가 모두 같으므로 하나로 취급되어, 집합의 원소 개수는 2가 됩니다.
단, 해시값을 계산하는 데 사용한 속성을 나중에 변경하면 집합에서 찾을 수 없게 되므로, __hash__를 정의한 객체는 해당 속성을 변경하지 않는 것이 좋습니다.
total_ordering
from functools import total_ordering
@total_ordering
class Version:
def __init__(self, major, minor):
self.major = major
self.minor = minor
def __eq__(self, other):
return (self.major, self.minor) == (other.major, other.minor)
def __lt__(self, other):
return (self.major, self.minor) < (other.major, other.minor)
def __repr__(self):
return f"{self.major}.{self.minor}"
versions = [Version(3, 14), Version(3, 7), Version(3, 12)]
print(sorted(versions))
print(max(versions))
print(Version(3, 14) >= Version(3, 12))- 결과
- [3.7, 3.12, 3.14]
3.14
True
@total_ordering은 __eq__와 __lt__(또는 __le__, __gt__, __ge__ 중 하나)만 정의하면 나머지 비교 연산자를 자동으로 만들어 줍니다.
sorted()는 내부적으로 < 비교를 사용하므로 직접 정의한 __lt__로 정렬합니다. 반면 max()는 > 비교를 사용하므로 total_ordering이 만들어 준 __gt__로, >=는 __ge__로 처리됩니다.
total_ordering이 없다면 __gt__와 __ge__가 없으므로, max()는 역방향 연산자인 __lt__로 대신 처리되지만 >= 비교는 __ge__와 그 역방향인 __le__가 모두 없어 TypeError가 발생합니다.
예제는 버전을 (major, minor) 튜플로 비교합니다. 튜플은 앞 요소부터 차례로 비교하므로, 문자열 "3.7"과 "3.12"를 비교할 때와 달리 3.7이 3.12보다 작다고 올바르게 판단합니다.
| 비교 연산자 | 의미 |
|---|---|
| object.__lt__(self, other) | object < other 연산 정의 |
| object.__le__(self, other) | object <= other 연산 정의 |
| object.__eq__(self, other) | object == other 연산 정의 |
| object.__ne__(self, other) | object != other 연산 정의 |
| object.__gt__(self, other) | object > other 연산 정의 |
| object.__ge__(self, other) | object >= other 연산 정의 |
형식 변환 정의
class Daeheeyun:
def __init__(self, value):
self.value = value
def __index__(self):
return 2
L = ["A", "B", "C", "D", "E"]
instance = Daeheeyun(100)
print(L[instance])- 결과
- C
형식 변환은 인스턴스의 형식을 변환할 때 사용됩니다.
그 중, __index__는 객체를 정수로 손실 없이 변환하는 방법을 정의하며, 리스트의 인덱싱이나 슬라이싱에서 정수 대신 사용될 때 호출됩니다.
예제의 index의 값을 2로 정의하여, 리스트에서 C가 출력됩니다.
__int__, __float__, __bool__
class Score:
def __init__(self, value):
self.value = value
def __int__(self):
return int(self.value)
def __float__(self):
return float(self.value)
def __bool__(self):
return self.value > 0
score = Score(87.5)
print(int(score), float(score))
print(bool(score), bool(Score(0)))
if not Score(-1):
print("점수가 없습니다.")
try:
bin(score)
except TypeError as e:
print("TypeError:", e)- 결과
- 87 87.5
True False
점수가 없습니다.
TypeError: ‘Score’ object cannot be interpreted as an integer
int(score)는 __int__를, float(score)는 __float__를 호출합니다. __int__는 87.5를 정수로 변환하므로 소수점 아래가 버려진 87을 반환합니다.
bool(score)는 __bool__을 호출하며, 예제는 값이 0보다 클 때만 True를 반환합니다.
__bool__은 bool()뿐만 아니라 if, while, not처럼 참·거짓을 판단하는 모든 곳에서 호출됩니다. 그러므로 if not Score(-1): 조건이 참이 되어 메시지가 출력됩니다.
bin(), hex(), 인덱싱은 __int__가 아닌 __index__를 요구하므로, __int__만 정의한 Score는 TypeError가 발생합니다.
__int__ 와 __index__의 차이
class Daeheeyun:
def __index__(self):
return 10
instance = Daeheeyun()
print(bin(instance), hex(instance))
print(list(range(instance)))
print(int(instance))
print("ABCDEFGHIJKL"[instance:])- 결과
- 0b1010 0xa
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
10
KL
__index__를 정의하면 bin(), hex(), range(), 슬라이싱처럼 정수만 허용하는 곳에서 인스턴스를 사용할 수 있습니다.
__int__가 없어도 int(instance)가 10을 반환하는 이유는, int()가 __int__가 없을 때 __index__를 대신 사용하기 때문입니다.
__int__는 87.5 → 87처럼 값을 잃어도 되는 변환에, __index__는 객체가 원래 정수와 같은 의미를 가질 때 사용합니다. 예를 들어 float은 __int__는 있지만 __index__가 없으므로 [1, 2][1.0]처럼 인덱스로 사용할 수 없습니다.
class Bag:
def __init__(self, items):
self.items = items
def __len__(self):
return len(self.items)
print(bool(Bag([])), bool(Bag([1, 2])))
class Empty:
pass
print(bool(Empty()))- 결과
- False True
True
__bool__이 정의되지 않았다면, bool()은 __len__의 결과가 0인지 확인합니다. 그러므로 빈 Bag은 False, 요소가 있는 Bag은 True입니다.
__bool__과 __len__이 모두 없다면 객체는 항상 True로 판단됩니다.
| 형식 변환 | 의미 |
|---|---|
| object.__int__(self) | int() 연산 정의 |
| object.__float__(self) | float() 연산 정의 |
| object.__complex__(self) | complex() 연산 정의 |
| object.__bool__(self) | bool() 연산 정의 |
| object.__hash__(self) | hash() 연산 정의 |
| object.__index__(self) | 인덱싱, 슬라이싱, bin(), hex() 등의 정수 변환 정의 |
- Tip :
Python 3.14이상부터int()는 더 이상__trunc__를 대신 호출하지 않습니다.int()로 변환할 수 있게 하려면__int__나__index__를 정의해야 합니다.
정리
| 분류 | 매직 메서드 | 호출하는 문법/내장 함수 | 특징 |
|---|---|---|---|
| 산술 | __add__, __sub__, __mul__ 등 |
x + y, x - y, x * y |
처리 못 하면 NotImplemented 반환 |
| 역방향 | __radd__, __rsub__ 등 |
y + x (왼쪽이 처리 못 할 때) |
self가 오른쪽 피연산자 |
| 복합 대입 | __iadd__, __isub__ 등 |
x += y |
반환값이 x에 할당, 보통 self 반환 |
| 단항 | __neg__, __pos__, __abs__, __invert__ |
-x, +x, abs(x), ~x |
매개변수는 self만 |
| 비트 | __and__, __or__, __xor__ 등 |
x & y, x | y, x ^ y | 역방향·복합 대입 존재 |
| 비교 | __eq__, __lt__ 등 |
==, <, sorted(), max() |
__eq__ 정의 시 __hash__는 None |
| 형식 변환 | __int__, __float__, __bool__ |
int(), float(), bool(), if |
반환 형식이 정해져 있음 |
| 정수 변환 | __index__ |
인덱싱, 슬라이싱, bin(), range() |
손실 없는 정수 변환 |
연산자 매직 메서드는 원본을 바꾸지 않고 새 객체를 반환하는 것이 기본이며, 원본을 수정하는 동작은 복합 대입 메서드에서만 처리합니다.
처리할 수 없는 형식은 오류 대신 NotImplemented를 반환해, Python이 상대 객체의 역방향 메서드를 시도하도록 합니다.
댓글 남기기