Skip to content

연산자 규칙

Python의 연산자(+, -, * 등)는 특수 메서드(dunder method)로 구현된다. a + b는 내부적으로 a.__add__(b)를 호출하는 것이다.

이항 연산자 dispatch 순서

a + b를 실행하면 Python은 다음 순서로 시도한다.

  1. a.__add__(b) 호출
  2. NotImplemented를 반환하면 → b.__radd__(a) 호출
  3. 둘 다 NotImplemented면 → TypeError 발생

__radd__는 reflected(역방향) 연산자다. ab와의 덧셈을 모르면 b에게 기회를 넘기는 구조다.

class A:
def __add__(self, other):
return NotImplemented
class B:
def __radd__(self, other):
return "B.__radd__"
A() + B() # A.__add__ → NotImplemented → B.__radd__

각 연산자의 정방향/역방향 메서드:

  • +: __add__ / __radd__
  • -: __sub__ / __rsub__
  • *: __mul__ / __rmul__
  • /: __truediv__ / __rtruediv__
  • //: __floordiv__ / __rfloordiv__
  • **: __pow__ / __rpow__
  • %: __mod__ / __rmod__

서브클래스 우선 규칙

b가 a의 서브클래스이면 dispatch 순서가 뒤집힌다b.__radd__(a)를 먼저 호출한다.

class A:
def __add__(self, other):
return "A.__add__"
class B(A):
def __radd__(self, other):
return "B.__radd__"
A() + B() # B.__radd__ — 서브클래스의 radd가 먼저 호출됨

이 규칙이 없으면 A.__add__가 항상 먼저 호출된다. A.__add__NotImplemented 없이 값을 바로 반환해버리면 B.__radd__는 호출될 기회가 없다. 서브클래스 우선 규칙은 이런 경우에도 서브클래스가 동작을 override할 수 있도록 보장한다.

단, 서브클래스가 __radd__를 직접 정의하지 않고 부모에게서 상속받은 경우에는 순서를 뒤집지 않는다. 서브클래스가 명시적으로 동작을 바꾸려는 의도가 있을 때만 우선권을 준다.

도입

커밋 4bb1e36 (2001-09-28, Guido van Rossum). Python 2.2의 type/class 통합 작업 중 추가되었다. 구현은 Objects/abstract.cbinary_op1()에서 PyType_IsSubtype()으로 수행한다.

  • Both arguments are new-style classes
  • Both arguments are new-style numbers
  • Their implementation slots for tp_op differ
  • Their types differ
  • The right argument’s type is a subtype of the left argument’s type

Python 2.2 문서의 근거:

This is done so that a subclass can completely override binary operators. Otherwise, the left operand’s __op__() would always accept the right operand: when an instance of a given class is expected, an instance of a subclass of that class is always acceptable.

bpo-30140 (2017): 서브클래스가 __radd__를 상속만 하고 직접 정의하지 않으면 우선 규칙이 적용되지 않는 동작이 발견됨. 하위 호환성 문제로 won’t fix 처리.

NotImplemented

NotImplemented는 이 연산을 처리할 수 없다는 뜻이다.

class A:
def __add__(self, other):
if isinstance(other, A):
return A()
return NotImplemented # 다른 타입이면 처리를 넘긴다

NotImplemented 대신 Error를 직접 raise하면 상대방에게 기회를 주지 않고 즉시 실패한다. 따라서 연산자 메서드에서는 지원하지 않는 타입에 대해 항상 NotImplemented를 반환해야 한다.

NotImplemented는 싱글턴이며, bool 값은 True다. if not result로 체크하면 안 되고 if result is NotImplemented로 비교해야 한다.

복합 대입 연산자

a += b는 먼저 a.__iadd__(b)를 시도하고, 정의되지 않았으면 a = a + b로 폴백한다.

class A:
def __init__(self, v):
self.v = v
def __iadd__(self, other):
self.v += other.v
return self # 반드시 self를 반환해야 in-place 동작
def __add__(self, other):
return A(self.v + other.v) # 새 객체 반환

__iadd__가 없으면 a += ba = a.__add__(b)가 되어 새 객체가 a에 바인딩된다. mutable 타입(list 등)은 __iadd__로 in-place 수정을 하고, immutable 타입(int, str 등)은 __iadd__를 정의하지 않아서 항상 새 객체를 만든다.

비교 연산자

비교 연산자는 reflected 관계가 약간 다르다. 역방향이 별도의 __r*__가 아니라 대칭 메서드다.

  • ==: __eq____eq__
  • !=: __ne____ne__
  • <: __lt____gt__
  • >: __gt____lt__
  • <=: __le____ge__
  • >=: __ge____le__

a < b에서 a.__lt__(b)NotImplemented를 반환하면 b.__gt__(a)를 시도한다. 서브클래스 우선 규칙도 동일하게 적용된다.

@functools.total_ordering을 사용하면 __eq__와 하나의 비교 메서드(__lt__ 등)만 정의해도 나머지를 자동 생성해준다.

단항 연산자

단항 연산자는 reflected가 없다.

  • -a: __neg__
  • +a: __pos__
  • ~a: __invert__
  • abs(a): __abs__

참고