Python

[파이썬 코드 업] 9-2장. 클래스와 매직 메서드

patrick-star 2023. 5. 7. 22:46
728x90

9.9 매직 메서드 개요

파이썬은 의미를 미리 정의한 여러 메서드 이름이 있다. 모든 이름은 언더바 2개(__)로 시작하고 끝난다.
이런 메서드를 던더(dunder, double underscore) 메서드라 부른다.

그래서 메서드 이름을 지을 때 언더바 2개(__)를 아예 사용하지 않는다면 던더 메서드의 이름과 겹치지 않는다.

미리 정의된 이름을 사용하는 메서드를 매직 메서드라고 부른다.
다른 메서드와 똑같은 방식으로 호출되지만 특정 조건에 따라 자동으로 호출되기도 한다.

ex) __init__ 메서드 : 해당 클래스의 인스턴스가 생성될 때 마다 자동으로 호출되는 매직 메서드

그렇다면 어떤 매직 메서드가 있는지 하나씩 살펴보겠다.

9.10 매직 메서드 상세

앞으로 소개할 각 섹션은 중/고급 파이썬 프로그래머가 되기 위해 유용한 주요 매직 메서드를 상세하게 다루고 있다.
일부는 거의 직접 구현할 일이 없기 때문에 다루지 않는다.

① 파이썬 클래스의 문자열 표현

__format__, __str__, __repr__을 포함한 메서드 몇 가지로 클래스 자체를 표현할 수 있다.

ex) format 함수

# 파이썬에서 아래 코드를 실행
format(6, 'b') 

# format 함수가 호출되면서 정수 클래스 int의 __format__ 메서드가 호출 & 포맷 규약으로 'b'가 전달됨 
# 메서드는 정수 6의 이진 값을 반환함
==> '110'

② 객체 표현 메서드

메서드 문법 설명
__format__(self, spec) format 함수에 객체를 직접 전달했을 때 호출됨
__str__(self) 사용자가 원하는 형태로 객체 데이터를 담은 문자열을 반환. 직접 구현하지 않았다면 __repr__ 호출
__repr(self)__ __str__과 다르게 객체 표준 표현 방식으로 문자열을 반환함
__hash__(self) hash 함수에서 객체가 매개변수로 전달되었을 때 호출됨
__bool__(self) bool 함수를 호출하고 나면 항상 호출된다
__nonzero__(self) 파이썬 3.0 부터는 불리언 반환을 하려면 __bool__을 구현해야 한다

ex)

class Point : 
    big_prime1 = 1200330304
    big_prime2 = 2323039292

    def __init__ (self, x = 0, y = 0) :
        self.x = x
        self.y = y

    # 사용자가 원하는 형태로 데이터를 문자열로 반환함 
    def __str__(self) :
        return str(self.x) + ',' + str(self.y)

    # 객체의 표준 출력 방식을 정의했음  
    def __repr__(self) :
        return 'Point(' + str(self.x) + ', ' + str(self.y) + ')'

    # Point라는 객체에서 해시코드를 만드는 방식을 정의함 
    # 해당 메서드는 hash 메서드에 Point 객체의 인스턴스를 매개변수로 전달했을 때 호출된다. 
    def __hash__(self) :
        return (self.x * self.big_prime1 + self.y) % self.big_prime2

    # Poinst 객체에서 true/false가 출력되는 기준을 정의함
    # 해당 메서드는 bool 메서드에 Point 객체의 인스턴스를 매개변수로 전달했을 때 호출된다. 
    def __bool__(self) : 
        return self.x == self.y

>>> pt = Point(3, 4)

>>> pt # Point 객체에서 정의한 __repr__ 호출됨 
Point(3, 4)

>>> print(pt) # Point 객체에서 정의한 __str__ 호출됨
3,4

>>> hash(pt) # Point 객체에서 정의한 __hash__ 호출됨
1277951624

>>> bool(pt) # Point 객체에서 정의한 __bool__ 호출됨
False

③ 비교 메서드

비교 메서드는 클래스 객체를 ==, !=, >, <, >=, <= 를 사용해서 비교할 수 있도록 해준다.

  • 파이썬 비교 연산자의 특징

1) 클래스의 객체들을 정렬하고 싶다면 < 연산자를 정의해야 한다
2) 컬렉션이 서로 다른 클래스의 객체들을 동시에 갖고 있을 때 비교하고 싶다면 대칭(symmetry)을 이용한다
==> 대칭 규칙이 있기 때문에 모든 메서드를 구현할 필요 없이 자동으로 여러 연산자를 갖게 된다
(ex. 파이썬은 A > B는 B < A와 동일하다는 걸 추론할 수 있다)

class Dog : 
    def __init__(self, n) :
        self.n = n

    # 동일함 테스트 
    def __eq__(self, other) :
        ''' ==를 구현한다. 
            이에 자동으로 !=도 구현된다. '''
        return self.n == other.n

    # lt = less than
    # lt를 구현함으로써 gt가 자동으로 구현됨
    def __lt__(self, other) :
        ''' <를 구현한다.
            이에 자동으로 >도 구현된다. '''
        return self.n < other.n

    # le = less that or equal to
    # le를 구현함으로써 ge가 자동으로 구현됨
    def __le__(self, other) :
        ''' <=를 구현한다.
            이에 자동으로 >=도 구현된다'''
        return self.n <= other.n

ex) Dog 클래스와 int형 정수를 같이 정렬하는 경우

Dog 클래스int형 정수를 서로 정렬하게 하려면 다음과 같은 비교가 가능해야 한다.

Dog < Dog # 같은 객체 간의 비교
Dog < int # int가 더 큰 경우
int < Dog # Dog이 더 큰 경우 

위 3가지를 정의할 수 있어야 한다. 

어떻게 정의하면 될까. 아래의 코드와 함께 살펴보자.

class Dog : 
    def __init__(self, d) :
        self.d = d

    # Dog과 Dog 간의 비교 
    # 또는 Dog이 int보다 큰 경우
    def __gt__(self, other) :

        if type(other) == Dog :
            return self.d > other.d
        else :
            return self.d > other

    # Dog과 Dog 간의 비교 
    # 또는 int가 Dog보다 큰 경우
    def __lt__ (self, other) :

        if type(other) == Dog :
            return self.d < other.d
        else :
            return self.d < other

    # 문자 표현 
    def __repr__(self) :
        return "Dog(" + str(self.d) + ")"

d1, d5, d10 = Dog(1), Dog(5), Dog(10)

a_list = [50, d10, 100, d1, -20, d5, 3]
a_list.sort()

[-20, Dog(1), 3, Dog(5), Dog(10), 50, 100] # 같은 객체 끼리 비교 & int형 정수와 Dog 객체의 값 비교 

④ 산술 연산자 메서드

메서드 문법 설명
__add__(self, other) 덧셈. 클래스의 인스턴스가 + 좌측에 있을 때 호출됨. other는 + 우측에 위치한 참조
__sub__(self, other) 뺄셈. 클래스의 인스턴스가 - 좌측에 있을 때 호출됨. other는 - 우측에 위치한 참조
__mul__(self, other) 곱셈. 클래스의 인스턴스가 * 좌측에 있을 때 호출됨. other는 * 우측에 위치한 참조
__floordiv__(self, other) 정수 나눗셈. 소수점을 버린 몫 반환. 클래스의 인스턴스가 // 좌측에 있을 때 호출됨. other는 // 우측에 위치한 참조
__truediv__(self, other) 일반 나눗셈. 소수점을 버리지 않은 몫 반환. 클래스의 인스턴스가 / 좌측에 있을 때 호출됨. other는 / 우측에 위치한 참조
__divmod__(self, other) 몫과 나머지 2개의 값을 가진 튜플을 반환
__pow__(self, other) 제곱 ex) 2**4 = 16

+ 연산자를 호출했다면 자동으로 __add__가 호출되는 거고
/ 연산자를 호출했다면 자동으로 __truediv__가 호출되는 거다.

이런 기본 연산자들을 객체에서도 사용하고 싶다면 위에서 얘기한 메소드들을 직접 구현해줘야 한다.

ex) Point 클래스

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

    def __add__(self, other) :
        '''Point 객체끼리 + 연산자를 호출했을 때 
           동작하는 방식을 정의'''
        return Point(self.x + other.x, self.y + other.y)

    def __sub__(self, other) :
        '''Point 객체끼리 - 연산자를 호출했을 때 
           동작하는 방식을 정의'''
        return Point(self.x - other.x, self.y - other.y)

    def __mul__(self, n) :
        '''Point 객체와 정수 연산에서 * 연산자를 호출했을 때 
           동작하는 방식을 정의'''
        return Point(self.x * n, self.y * n)

    def __repr__(self) :
        return "Point(" + str(self.x) + ", " + str(self.y) + ")"

>>> pt1 + pt2
Point(13, 17)
>>> pt1 - pt2 
Point(7, 13)

⑤ 단항 산술 연산자

ex) my_pt = Point(3, 4)를 실행했다.

이때, +my_pt, -my_pt, math.abs(my_pt), ~my_pt, bool(my_pt),
math.round(my_pt, 3), math.floor(my_pt), math.ceil(my_pt), math.trunc(my_pt) 을 실행할 때

아래의 매직 메서드들을 호출하면서 실행된다.

메서드 문법 설명
__pos__(self) + 가 붙는 경우. 거의 대부분은 원래 상태 그대로 반환하는 걸로 정의됨
__neg__(self) - 가 붙는 경우
__abs__(self) 절댓값. abs 함수에 의해 호출된다
__invert__(self) 비트 반전. 비트값 1은 0으로 & 0은 1로 변환함
__bool__(self) bool() 뿐만 아니라 not이나 조건문에 응답하는 제어 구조와 같은 논리 연산자를 사용하는 경우
__round__(self, n) 반올림 함수
__floor(self)__ 내림 함수. 객체의 값보다 큰 가장 작은 정수를 반환. math.floor 함수에 의해 호출됨
__ceil(self)__ 올림함수. 객체의 값보다 작은 가장 큰 정수를 반환. math.ceil 함수에 의해 호출됨
__trunc__(self) 소수점 버림 함수

ex) Point 클래스에 __neg__(self) 정의

class Point : 
    def __neg__(self) : 
        return Point(-self.x, -self.y) # 해당 return문을 통해 새로운 Point 객체가 생성된다는 걸 알 수 있다.

    def __trunc__(self) :
        return Point(self.x.__trunc__(), self.y.__trunc__())

pt1 = Point(3.3, -4.5) 
pt2 = -pt1 # Point(-3.3, 4.5)가 저장됨

pt3 = math.trunc(pt1) # Point(3.0, -4.0)이 저장됨

⑥ 리플렉션(역방향) 메서드

pt1 * 5 # 좌측 피연산자가 pt1이다. 
        # 그래서, 연산을 위해서 pt1에서 __mul__이 정의되어 있어야 한다. 

5 * pt1 # pt1이 좌측에 있지 않고 우측에 있다.
        # 이럴때 정의해야 하는게 __mul__의 리플렉션 메서드 __rmul__이다.

위와 같이 객체우측에서 계산되는 경우가 존재할 수 있다. 이를 위해서 정의하는게 리플렉션 연산자 메서드이다.

내용은 ④에서 다뤘던 내용과 동일하다.
다만, 좌측 피연산자가 ④의 연산자를 정의하지 않았거나 NotImplemented를 반환한다면 수행된다는 점에서 차이가 있다.

| 문법 | 설명 |
| __radd__(self, other) | 우측 덧셈 연산자 |
| __rsub__(self, other) | 우측 뺄셈 연산자 |
| __rmul__(self, other) | 우측 스칼라 곱셈 연산자 |
| __rfloordiv__(self, other) | 우측 정수 나눗셈 연산자(//) |
| __rtruediv__(self, other) | 우측 나눗셈 연산자(/) |
| __rmod__(self, other) | 우측 나머지 나눗셈 연산자(%) |
| __rdivmod__(self, other) | 우측 몫/나머지 반환 함수(divmode) |
| __rpow__(self, other) | 우측 제곱 연산자 |

⑦ 교체 연산자 메서드

모든 클래스에 +=, -=, *=, /= 등등의 산술 & 대입 연산자 기능을 제공하는 매직 메서드가 있다.
여기서 쓰이는 i는 in-place의 약자이다.

문법 설명
__iadd__(self, other) += 연산을 구현. 교체한 연산이 성공적으로 구현되려면 self를 반환해야 한다
__isub__(self, other) -= 연산을 구현. 이제부터 나머지 내용은 동일
__imul__(self, other) *= 연산을 구현
__idiv__(self, other) /= 연산을 구현
__igrounddiv__(self, other) //= 연산을 구현
__imod__(self, other) %= 연산을 구현
__ilshift__(self, other) 비트 좌측 시프트를 수행하는 <<= 연산을 구현
__irshift__(self, other) 비트 우측 시프트를 수행하는 >>= 연산을 구현
__iand__(self, other) 바이너리 AND를 수행하는 &= 연산을 구현
__ior__(self, other) 바이너리 OR를 수행하는`
__ixor__(self, other) 바이너리 exclusive-OR를 수행하는 ^= 연산을 구현
__ipow__(self, other) 제곱 연산자 수행하는 **= 연산을 구현

ex)

a = MyClass(10) 
b = a
a += 1

print(a, b) # a와 b는 같을까? 
  • MyClass 클래스가 __add__ 를 지원 & __iadd__는 지원 안 하는 경우

⇒ 파이썬은 대입 연산을 지원한다. 다만, 메모리의 값을 교체하는 게 아니라 새로운 객체를 생성해서 변수에 대입한다.

즉, a와 b는 a += 1 연산을 실행함으로써 서로 다른 객체를 가리키게 된다.

ex2) __iadd__, __imul__ 메서드 구현 예시

def __iadd__(self, other) :
    self.x += other.x
    self.y += other.y
    return self

def __imul__(self, other) :
    self.x *= other.x
    self.y *= other.y
    return self

⑧ 변환 메서드

문법 설명
__int__(self) int 변환 함수 사용 시 호출. 정수 객체를 반환해야 함
__float__(self) float 변환 함수 사용 시 호출. 부동소수점 객체를 반환해야 함
__complex__(self) 복소수(complex) 변환 함수 사용 시 호출. 복소수-숫자 객체를 반환해야 함
__index__(self) 객체가 컬렉션의 색인 or 슬라이싱 연산의 범위로 주어진 경우 실제 인덱스 정수 숫자를 반환해야 한다
__bool__(self) 이미 다룬 내용

ex) Point 클래스의 변환 메서드 정의

class Point : 
    def __init__(self, x = 0, y = 0) :
        self.x = x
        self.y = y

    def __int__ (self) : # int 변환함수를 사용할 때 x,y의 정수값의 합을 반환하도록 함
        return int(self.x) + int(self.y)

    def __float__(self) : # float 변환함수를 사용할 때 x,y의 부동소수점 값의 합을 반환하도록 함
        return float(self.x) + float(self.y)

>>> p = Point(100, 20)
>>> int(p)
120
>>> float(p)
120.0

__iter____next__ 구현하기

  • 용어 정리

1) 이터러블(iterable) : 여러 항목을 한 번에 한 항목씩 접근해 첫 항목부터 끝 항목까지 관통할 수 있는 객체를 의미
객체가 iterable이 되려면 __iter__ 메서드는 반드시 객체를 반환해야 한다

2) 이터레이터(iterator) :
- __iter__가 반드시 반환해야 하는 객체. 컬렉션 객체를 관통하는데 사용됨
- __next__ 메서드가 반환해야 하는 객체. __next__는 아무 작업을 하지 않더라도 구현되어야 한다.

이 메서드들은 파이썬의 for문에서 클래스 인스턴스를 사용하려면 반드시 필요하다.

ex) 4차원의 Point 객체가 있다고 하자

이터레이터가 4개의 좌표값을 한 번에 하나씩 가져올 수 있다면 다음과 같이 사용할 수 있다.
이렇게 사용할 수 있다는 건 __iter__ 메서드는 self를 반환하고 __next__ 메서드를 자체적으로 구현했을 것이다

my_pt = Point() 

for i in my_pt : 
    print(i) 

컨테이너의 복잡성과 유연성 수준에 따라 여러 방법으로 구현할 수 있다.

1) 대상 내에 저장된 컬렉션 객체의 __iter__를 호출 ⇒ 가장 쉬운 방법 & 다른 누군가가 순회 작업을 제어해야 함
2) 컬렉션 클래스 자체적으로 __iter__, __next__를 모두 구현 ⇒ 한 번에 한 루프 이상 순회할 수 없음
3) __iter__ 메서드가 컬렉션 클래스를 관통하는 이터레이션을 지원하는 목적으로 만들어진 자체 이터레이터 객체를 반환 ⇒ 가장 강력한 방법

9.11 다중 인수 타입 지원

ex)

    def __mul__(self, other) :
        if type(other) == Point : # Point 객체와 곱해지는 값이 Point 객체라면 
                                  # 두 객체의 x, y 값을 각각 곱한 새로운 객체를 반환한다.
            return Point(self.x * other.x, self.y * other.y) 

        elif type(other) == int or type(other) == float : # 실수 또는 정수 값과 곱해진다면 
                                                          # 스칼라 곱셈을 한 새로운 객체를 반환한다 
            return Point(self.x * other, self.y * other)

        else : # 이도 저도 아니면 NotImplemented를 반환한다. 
               # 그러면 파이썬에서 우측 피연산자의 __rmul__ 메서드가 있는지 확인할 것이다. 
            return NotImplemented

똑같은 내용을 isinstance 메서드를 이용해서 작성할 수 있다.

    def __mul__(self, other) :
        if isinstance(other, Point) : # Point 객체와 곱해지는 값이 Point 객체라면 
                                        # 두 객체의 x, y 값을 각각 곱한 새로운 객체를 반환한다.
            return Point(self.x * other.x, self.y * other.y) 

        elif isinstance(other, (int, float)) : # 실수 또는 정수 값과 곱해진다면 
                                               # 스칼라 곱셈을 한 새로운 객체를 반환한다 
            return Point(self.x * other, self.y * other)

        else : # 이도 저도 아니면 NotImplemented를 반환한다. 
               # 그러면 파이썬에서 우측 피연산자의 __rmul__ 메서드가 있는지 확인할 것이다. 
               # 있다면 그걸 이용해서 곱셈을 진행하면 되고 
               # 없다면 클래스에서 __ruml__ 메소드를 작성해줘야 한다. 
            return NotImplemented

9.12 동적 속성 설정 및 조회

파이썬 객체는 수많은 속성(인스턴스 변수, 메서드, 매개변수 등등) 을 가질 수 있다.
일반적으로 이런 속성들은 하드 코딩되어 있어서 이름이 정해져 있다.

하지만, 때때로 동적으로 속성을 설정하는 것이 유용할 때가 있다.
프로그램 실행 시점에 특정 조건에 따라 속성 이름을 결정하는 것이다.

ex)

>>> class Dog :
...     pass

>>> d = Dog()
>>> setattr(d, 'breed', 'Great Dane') # breed라는 속성을 추가 & 속성 값은 'Great Dane'으로 설정 
>>> getattr(d, 'breed') # 객체 d의 breed 속성의 값을 반환한다 
'Great Dane'