คุณลักษณะภาษาที่มักเกี่ยวข้องกับการเขียนโปรแกรมเชิงวัตถุคือ การสืบทอด (inheritance) การสืบทอดเป็นความสามารถในการกำหนดคลาสใหม่ที่เป็นรุ่นที่แก้ไขของคลาสที่มีอยู่เดิม ในบทนี้ผมสาธิตการสืบทอดโดยใช้คลาสที่แสดงถึงการเล่นไพ่ สำรับไพ่ และไพ่โป๊กเกอร์
ถ้าคุณไม่เล่นโปกเกอร์ สามารถอ่านได้ที่ http://en.wikipedia.org/wiki/Poker แต่คุณไม่จำเป็นต้องอ่าน ผมจะบอกคุณถึงสิ่งที่คุณต้องรู้สำหรับการฝึกหัด
ตัวอย่างโค้ดจากบทนี้หาได้จาก http://thinkpython2.com/code/Card.py
ในสำรับมีไพ่ 52 ใบ แบ่งออกเป็นสี่ชุด แต่ละชุดมี 13 อันดับ สี่ชุดนั้นได้แก่ โพธิ์ดำ โพธิ์แดง ข้าวหลามตัด และดอกจิก (เรียงจากมากไปน้อยในบริดจ์) เรียงตามลำดับดังนี้ เอซ, 2, 3, 4, 5, 6, 7, 8, 9, 10, แจ็ค, ควีน, และคิง เอซอาจสูงกว่าคิงหรือต่ำกว่า 2 ขึ้นอยู่กับเกมที่คุณกำลังเล่น
หากเราต้องการประกาศออบเจ๊คต์ใหม่เพื่อใช้แทนไพ่หนึ่งใบ เป็นที่ชัดเจนว่าแอตทริบิวต์ควรเป็นอันดับ (rank)
และ ชุด (suit)
แต่ไม่ชัดเจนว่าแอตทริบิวต์ควรจะเป็นข้อมูลชนิดใด ความเป็นไปได้อย่างหนึ่งคือการใช้สตริงที่มีคำเช่น ’โพธิ์ดำ’ (Spade)
สำหรับชุด และ ’ควีน’ (Queen)
สำหรับอันดับ ปัญหาหนึ่งของวิธีนี้คือ มันไม่ง่ายที่จะเปรียบเทียบไพ่เพื่อดูว่าใบใดมีอันดับหรือชุดที่สูงกว่า
อีกทางเลือกหนึ่งคือการใช้จำนวนเต็มเพื่อเข้ารหัสอันดับและชุด ในบริบทนี้ “เข้ารหัส” (encode) หมายความว่าเราจะกำหนดการจับคู่ระหว่างตัวเลขกับชุด หรือระหว่างตัวเลขกับอันดับ การเข้ารหัสแบบนี้ไม่ได้ต้องการให้เป็นความลับ (แบบ “encryption”)
ตัวอย่างเช่น ตารางต่อไปนี้แสดงชุดและรหัสจำนวนเต็มที่เข้าคู่กัน
Spades | $\mapsto$ | 3 |
Hearts | $\mapsto$ | 2 |
Diamonds | $\mapsto$ | 1 |
Clubs | $\mapsto$ | 0 |
รหัสนี้ช่วยให้การเปรียบเทียบการ์ดทำได้ง่าย เนื่องจากชุดที่สูงกว่าจับคู่กับตัวเลขที่สูงกว่า เราสามารถเปรียบเทียบชุดโดยเปรียบเทียบรหัสของชุด
การจับคู่สำหรับอันดับนั้นค่อนข้างชัดเจน แต่ละอันดับของตัวเลขจะจับคู่กับจำนวนเต็มที่ตรงกัน และสำหรับไพ่รูปหน้า
Jack | $\mapsto$ | 11 |
Queen | $\mapsto$ | 12 |
King | $\mapsto$ | 13 |
ผมใช้สัญลักษณ์ $\mapsto$ เพื่อให้ชัดเจนว่าการจับคู่เหล่านี้ไม่ได้เป็นส่วนหนึ่งของโปรแกรมไพธอน แต่เป็นส่วนหนึ่งของการออกแบบโปรแกรม และไม่ปรากฏอย่างชัดเจนในโค้ด
นิยามของคลาส Card มีลักษณะดังนี้
class Card: """Represents a standard playing card.""" def __init__(self, suit=0, rank=2): self.suit = suit self.rank = rank
ตามปกติเมธอด init จะใช้พารามิเตอร์ทางเลือกสำหรับแต่ละแอตทริบิวต์ มีค่าเริ่มต้นเป็นอันดับ 2 ของชุดดอกจิก
ในการสร้าง Card คุณเรียกใช้ Card
ด้วยชุดและอันดับของไพ่ที่คุณต้องการ
queen_of_diamonds = Card(1, 12)
ในการพิมพ์ออบเจ๊คต์ของ Card ในแบบที่ผู้คนสามารถอ่านได้ง่าย เราจำเป็นต้องมีการแปลงจากรหัสจำนวนเต็มไปยังอันดับและชุดที่ตรงกัน วิธีธรรมชาติในการทำเช่นนั้นคือใช้ลิสต์ของสตริง เรากำหนดรายการเหล่านี้ให้กับคลาสแอตทริบิวต์
# 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_names
และ rank_names
ซึ่งถูกกำหนดไว้ภายในคลาสแต่อยู่นอกเมธอดใดๆ จะถูกเรียกว่าคลาสแอตทริบิวต์เนื่องจากถูกผูกติดกับคลาสอ็อบเจ๊คต์ Card
คลาสแอตทริบิวต์แยกความแตกต่างจากตัวแปร เช่น suit
และ rank
ซึ่งเรียกว่า อินสแตนซ์แอตทริบิวต์ (instance attributes) เนื่องจากพวกมันถูกผูกกับอินสแตนซ์เฉพาะราย
แอตทริบิวต์ทั้งสองชนิดสามารถเข้าถึงได้โดยใช้สัญกรณ์จุด ตัวอย่างเช่น ในเมธอด __str__
self
คือออบเจ๊คต์ Card และ self.rank
คืออันดับ ในทำนองเดียวกัน Card
เป็นคลาสออบเจ๊คต์ และ Card.rank_names
คือลิสต์ที่ผูกติดอยู่กับคลาส
ไพ่ทุกใบมี ชุด
และ อันดับ
ของตัวเอง แต่มี suit_names
และ rank_names
เพียงชุดเดียวเท่านั้น
เมื่อรวมทั้งหมดเข้าด้วยกัน นิพจน์ Card.rank_names[self.rank]
หมายถึง “ใช้แอตทริบิวต์ rank
จากออบเจ๊คต์ self
เป็นดัชนีในลิสต์ rank_names
จากคลาส Card
และเลือกสตริงที่เหมาะสม”
อิลิเมนต์แรกของ rank_names
คือ None
เนื่องจากไม่มีไพ่ที่มีอันดับเป็นศูนย์ เมื่อรวม None
ไว้เป็นตัวกันที่ เราจะได้การจับคู่ที่ดีที่ดัชนี 2 จับคู่กับข้อความ '2'
และดัชนีอื่นๆ ก็เช่นกัน หากต้องการหลีกเลี่ยงการปรับแต่งนี้ เราสามารถใช้ดิกชั่นนารีแทนลิสต์ได้
ด้วยเมธอดที่เรามีก่อนหน้านี้ เราสามารถสร้างและพิมพ์ไพ่ได้
>>> card1 = Card(2, 11) >>> print(card1) Jack of Hearts
รูปที่ 18.1 เป็นแผนภาพของคลาสออบเจ๊คต์ Card
และหนึ่งอินสแตนซ์ของ Card Card
เป็นคลาสออบเจ๊คต์ จึงมีชนิดเป็น type
ส่วน card1 เป็นอินสแตนซ์ของ Card
จึงมีชนิดเป็น Card
เพื่อประหยัดพื้นที่ ผมไม่ได้วาดเนื้อหาของ suit_names
และ rank_names
สำหรับชนิดที่มีอยู่แล้วในตัว มีตัวดำเนินการเชิงสัมพันธ์ (<
, >
, ==
, ฯลฯ) ที่เปรียบเทียบค่าและกำหนดว่าค่าใดค่าหนึ่งมากกว่า น้อยกว่า หรือเท่ากับค่าอื่น สำหรับชนิดข้อมูลที่กำหนดโดยโปรแกรมเมอร์ เราสามารถแทนที่พฤติกรรมของตัวดำเนินการในตัวได้โดยการจัดเตรียมเมธอดที่ชื่อ __lt__
ซึ่งย่อมาจาก “less than”
__lt__
รับพารามิเตอร์สองตัวคือ self
และ other
และคืนค่า True
หาก self
มีค่าน้อยกว่า other
อย่างแน่นอน
ลำดับที่ถูกต้องสำหรับการ์ดไม่ชัดเจน ตัวอย่างเช่น อันไหนดีกว่าระหว่าง 3 ของดอกจิก หรือ 2 ของข้าวหลามตัด? ใบหนึ่งมีอันดับสูงกว่า แต่อีกใบมีชุดสูงกว่า เพื่อเปรียบเทียบไพ่ คุณต้องตัดสินใจว่าอันดับหรือชุดมีความสำคัญมากกว่า
คำตอบอาจขึ้นอยู่กับว่าคุณกำลังเล่นเกมอะไรอยู่ แต่เพื่อให้ง่ายขึ้น เราจะทำการเลือกตามอำเภอใจซึ่งชุดนั้นสำคัญกว่า ดังนั้นโพดำทั้งหมดจึงมีอันดับเหนือกว่าข้าวหลามตัดทั้งหมด และอื่นๆ
ด้วยการตัดสินใจนั้น เราสามารถเขียน __lt__
ดังนี้
# inside class Card: def __lt__(self, other): # check the suits if self.suit < other.suit: return True if self.suit > other.suit: return False # suits are the same... check ranks return self.rank < other.rank
คุณสามารถเขียนให้กระชับยิ่งขึ้นได้โดยใช้การเปรียบเทียบทูเพิล
# inside class Card: def __lt__(self, other): t1 = self.suit, self.rank t2 = other.suit, other.rank return t1 < t2
เพื่อเป็นการฝึก ให้เขียนเมธอด __lt__
สำหรับออบเจ๊คต์ Time คุณสามารถใช้การเปรียบเทียบทูเพิล แต่คุณอาจพิจารณาเปรียบเทียบจำนวนเต็มด้วย
ตอนนี้เรามีไพ่แล้ว ขั้นตอนต่อไปคือการประกาศ สำรับ (Deck) เนื่องจากสำรับประกอบด้วยไพ่ จึงเป็นเรื่องธรรมดาที่แต่ละสำรับจะมีรายการไพ่เป็นแอตทริบิวต์
ต่อไปนี้เป็นนิยามสำหรับคลาส Deck เมธอด init สร้างแอตทริบิวต์ cards และสร้างชุดมาตรฐานของไพ่ห้าสิบสองใบ
class Deck: 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 การวนซ้ำแต่ละครั้งจะสร้างการ์ดใหม่ด้วยชุดและอันดับปัจจุบัน และผนวกเข้ากับ self.cards
นี่คือเมธอด __str__
สำหรับ Deck
#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 ของ list
#inside class Deck: def add_card(self, card): self.cards.append(card)
วิธีการแบบนี้ที่ใช้เมธอดอื่นโดยไม่ต้องทำอะไรมาก บางครั้งเรียกว่า วีเนียร์ (veneer) คำอุปมานี้มาจากงานไม้ โดยที่แผ่นไม้อัดเป็นชั้นไม้คุณภาพดีบางๆ ที่ติดกาวบนพื้นผิวของแผ่นไม้ที่ราคาถูกกว่าเพื่อปรับปรุงรูปลักษณ์
ในกรณีนี้ add_card
เป็น เมธอด “บาง” ที่ใช้การดำเนินการของลิสต์ในแง่ที่เหมาะสมสำหรับสำรับ เพื่อปรับปรุงลักษณะที่ปรากฏหรือส่วนต่อประสานของการใช้งาน
อีกตัวอย่างหนึ่ง เราสามารถเขียนเมธอดของ Deck ชื่อ shuffle
โดยใช้ฟังก์ชัน shuffle
จากโมดูล random
:
# inside class Deck: def shuffle(self): random.shuffle(self.cards)
อย่าลืมนำเข้า random
เพื่อเป็นการฝึกหัด ให้เขียนเมธอดของ Deck ชื่อ sort
ซึ่งใช้เมธอด sort
ของลิสต์ เพื่อจัดเรียงไพ่ในสำรับ โดยให้ sort ใช้เมธอด __lt__
ที่เราได้นิยามไว้เพื่อกำหนดลำดับก่อนหลัง
การสืบทอดคือความสามารถในการสร้างคลาสใหม่ที่เป็นรุ่นที่แก้ไขของคลาสที่มีอยู่ ตัวอย่างเช่น สมมติว่าเราต้องการให้คลาสเป็นตัวแทนของ “hand” ซึ่งเป็นไพ่ที่ผู้เล่นคนหนึ่งถือไว้ในมือ hand คล้ายกันกับ Deck คือทั้งสองประกอบด้วยชุดไพ่ และทั้งคู่ต้องมีการดำเนินการเช่นการเพิ่มและนำไพ่ออก
มือ ก็แตกต่างจากสำรับ คือมีการดำเนินการที่เราต้องการสำหรับมือ ที่ไม่สมเหตุสมผลสำหรับสำรับ ตัวอย่างเช่น ในโป๊กเกอร์ เราอาจเปรียบเทียบสองมือเพื่อดูว่าใครชนะ ในบริดจ์เราอาจคำนวณคะแนนสำหรับมือเพื่อบิด
ความสัมพันธ์ระหว่างคลาสนี้คล้ายกันแต่ต่างกันนำไปสู่การสืบทอด ในการประกาศคลาสใหม่ที่สืบทอดมาจากคลาสที่มีอยู่ คุณใส่ชื่อของคลาสที่มีอยู่ในวงเล็บ:
class Hand(Deck): """Represents a hand of playing cards."""
ในการประกาศนี้บ่งชี้ว่า Hand
สืบทอดมาจาก Deck
นั่นหมายความว่าเราสามารถใช้วิธีต่างๆ เช่น pop_card
และ add_card
สำหรับ Hands เช่นเดียวกับ Decks
เมื่อคลาสใหม่สืบทอดมาจากคลาสที่มีอยู่ คลาสที่มีอยู่จะเรียกว่า พาเรนต์ (parent) และคลาสใหม่จะถูกเรียกว่า ลูก (child)
ในตัวอย่างนี้ Hand
สืบทอด __init__
จาก Deck
แต่มันไม่ได้ทำในสิ่งที่เราต้องการ แทนที่จะเติมไพ่ใหม่ 52 ใบบนมือ เมธอด init สำหรับ Hands ควรเริ่มต้น cards
ด้วยรายการที่ว่างเปล่า
ถ้าเราจัดเตรียมเมธอด init ในคลาส Hand
มันจะไปแทนที่เมธอดในคลาส Deck
# inside class Hand: def __init__(self, label=''): self.cards = [] self.label = label
เมื่อคุณสร้างอินสแตนซ์ของ Hand ไพธอนจะเรียกใช้เมธอด init นี้ ไม่ใช่เมธอดใน Deck
>>> hand = Hand('new hand') >>> hand.cards [] >>> hand.label 'new hand'
เมธอดอื่นๆ นั้นสืบทอดมาจาก Deck
ดังนั้นเราจึงสามารถใช้ pop_card
และ add_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 ก็ได้ ไม่ว่าจะเป็นชนิดใด ต่างก็สามารถใช้ในนาม Deck ได้
การสืบทอดเป็นคุณลักษณะที่มีประโยชน์ บางโปรแกรมที่ทำซ้ำๆ โดยไม่มีการสืบทอดจะสามารถเขียนได้อย่างสวยงามยิ่งขึ้นด้วยการสืบทอด การสืบทอดสามารถอำนวยความสะดวก ในการนำรหัสมาใช้ซ้ำ เนื่องจากคุณสามารถปรับแต่งพฤติกรรมของคลาสพาเรนต์โดยไม่ต้องแก้ไขพวกมัน ในบางกรณีโครงสร้างการสืบทอดจะสะท้อนถึงโครงสร้างตามธรรมชาติของปัญหา ซึ่งทำให้สามารถเข้าใจการออกแบบได้ง่ายขึ้น
ในทางกลับกัน การสืบทอดอาจทำให้โปรแกรมอ่านยาก เมื่อมีการเรียกใช้เมธอด บางครั้งก็ไม่ชัดเจนว่าจะหานิยามได้จากที่ใด รหัสที่เกี่ยวข้องอาจกระจายไปทั่วหลายโมดูล นอกจากนี้ หลายๆ อย่างที่สามารถทำได้โดยใช้การสืบทอด ก็สามารถทำได้เช่นกันหรือทำได้ดีกว่าโดยไม่ใช้การสืบทอด
จนถึงตอนนี้ เราได้เห็นแผนภาพสแต็ก ซึ่งแสดงสถานะของโปรแกรม และแผนภาพของออบเจ๊คต์ ซึ่งแสดงแอตทริบิวต์ของออบเจ๊คต์และค่าของมัน แผนภาพเหล่านี้แสดงสแนปชอตในการทำงานของโปรแกรม ดังนั้นจจึงมีการเปลี่ยนแปลงขณะโปรแกรมทำงาน
นอกจากนี้ยังมีรายละเอียดสูงเพื่อจุดประสงค์บางอย่างที่ละเอียดเกินไป แผนภาพคลาสเป็นตัวแทนของโครงสร้างของโปรแกรมที่เป็นนามธรรมมากขึ้น แทนที่จะแสดงแต่ละออบเจ๊คต์ จะแสดงคลาสและความสัมพันธ์ระหว่างออบเจ๊คต์
มีความสัมพันธ์ระหว่างคลาสหลายประเภท
แผนภาพคลาสเป็นการแสดงกราฟิกของความสัมพันธ์เหล่านี้ ตัวอย่างเช่น รูปที่ 18.2 แสดงความสัมพันธ์ระหว่าง Card
, Deck
และ Hand
ลูกศรที่มีหัวสามเหลี่ยมกลวงแสดงถึงความสัมพันธ์แบบ เป็น (IS-A) ในกรณีนี้แสดงว่ามือนั้นสืบทอดมาจากสำรับ
หัวลูกศรมาตรฐานแสดงถึงความสัมพันธ์แบบ มี (HAS-A) ในกรณีนี้เด็คมีการอ้างอิงถึงอ็อบเจ็คต์ของการ์ด
ดาว (*
) ใกล้หัวลูกศรเป็นความหลายหลาก (multiplicity) มันบ่งบอกว่าสำรับมีการ์ดกี่ใบ ความหลายหลากอาจเป็นตัวเลขธรรมดา เช่น 52
หรือเป็นช่วง เช่น 5..7
หรือเป็นดาว ซึ่งบ่งชี้ว่าสำรับสามารถมีการ์ดจำนวนเท่าใดก็ได้
ไม่มีการพึ่งพาในแผนภาพนี้ โดยปกติจะแสดงด้วยลูกศรประ หรือหากมีการพึ่งพากันมากบางครั้งก็ละเว้น
แผนภาพที่มีรายละเอียดมากขึ้นอาจแสดงว่าสำรับมีลิสต์ของไพ่ แต่ชนิดข้อมูลภายใน เช่น ลิสต์และดิกต์ มักจะไม่รวมอยู่ในแผนภาพคลาส
การสืบทอดอาจทำให้การดีบักทำได้ยาก เนื่องจากเมื่อคุณเรียกใช้เมธอดบนออบเจ๊คต์ อาจเป็นเรื่องยากที่จะทราบว่าเมธอดใดจะถูกเรียกใช้
สมมติว่าคุณกำลังเขียนฟังก์ชันที่ทำงานกับอ๊อบเจ็คต์ Hand คุณต้องการให้มันทำงานกับทุกประเภทของ Hand เช่น PokerHands, BridgeHands เป็นต้น หากคุณเรียกใช้เมธอดเช่น 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() >>> find_defining_class(hand, 'shuffle') <class 'Card.Deck'>
ดังนั้นเมธอด shuffle
สำหรับ Hand นี้เป็นเมธอดหนึ่งใน Deck
find_defining_class
ใช้เมธอด mro
เพื่อรับรายการคลาสออบเจ๊คต์ (ชนิดข้อมูล) ที่จะค้นหาเมธอด “MRO” ย่อมาจาก “ลำดับการแก้ไขเมธอด (method resolution order)” ซึ่งเป็นลำดับของคลาสที่ไพธอนค้นหาเพื่อ “ตัดสินใจ (resolve)” ชื่อเมธอด
นี่คือคำแนะนำการออกแบบ: เมื่อคุณลบล้างเมธอด อินเทอร์เฟซของเมธอดใหม่ควรเหมือนกับเมธอดเก่า ควรใช้พารามิเตอร์เดียวกัน ส่งคืนประเภทเดียวกัน และปฏิบัติตามเงื่อนไขเบื้องต้นและเงื่อนไขภายหลังเดียวกัน หากคุณทำตามกฎนี้ คุณจะพบว่าฟังก์ชันใดๆ ที่ออกแบบมาเพื่อทำงานกับอินสแตนซ์ของคลาสหลัก เช่น เด็ค จะทำงานกับอินสแตนซ์ของคลาสย่อย เช่น Hand และ PokerHand
หากคุณละเมิดกฎนี้ ซึ่งเรียกว่า “หลักการทดแทนของ Liskov” รหัสโปรแกรมของคุณจะพังเหมือน (น่าสลดใจ) บ้านไพ่
บทก่อนหน้านี้แสดงให้เห็นถึงแผนการพัฒนาที่เราอาจเรียกว่า “การออกแบบเชิงวัตถุ” เราจำแนกออบเจ๊คต์ที่เราต้องการ เช่น Point
, Rectangle
และ Time
และกำหนดคลาสเพื่อเป็นตัวแทนของพวกมัน ในแต่ละกรณี มีความสอดคล้องกันอย่างชัดเจนระหว่างวัตถุและเอนทิตีบางอย่างในโลกแห่งความเป็นจริง (หรืออย่างน้อยก็โลกทางคณิตศาสตร์)
แต่บางครั้งก็ไม่ชัดเจนว่าคุณต้องการอ๊อบเจ็คต์อะไรและควรโต้ตอบอย่างไร ในกรณีนั้นคุณต้องมีแผนการพัฒนาที่แตกต่างออกไป ในลักษณะเดียวกับที่เราค้นพบอินเทอร์เฟซของฟังก์ชันโดยการห่อหุ้มและการวางนัยทั่วไป เราสามารถค้นพบอินเทอร์เฟซของคลาสได้โดยการห่อหุ้มข้อมูล (data encapsulation)
การวิเคราะห์ Markov จากส่วนที่ 13.8 เป็นตัวอย่างที่ดี หากคุณดาวน์โหลดโค้ดของผมจาก http://thinkpython2.com/code/markov.py คุณจะเห็นว่ามันใช้ตัวแปรส่วนกลางสองตัว suffix_map
และ prefix
ที่ถูกอ่านและเขียนจากหลายฟังก์ชัน
suffix_map = {} prefix = ()
เนื่องจากตัวแปรเหล่านี้เป็นตัวแปรส่วนกลาง เราจึงสามารถเรียกใช้การวิเคราะห์ได้ครั้งละหนึ่งรายการเท่านั้น ถ้าเราอ่านสองข้อความ คำนำหน้าและส่วนต่อท้ายจะถูกเพิ่มในโครงสร้างข้อมูลเดียวกัน (ซึ่งทำให้บางข้อความที่สร้างขึ้นน่าสนใจ)
เพื่อที่จะเรียกใช้การวิเคราะห์หลายรายการและวิเคราะห์แยกกัน เราสามารถห่อหุ้มสถานะของการวิเคราะห์แต่ละรายการในออบเจ็กต์ได้ มีหน้าตาประมาณนี้
class Markov: 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)
การแปลงโปรแกรมในลักษณะนี้ คือการเปลี่ยนการออกแบบโดยไม่เปลี่ยนลักษณะการทำงาน เป็นอีกตัวอย่างหนึ่งของการปรับโครงสร้างใหม่ (ดูส่วนที่ 4.7)
ตัวอย่างนี้แนะนำแผนการพัฒนาสำหรับการออกแบบออบเจ๊คต์และเมธอด
เพื่อเป็นการฝึกหัด ให้ดาวน์โหลดโค้ด Markov ของผมจาก http://thinkpython2.com/code/markov.py และทำตามขั้นตอนที่อธิบายไว้ข้างต้นเพื่อห่อหุ้มตัวแปรส่วนกลางเป็นแอตทริบิวต์ของคลาสใหม่ที่เรียกว่า Markov
เฉลย: http://thinkpython2.com/code/Markov.py (สังเกตตัวพิมพ์ใหญ่ M)
แบบฝึกหัด 1
สำหรับโปรแกรมต่อไปนี้ ให้วาดแผนภาพคลาส UML ที่แสดงคลาสเหล่านี้และความสัมพันธ์ระหว่างคลาสเหล่านี้
class PingPongParent: pass class Ping(PingPongParent): def __init__(self, pong): self.pong = pong class Pong(PingPongParent): def __init__(self, pings=None): if pings is None: self.pings = [] else: self.pings = pings def add_ping(self, ping): self.pings.append(ping) pong = Pong() ping = Ping(pong) pong.add_ping(ping)
แบบฝึกหัด 2
เขียนเมธอดสำหรับ Deck ที่เรียกว่า deal_hands
ซึ่งใช้พารามิเตอร์สองตัว คือ จำนวนมือและจำนวนไพ่ต่อมือ มันควรสร้างจำนวนของออบเจ๊คต์มือ (Hand) ที่เหมาะสม แจกไพ่ตามจำนวนที่เหมาะสมต่อมือ และส่งคืนลิสต์ของมือ
แบบฝึกหัด 3
ต่อไปนี้เป็นมือที่เป็นไปได้ในโป๊กเกอร์ โดยเรียงตามมูลค่าที่เพิ่มขึ้นและลำดับความน่าจะเป็นที่ลดลง
เอซ-2-3-4-5
จึงเป็นไพ่สเตรท และ 10-แจ๊ค-แหม่ม-คิง-เอซ
ก็เช่นกัน แต่ แหม่ม-คิง-เอซ-2-3
ไม่ใช่.)เป้าหมายของแบบฝึกหัดเหล่านี้คือการประมาณความน่าจะเป็นของการจั่วไพ่แบบต่างๆ เหล่านี้
Card.py
เวอร์ชันที่สมบูรณ์ของคลาส Card
, Deck
และ Hand
ในบทนี้PokerHand.py
การพัฒนามือของโป๊กเกอร์ที่ยังไม่สมบูรณ์ และโค้ดบางส่วนที่ใช้ทดสอบPokerHand.py
จะแจกไพ่โป๊กเกอร์ 7 ใบ และตรวจดูว่ามีไพ่ในมือที่เป็นฟลัชหรือไม่ อ่านรหัสนี้อย่างละเอียดก่อนดำเนินการต่อhas_pair
, has_twopair
เป็นต้น ไปยัง PokerHand.py
ซึ่งคืนค่าเป็น True หรือ False โดยขึ้นอยู่กับว่ามือนั้นตรงตามเกณฑ์ที่เกี่ยวข้องหรือไม่ โค้ดของคุณควรทำงานอย่างถูกต้องสำหรับ “มือ” ที่มีการ์ดจำนวนเท่าใดก็ได้ (แม้ว่า 5 และ 7 จะเป็นขนาดทั่วไป)classify
ซึ่งคำนวณการจำแนกประเภทที่มีมูลค่าสูงสุดสำหรับมือและตั้งค่าแอตทริบิวต์ label
ตามนั้น ตัวอย่างเช่น ไพ่ 7 ใบอาจมีฟลัชและคู่ ควรมีข้อความว่า “flush”PokerHand.py
ที่สับไพ่สำรับ แบ่งออกเป็นมือ จำแนกมือ และนับจำนวนครั้งที่การจำแนกประเภทต่างๆ ปรากฏขึ้นเฉลย: http://thinkpython2.com/code/PokerHandSoln.py
https://greenteapress.com/thinkpython2/html/thinkpython2019.html