Computer Engineering/Fluent Python 정리

Fluent Python Chapter 13. 연산자 오버로딩(feat. 제대로 하기)

jordan.bae 2022. 2. 15. 01:05

Introduction

12장에서는 파이썬의 연산자를 오버로딩을 적절하게 하는 법을 다룬다. 파이썬은 원래 오버로딩을 지원하지 않으나 argument의 type이 다른 인자를 사용할 수 있다는 측면에서는 부분적으로 오버로딩이 가능하다고 할 수?도 있다.

책에서는 내장 자료형의 연산자를 오버로딩 하지 말아야 하며, is, and, or, not을 제외한 기존 연산자만 오버로딩할 수 있다.

또, 피연자를 변형하지 않아야 하고(가변 객체일 수도 있기 때문에), 다른 자료형과의 연산을 지원할 때는 예외를 발생시키지 않고 실제로 동작 순서를 살펴보면서 NotImplmented를 반환함으로썬 파이썬 인터프리터가 그 연산자의 역순 메서드( ex. __radd__())를 호출해볼 수 있게 해줘야 한다. 이외에도 서로 다른 자료형의 연산을 지원할 때 덕 타이핑과 구스 타이핑을 이용하는 방법을 각각 살펴본다.

 

 

책 정리를 시작한 이유

책 정리를 시작한 이유

Chapter1의 Introduction 부분에서 이야기 한 것처럼 지난 5년간 다양한 언어나 프레임워크 및 프로그램을 공부하고 이용하여 소프트웨어를 개발했는데 이것저것 하다 보니 자주 사용하는 언어임에도 불구하고 파이썬을 잘 활용하고 있느냐에 대한 답변을 자신 있게 하기 어렵다고 느껴서 Fluent Python이라는 책을 공부하며 정리하고 있습니다. 올해에는 새로운 기술들도 좋지만 기존에 활용하던 언어나 프레임워크 그리고 소프트웨어를 더 잘 사용할 수 있도록 깊게 공부할 수 있는 한 해를 만들고 싶은 소망을 가지고 있습니다. 21장까지 정리를 성공하고 맛있는 걸 먹으면서 스스로 축하할 날이 어서 왔으면 좋겠네요! 혹시, 제가 정리한 글이 괜찮으시면 구독 하시면 다음편이 나오면 바로 알림을 받으실 수 있습니다.

 

지난 chapter 정리한 포스팅

Fluent Python Chapter 1. 파이썬 데이터 모델 (Feat. 일관성 있는 언어)

Fluent Python Chapter 2-1. Sequence (Sequence의 분류, listcomp, generator, tuple, slicing

Fluent Python Chapter 2-2. Sequence (bisect, deque, memoryview)

Fluent Python Chapter 3. Dictionary (feat. hashtable)

Fluent Python Chapter 4. 텍스트와 바이트 (feat. 깨지지마라..)

Fluent Python Chapter 5. 일급 함수

Fluent Python Chapter 6. 일급 함수 디자인 패턴

Fluent Python Chapter 7. 함수 데커레이터와 클로저 (feat. 메타프로그래밍)

Fluent Python Chapter 8. 객체 잠조, 가변성, 재활용

Fluent Python Chapter 9. 파이썬스러운 객체

Fluent Python Chapter 10. 시퀀스 해킹, 해시, 슬라이스

Fluent Python Chapter 11. 인터페이스: 프로토콜에서 ABC까지

Fluent Python Chapter 12. 내장 자료형 상속과 다중 상속

 

 

 

연산자 오버로딩 기본 지식

해당 스타일의 구현을 좋아하지 않는 사람들도 많다. 아무래도 구현에 자유도가 있기 때문에 예상치 못하게 구현하는 경우도 많이 있기 때문일 것이다. 그렇지만 책에서는 잘 사용하면 코드의 가독성이 향상되고 만족스러운 API를 구현할 수 있다고 소개하고 있다. 그래서 무조건 막기보다는 다음과 같은 제한을 두어 융퉁성, 사용성, 안정성을 적절히 유지한다.

- 내장 자료형에 대한 연산자는 오버로딩할 수 없다.

- 새로운 연산자를 생성할 수 없으며, 기존 연산자를 오버로딩만 할 수 있다.

- is, and, or, not 연산자는 오버로딩할 수 없다. (그러나 &, |, ~ 비트 연산자는 가능하다.)

 

 

 

단항 연산자 오버로딩

단항 연사자는 self 인수 하나를 받는 적절한 특별 메서드를 구현하면 된다.

전에 구현했던 Vector 클래스에 몇 개의 단항 연산자를 오버로딩 해본다.

class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)
        
    ...생략
    def __abs__(self):
        return math.sqrt(sum(x * x for x in self))
	
    # - 단항연산자
    def __neg__(self):
        return Vector(-x for x in self)  # <1>
	
    # + 단항연산자
    def __pos__(self):
        return Vector(self)  # <2>

 

간단하게 abs(), -, + 단항 연산자를 오버로딩했다. +단항연산자는 항상 같다고 생각할 수 있는데 책에서 하나의 예외 케이스로 Decimal 객체에서 달라질 수 있다는 것을 예시로 보여주었다. 간단히 읽고만 지나가면 될 것 같다. (돈과 관련된 로직을 다룰 때는 조심해야 할 수 있을 것 같다.) 

 

+ 중위 연산자 오벌로딩 하기

아래와 같이 __add__() 매직 메서드를 구현되어 있다고 해보자.

from array import array


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)
    
    def __iter__(self):
        return iter(self._components)
    
    def __add__(self, other_vector):
        return Vector([a+b for a, b in zip(self, other_vector)])
        

v1, v2 = Vector([1,2,3]), Vector([2,4,6])
v3 = v1 + v2
v3._components
output: array('d', [3.0, 6.0, 9.0])

위와 같이 구현되어 있으면 같은 길이의 Vector 객체끼리는 + 연산이 가능하지만 짧은 쪽 벡터와 연산이 가능하다. 긴쪽 벡터에 맞추고 짧은 쪽의 벡터를 0를 채우도록 수정해 볼 수 있다.

import itertools
from array import array


class Vector:
    typecode = 'd'

    def __init__(self, components):
        self._components = array(self.typecode, components)
    
    def __iter__(self):
        return iter(self._components)
    
    def __add__(self, other):
        try:
            pairs = itertools.zip_longest(self, other, fillvalue=0.0)
            return Vector(a + b for a, b in pairs)
        except TypeError:
            return NotImplemented
            
v1, v2 = Vector([1,2,3]), Vector([2,4])
v3 = v1 + v2
v3._components
output: array('d', [3.0, 6.0, 3.0])

pairs는 짧은 쪽의 시퀀스를 0으로 채워서 generator 표현식을 사용해서 새로운 Vector 객체를 만들 수 있게 한다.

여기서 주의 할 부분은 피연산자를 변경하면 안 되며(즉, 여기서느 other 객체를 변경하는 행위) 연산의 결과로 새로운 객체를 생성해야 한다는 점이다. 복합 할당 연산자만이 self를 변경해야 한다.

 

또, 여기서 재밌는 점은 우리는 zip_longest라는 객체의 타입과 상관없이(iterable한 객체라면) 제너레이터를 생성해서 Vector를 만들 수 있다는 것이다.

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

v3 = v1 + v2
v3._components
output: array('d', [3.0, 6.0, 3.0])

그러나 여기서 피연산자의 순서를 바꾸면 실패하는데 파이썬이 어떻게 연산잔를 동작시키는지 확인해본다.

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

v3 = v1 + v2
v3._components
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [46], in <module>
----> 1 v3 = v1 + v2
      2 v3._components

TypeError: can only concatenate list (not "Vector") to list

파이썬은 서로 다른 객체형에 대한 연산을 지원하기 위해서 a+b 를 수행할 때 다음과 같은 매커니즘으로 동작한다.

 

1. a에 __add__() 메서드가 있으면 호출하고, 결과가 NotImplmented가 아니면 반환한다.

2. a에 __add__()가 정의되어 있지 않거나, 정의되어 있으나 NotImplemented가 반환되면 b에 __radd__() 메서드가 정의되어 있는지 확인해서 b.__add__(a)를 호출하고, 결과가 NotImplemented가 아니면 반환하다.

3. b에 __radd__()가 정의되 있지 않거나 NotImplemented를 반환하면 지원되지 않는 피연산자형 (unsupported Operator type ~~)라는 메시지와 TypeError가 발생한다.

 

위의 예에서는 1번에서 동작을 수행하다가 TypeError가 발생했다.

 

class Vector:
    typecode = 'd'
    
    def __add__(self, other):
        try:
            pairs = itertools.zip_longest(self, other, fillvalue=0.0)
            return Vector(a + b for a, b in pairs)
        except TypeError:
            return NotImplemented

    def __radd__(self, other):
        return self + other

위와 같이 구현을 하면 __add__()가 동작하다가 TypeError가 발생해도 NotImplemented 값을 반환해서 피연산자의 역순 메서드를 호출하려고 시도할 수 있도록 기회를 주게된다. 여기서는 덕타이핑 방식으로 구현한 것을 볼 수 있다. 타입을 체크하는 대신 예외를 잡은 후 NotImplemented를 반환했다.

 

책에서는 * 를 오버로딩해서 벡터를 스칼라와 곱할 수 있도록 했고, 여기서도 마찬가지로 역순으로 동작할 수 있도록 구현한다. 하지만, 곱하기 구현에서는 isinstance를 사용해서 명시적으로 타입을 checking하는데 구스타이핑을 사용했다.

class Vector:
    typecode = 'd'
    
    def __mul__(self, scalar):
        if isinstance(scalar, numbers.Real):
            return Vector(n * scalar for n in self)
        else:
            return NotImplemented

    def __rmul__(self, scalar):
        return self * scalar

 

 

향상된 비교 연산자

파이선 인터프리터가 비교 연산자(==, !=, >, < 등)를 동작시키는 매커니즘과 비슷하지만 (정방향 메서드를 실행하고, NotImplemented가 반환되면 역순 메서드를 실행.) 조금 더 향상된 점이 있다.

 

- __gt__()메서드를 호출하는 경우, 역순으로는 인수를 바꿔서 __lt__() 메서드를 호출한다.

- ==와 != 연산자의 경우 역순 메서드가 실패하면, 파이썬은 TypeError를 발생시키는 대신 객체의 ID를 비교한다.

 

 

복합 할당 연산자

위에서 구현된 Vector 클래스는 이미 +=의 할당 연산자를 지원하고 있다.

가변형의 경우는 a+=b a = a + b로 동작하는게 타당하고, __add__()메서드가 구현되어 있으면 아무런 코드를 추가하지 않고도 +=연산자가 작동한다.  그러나 __iadd__()메서드를 정의하면 정의된 메서드가 호출되고 새로운 객체를 생성하지 않고 왼ㅉ고에 나온 피연산자를 직접 변경한다.

 

- __add__(): 새로 생성한 객체를 반환한다.

- __iadd__(): 객체 자신을 변경한 후 self를 반환한다.

 

 

마무리

사용자가 만든 객체의 연산자를 오버로딩을 해서 코드의 가독성을 높이고 API를 풍부하게 만들 수 있습니다. 하지만, 자유도가 높기 때문에 예상치 못하게 구현되는 경우도 많이 발생하기 때문에 책에서 권장하는 범위 안에서 잘 구현해야 할 것 같다. 몇가지 연산자를 오버로딩하면서 느낀 점은 파이썬 인터프리터가 해당 연산을 위해서 다양한 노력을 한다는 것이다. (역방향 연산자 실행 등.)

 

 

Reference

Fluent Python Chapter 13

https://github.com/fluentpython/example-code/tree/master/13-op-overloading

반응형