วิธีคิดแบบนักวิทยาการคอมพิวเตอร์/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.”