วิธีคิดแบบนักวิทยาการคอมพิวเตอร์/Inheritance
บทที่ 16 Inheritance
แก้ไข16.1 Inheritance
แก้ไขลักษณะของปัญหาส่วนใหญ่บ่อยครั้งถูกเชื่อมโยงกับ object-oriented programming คือ inheritance ความสามารถในการกำหนด class ใหม่ที่ซึ่งถูกแก้ไขเวอร์ชันของ class ที่มีอยู่
ข้อได้เปรียบที่สำคัญของลักษณะนี้นี้คุณสามารถเพิ่ม method ใหม่ไปใน class โดยปราศจากการแก้ไข class เดิมที่มีอยู่ ซึ่งมันเรียกว่า inheritance เนื่องจาก class ใหม่สืบทอด methods ทั้งหมดมาจาก class ที่มีอยู่ การขยายคำเปรียบเทียบนี้ class ที่มีอยู่บางครั้งถูกเรียกว่า parent class และ class ใหม่อาจถูกเรียกว่า child class หรือบางครั้งเรียกว่า sub class
Inheritance เป็นลักษณะเฉพาะที่มีประสิทธิภาพ บางโปรแกรมมีความซับซ้อนมากโดยไม่มี inheritance เราสามารถเขียนให้สั้น รัดกุม และง่ายด้วยมัน (inheritance) เช่นเดียวกัน inheritance สามารถทำให้สะดวกขึ้นในการนำโค้ดไปใช้ใหม่ เนื่องจากคุณสามารถปรับแต่พฤติกรรมการกระทำของ parent classes โดยไม่มีการแก้ไข classes เหล่านั้น ในบางกรณี โครงสร้างของ inheritance สะท้อนให้เห็นปัญหาของโครงสร้างปกติ ซึ่งทำให้โปรแกรมง่ายต่อการเข้าใจ
ในทางตรงกันข้าม inheritance สามารถทำให้โปรแกรมยากต่อการอ่าน เมื่อ method ถูกเรียก บางครั้งไม่ชัดเจนที่ซึ่งจะหาคำจำกัดความของมัน โค้ดที่เกี่ยวเนื่องกันอาจกระจัดกระจายในระหว่างหลายๆ modules เช่นเดียวกัน หลายสิ่งเหล่านี้สามารถจัดการด้วยการใช้ inheritance สามารถจัดการได้เป็นอย่างดี ถ้าโครงสร้างปกติของปัญหาไม่ให้ยืมตัวมันเองเพื่อ inheritance รูปแบบของการเขียนโปรแกรมนี้จะทำให้เสียหายมากว่าทำให้ดี
ในบทนี้เราจะพิสูจน์การใช้ของ inheritance เป็นส่วนของโปรแกรมซึ่งเล่น card game Old Maid หนึ่งในเป้าหมายของเราคือ เขียนโค้ดทีสามารถนำไปใช้ใหม่กับ card game อื่นๆ
16.2 A hand of cards
แก้ไขเกือบจะทุกๆ card game เราต้องการแสดงให้เห็น hand of cards แน่นอน hand คล้ายกับ deck ทั้งสองอย่างสร้างขึ้นจากชุดของไพ่ และทั้งสองต้องการการกระทำ เช่น การเพิ่มและการนำไพ่ออก เช่นเดียวกัน เราต้องการความสามารถในการสับไพ่ของทั้ง deck และ hand
Hand แตกต่างจาก deck ด้วยเช่นกันขึ้นอยู่กับเกมที่จะเล่น เราต้องการแสดงการกระทำบางอย่างบน hand ที่ทำให้ไม่รู้สึกเป็น deck ตัวอย่างใน poker เราอาจจัดกลุ่ม hand (straight, flush, etc.)
หรือเปรียบเทียบมันกับอีก hand หนึ่ง ใน bridge เราอาจต้องการคำนวณแต้มจาก hand เพื่อทำการประมูล
สถานการณ์นี้แนะนำให้ใช้ inheritance ถ้า hand เป็น subclass ของ deck มันจะมี methods ทั้งหมดของ deck และสามารถเพิ่ม method ใหม่เข้าไปได้
ใน class definition ชื่อของ parent class ปรากฏอยู่ในวงเล็บ
class Hand(Deck):
pass
statement นี้บ่งบอกว่า class hand สืบทอดจาก class deck ที่มีอยู่
hand สร้าง attributes เริ่มต้นสำหรับ hand ซึ่งคือ name และ card โดย string name ระบุถึง hand ซึ่งเป็นไปได้มากด้วยชื่อของผู้เล่นที่ถือมันไว้ name เป็น optional parameter ด้วย string ที่ว่างเปล่าเป็นค่าเริ่มต้น cards เป็น list ของไพ่ใน hand เริ่มต้นด้วย list ที่ว่างเปล่า
class Hand(Deck):
def __init__(self, name=""):
self.cards = []
self.name = name
ทุกๆ card game มันจำเป็นที่จะต้องเพิ่มและเอาไพ่ออกจาก deck การเอาไพ่ออกได้ถูกเอาใจใส่เรียบร้อยแล้ว ตั้งแต่ hand สืบทอด removeCard จาก deck แต่เราต้องเขียน addCard
class Hand(Deck):
...
def addCard(self,card) :
self.cards.append(card)
อีกครั้ง การละไว้บ่งบอกว่าเราต้องละเลย method อื่นๆ list เชื่อมต่อ method เพิ่มไพ่ใหม่ไปยังท้ายสุดของ list ของไพ่
16.3 Dealing cards
แก้ไขตอนนี้เรามี class hand เราต้องการแจกไพ่จาก deck เข้าไปยัง hands มันจะไม่ชัดเจนในทันทีไม่ว่า method นี้จะเข้าไปอยู่ใน class hand หรือไม่ ใน class deck ตั้งแต่มันทำงานบน deck เดียวและหลายๆ hands มันเป็นธรรมดาที่วางมันลงใน deck
การแจกไพ่ควรยุติธรรม ต่างเกมต่างมีความต้องการที่แตกต่างกัน เราอาจต้องการแจกไพ่ออกจาก deck ทั้งหมดในครั้งเดียวหรือเพิ่มไพ่ในแต่ละ hand
Deal มีพารามิเตอร์ 2 ตัว เป็น list ของ hands และจำนวนไพ่ทั้งหมดที่แจก หากใน deck มีไพ่ไม่เพียงพอ method deal จะทำการหยุด
class Deck :
...
def deal(self, hands, nCards=999):
nHands = len(hands)
for i in range(nCards):
if self.isEmpty(): break # break if out of cards
card = self.popCard() # take the top card
hand = hands[i % nHands] # whose turn is next?
hand.addCard(card) # add the card to the hand
พารามิเตอร์ตัวที่สอง nCards เป็น optional ค่าเริ่มต้นเป็น large number ที่มีประสิทธิภาพ หมายถึงไพ่ทั้งหมดใน deck จะถูกแจกจนหมด
ตัวแปร loop i เริ่มจาก 0 ถึง nCards -1 แต่ละครั้งที่ผ่านเข้าไปใน loop ไพ่จะถูกเอาออกจาก deck โดยใช้ list method pop ซึ่งจะเอาออกและคืนรายการสุดท้ายใน list
Modulus operator (%) ยอมให้เราแจกไพ่ 1 ใบต่อครั้งของแต่ละ hand เมื่อ i เท่ากับจำนวน hands ใน list expression i% nHands กลับไปยังจุดเริ่มต้นของ list (index 0)
16.4 Printing a Hand
แก้ไขการ print หัวข้อของ hand เราสามารถได้เปรียบของ printDeck และ __str__ method สืบทอดจาก deck ตัวอย่างเช่น
class Deck :
>>> deck = Deck()
>>> deck.shuffle()
>>> hand = Hand("frank")
>>> deck.deal([hand], 5)
>>> print hand
Hand frank contains
2 of Spades
3 of Spades
4 of Spades
Ace of Hearts
9 of Clubs
มันไม่ใช่ hand ที่ใหญ่ แต่มันเป็นการทำ straight flush
ถึงแม้มันสะดวกที่จะสืบทอด existing methods มีข้อมูลที่มากขึ้นใน Hand object เราอาจต้องการรวมเมื่อเรา print ออกมา ทำโดยคุณสามารถให้ __str__ method ใน Hand class ผ่านข้ามหนึ่งใน Deck class
class Hand(Deck)
...
def __str__(self):
s = "Hand " + self.name
if self.isEmpty():
return s + " is empty\n"
else:
return s + " contains\n" + Deck.__str__(self)
ในขั้นต้น s คือ string ที่แสดงถึง hand ถ้า hand ว่างเปล่า โปรแกรมจะให้คำว่า empty และ return ค่าผลลัพธ์ออกมา
ไม่เช่นนั้น โปรแกรมจะผนวกการบรรจุคำ และ string เป็นตัวแทนของ deck คำนวณโดยการสร้าง __str__ method ใน Deck class บน self
มันอาจดูแปลกที่ส่ง self ซึ่งอ้างถึง Hand ปัจจุบัน จนกระทั่งคุณจำได้ว่า Hand คือ ชนิดของ Deck ดังนั้น Hand objects จึงสามารถทำทุกสิ่งที่ Deck objects สามารถทำได้ ดังนั้น มันถูกต้องที่จะส่ง Hand ไปยัง Deck method
โดยทั่วไป มันจะถูกต้องเสมอที่ใช้ instance ของ subclass ในตำแหน่งของ instance ของ parent class
16.5 The CardGame class
แก้ไขCardGame class ดูแลงานพื้นฐานทั่วไปของเกมทั้งหมด เช่น การสร้าง deck และ การสับไพ่
class CardGame:
def __init__(self):
self.deck = Deck()
self.deck.shuffle()
นี้คือ case แรกที่เราได้เห็น initialization method กระทำการคำนวณที่สำคัญหลัง initializing attributes
การ implement เกมโดยเฉพาะ เราสามารถสืบทอดจาก CardGame และเพิ่มลักษณะต่างๆ เข้าไปสำหรับเกมใหม่ ตัวอย่างเช่น พวกเราจะเขียนเลียนแบบของ Old Maid
Object ของ Old Maid คือ กำจัดไพ่ในมือให้หมดไป คุณทำโดยการจับคู่ rank และ สี ของไพ่ ตัวอย่างเช่น 4 โพธิ์ดำจับคู่กับ 4 ดอกจิก เป็นสีดำทั้งคู่ แจ๊คหัวใจจับคู่กับแจ๊คข้าวหลามตัดซึ่งเป็นสีแดงทั้งคู่
เมื่อเริ่มเกม แหม่มโพธิ์ดำจะถูกเอาออกไปจาก deck ดังนั้น แหม่มดอกจิกไม่สามารถจับคู่ได้ ไพ่ที่ยังคงเหลือ 51 ใบ ถูกแจกให้กับผู้เล่น หลังจากแจกเสร็จ ผู้เล่นทั้งหมดจับคู่และทิ้งไพ่ที่เป็นไปได้มาก
เมื่อไม่สามารถจับคู่ได้มากกว่านี้ การเริ่มเล่น ในรอบ แต่ละผู้เล่นหยิบไพ่มา 1 ใบ (ห้ามดู) จากเพื่อนที่ใกล้ที่สุดไปทางซ้ายผู้ซึ่งยังคงมีไพ่ ถ้าเลือกเจอไพ่ที่ตรงกับในมือของผู้เล่น ในที่สุดการจับคู่ที่เป็นไปได้จะเกิดขึ้น จะเหลือเพียงแหม่มดอกจิกในมือของผู้แพ้
ในเกมจำลองคอมพิวเตอร์ของเรา คอมพิวเตอร์เป็นผู้เล่นทั้งหมด โชคไม่ดีที่ความแตกต่างน้อยมากที่จะแพ้ในเกมจริงๆ ในเกมจริงๆ ผู้เล่นมี Old Maid ต้องพยายามให้ผู้เล่นใกล้เคียงหยิบไพ่นั้นโดยแสดงมันให้มีความเด่นชัดน้อยกว่า หรือ บางทีพลาดที่จะแสดงมันออกมา หรือแม้พลาดและพลาดอีกที่จะแสดงไพ่ออกมา คอมพิวเตอร์จะหยิบอย่างง่ายๆ จากคนข้างๆ โดยการสุ่ม
16.6 OlaMaidHand class
แก้ไขผู้เล่น Ola Maid มีความต้องการความสามารถบ้าง ที่เหนือกว่าความสามารถทั่วไปของ Hand เราจะระบุ class ใหม่ OldMaidHand ซึ่งสืบทอดจาก Hand และให้ method ที่เพิ่มขึ้นนี้เรียกว่า removeMatches
class OldMaidHand(Hand):
def removeMatches(self):
count = 0
originalCards = self.cards[:]
for card in originalCards:
match = Card(3 - card.suit, card.rank)
if match in self.cards:
self.cards.remove(card)
self.cards.remove(match)
print "Hand %s: %s matches %s" % (self.name,card,match)
count = count + 1
return count
เราเริ่มโดยการคัดลอก list ของไพ่ ดังนั้นเราสามารถตรวจการคัดลอกขณะที่กำลังนำไพ่ออกจากต้นฉบับ ตั้งแต่ self.cards ถูกแก้ไขใน loop เราไม่ต้องการใช้มันในการควบคุมสิ่งที่มาขวาง python ค่อนข้างจะสับสน ถ้ามันตรวจสอบ list ที่มีการเปลี่ยนแปลง
สำหรับไพ่แต่ละใบในมือ เรามองว่าควรจะจับคู่กับอะไรและไปหามัน การจับคู่ไพ่มี rank ที่เหมือนกัน และ suit ที่สีเหมือนกัน expression 3 – card.suit เปลี่ยนโพธิ์ดำเป็นดอกจิก และ ข้าวหลามตัดเป็นหัวใจ คุณควรพอใจในการทำงานของ opposite operation ถ้าการจับคู่ไพ่อยู่ในมือ ไพ่ทั้งสองก็จะถูกทิ้งไป
ตามตัวอย่างซึ่งพิสูจน์การใช้ removeMatches :
>>> game = CardGame()
>>> hand = OldMaidHand("frank")
>>> game.deck.deal([hand], 13)
>>> print hand
Hand frank contains
Ace of Spades
2 of Diamonds
7 of Spades
8 of Clubs
6 of Hearts
8 of Spades
7 of Clubs
Queen of Clubs
7 of Diamonds
5 of Clubs
Jack of Diamonds
10 of Diamonds
10 of Hearts
>>> hand.removeMatches()
Hand frank: 7 of Spades matches 7 of Clubs
Hand frank: 8 of Spades matches 8 of Clubs
Hand frank: 10 of Diamonds matches 10 of Hearts
>>> print hand
Hand frank contains
Ace of Spades
2 of Diamonds
6 of Hearts
Queen of Clubs
7 of Diamonds
5 of Clubs
Jack of Diamonds
สังเกตว่าไม่มี __init__ method สำหรับ OldMaidGame class เราสืบทอดมันมาจาก Hand
16.7 OldMaidGame class
แก้ไขตอนนี้เราสามารถเปลี่ยนความตั้งใจของเราเพื่อเกมด้วยตัวมันเอง OldMaidGame คือ subclass ของ CardGame มี method ใหม่ เรียกว่า play ซึ่งรับค่า list ของผู้เล่นเป็นพารามิเตอร์
ตั้งแต่ __init__ ถูกสืบทอดจาก CardGame โดย OldMaidGame ใหม่จะบรรจุการแจกไพ่สำรับใหม่
class OldMaidGame(CardGame):
def play(self, names):
# remove Queen of Clubs
self.deck.removeCard(Card(0,12))
# make a hand for each player
self.hands = []
for name in names :
self.hands.append(OldMaidHand(name))
# deal the cards
self.deck.deal(self.hands)
print "---------- Cards have been dealt"
self.printHands()
# remove initial matches
matches = self.removeAllMatches()
print "---------- Matches discarded, play begins"
self.printHands()
# play until all 50 cards are matched
turn = 0
numHands = len(self.hands)
while matches < 25:
matches = matches + self.playOneTurn(turn)
turn = (turn + 1) % numHands
print "---------- Game is Over"
self.printHands()
บางขั้นตอนของเกมได้ถูกแยกไปใน methods ซึ่ง removeAllMatches ข้าม list ของ hands และสร้าง removeMatches ขึ้น
class OldMaidGame(CardGame):
...
def removeAllMatches(self):
count = 0
for hand in self.hands:
count = count + hand.removeMatches()
return count
count คือ ตัวเก็บสะสมหน่วยความจำตัวเลขที่จับคู่กันที่เพิ่มเข้าไปในแต่ละมือ และ return ค่าผลรวมออกมา
เมื่อจำนวนตัวเลขที่จับคู่กันถึง 25 ไพ่ทั้ง 50 ใบ ได้ถูกทิ้งไปจากมือแล้ว ซึ่งหมายความว่ามีไพ่หนึ่งใบที่เหลือและเกมจะจบลง
ตัวแปร turn เก็บค่า track การเปลี่ยนแปลงของผู้เล่น มันเริ่มที่ 0 และเพิ่มขึ้นทีละ1 ในแต่ละครั้ง เมื่อมันถึง numHands ใน modulus มันจะกลับไปยังที่ 0
Method playOne รับค่าพารามิเตอร์ที่ระบุถึง turn ที่มันเป็น
Return value คือจำนวนการจับคู่ที่ทำระหว่าง turn นี้
class OldMaidGame(CardGame):
...
def playOneTurn(self, i):
if self.hands[i].isEmpty():
return 0
neighbor = self.findNeighbor(i)
pickedCard = self.hands[neighbor].popCard()
self.hands[i].addCard(pickedCard)
print "Hand", self.hands[i].name, "picked", pickedCard
count = self.hands[i].removeMatches()
self.hands[i].shuffle()
return count
ถ้ามือของผู้เล่นว่าง ผู้เล่นจะถูกออกจากเกม ดังนั้นเขาหรือหล่อนไม่ได้ทำอะไร และ return ค่า 0
อีกอย่างหนึ่ง หนึ่ง turn ประกอบด้วยการหาผู้เล่นคนแรกทางซ้ายซึ่งมีไพ่ การรับไพ่มาจากคนข้างๆ และตรวจดูว่าเข้าคู่หรือไม่ ก่อนจะเวียนไพ่ในมือที่ถูกสับไพ่แล้ว ดังนั้นการเลือกของผู้เล่นคนถัดไปคือการสุ่ม
Method findNeighbor เริ่มมีผู้เล่นทันทีทางซ้ายและต่อเนื่องเป็นวงกลมจนกระทั้งหาผู้เล่นที่ยังคงมีไพ่
class OldMaidGame(CardGame):
...
def findNeighbor(self, i):
numHands = len(self.hands)
for next in range(1,numHands):
neighbor = (i + next) % numHands
if not self.hands[neighbor].isEmpty():
return neighbor
ถ้า findNeighbor เวียนเป็นวงกลมเรื่อยๆ โดยปราศจากการหาไพ่ มันจะ return ค่า None และเป็นเหตุให้เกิด error ขึ้นในโปรแกรม โชคดีที่เราสามารถตรวจสอบว่าสิ่งนั้นจะไม่เกิดขึ้น
เราได้ละเลย printHands ไป คุณสามารถเขียน method นี้ด้วยตัวคุณเอง
ตามผลลัพธ์ที่แสดงออกมาจากรูปแบบที่สั้นลงของเกมซึ่งมีเพียง 15 ใบด้านบนสุด ซึ่งแจกให้ผู้เล่น 3 คน มีสำรับเล็ก โดยหยุดเล่นหลังจากจับคู่ได้ 7 คู่ แทนที่จะเป็น 25 คู่
>>> import cards
>>> game = cards.OldMaidGame()
>>> game.play(["Allen","Jeff","Chris"])
---------- Cards have been dealt
Hand Allen contains
King of Hearts
Jack of Clubs
Queen of Spades
King of Spades
10 of Diamonds
Hand Jeff contains
Queen of Hearts
Jack of Spades
Jack of Hearts
King of Diamonds
Queen of Diamonds
Hand Chris contains
Jack of Diamonds
King of Clubs
10 of Spades
10 of Hearts
10 of Clubs
Hand Jeff: Queen of Hearts matches Queen of Diamonds
Hand Chris: 10 of Spades matches 10 of Clubs
---------- Matches discarded, play begins
Hand Allen contains
King of Hearts
Jack of Clubs
Queen of Spades
King of Spades
10 of Diamonds
Hand Jeff contains
Jack of Spades
Jack of Hearts
King of Diamonds
Hand Chris contains
Jack of Diamonds
King of Clubs
10 of Hearts
Hand Allen picked King of Diamonds
Hand Allen: King of Hearts matches King of Diamonds
Hand Jeff picked 10 of Hearts
Hand Chris picked Jack of Clubs
Hand Allen picked Jack of Hearts
Hand Jeff picked Jack of Diamonds
Hand Chris picked Queen of Spades
Hand Allen picked Jack of Diamonds
Hand Allen: Jack of Hearts matches Jack of Diamonds
Hand Jeff picked King of Clubs
Hand Chris picked King of Spades
Hand Allen picked 10 of Hearts
Hand Allen: 10 of Diamonds matches 10 of Hearts
Hand Jeff picked Queen of Spades
Hand Chris picked Jack of Spades
Hand Chris: Jack of Clubs matches Jack of Spades
Hand Jeff picked King of Spades
Hand Jeff: King of Clubs matches King of Spades
---------- Game is Over
Hand Allen is empty
Hand Jeff contains
Queen of Spades
Hand Chris is empty
So Jeff loses.
16.8 Glossary
แก้ไขinheritance: The ability to define a new class that is a modified version of a previously defined class.
parent class: The class from which a child class inherits.
child class: A new class created by inheriting from an existing class; also called a “subclass.”