Chương 18: Thừa kế

Trở về Mục lục cuốn sách

Trong chương này ta sẽ xây dựng các lớp để biểu diễn cho quân bài tây, bộ bài, và phần bài của người chơi trong trò poker. Nếu không biết chơi poker, bạn có thể xem ở wikipedia.org/wiki/Poker, dù điều này không cần thiết vì tôi sẽ nói những điều cần để làm bài tập.

Thông tin về bộ bài tây thông dụng có ở wikipedia.org/wiki/Playing_cards.

Đối tượng lá bài (Card)

Có 52 lá bài trong một bộ, mỗi lá bài thuộc về một trong bốn chất và một trong 13 bậc. Các chất gồm Pích, Cơ, Rô, và Nhép (theo thứ tự giảm dần trong trò bridge). Các bậc gồm có A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, và K. Tùy theo trò chơi mà bạn quân A có thể cao hơn K hoặc thấp hơn 2.

Nếu bạn muốn định nghĩa một đối tượng mới để biểu diễn cho lá bài, rõ ràng các thuộc tính phải là: rank (bậc) và suit (chất). Còn việc chọn kiểu dữ liệu cho các thuộc tính lại không hiển nhiên. Một khả năng là dùng các chuỗi gồm những từ như 'Spade' (Pích) cho chất và 'Queen' cho bậc. Một vấn đề đặt ra với cách làm này là sẽ không dễ so sánh xem lá bài nào có bậc hoặc chất cao hơn.

Một cách khác là dùng số nguyên để đánh số cho các bậc và chất. Ở đây, “đánh số” có nghĩa là lập một phép ánh xạ từ số đến chất, cũng như từ số đến bậc. Kiểu đánh số này khác với mã hóa (với hàm ý bí mật).

Chẳng hạn, bảng dưới đây cho thấy các chất và mã số tương ứng:

Spades   ↦   3
Hearts   ↦   2
Diamonds   ↦   1
Clubs   ↦   0

Mã số này giúp so sánh các lá bài dễ hơn; vì chất cao hơn được ánh xạ đến số lớn hơn, và ta có thể so sánh chất bằng cách so các mã số của chúng.

Ánh xạ đối với bậc thì khá dễ thấy; mỗi bậc số thì ánh xạ đến chính số nguyên tương ứng, còn với các bậc chữ:

Jack   ↦   11
Queen   ↦  12
King   ↦   13

Ở đây tôi dùng kí hiệu  ↦  để làm rõ rằng phép ánh xạ này không phải là một phần của chương trình Python. Đó là một phần của khâu thiết kế chương trình, nhưng không xuất hiện một cách cụ thể trên mã lệnh.

Lời định nghĩa lớp cho Card sẽ như sau:

class Card(object):
    """bieu thi cho mot la bai tay."""

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

Như thường lệ, phương thức init nhận vào một tham biến tùy chọn cho mỗi thuộc tính. Lá bài mặc định là 2 Nhép.

Để tạo ra một Card, bạn gọi Card cùng với chất và bậc của lá bài mà bạn muốn.

queen_of_diamonds = Card(1, 12)

Để in ra đối tượng Card theo cách mà mọi người dễ đọc, ta cần ánh xạ từ mã số đến các bậc và chất tương ứng. Một cách làm tự nhiên là dùng danh sách các chuỗi. Ta gán những danh sách này cho các thuộc tính lớp:

# ben trong lop 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])

Các biến như suit_namesrank_names, vốn được định nghĩa bên trong một lớp nhưng bên ngoài bất kì một phương thức nào, được gọi là thuộc tính lớp và chúng gắn liền với đối tượng lớp Card.

Chúng khác với những biến như suitrank, vốn được gọi là thuộc tính cá thể vì gắn với một cá thể nhất định.

Cả hai loại thuộc tính đều được truy cập bằng kí hiệu đấu chấm. Chẳng hạn, trong __str__, self là một đối tượng Card, còn self.rank là bậc của nó. Tương tự, Card là một đối tượng lớp, còn Card.rank_names là một danh sách các chuỗi gắn với lớp này.

Mỗi lá bài đều có suitrank riêng của nó, nhưng chung quy chỉ có suit_names và một rank_names.

Tóm lại, biểu thức Card.rank_names[self.rank] có nghĩa là “dùng thuộc tính rank của đối tượng self làm chỉ số cho danh sách rank_names thuộc lớp Card, và chọn lấy chuỗi tương ứng”.

Phần tử đầu tiên của rank_namesNone vì không có lá bài nào với bậc bằng 0. Với việc thêm None vào để giữ chỗ, ta thu được một phép ánh xạ rất hay trong đó chỉ số 2 ánh xạ trực tiếp đến chuỗi '2', và cứ như vậy. Để tránh cách sửa chữa này, ta cũng có thể dùng một từ điển thay vì danh sách.

Với những phương thức được xét đến giờ, ta có thể tạo và in ra những lá bài:

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

Sau đây là một sơ đồ cho thấy đối tượng lớp Card và một cá thể Card:

lá bài (1)

Card là một đối tượng lớp, vì vậy nó có kiểu là type. card1 có kiểu là Card. (Để tiết kiệm chỗ, tôi đã không vẽ nội dung của suit_namesrank_names).

So sánh các lá bài

Với những kiểu dữ liệu có sẵn, các toán tử quan hệ (<, >, ==, v.v.) giúp so sánh và xác định xem một giá trị lớn hơn, nhỏ hơn hoặc bằng giá trị kia. Với các kiểu dữ liệu do người dùng định nghĩa, ta có thể thay thế tính năng mặc định của những toán tử có sẵn bằng cách cung cấp một phương thức có tên __cmp__.

__cmp__ nhận vào hai tham số, selfother, rồi trả về một số dương nếu như đối tượng thứ nhất lớn hơn, một số âm nếu như đối tượng thứ hai lớn hơn, và 0 nếu chúng bằng nhau.

Thứ tự đúng đắn của các quân bài thật không rõ ràng. Chẳng hạn, lá bài nào tốt hơn, 3 Nhép hay 2 Rô? Một lá thì có bậc cao hơn, nhưng lá kia lại có chất cao hơn. Để so sánh các quân bài, bạn cần phải quyết định xem giữa bậc và chất, cái nào quan trọng hơn.

Câu trả lời có thể tùy vào trò chơi đang xét, nhưng để giữ cho mọi thứ đơn giản, ta sẽ tạm quy ước là chất quan trọng hơn, như vậy tất cả các quân bài chất Pích sẽ cao hơn các quân chất Rô, và cứ như vậy.

Khi đã xác định như vậy, ta có thể viết __cmp__:

# ben trong lop Card:

    def __cmp__(self, other):
        # kiem tra chat
        if self.suit > other.suit: return 1
        if self.suit < other.suit: return -1

        # chat giong nhau... kiem tra bac
        if self.rank > other.rank: return 1
        if self.rank < other.rank: return -1

        # bac giong nhau... hai quan bang nhau
        return 0    

Bạn có thể viết gọn đoạn mã trên bằng cách so sánh các bộ:

# ben trong lop Card:

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

Hàm có sẵn cmp cũng có giao diện giống như của phương thức __cmp__: nó nhận hai giá trị và trả về một số dương nếu giá trị thứ nhất lớn hơn, một số âm nếu giá trị thứ hai lớn hơn, và 0 nếu chúng bằng nhau.

Hãy viết một phương thức __cmp__ cho đối tượng Time. Gợi ý: bạn có thể dùng so sánh bộ, nhưng cũng nên cân nhắc dùng phép trừ số nguyên.

Bộ bài

Bây giờ, khi đã có các đối tượng Card, ở bước tiếp theo ta sẽ định nghĩa Deck (bộ bài). Vì một bộ bài được tạo thành từ những quân bài, nên cách làm tự nhiên sẽ là cho mỗi Deck chứa một danh sách các quân bài làm thuộc tính.

Sau đây là đoạn mã định nghĩa Deck. Phương thức init tạo ra thuộc tính cards và phát sinh một bộ bài tiêu chuẩn gồm 52 lá:

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)

Cách dễ nhất để tạo ra bộ bài là dùng một vòng lặp lồng ghép. Vòng lặp ngoài đếm số chất từ 0 đến 3. Vòng lặp trong đếm số bậc từ 1 đến 13. Mỗi lần lặp sẽ tạo ra một Card mới theo chất và bậc hiện có, rồi bổ sung nó vào self.cards.

In bộ bài

Sau đây là phương thức __str__ của Deck:

#ben trong lop Deck:

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

Phương thức này cho thấy một cách hiệu quả để tích lũy thành một chuỗi lớn: tạo ra một danh sách các chuỗi rồi dùng join. Hàm có sẵn str gọi phương thức __str__ với mỗi lá bài và trả về chuỗi biểu diễn của nó.

Vì ta gọi join từ một kí tự xuống dòng, nên các quân bài được phân tách bởi theo dòng. Kết quả sẽ trông như sau:

>>> 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

Mặc dù kết quả hiện ra trên 52 dòng, nhưng thật ra nó là một chuỗi dài trong đó có các kí tự xuống dòng.

Thêm, bớt, trộn bài và sắp xếp

Để chia bài, ta cần có một phương thức nhằm lấy một lá từ bộ bài và trả lại nó. Phương thức pop của danh sách cho ta một cách làm tiện lợi:

#ben trong lop Deck:

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

pop lấy ra lá bài cuối cùng trong danh sách, nên ta chia từ phía dưới của bộ bài. Ngoài đời, người ta sẽ nhướn lông mày khi thấy cách chia bài như vậy1, nhưng để phục vụ mục đích lập trình thì không sao.

Để thêm một lá bài, ta có thể dùng phương thức append của danh sách:

#ben trong lop Deck:

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

Một phương thức như thế này, vốn dùng một hàm khác mà không tính toán gì đáng kể, còn được gọi là véc-ni. Từ này là hình ảnh ẩn dụ xuất phát từ nghề mộc, ở đó người ta thường dùng keo dán một lớp gỗ tốt lên trên bề mặt đồ gỗ rẻ tiền hơn.

Trong trường hợp này ta định nghĩa một phương thức “mỏng” để biểu thị một toán tử thao tác với danh sách, vốn rất phù hợp với đối tượng bộ bài.

Lấy một ví dụ khác, ta có thể viết một phương thức của Deck tên là shuffle (trộn) dùng hàm shuffle từ module random:

# ben trong lop Deck:

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

Đừng quên việc nhập random.

Hãy viết một phương thức của Deck có tên là sort trong đó dùng phương thức sort của danh sách để sắp xếp các lá bài trong Deck. sort sử dụng phương thức __cmp__ mà ta đã định nghĩa để xác định thứ tụ sắp xếp.

Thừa kế

Đặc điểm của ngôn ngữ mà thường gắn liền nhất với lập trình hướng đối tượng là thừa kế. Thừa kế là khả năng định nghĩa một lớp mới dưới dạng một phiên bản được sửa đổi từ một lớp sẵn có.

Sở dĩ gọi là “thừa kế” vì lớp mới sẽ hưởng lại toàn bộ các phương thức từ lớp sẵn có. Bằng việc mở rộng phép ẩn dụ này, lớp sẵn có được gọi là lớp cha mẹ và lớp mới được gọi là lớp con.

Lấy ví dụ, giả sử ta muốn có một lớp biểu diễn phần bài thuộc về một người chơi. Phần bài này cũng giống bộ bài ở chỗ: chúng đều là tập hợp từ những lá bài, và chúng đều cần các thao tác như thêm và bớt các lá bài.

Một phần bài khác với một bộ bài ở chỗ có những thao tác chúng ta muốn áp dụng cho phần bài nhưng lại không dùng cho bộ bài. Chẳng hạn, trong trò chơi poker ta có thể so sánh hai phần bài xem ai thắng. Trong trò chơi bridge ta muốn tính điểm cho một phần bài để đặt cược.

Mối liên hệ này giữa các lớp—giống mà khác—dẫn đến sự hình thành tính thừa kế.

Việc định nghĩa của một lớp con cũng giống như các định nghĩa lớp khác, nhưng tên của lớp cha mẹ phải được đặt trong cặp ngoặc đơn:

class Hand(Deck):
    """bieu thi phan bai cua moi nguoi choi"""

Định nghĩa này chỉ ra rằng Hand kế thừa từ Deck; nghĩa là ta có thể dùng những phương thức như pop_cardadd_card cho cả Hand lẫn Deck.

Hand cũng thừa kế __init__ từ Deck, nhưng đây sẽ không phải là điều ta muốn: thay vì phát sinh toàn bộ 52 quân bài cho một phần, phương thức init của Hands cần khởi tạo cards là một danh sách rỗng.

Nếu ta viết một phương thức init bên trong lớp Hand, nó sẽ thay thế cho init của lớp Deck:

# ben trong lop Hand:

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

Như vậy khi bạn tạo một Hand, Python sẽ gọi phương thức init này:

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

Nhưng các phương thức khác vẫn được thừa kế từ Deck, vì vậy ta có thể dùng pop_cardadd_card để chia bài:

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

Một cách tự nhiên, bước tiếp theo sẽ là gói đoạn mã trên vào một phương thức có tên move_cards:

#ben trong lop Deck:

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

move_cards nhận hai đối số, một đối tượng Hand và số lá bài cần chia. Nó làm thay đổi cả selfhand, rồi trả về None.

Trong một số trò chơi, các lá bài được chuyển từ phần này sang phần kia, hoặc từ một phần trở về bộ bài. Bạn có thể dùng move_cards để thực hiện bất cứ thao tác nào kể trên: self có thể là Deck hoặc Hand, và hand, dù tên nó là vậy, cũng có thể là Deck.

Hãy viết một phương thức của Deck có tên là deal_hands nhận vào hai tham biến là số phần bài và số lá bài trong mỗi phần, rồi tạo ra các đối tượng Hand mới, chia đủ số lá bài cho mỗi phần, và trả về một danh sách các đối tượng Hand.

Thừa kế là một tính năng có ích. Có những chương trình sẽ trở nên tự lặp lại nếu không dùng thừa kế, và sẽ đẹp hơn khi dùng nó. Thừa kế có ghể giúp sử dụng lại mã lệnh, vì bạn có thể tự sửa tính chất có ở lớp cha mẹ mà không cần phải sửa lớp đó. Trong một số trường hợp, cấu trúc thừa kế phản ánh đúng cấu trúc tự nhiên của vấn đề, từ đó làm cho chương trình trở nên dễ hiểu hơn.

Mặt khác, thừa kế có thể làm cho chương trình khó đọc hơn. Khi một phương thức được kích hoạt, đôi khi không dễ tìm được đoạn định nghĩa của nó. Phần mã lệnh có liên quan đến có thể nằm rải rác trong một số module. Hơn nữa, nhiều việc làm bằng cách thừa kế cũng có thể làm tốt bằng, chưa kể là tốt hơn, khi không thừa kế.

Sơ đồ lớp

Cho đến giờ ta đã thấy sơ đồ ngăn xếp để chỉ ra trạng thái của một chương trình, và sơ đồ đối tượng để chỉ ra các thuộc tính của một đối tượng cùng những giá trị của chúng. Các sơ đồ kể trên đều biểu diễn một khoảnh khắc trong quá trình thực thi chương trình. Vì vậy khi chương trình chạy cũng thay đổi đi.

Những sơ đồ đó cũng rất chi tiết; và ở khía cạnh nào đó, quá chi tiết. Một sơ đồ lớp, trái lại, biểu diễn một cách trừu tượng cấu trúc chương trình. Thay vì cho thấy các đối tượng riêng biệt, nó cho thấy các lớp và mối liên hệ giữa các lớp đó.

Có một số mối liên hệ giữa các lớp như sau:

  • Đối tượng trong một lớp có thể chứa những tham chiếu đến những đối tượng thuộc lớp khác. Chẳng hạn, mỗi Rectangle chứa một tham chiếu đến một Point, và mỗi Deck chứa nhiều tham chiếu đến nhiều Card. Dạng liên hệ này được gọi là HAS-A (“có một”), như câu nói, “một Rectangle có một Point”.
  • Một lớp có thể thừa kế từ lớp khác. Mối liên hệ này được gọi là IS-A (“là một”), như cách nói, “một Hand là một kiểu Deck”.
  • Một lớp có thể phụ thuộc vào lớp khác theo nghĩa sự thay đổi của lớp này sẽ yêu cầu những thay đổi ở lớp kia.

Một sơ đồ lớp là một cách biểu diễn bằng hình vẽ cho những mối liên hệ như vậy.2 Chẳng hạn, sơ đồ này cho thấy mối liên hệ giữa Card, DeckHand.

sơ đồ Lớp (1)

Mũi tên với đầu hình tam giác trắng biểu thị một liên hệ IS-A; ở đây nó cho thấy Hand thừa kế từ Deck.

Mũi tên bình thường biểu thị một liên hệ HAS-A; ở đây Deck có những tham chiếu đến các đối tượng Cards.

Dấu sao (*) gần đầu mũi tên biểu thị một phép nhân; nó cho biết có bao nhiêu Card trong mỗi Deck. Một phép nhân có thể đi kèm với con số, như 52, một khoảng số, như 5..7 hoặc chỉ một dấu sao đơn lẻ, ngụ ý rằng Deck có thể có số lá bài bất kì.

Một sơ đồ chi tiết hơn có thể chỉ ra rằng Deck thực chất bao gồm một danh sách các Card, nhưng các kiểu dữ liệu có sẵn như danh sách và từ điển thường không được ghi trong sơ đồ lớp.

Hãy đọc TurtleWorld.py, World.pyGui.py rồi vẽ một sơ đồ lớp để chỉ ra những mối liên hệ giữa các lớp được định nghĩa trong đó.

Gỡ lỗi

Thừa kế có thể làm cho việc gỡ lỗi khó khăn vì khi bạn gọi một phương thức từ một đối tượng, bạn có thể không biết rằng phương thức nào sẽ được kích hoạt.

Giả sử rằng bạn đang viết một hàm để thao tác với các đối tượng Hand. Bạn muốn nó dùng được với mọi loại Hand, như PokerHand, BridgeHand, (các phần bài trong những loại trò chơi khác nhau), v.v. Nếu gọi một phương thức như shuffle, bạn có thể sẽ được phương thức định nghĩa trong Deck, nhưng chỉ cần một lớp con có phương thức thay thế thì bạn sẽ gọi vào phương thức đó. Bất cứ lúc nào bạn không chắc chắn về luồng thực hiện chương trình, cách làm đơn giản nhất là thêm lệnh print vào đầu những phương thức có liên quan. Nếu Deck.shuffle in ra một thông báo chẳng hạn như Running Deck.shuffle, thì khi chương trình chạy nó sẽ theo dõi luôn luồng thực hiện.

Một cách làm khác là dùng hàm sau đây, vốn nhận vào một đối tượng và một tên phương thức (dạng chuỗi) và trả về một lớp trong đó có định nghĩa của phương thức:

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

Sau đây là một ví dụ:

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

Như vậy phương thức shuffle của Hand này chính là phương thức trong Deck.

find_defining_class dùng phương thức mro để thu được danh sách các đối tượng lớp (kiểu) sẽ được dùng để tìm kiếm phương thức. “MRO” là viết tắt của “method resolution order” (thứ tự phân định phương thức).

Sau đây là một gợi ý về việc thiết kế chương trình: mỗi khi lập một phương thức thay thế, giao diện của phương thức mới cần phải giống như của phương thức cũ. Nó cần phải nhận cùng các tham biến, trả về cùng kiểu, và tuân theo cùng loại điều kiện trước và điều kiện sau. Nếu tuân theo quy tắc này, bạn sẽ thấy hàm nào được lập ra cho cá thể của một lớp cha mẹ, như Deck, cũng sẽ hoạt động được với các cá thể của những lớp con như Hand hoặc PokerHand.

Nếu vi phạm quy tắc này, chương trình của bạn (rất tiếc) sẽ sụp đổ như một đống bài.

Thuật ngữ

đánh số:
Biểu thị một tập hợp các giá trị bằng một tập hợp giá trị khác bằng cách thiết lập quy tắc ánh xạ giữa chúng.

thuộc tính lớp:
Thuộc tính gắn với một đối tượng lớp. Thuộc tính lớp được định nghĩa bên trong lời định nghĩa hàm nhưng ở ngoài các phương thức.

thuộc tính cá thể:
Thuộc tính gắn với một cá thể của một lớp.

véc-ni:
Phương thức hoặc hàm cung cấp giao diện khác tới một hàm khác mà không thực hiện tính toán gì đáng kể.

thừa kế:
Khả năng định nghĩa một lớp mới là một dạng sửa đổi từ một lớp đã định nghĩa từ trước.

lớp cha mẹ:
Lớp mà từ đó các lớp con kế thừa.

lớp con:
Lớp mới được tạo ra bằng cách kế thừa từ một lớp sẵn có.

liên hệ IS-A:
Mối liên hệ giữa một lớp con và lớp cha mẹ của nó.

liên hệ HAS-A:
Mối liên hệ giữa hai lớp trong đó các cá thể của một lớp chứa tham chiếu đến các cá thể của lớp kia.

sơ đồ lớp:
Sơ đồ thể hiện các lớp trong một chương trình và mối liên hệ giữa chúng.

phép nhân:
Cách viết trong sơ đồ lớp dành cho mối liên hệ HAS-A, để chỉ ra có bao nhiêu tham chiếu đến các cá thể của lớp khác.

Bài tập

Sau đây là các khả năng xảy ra đối với một phần bài poker, xếp theo thứ tự tăng dần về giá trị (và giảm dần về xác suất):

đôi:
hai lá bài có cùng bậc

hai đôi:
hai đôi cùng bậc với nhau

ba:
ba lá bài có cùng bậc

straight:
năm lá bài với bậc tăng dần (quân Át có thể tính là cao hoặc thấp, vì vậy cả A-2-3-4-5 lẫn { 10-Jack-Queen-King-Ace} đều hợp lệ, nhưng Queen-King-Ace-2-3 thì không.)

flush:
năm lá bài có cùng chất

full house:
ba lá bài có cùng bậc, hai lá còn lại có bậc khác

tứ quý:
bốn lá bài có cùng bậc

dây:
năm lá bafi có bậc tăng dần (đã giải thích ở trên) và có cùng chất

Mục đích của bài tập này là ước tính xác suất rút được những lá bài như vậy trong một phần bài.

  1. Tải các file sau từ thinkpython.com/code:
    Card.py
    : Một bản đầy đủ của các lớp Card, DeckHand trong chương này.

    PokerHand.py
    : Một bản chưa đầy đủ gồm một lớp biểu diễn phần bài poker, mà mã lệnh để chạy thử nó.

  2. Khi bạn chạy PokerHand.py, nó sẽ chia 7 phần bài, mỗi phần có 7 lá, và kiểm tra xem liệu có phần nào chứa một flush hay không. Hãy đọc mã lệnh cẩn thận trước khi tiếp tục.
  3. Bổ sung vào PokerHand.py các phương thức có tên has_pair, has_twopair, v.v. để trả về True hoặc False tùy theo phần bài có đạt yêu cầu (có 1 đôi, 2 đôi) hay không. Mã lệnh bạn viết cần chạy được với những “phần bài” chứa số lá bài bất kì (mặc dù số lượng thông dụng nhất là 5 và 7).
  4. Viết một phương thức có tên classify để phân loại bằng cách tìm ra trường hợp có giá trị cao nhất cho một phần bài và đặt thuộc tính label tương ứng cho nó. Chẳng hạn, một phần bài 7 lá có thể chứa cả một flush và một đôi; nó cần được xếp loại “flush”.
  5. Khi bạn đã chắc rằng các phương thức phân loại viết ra đều đúng, bước tiếp theo là ước tính xác suất của các trường hợp khác nhau. Hãy viết một hàm trong PokerHand.py để trộn bộ bài, chia thành các phần, phân loại từng phần, và đêm số lần mà mỗi loại khác nhau đã xuất hiện.
  6. In ra một bảng phân loại cùng các xác suất tương ứng. Chạy chương trình với số lần chia bài tăng dần đến khi kết quả hội tụ về một giá trị có độ chính xác chấp nhận được. So sánh kết quả của bạn với những giá trị liệt kê tại wikipedia.org/wiki/Hand_rankings.

Bài tập này sử dụng TurtleWorld từ Chương {turtlechap}. Bạn sẽ viết mã để điều khiển Turtle (con rùa) chơi tag (một trò chơi đuổi bắt). Nếu bạn chưa biết luật chơi, hãy xem wikipedia.org/wiki/Tag_(game).

  1. Tải về file thinkpython.com/code/Wobbler.py và chạy nó. Bạn sẽ thấy một TurtleWorld với ba Turtle. Nếu bạn ấn nút {Run}, các Turtle sẽ chạy một cách ngẫu nhiên.
  2. Hãy đọc mã lệnh và nắm vững cách hoạt động của nó. Lớp Wobbler thừa kế từ Turtle, có nghĩa là các phương thức của Turtle gồm có lt, rt, fdbk đều có tác dụng với Wobblers.

    Phương thức step được gọi bởi TurtleWorld. Nó gọi steer, để rẽ Turtle theo hướng mong muốn, wobble, để rẽ theo hướng ngẫu nhiên tùy theo mức độ mất trật tự của Turtle và move, để đi tiến vài pixel về phía trước, tùy theo tốc độ của Turtle.

  3. Hãy tạo một file Tagger.py. Nhập tất cả mọi thứ từ Wobbler, rồi định nghĩa một lớp tên là Tagger để thừa kế từ Wobbler. Gọi make_world có chuyển đối tượng lớp Tagger làm đối số.
  4. Thêm phương thức có tên steerTagger để thay thế cho phương thức ở Wobbler. Hãy bắt đầu bằng việc viết một bản chương trình để luôn luôn hướng Turtle về gốc tọa độ. Gợi ý: dùng hàm toán học atan2 và các thuộc tính của Turtle bao gồm x, yheading.
  5. Sửa lại steer sao cho các Turtle hoạt động trong phạm vi cho phép. Để gỡ lỗi, có thể bạn sẽ cần dùng đến nút {Step}, có tác dụng gọi step mỗi lần cho từng Turtle.
  6. Sửa lại steer sao cho các Turtle hướng về phía con gần nó nhất. Gợi ý: Turtle có một thuộc tính, world, vốn là một tham chiếu đến TurtleWorld mà chúng sống trong đó; còn TurtleWorld lại có một thuộc tính, animals, là danh sách tất cả những Turtles có trong vùng.
  7. Sửa lại steer để các Turtle chơi tag. Bạn có thể thêm các phương thức vào Tagger đồng thời cũng có thể thay thế đè lên steer__init__, nhưng bạn không được sửa đổi hoặc thay đè vào step, wobble hay move. Ngoài ra, steer được phép thay đổi phương hướng của Turtle nhưng không được thay đổi vị trí.

    Hãy chỉnh sửa luật chơi và phương thức steer để trò chơi hấp dẫn hơn; chẳng hạn, có thể làm cho những Turtle bắt được Turtle sau quá trình đuổi lâu dài.

Bạn có thể tham khảo lời giải của tôi tại thinkpython.com/code/Tagger.py.


  1. Xem wikipedia.org/wiki/Bottom_dealing
  2. Sơ đồ mà tôi dùng ở đây cũng giống như UML (xem wikipedia.org/wiki/Unified_Modeling_Language), với một số lược bớt.

3 bình luận

Filed under Think Python

3 responses to “Chương 18: Thừa kế

  1. Pingback: Think Python: Cách tư duy như nhà khoa học máy tính | Blog của Chiến

  2. Pingback: Bổ sung Think Python: trò chơi bài Old Maid | Blog của Chiến

  3. Pingback: Phụ lục: Lumpy | Blog của Chiến

Bình luận về bài viết này