본문 바로가기

python

Fluent Python (챕터1)

파이썬 데이터 모델

 

기본적인 객체 연산을 수행할 때, 파이썬 모델이라는 것이 제공하는 API 를 통해 파이썬 상용구를 적용할 수 있다.

이는 파이썬스러움(pythonic)의 핵심이다.

이 파이썬 모델을 사용할 때, 파이썬 인터프리터에서 특별한 구문을 호출한다. magic method(dunder method) 가 이 특별한 구문이며 이를 명확하게 이해하면, 구현한 객체를 좀 더 다양하게 활용할 수 있다.

 

import collections

Card = collections.namedtuple("Card", ["rank", "suit"])


class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list("JQKA")
    suits = "spades diamonds clubs hearts".split()

    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits for rank in self.rank]

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]

 

collections.namedtuple() 을 이용해서 개별 카드를 나타내는 클래스를 구현했다. 이는 python 3.7 에서 표준 라이브러리로 추가된 dataclass를 사용하여 API 의 input 과 output 을 객체 형태로 표현하는 것과 같다.

 

beer_card = Card('7', 'diamonds')
print(beer_card)

>> Card(rank='7', suit='diamonds')

 

다시 돌아가서 클래스 FrenchDeck 의 instance 를 생성하고 위의 구현된 magic method 를 확인해보면 아래와 같다.

 

deck = FrenchDeck()

print(len(deck))
>> 52

print(deck[0])
>> Card(rank='2', suit='spades')

 

첫 번째는 len() 함수를 활용해 가지고 있는 카드의 전체 수를 반환한다.

두 번째는 __getitem__() 를 활용하여 특정 카드를 반환할 수 있다. 이는 list 의 인덱스를 활용할 수 있게 한다.

인덱스를 활용할 수 있다는 것은 슬라이싱도 사용할 수 있음을 뜻한다.

 

print(deck[12::13])
>> [Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'), Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

 

참고) 슬리이싱에서 list[start:end:step] 방식으로 step 의 개수만큼 건너뛰면서 가져올 수 있다.

 

++ 추가(doctest)

doctest 모듈은 대화형 파이썬 세션처럼 보이는 텍스트를 검색한 다음, 해당 세션을 실행하여 표시된 대로 정확하게 작동하는지 검증한다.

 

def test_doctest():
    """
    >>> test_doctest() # doctest: +ELLIPSIS
    Card(rank='A', suit='spades')
    ...
    """
    for card in deck[:5]:
        print(card)


if __name__ == "__main__":
    import doctest
    doctest.testmod()

 

위의 간단한 예제를 보면, 주석처리 된 공간 안에 실행문과 예상되는 결과를 넣고 doctest 모듈의 testmod() 함수를 실행한다.

>>> : 실행문이다. 결과문이 길어질 경우 # doctest: +ELLIPSIS 문을 추가하여 통과하게 만들 수 있다.

실행문 바로 밑에 예상되는 결과문을 적는다.

 

def spade_high(card):
    rank_value = FrenchDeck.ranks.index(card.rank)
    return rank_value * len(suit_values) + suit_values[card.suit]


for card in sorted(deck, key=spade_high):
    print(card)

 

다시 위의 카드 예제에서 다음과 같이 정렬 기능도 수행할 수 있다.

 

__len__(), __getitem__() 매직 메서드를 구현함으로써 표준 파이썬 시퀀스처럼 작용하므로 반복 및 슬라이싱 등의 기능 및 sorted(), reversed() 함수를 사용하여 표준 라이브러리를 사용할 수 있다. __len__(), __getitem__() 메서드는 모든 작업을 list 객체인 self._cards 에 넘길 수 있다.

 

이러한 특별 메서드는 파이썬 인터프리터가 호출하기 위한 것이며, 반복문을 사용하는 경우 iter(x) -> x.__iter__() 를 호출하는 것처럼 암묵적으로 호출되기도 한다.

 

사칙연산과 같은 수치형 연산자도 특별 메서드가 있다.

 

class Vector:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
    
    def __repr__(self):
        return 'Vector(%r, %r)' % (self.x, self.y)
    
    def __abs__(self):
        return hypot(self.x, self.y)
    
    def __bool__(self):
        return bool(abs(self))
    
    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return Vector(x, y)
    
    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

 

 

__repr__() 는 객체를 문자열로 표현하기 위해 repr() 내장 메서드에 의해 호출된다. __repr__() 를 구현하지 않으면 Vector 객체는 콘솔에 <__main__.Vector object at 0x10c900100> 와 같은 형태로 출력된다.

 

__repr__() 메서드가 반환한 문자열은 명확해야 하며, 가능하면 표현된 객체를 재생성하는데 필요한 소스 코드와 일치해야 한다.

__str__() 메서드는 str() 생성자에 의해 호출되며, print() 함수에 의해 암묵적으로 사용된다.  __str__() 은 사용자에게 보여주기 적당한 형태의 문자열로 반환해야 한다.

 

이 두 메서드 중에서 하나만 구현해야 한다면 __repr__() 메서드를 구현해야 한다. 그 이유는 파이썬 인터프리터가 __str__() 메서드가 구현되어 있지 않을 때, 자동으로 __repr__() 메서드를 호출하기 때문이다.

 

__repr__() 메서드는 객체 자체를 표현하기 위한 목적이며, __str__() 메서드는 문자화하는 데에 목적이 있다. 비슷해보이지만 의미상 약간의 차이가 있으니 주의할 필요가 있을 것 같다.

 

파이썬은 bool(x) 를 통해 True, False 를 반환한다. __bool__() 이나 __len__() 이 구현되지 않은 경우, 기본적으로 사용자 정의 클래스의 객체는 참으로 간주한다. bool(x) 는 x.__bool__() 를 호출한 결과를 이용하며, __bool__() 이 구현되어 있지 않은 경우에는 x.__len__() 을 호출하여 0이 반환되면 False, 그렇지 않으면 True 를 반환한다.

 

위의 Vector.__bool__() 을 더 빠르게 구현하면 아래와 같다.

 

def __bool__(self):
	return bool(self.x or self.y)

 

or 연산자는 x가 참인 경우에는 x를 반환하고 아니면 y 를 반환한다. 이는 abs() 연산을 수행하는 것보다 빠르기 때문이다.

 

이 외에도 다양한 특별 메서드가 존재한다.

그 중 특별히 len() 함수는 __len__() 메서드를 통해 호출되지만, 자주 사용되는 연산이므로 내장 객체에 대해서 메서드를 호출하지 않고 C 구조체의 필드 값을 반환하는 함수로 작용한다. 따라서 len() 은 메서드가 아닌 함수로 특별 취급 받는다. 내장형 객체의 효율성을 위해 함수로 작용하지만 __len__() 메서드를 통해 직접 정의할 수 있도록 함으로써 효율성과 일관성의 타협점을 찾은 것으로 볼 수 있다.

 

 

지금까지 알아본 특별 메서드를 통해 파이썬스러운 코딩 스타일을 구사할 수 있다.

파이썬 객체를 문자열로 제공하는 데에 있어서, 디버깅이나 로그에 사용하는 형태와 사용자에게 단순히 보여주기 위한 형태가 있을 수 있다. 그 목적에 따라 __repr__() 과 __str__() 메서드가 구분되어 사용될 수 있다.