계승

이 장에서, 저는 게임 카드(playing cards), 카드 덱(decks of cards), 포커 패(poker hands)를 표현하는 클래스를 제시합니다. 여러분이 포커를 하지 않는다면 http://en.wikipedia.org/wiki/Poker 에서 설명을 읽을 수 있습니다만, 그럴 필요는 없습니다; 연습을 위해 필요한 만큼은 제가 설명할 겁니다. 이 장의 코드 예들은 http://thinkpython.com/code/Card.py에 있습니다.

영미 식(anglo-american) 카드에 익숙하지 않다면, http://en.wikipedia.org/wiki/Playing_cards 를 읽어보세요.

카드 객체

하나의 덱에는 52장의 카드가 있는데, 각각은 4개의 무늬(suit) 와 13개의 숫자(rank)중 하나에 속합니다. 무늬는 스페이드(spade), 하트(heart), 다이아몬드(diamond), 클로버(club)입니다(브리지에서 높은 것부터). 숫자는 에이스(Ace), 2, 3, 4, 5, 6, 7, 8, 9, 10, 잭(Jack), 퀸(Queen), 킹(King)입니다. 게임의 종류에 따라, 에이스는 킹보다 높거나 2보다 낮습니다.

카드를 표현할 새 객체를 정해야 한다면, 애트리뷰트가 뭐가 되어야 할지는 명백합니다: rank 와 suit. 애트리뷰트의 형이 뭐가 되어야 할지는 그렇게 자명하지 않습니다. 한가지 가능성은 무늬로는 'Spade' 숫자로는 'Queen' 과 같은 단어들을 담는 문자열을 사용하는 것입니다. 이 구현의 한가지 문제점은, 카드를 비교해서 어떤 것이 더 높은 숫자나 무늬인지 확인하는 것이 쉽지 않다는 것입니다.

대안은 정수를 사용해서 숫자와 무늬를 코드화하는(encode) 것입니다. 이 문맥에서, “코드화(encode)”는 코드와 무늬나 코드와 숫자간의 대응을 정의하려고 한다는 뜻입니다. 이런 종류의 코드화는 비밀(암호)이 되려는 것이 아닙니다.

예를 들어, 이 표는 무늬와, 이에 대응하는 정수 코드를 보여줍니다:

스페이드 \(\mapsto\) 3
하트 \(\mapsto\) 2
다이아몬드 \(\mapsto\) 1
클로버 \(\mapsto\) 0

이 코드는 카드들을 비교하기 쉽게 만듭니다; 더 높은 무늬가 더 높은 숫자에 대응되기 때문에, 코드를 비교해서 무늬를 비교할 수 있습니다.

숫자의 대응은 꽤 명백합니다; 각 카드 숫자들을 해당 정수에 대응시키고, 그림 카드들은:

\(\mapsto\) 11
\(\mapsto\) 12
\(\mapsto\) 13

이 대응들이 파이썬 프로그램의 일부가 아님을 명확히 하기 위해 \(\mapsto\) 기호를 사용하고 있습니다. 프로그램 설계의 일부이기는 하지만, 코드에 구체적으로 등장하지는 않습니다.

Card 의 클래스 정의는 이렇게 됩니다:

class Card(object):
    """Represents a standard playing card."""

    def __init__(self, suit=0, rank=2):
        self.suit = suit
        self.rank = rank

여느 때와 마찬가지로, init 메쏘드는 각 애트리뷰트들에 대한 선택적인 매개변수들을 받아들입니다. 내정된 카드는 클로버 2입니다.

카드를 만들려면, 원하는 무늬와 숫자로 Card 를 호출하면 됩니다.

queen_of_diamonds = Card(1, 12)

클래스 애트리뷰트

[class.attribute]

카드 객체를 사람들이 쉽게 읽을 수 있게 인쇄하기 위해, 정수 코드를 해당 숫자와 무늬로 대응시킬 수 있어야 합니다. 자연스러운 방법은 문자열의 리스트입니다. 이 리스트를 클래스 애트리뷰트(class attribute)에 대입합니다:

# inside class Card:

    suit_names = ['Clubs', 'Diamonds', 'Hearts', 'Spades']
    rank_names = [None, 'Ace', '2', '3', '4', '5', '6', '7',
              '8', '9', '10', 'Jack', 'Queen', 'King']

    def __str__(self):
        return '%s of %s' % (Card.rank_names[self.rank],
                             Card.suit_names[self.suit])

클래스 내부이기는 하지만 메쏘드들의 바깥에서 정의된 suit_namesrank_names 같은 변수들을 클래스 애트리뷰트라고 하는데 클래스 객체 Card 와 결합하기 때문입니다.

이 용어는 그들을 suit 와 rank 같은 변수들과 구분 짓는데, 후자는 특정 인스턴스와 결합하기 때문에 인스턴스 애트리뷰트(instance attributes)라고 부릅니다.

두 가지 종류의 애트리뷰트 모두 점 표기법(dot notation)으로 접근합니다. 예를 들어, __str__ 에서, self 는 Card 객체고, self.rank 는 그 것의 숫자입니다. 비슷하게, Card 는 클래스 객체고, Card.rank_names 는 그 클래스와 결합한 문자열의 리스트입니다.

모든 카드는 자신만의 suit 와 rank 가 있습니다만, suit_namesrank_names 는 단 하나만이 있습니다.

모두 합쳐서, 표현식 Card.rank_names[self.rank] 는 “클래스 Card 의 리스트 rank_names 로의 지수로 객체 self의 애트리뷰트 rank를 사용해서, 적절한 문자열을 선택하시오” 라는 뜻입니다.

rank_names 의 첫 번째 요소는 None인데, 숫자 0 인 카드는 없기 때문입니다. None 을 자리 채우는 용도로 포함시킴으로써, 지수 2 가 문자열 '2'에 대응하는 식으로 깔끔한 대응을 얻게 됩니다. 이런 비틀기를 피하려면, 리스트 대신에 딕셔너리를 사용할 수도 있습니다.

지금까지의 방법들로, 카드를 만들고 인쇄할 수 있습니다:

>>> card1 = Card(2, 11)
>>> print card1
Jack of Hearts

card1

[fig.card1]

그림 [fig.card1]은 Card 클래스 객체와 하나의 Card 객체의 다이어그램입니다. 는 클래스 객체라서 type 형입니다. card1은 Card 형입니다. (공간을 절약하기 위해서, suit_namesrank_names의 내용은 그리지 않았습니다).

카드 비교하기

[comparecard]

내장형들의 경우에는, 어떤 것이 다른 것보다 크거나, 작거나, 같은지를 판단할 수 있는 비교 연산자(<, >, ==, 등)가 있습니다. 사용자 정의형의 경우에는, __cmp__ 라는 메쏘드를 제공함으로써 내장 연산자들의 동작을 바꿀 수 있습니다.

__cmp__는 두 개의 매개변수, self 와 other,를 받아서 첫 번째 객체가 더 크면 양수를, 두 번째 객체가 더 크면 음수를, 같으면 0을 돌려줍니다.

카드의 올바른 순서는 뻔하지 않습니다. 예를 들어, 클로버 3과 다이아몬드 2중 어떤 것이 더 좋습니까? 하나는 더 높은 숫자를 가졌지만, 다른 하나는 더 높은 무늬를 가졌습니다. 카드를 비교하기 위해서는, 숫자와 무늬 중 어느 것이 더 중요한지 결정해야 합니다.

답은 여러분이 어떤 게임을 하고 있느냐에 달려있습니다만, 단순하게 만들기 위해, 그래서 모든 스페이드는 모든 다이아몬드보다 높은 식으로, 무늬가 더 중요하다고 임의로 선택할 것입니다.

이런 결정에 따라, __cmp__를 작성할 수 있습니다:

# inside class Card:

    def __cmp__(self, other):
        # 무늬 검사
        if self.suit > other.suit: return 1
        if self.suit < other.suit: return -1

        # 무늬는 동일... 숫자 검사
        if self.rank > other.rank: return 1
        if self.rank < other.rank: return -1

        # 숫자도 동일... 동률
        return 0

튜플 비교를 사용해서 더 간결하게 작성할 수 있습니다:

# inside class Card:

    def __cmp__(self, other):
        t1 = self.suit, self.rank
        t2 = other.suit, other.rank
        return cmp(t1, t2)

내장 함수 cmp는 메쏘드 __cmp__와 동일한 인터페이스를 갖습니다: 두 개의 값을 받아서 첫 번째가 크면 양수를, 두 번째가 크면 음수를, 같으면 0을 들려줍니다.

파이썬 3에서, cmp 는 더 이상 존재하지 않고, __cmp__ 메쏘드도 지원되지 않습니다. 대신에 self 가 other 보다 작으면 True를 돌려주는 __lt__ 를 제공해야만 합니다. __lt__를 튜플과 < 연산자로 구현할 수 있습니다.

[연습 18.1.]

Time 객체에 대해 __cmp__ 메쏘드를 작성하세요. 힌트: 튜플 비교를 사용할 수 있습니다만, 정수 뺄셈을 사용하는 것 또한 고려할 수 있습니다.

카드 덱

이제 Card 가 있고, 다음 단계는 덱(deck)을 정의하는 것입니다. 덱은 카드로 만들어지기 때문에, 각 덱이 애트리뷰트로 카드의 리스트를 갖는 것이 자연스럽습니다.

다음은 Deck의 클래스 정의입니다. init 메쏘드는 애트리뷰트 cards를 만들고 52 장의 카드 표준 세트를 생성합니다:

class Deck(object):

    def __init__(self):
        self.cards = []
        for suit in range(4):
            for rank in range(1, 14):
                card = Card(suit, rank)
                self.cards.append(card)

덱을 채우는 가장 쉬운 방법은 중첩된 반복입니다. 바깥의 반복은 무늬를 0 에서 3까지 나열합니다. 안쪽의 반복은 숫자를 1 에서 13까지 나열합니다. 각 반복은 현재 무늬와 숫자로 새 Card 를 만들어서 self.cards 에 덧붙입니다.

덱 인쇄하기

[printdeck]

여기 Deck의 __str__ 메쏘드가 있습니다:

#inside class Deck:

    def __str__(self):
        res = []
        for card in self.cards:
            res.append(str(card))
        return '\n'.join(res)

이 메쏘드는 큰 문자열을 합치는 빠른 방법을 보여줍니다: 문자열의 리스트를 만든 다음 join을 사용하는 것. 내장 함수 str 은 각 카드들의 __str__ 함수를 호출하고 문자열 표현을 돌려줍니다.

join 을 줄 넘김 문자에 대해 호출하기 때문에, 카드들은 줄 넘김 문자로 분리됩니다. 결과는 이렇게 됩니다:

>>> deck = Deck()
>>> print deck
Ace of Clubs
2 of Clubs
3 of Clubs
...
10 of Spades
Jack of Spades
Queen of Spades
King of Spades

결과가 52 줄에 걸쳐 나타나지만, 줄 넘김 문자를 포함한 하나의 문자열일 뿐입니다.

넣고, 빼고, 섞고, 정렬하기

카드를 나누기 위해, 카드를 덱에서 제거한 후 돌려주는 메쏘드가 필요합니다. 리스트 메쏘드는 pop은 편리한 방법을 제공합니다:

#inside class Deck:

    def pop_card(self):
        return self.cards.pop()

pop 은 리스트의 마지막 카드를 제거하기 때문에, 덱의 바닥에서부터 나누는 샘입니다. 실제 생활에서 “바닥부터 나누기”는 눈살을 찌푸리게 합니다만 지금 상황에서는 상관없습니다.

카드를 넣으려면, 리스트 메쏘드 append를 사용할 수 있습니다:

#inside class Deck:

    def add_card(self, card):
        self.cards.append(card)

이런 메쏘드처럼 그다지 실질적인 일을 하는 것 없이 다른 함수를 사용하는 것들을 버니어(veneer)라고 부르기도 합니다. 목공에서 나온 은유인데, 싸구려 나무 조각의 겉에 얇은 양질의 나무를 붙이는 것을 뜻합니다.

이 경우에는 리스트 연산을 덱에 적합한 방식으로 표현하는 “얇은” 메쏘드를 정의하고 있습니다.

또 하나의 예로, Deck 메쏘드 shuffle 를 random 모듈의 함수 shuffle을 사용해서 작성할 수 있습니다:

# inside class Deck:

    def shuffle(self):
        random.shuffle(self.cards)

random 을 들여오는 걸 잊지 마세요.

[연습 18.2.]

리스트 메쏘드 sort 를 사용해서 Deck 에 있는 카드들을 정렬하는 Deck 메쏘드 sort를 작성하세요. sort 는 정렬 순서를 정하기 위해 정의한 __cmp__ 메쏘드를 사용합니다.

계승

개체 지향적 프로그래밍과 가장 빈번히 결합되는 언어 기능은 계승(inheritance)입니다. 계승은 이미 존재하는 클래스의 수정된 버전의 형태로 새 클래스를 정의하는 방법입니다.

새 클래스가 기존 클래스의 메쏘드들을 물려받기 때문에 “계승”이라고 부릅니다. 이 은유를 확장해서, 기존 클래스를 부모(parent)라고, 새 클래스를 자식(child)이라고 부릅니다.

예를 들어, “패(hand)”, 즉 플레이어가 들고 있는 카드 세트,를 나타내는 클래스를 원한다고 합시다. 패는 덱과 비슷합니다: 둘 다 여러 장의 카드로 만들어지고, 둘 다 넣거나 카드를 넣거나 빼는 것과 같은 연산을 필요로 합니다.

패는 덱과 다르기도 합니다; 패의 경우에는 필요하지만 덱의 경우에는 말이 안 되는 연산들이 있습니다. 예를 들어, 포커에서 누가 이겼는지 보기 위해 두 패를 비교할 수 있습니다. 브리지에서는 선언하기(make a bid)위해 패의 점수를 계산할 수 있습니다.

이런 클래스들간의 관계—비슷하지만 다른—는 계승에 적합합니다.

자식 클래스의 정의는 다른 클래스 정의들과 같습니다만, 부모 클래스의 이름이 괄호에 나타납니다:

class Hand(Deck):
    """Represents a hand of playing cards."""

이 정의는 Hand 가 Deck을 계승함을 가리킵니다; pop_cardadd_card 와 같은 메쏘드들을 Deck 뿐만 아니라 Hand 에서도 쓸 수 있다는 뜻입니다.

Hand 는 Deck 으로부터 __init__ 또한 계승합니다만, 우리가 실제로 원하는 것을 하지는 않습니다: 패를 52장의 카드로 채우는 대신에, 패의 init 메쏘드는 cards 를 빈 리스트로 초기화해야 합니다.

Hand 클래스에 init 메쏘드를 제공하면, Deck 클래스에 있는 것을 번복합니다:

# inside class Hand:

    def __init__(self, label=''):
        self.cards = []
        self.label = label

그래서 Hand 를 만들 때, 파이썬은 이 init 메쏘드를 호출합니다:

>>> hand = Hand('new hand')
>>> print hand.cards
[]
>>> print hand.label
new hand

그러나 다른 메쏘드들은 Deck 으로부터 물려받았기 때문에, 카드를 나누는데 pop_cardadd_card를 사용할 수 있습니다:

>>> deck = Deck()
>>> card = deck.pop_card()
>>> hand.add_card(card)
>>> print hand
King of Spades

자연스러운 다음 단계는 이 코드를 move_cards 라는 메쏘드로 캡슐화하는 것입니다:

#inside class Deck:

    def move_cards(self, hand, num):
        for i in range(num):
            hand.add_card(self.pop_card())

move_cards 는 두 개의 인자, Hand 객체와 나눌 카드의 갯수,를 받아들입니다. self 와 hand 모두를 수정한 후 None을 돌려줍니다.

어떤 게임에선, 카드가 한 사람의 패에서 다른 사람의 패로 옮겨지거나, 패에서 덱으로 돌아갑니다. 이 연산들 모두에서 move_cards 를 사용할 수 있습니다: self 는 Deck 과 Hand 모두 될 수 있고, hand는, 그 이름에도 불구하고, Deck도 될 수 있습니다.

[연습 18.3.]

두 개의 매개변수, 패를 든 사람의 수와 패당 카드 수,를 받아서 새 Hand 객체들을 만들고, 패마다 적당한 수의 카드를 나눈 다음, Hand 객체의 리스트를 돌려주는 Deck 이 메쏘드 deal_hands를 작성하세요.

계승은 유용한 기능입니다. 계승 없이는 반복적이 될 어떤 프로그램들은 계승을 통해 좀 더 우아하게 작성될 수 있습니다. 계승은 코드 재사용을 촉진하는데, 부모 클래스의 코드를 수정하지 않고도 동작을 변경할 수 있기 때문입니다. 어떤 경우에는, 계승 구조가 문제의 자연스러운 구조를 반영하는데, 프로그램을 더 이해하기 쉽게 만듭니다.

반면에, 계승은 프로그램을 읽기 어렵게 만들 수 있습니다. 메쏘드가 호출될 때, 때로 정의를 어디에서 찾아야 할지 분명하지 않습니다. 관련 코드들은 여러 모듈에 흩어져있을 수 있습니다. 또한, 계승으로 가능한 많은 것들이, 계승 없이도 비슷하거나 더 훌륭하게 구현될 수 있습니다.

클래스 다이어그램

[class.diagram]

지금까지 프로그램의 상태를 보여주는 스택 다이어그램과 객체의 애트리뷰트와 그 값을 보여주는 객체 다이어그램을 봤습니다. 이 다이어그램들은 프로그램 실행중의 스냅샷을 표현하기 때문에 프로그램이 실행됨에 따라 변합니다.

그들은 또한 아주 상세합니다; 어떤 목적에서는 너무 상세합니다. 클래스 다이어그램은 프로그램의 구조에 대한 더 추상화된 표현입니다. 개별 객체를 보여주는 대신에, 클래스와 그들간의 관계를 보여줍니다.

클래스들 간에는 여러 종류의 관계가 있습니다:

  • 한 클래스의 객체들이 다른 클래스의 객체들에 대한 참조를 갖고 있을 수 있습니다. 예를 들어, 각 Rectangle 은 Point 로의 참조를 갖고 있고, 각 Deck 은 여러 개의 Card 들에 대한 참조를 갖고 있습니다. 이런 종류의 관계를 “a Rectangle has a Point” 에서처럼 HAS-A 라고 부릅니다.
  • 한 클래스가 다른 클래스를 계승할 수 있습니다. 이 관계를 “a Hand is a kind of a Deck” 에서처럼 IS-A라고 부릅니다.
  • 한 클래스의 변경이 다른 클래스의 변경을 수반한다는 점에서, 한 클래스가 다른 클래스에 종속될 수 있습니다.

클래스 다이어그램(class diagram)은 이런 관계들의 도식화된 표현입니다. 예를 들어, 그림 [fig.class1]은 Card, Deck, Hand 간의 관계를 보여줍니다.

class1

[fig.class1]

빈 삼각형이 붙은 화살표는 IS-A 관계를 나타냅니다; 이 경우에는 Hand 가 Deck을 계승함을 나타냅니다.

표준 화살표는 HAS-A 관계를 나타냅니다; 이 경우에는 Deck 이 Card 객체로 가는 참조를 갖고 있습니다.

화살표 머리근처의 별표() 는 다중도(multiplicity)입니다; Deck 이 얼마나 많은 Card 를 갖는지를 나타냅니다. 다중도는 52 같은 간단한 숫자나, 5..7 같은 범위나 Deck 이 무제한의 Card 를 가질 수 있음을 가리키는 별표가 될 수 있습니다.

더 자세한 다이어그램으로 Deck 이 실제로 Card 의 리스트를 포함한다는 것을 보여줄 수 있겠지만, 리스트나 딕셔너리와 같은 내장형들은 클래스 다이어그램에 포함시키지 않습니다.

[연습 18.4.]

TurtleWorld.py, World.py, Gui.py를 읽고 거기에서 정의된 클래스들간의 관계를 보여주는 클래스 다이어그램을 그리세요.

디버깅

객체의 메쏘드를 호출할 때, 어떤 메쏘드가 호출될지 모를 수 있기 때문에, 계승은 디버깅을 어렵게 만들 수 있습니다.

Hand 객체와 함께 쓰이는 함수를 작성하고 있다고 가정해보세요. 여러분은 PokerHands, BridgeHands 등의 모든 종류의 Hand 에서 작동하게 하고 싶을 겁니다. shuffle 과 같은 메쏘드를 호출하면, Deck 에서 정의된 것을 얻을 수 있습니다만, 하위클래스들이 이 메쏘드를 재정의하면 다른 버전을 얻게 됩니다.

언제건 프로그램의 실행 흐름에 대해 확신할 수 없는 경우, 가장 간단한 해법은 관련 메쏘드들의 시작 부분에 인쇄 문을 추가하는 것입니다. Deck.shuffle 이 Running Deck.shuffle 과 같은 메시지를 인쇄한다면, 프로그램이 실행됨에 따라 실행 흐름을 추적할 수 있습니다.

대안으로, 이 함수를 사용할 수 있는데, 객체와 (문자열로) 메쏘드 이름을 받아서 그 메쏘드의 정의를 제공하는 클래스를 돌려줍니다:

def find_defining_class(obj, meth_name):
    for ty in type(obj).mro():
        if meth_name in ty.__dict__:
            return ty

여기 예가 있습니다:

>>> hand = Hand()
>>> print find_defining_class(hand, 'shuffle')
<class 'Card.Deck'>

그래서 이 Hand 의 shuffle 메쏘드는 Deck 에 있는 것입니다.

find_defining_class 는 메쏘드를 찾기 위해 탐색되는 클래스 객체(형)들 의 목록을 얻기 위해 mro 메쏘드를 사용합니다. “MRO” 는 “메쏘드 확인 순서(method resolution order)” 의 줄임 말입니다.

여기 프로그램 설계 제안이 있습니다: 메쏘드를 재정의할 때마다, 새 메쏘드의 인터페이스는 예전 것과 같아야 합니다. 같은 매개변수를 받고, 같은 형을 돌려주며, 같은 사전조건과 사후조건을 만족해야 합니다. 이 규칙을 따른다면, Deck 과 같은 부모 클래스의 인스턴스와 사용되도록 설계된 모든 함수들이 Hand 나 PokerHand 와 같은 자식 클래스들의 인스턴스들과도 동작함을 발견하게 될 것입니다.

이 규칙을 어긴다면, 여러분의 코드는 (미안하지만) 카드로 만든 집처럼 무너질 겁니다.

데이터 캡슐화

[time] 장은 “객체 지향적 설계(object-oriented design)” 라고 부르는 개발 계획을 보여줍니다. 필요한 객체들—Time, Point, Rectangle—을 찾고 그들을 표현할 클래스들을 정의했습니다. 각각의 경우에 객체와 실 세계(또는 적어도 수학 세계)의 실재간에 분명한 관련이 있습니다.

그러나 때로 필요한 객체가 무엇이고 그들이 어떻게 상호작용하는지 분명하지 않습니다. 그런 경우에 다른 개발 계획이 필요합니다. 캡슐화와 일반화로 함수의 인터페이스를 발견한 것과 같은 방법으로, 데이터 캡슐화(data encapsulation)로 클래스 인터페이스를 발견할 수 있습니다.

[markov] 절의 마르코프 분석은 좋은 예를 제공합니다. 제 코드를 http://thinkpython.com/code/markov.py에서 다운로드 하면, 여러 함수가 읽고 쓰는 두 개의 전역변수—suffix_mapprefix—를 사용하고 있음을 보게 될 것입니다.

suffix_map = {}
prefix = ()

이 변수들이 전역이기 때문에 한번에 오직 한 개의 분석만을 실행할 수 있습니다. 만약 두 개의 글을 읽으면, 그들의 접두어와 접미어는 같은 자료 구조에 더해질 것입니다(그래서 약간의 흥미로운 글들이 만들어지기도 합니다).

여러 개의 분석을 실행하고, 그들을 따로 유지하려면, 각 분석의 상태를 객체에 캡슐화할 수 있습니다. 이런 식입니다:

class Markov(object):

    def __init__(self):
        self.suffix_map = {}
        self.prefix = ()

다음에, 함수들을 메쏘드로 바꿉니다. 예를 들어, 여기 process_word 가 있습니다:

def process_word(self, word, order=2):
    if len(self.prefix) < order:
        self.prefix += (word,)
        return

    try:
        self.suffix_map[self.prefix].append(word)
    except KeyError:
        # if there is no entry for this prefix, make one
        self.suffix_map[self.prefix] = [word]

    self.prefix = shift(self.prefix, word)

이런 식으로 프로그램을 변환하는 것—기능을 바꾸지 않고 설계를 바꾸는 것—은 리팩터링([refactoring] 절을 보세요)의 또 다른 예입니다.

이 예는 객체와 메쏘드를 설계하는 개발 계획을 제안합니다:

  1. (필요할 때) 전역 변수를 읽고 쓰는 함수를 작성하는 것으로 시작하세요.
  2. 일단 프로그램이 동작하면, 전역 변수들과 그들을 사용하는 함수들의 관계를 살피세요.
  3. 관련 변수들을 객체의 애트리뷰트로 캡슐화하세요.
  4. 관련 함수들을 새 클래스의 메쏘드로 변환하세요.

[연습 18.5.]

[markov] 절의 제 코드를 다운로드 하고 (http://thinkpython.com/code/markov.py), 앞에서 설명한 전역 변수들을 새 클래스의 애트리뷰트로 캡슐화하는 절차를 따르세요. 답: http://thinkpython.com/code/Markov.py (대문자 M에 주의하세요).

용어

코드화 encode:
한쪽에서 다른 한쪽으로의 대응을 구성함으로써 한 세트의 값들을 다른 세트의 값들로 표현하는 것.
클래스 애트리뷰트 class attribute:
클래스 객체와 결합된 애트리뷰트. 클래스 애트리뷰트는 클래스 정의 안에서 정의되지만 메쏘드의 밖에 위치한다.
인스턴스 애트리뷰트 instance attribute:
클래스의 인스턴스와 결합된 애트리뷰트.
버니어 veneer:
별다른 계산 없이 다른 함수에 다른 인터페이스를 제공하는 메쏘드나 함수.
계승 inheritance:
이미 정의된 클래스의 수정된 버전으로 새 클래스를 정의할 수 있는 능력.
부모 클래스 parent class:
자식 클래스가 계승하는 클래스.
자식 클래스 child class:
기존 클래스를 계승해서 만들어지는 새 클래스; “서브클래스(subclass)”라고도 부린다.
IS-A 관계 IS-A relationship:
자식 클래스와 부모 클래스 간의 관계.
HAS-A 관계 HAS-A relationship:
한 클래스의 인스턴스가 다른 클래스의 인스턴스로 가는 참조를 갖고 있는 관계.
클래스 다이어그램 class diagram:
프로그램에 있는 클래스들과 그들간의 관계를 보여주는 다이어그램.
다중도 multiplicity:
클래스 다이어그램의 HAS-A 관계에서 다른 클래스의 인스턴스로 가는 참조의 갯수가 얼마나 많은지 보여주는 표기법.

연습

[연습 18.6.] [poker]

다음은 포커에서 가능한 패인데, 높아지는 순서입니다 (그리고 확률이 감소하는 순서입니다):

페어 pair:
같은 숫자의 카드 두 장
투페어 two pair:
같은 숫자를 갖는 카드 페어 두 개
트리플 three of a kind:
같은 숫자를 갖는 카드 세 장
스트레이트 straight:
연속된 숫자를 갖는 다섯 장의 카드 (에이스는 높을 수도 낮을 수도 있어서, 에이스-2-3-4-5 는 스트레이트고, 10-잭-퀸-킹-에이스도 그렇지만, 퀸-킹-에이스-2-3은 아닙니다.)
플러시 flush:
같은 무늬를 갖는 카드 다섯 장
풀하우스 full house:
같은 숫자의 카드 세 장과, 또 다른 숫자로 같은 카드 두 장
포카드 four of a kind:
같은 숫자를 갖는 카드 네 장
스트레이트 플러시 straight flush:
(위에서 정의한 것처럼) 연속되고 무늬도 같은 다섯 장의 카드

이 연습의 목표는 이렇게 다양한 패들을 만들 확률을 추정하는 것입니다.

  1. http://thinkpython.com/code에서 다음 파일들을 다운로드 하세요:

    Card.py

    : 이 장에서 나온 Card, Deck, Hand 클래스들의 완전한 버전.

    PokerHand.py

    : 포커 패를 표현하는 클래스의 불완전한 구현과 그 것을 검사하는 약간의 코드.

  2. PokerHand.py를 실행하면, 일곱 개의 세븐카드 포커 패를 나누고 그 중 플러시가 있는지 보려고 검사합니다. 계속 가기 전에 이 코드를 주의 깊게 읽으세요.

  3. 관련된 기준에 맞느냐에 따라 True 나 False 를 돌려주는 has_pair, has_twopair등의 이름의 메쏘드들을 PokerHand.py에 추가하세요. 여러분의 코드는 어떤 수의 카드로 구성된 “패”에서도 동작해야 합니다 (설사 5 와 7 이 가장 흔한 크기라 할지라도).

  4. 패의 가장 높은 등급을 계산한 후 그에 맞춰 label 애트리뷰트를 설정하는 메쏘드 classify를 작성하세요. 예를 들어, 세븐카드 패는 플러시와 페어를 갖고 있을 수 있습니다; 이 경우 “flush” 로 설정되어야 합니다.

  5. 등급 계산 메쏘드가 동작한다는 확신이 들면, 다음 단계는 여러 패들의 확률을 추정하는 것입니다. 카드 덱을 섞고, 패를 나눈 다음, 패를 분류하고, 여러 등급들이 등장하는 횟수를 세는 함수를 PokerHand.py 에 작성하세요.

  6. 등급과 그들의 확률을 표로 인쇄하세요. 출력 값이 합리적인 수준의 정확도로 수렴할 때까지, 패의 개수를 늘려가며 프로그램을 실행하세요. 여러분의 결과를 http://en.wikipedia.org/wiki/Hand_rankings에 있는 값들과 비교해보세요.

답: http://thinkpython.com/code/PokerHandSoln.py.

[연습 18.7.]

이 연습은 [turtlechap] 장의 TurtleWorld를 사용합니다. 여러분은 거북이가 태그(tag) 게임을 하도록 만드는 코드를 작성할 것입니다. 태그의 규칙에 익숙하지 않다면 http://en.wikipedia.org/wiki/Tag_(game)를 보세요.

  1. http://thinkpython.com/code/Wobbler.py를 다운로드하고 실행하세요. 세 마리의 거북이가 있는 TurtleWorld 가 나와야 합니다. Run 버튼을 누르면, 거북이들이 무작위로 돌아다닙니다.

  2. 코드를 읽고 어떻게 동작하는지 확실히 이해하도록 하세요. Wobbler 클래스는 Turtle을 계승하는데, Turtle 메쏘드 lt, rt, fd, bk 가 Wobbler 에서도 동작한다는 뜻입니다.

    step 메쏘드는 TurtleWorld 에 의해 호출됩니다. 그 것은 Turtle 을 원하는 방향으로 돌리는 steer를 호출하고, Turtle 의 서투른 정도에 비례해서 무작위로 방향전환하는 wobble을 호출하고, Turtle 의 속도에 따라 앞으로 몇 픽셀 움직이게 하는 move를 호출합니다.

  3. Tagger.py라는 이름의 파일을 만드세요. Wobbler 에서 모든 것들을 들여온 다음, Wobbler를 계승하는 클래스 Tagger를 정의하세요. Tagger 클래스 객체를 인자로 전달해서 make_world 를 호출하세요.

  4. Wobbler 에 있는 것을 번복하도록 Tagger에 steer 메쏘드를 추가하세요. 시작으로, 항상 Turtle이 원점을 향하도록 하는 버전을 작성하세요. 힌트: 수학 함수 atan2 와 Turtle 애트리뷰트 x, y, heading 를 사용하세요.

  5. steer를 수정해서 Turtle이 범위 안에 머무르게 하세요. 디버깅을 위해, Step 버튼을 사용할 수 있는데, 각 Turtle마다 한번씩 step을 호출합니다.

  6. steer를 수정해서 각 Turtle이 가장 가까운 이웃을 향하도록 만드세요. 힌트: Turtle 에는 애트리뷰트 world가 있는데, 그들이 살고 있는 TurtleWorld 를 가리키는 참조입니다. TurtleWorld 에는 애트리뷰트 animals 가 있는데, 세계에 있는 모든 Turtle 들의 리스트입니다.

  7. steer를 수정해서 Turtle 이 태그 게임을 하도록 만드세요. Tagger 에 메쏘드를 추가할 수 있고, steer 와 __init__ 를 번복할 수 있습니다만, step, wobble, move 를 번복하거나 수정할 수는 없습니다. 또한, steer는 Turtle 이 향하는 곳을 바꿀 수는 있지만 위치는 바꿀 수 없습니다.

    양질의 게임이 되도록 규칙과 여러분의 steer 메쏘드를 조정하세요; 예를 들어, 느린 Turtle이 더 빠른 Turtle을 언젠가는 태그하는 것이 가능해야 합니다.

답: http://thinkpython.com/code/Tagger.py.