[Python] Reflected method 또는 Reverse method - @typing.overload 에서 (260814)

WonTerry·7일 전

Python

목록 보기
28/28

NotImplemented

파이썬의 이항 연산자(Binary Operator)가 구현되지 않았을 때, 파이썬 인터프리터에게 "나는 이 타입에 대한 연산을 어떻게 처리해야 할지 모르겠으니, 상대방 객체에게 물어봐라"라고 알리는 아주 특별한 약속(Signal)입니다.


1. NotImplemented의 핵심 역할: "상대방에게 넘기기"

파이썬에서 a * b를 실행하면 내부적으로는 a.__mul__(b)가 호출됩니다.

만약 a.__mul__(b)를 실행했는데 결과로 NotImplemented가 반환되면, 파이썬은 즉시 에러를 내는 대신 "그럼 반대로 b야, 너 a랑 곱할 수 있니?"라며 b.__rmul__(a)(Reflected mul)를 호출합니다.

  • a.__mul__(b)NotImplemented를 반환함 \rightarrow b.__rmul__(a)를 시도함.
  • b.__rmul__(a)NotImplemented를 반환함 \rightarrow 그제서야 TypeError: unsupported operand type(s) for *: ... 에러를 발생시킴.

즉, 연산의 권한을 상대방 객체에게 넘겨주어, 객체 간의 상호작용(Polymorphism)을 가능하게 만드는 장치입니다.

2. NotImplemented vs NotImplementedError

가장 많이 하는 실수 중 하나가 이 둘을 혼동하는 것입니다. 이름은 비슷하지만 용도가 완전히 다릅니다.

구분NotImplementedNotImplementedError
정체값 (Constant)예외 (Exception)
사용 위치__add__, __mul__ 같은 이항 연산 매직 메서드return메서드의 본문 (Implementation)
의미"난 이 연산을 못 해. 상대방한테 물어봐!""이 메서드는 아직 구현되지 않았어! (개발자 실수)"
결과파이썬이 상대 객체의 __rmul__ 등을 확인한 후 계속 진행함프로그램이 즉시 중단되며 에러 메시지를 띄움

예시 상황:

  • return NotImplemented: "음, 나는 VectorString의 곱셈은 모르겠어. String 객체야, 너 이거 할 줄 알아?" (유연한 대응)
  • raise NotImplementedError: "이 메서드는 아직 코드를 안 짰어! 프로그램 멈춰!" (개발 단계의 오류)

3. 코드에서의 의미

작성하신 코드의 실제 구현 부분(Implementation)을 보면,

def __mul__(self, other):
    if isinstance(other, Vector):
        return Vector(self.x * other.x, self.y * other.y)
    elif isinstance(other, (int, float)):
        return Vector(self.x * other, self.y * other)
    
    # 만약 other가 Vector도 아니고, int/float도 아니라면?
    return NotImplemented
  1. v1 * v2 \rightarrow otherVector이므로 첫 번째 if문에 걸려 Vector 객체를 반환합니다. (정상)
  2. v1 * 3 \rightarrow otherint이므로 두 번째 elif문에 걸려 Vector 객체를 반환합니다. (정상)
  3. 만약 v1 * "Hello" 를 실행한다면?
    • otherstr 타입입니다.
    • ifelif 조건에 모두 해당하지 않습니다.
    • return NotImplemented가 실행됩니다.
    • 파이썬은 이제 "Hello".__rmul__(v1)를 호출합니다.
    • 문자열(str) 객체는 Vector를 곱하는 법을 모릅니다. 따라서 strNotImplemented를 반환하거나 에러를 냅니다.
    • 결국 파이썬은 TypeError를 발생시킵니다.

요약

작성하신 코드에서 return NotImplemented"내가 지원하지 않는 타입(예: 문자열, 리스트 등)이 들어오면, 일단 내가 해결하려 하지 말고 파이썬 엔진이 상대방 객체의 연산 능력을 한 번 더 확인해볼 수 있도록 양보하겠다"는 의미입니다. 이는 객체 지향 프로그래밍에서 서로 다른 타입 간의 연산을 매끄럽게 연결하기 위한 매너(Protocol)와 같습니다.

# @typing.overload()를 사용하는 방법 : 동일한 이름의 메서드를 타입에 따라 다르게 적용 가능 (260814)

'''
@typing.overload는 실제 런타임 동작을 바꾸는 기능이 아니라 타입 힌트(type hint)용이다.
따라서 여러 형태의  __mul()__() 시그니처를 정의해 IDE와 타입 검사기(mypy)가 이해하도록하고
실제 구현은 하나만 작성해야 한다.
'''

from __future__ import annotations 
# 파이썬이 모든 타입 힌트를 문자열처럼 취급한다. (사용하는 이유 설명)
# 클래스가 정의되는 시점에는 Vector 클래스가 아직 완전히 만들어지지 않았기 때문에 

from typing import overload

class Vector:
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    # 입력, 출력의 타입을 미리 정의함
    # Vector * Vector
    @overload
    def __mul__(self, other: Vector) -> Vector:   # 아직 Vector라는 형태가 완성되지 않은 문제가 있으므로
        ...

    # Vector * Scalar
    @overload
    def __mul__(self, other: int | float) -> Vector:
        ...

    # 실제 구현은 하나만 존재 : 타입 검사를 수행하고 어떤 함수를 사용할 지 결정
    def __mul__(self, other):
        if isinstance(other, Vector):
            return Vector(
                self.x * other.x,
                self.y * other.y
            )
        elif isinstance(other, (int, float)):
            return Vector(
                self.x * other,
                self.y * other
            )       

        return NotImplemented
 
    # --- 이 부분을 추가 작성 ---
    def __rmul__(self, other):
        # 숫자가 Vector를 곱하려 할 때, 결국 Vector가 곱셈의 주도권을 가져와서 
        # 위에서 정의한 __mul__을 다시 실행하게 합니다.
        return self.__mul__(other)


    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

v1 = Vector(1, 2)
v2 = Vector(3, 4)


print(v1*v2)
print(v1*3)

Reflected method 또는 Reverse method


1. 가장 포괄적인 명칭: Special Methods (Dunder Methods)

파이썬에서 __init__, __add__, __mul__ 처럼 앞뒤에 언더바가 두 개씩 붙은 메서드들을 통칭하여 Special Methods라고 합니다. 개발자들 사이에서는 "더블 언더스코어(Double Underscore)"의 줄임말인 "Dunder methods"라고도 부릅니다.

2. 연산 방향에 따른 구분

연산자(Operator)가 적용되는 방향에 따라 다음과 같이 구분합니다.

① Left-hand side (LHS) Method

a * b에서 a의 메서드인 __mul__을 의미합니다. 객체가 연산의 왼쪽에 위치할 때 호출됩니다.

② Right-hand side (RHS) Method (Reflected / Reverse Method)

a * b에서 a가 연산을 처리하지 못해 NotImplemented를 반환했을 때, b에게 "네가 대신 해줄래?"라고 요청하며 호출되는 __rmul__을 의미합니다.

  • Reflected Method: "반사된(Reflected)"이라는 뜻으로, 연산의 순서가 뒤집혀서(반사되어) 전달된다는 의미에서 공식적으로나 기술적으로 자주 사용되는 표현입니다.
  • Reverse Method: "역방향(Reverse)"이라는 뜻으로, 연산의 방향이 반대로 돌아간다는 의미에서 직관적으로 사용됩니다.

3. 요약 표

구분명칭 (English)의미호출되는 상황
__mul__LHS Method (Left-hand side)왼쪽 객체의 메서드Object * Other
__rmul__RHS Method (Right-hand side) / Reflected Method오른쪽 객체의 메서드Other * Object (LHS가 거절했을 때)

💡 기억하기 쉬운 팁

  • __add__: "내가 먼저 곱할게!" (LHS)
  • __radd__: "(LHS가 못하겠대서) 그럼 내가 거꾸로(Reverse) 해볼게!" (RHS/Reflected)

return self.__mul__(other)라는 코드는 "내가 이미 만들어 놓은 곱셈 규칙(__mul__)을 그대로 가져와서, 지금 이 상황에 다시 적용해라"라는 뜻입니다.

이것은 프로그래밍에서 매우 중요한 위임(Delegation)이라는 기법입니다. 왜 이렇게 하는지, 단계별로 아주 상세하게 쪼개서 보여드릴게요.


1. 연산의 흐름도 (Scenario: 3 * v1)

v1Vector(1, 2)이고, 3int라고 가정했을 때의 내부 동작 순서입니다.

  1. 사용자 명령: 3 * v1 실행
  2. 1단계 (LHS 시도): 파이썬이 숫자 3에게 물어봅니다.
    • "숫자야, Vector 객체랑 곱할 수 있니? (3.__mul__(v1))"
    • 결과: 숫자는 Vector를 곱하는 법을 모르므로 NotImplemented를 반환합니다.
  3. 2단계 (RHS 시도): 파이썬이 Vector 객체에게 물어봅니다.
    • "어이 Vector, 숫자가 너랑 곱하기 싫다는데, 네가 대신 해줄래? (v1.__rmul__(3))"
  4. 3단계 (위임 실행): 이제 v1__rmul__ 메서드가 실행됩니다.
    • 여기서 return self.__mul__(other)를 만납니다.
    • 이것은 v1.__mul__(3)을 호출하라는 뜻입니다.
  5. 4단계 (최종 계산): v1.__mul__(3)이 실행됩니다.
    • 코드에 이미 elif isinstance(other, (int, float)): 로직이 구현되어 있습니다.
    • 따라서 Vector(3, 6)이 정상적으로 계산되어 반환됩니다.

2. 왜 이렇게 하나요? (이유 2가지)

① 코드 중복 방지 (DRY: Don't Repeat Yourself)

이미 __mul__ 메서드 안에 Vector * VectorVector * Scalar에 대한 복잡한 계산 로직을 다 적어놓았습니다.
만약 __rmul__에서 self.__mul__(other)를 쓰지 않고 또 계산식을 적는다면, 똑같은 계산 로직을 두 번 써야 합니다.

  • 나쁜 예 (중복):
    def __mul__(self, other):
        # ... 계산 로직 ...
    def __rmul__(self, other):
        # 똑같은 계산 로직을 또 적어야 함 (수정할 때 두 번 수정해야 해서 위험함)
  • 좋은 예 (위임):
    def __mul__(self, other):
        # ... 계산 로직 ...
    def __rmul__(self, other):
        return self.__mul__(other) # "난 몰라, __mul__한테 시켜!"

② 일관성 (Consistency)

a * bb * a와 수학적으로 결과가 같아야 한다면(교환법칙), __mul____rmul__은 결국 같은 로직을 바라봐야 합니다. __rmul____mul__을 호출하게 만들면, __mul__에 적힌 규칙이 곧 __rmul__의 규칙이 되므로 두 연산이 완벽하게 일치하게 됩니다.


3. 요약하자면

return self.__mul__(other)는 다음과 같은 의미를 담고 있습니다.

"나는 왼쪽(LHS)에 있는 타입이 아니라 오른쪽(RHS)에 있는 타입이야. 하지만 내가 이 연산을 처리할 줄 안다는 건 이미 __mul__에 다 정의해 뒀다는 뜻이지. 그러니까 복잡하게 다시 계산하지 말고, 그냥 내가 원래 하던 방식(__mul__)대로 이 숫자(other)를 처리해버릴게!"

profile
Hello, I'm Terry! 👋 Enjoy every moment of your life! 🌱 My current interests are Signal processing, Machine learning, Python, Database, LLM & RAG, MCP & ADK, Multi-Agents, Physical AI, ROS2...

0개의 댓글