User Tools

Site Tools


python:inheritance

18. การสืบทอด

คุณลักษณะภาษาที่มักเกี่ยวข้องกับการเขียนโปรแกรมเชิงวัตถุคือ การสืบทอด (inheritance) การสืบทอดเป็นความสามารถในการกำหนดคลาสใหม่ที่เป็นรุ่นที่แก้ไขของคลาสที่มีอยู่เดิม ในบทนี้ผมสาธิตการสืบทอดโดยใช้คลาสที่แสดงถึงการเล่นไพ่ สำรับไพ่ และไพ่โป๊กเกอร์

ถ้าคุณไม่เล่นโปกเกอร์ สามารถอ่านได้ที่ http://en.wikipedia.org/wiki/Poker แต่คุณไม่จำเป็นต้องอ่าน ผมจะบอกคุณถึงสิ่งที่คุณต้องรู้สำหรับการฝึกหัด

ตัวอย่างโค้ดจากบทนี้หาได้จาก http://thinkpython2.com/code/Card.py

18.1 ออบเจ๊คต์ไพ่

ในสำรับมีไพ่ 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)

18.2 แอตทริบิวต์ของคลาส

ในการพิมพ์ออบเจ๊คต์ของ 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 แผนภาพออบเจ๊คต์

รูปที่ 18.1 เป็นแผนภาพของคลาสออบเจ๊คต์ Card และหนึ่งอินสแตนซ์ของ Card Card เป็นคลาสออบเจ๊คต์ จึงมีชนิดเป็น type ส่วน card1 เป็นอินสแตนซ์ของ Card จึงมีชนิดเป็น Card เพื่อประหยัดพื้นที่ ผมไม่ได้วาดเนื้อหาของ suit_names และ rank_names

18.3 การเปรียบเทียบไพ่

สำหรับชนิดที่มีอยู่แล้วในตัว มีตัวดำเนินการเชิงสัมพันธ์ (<, >, ==, ฯลฯ) ที่เปรียบเทียบค่าและกำหนดว่าค่าใดค่าหนึ่งมากกว่า น้อยกว่า หรือเท่ากับค่าอื่น สำหรับชนิดข้อมูลที่กำหนดโดยโปรแกรมเมอร์ เราสามารถแทนที่พฤติกรรมของตัวดำเนินการในตัวได้โดยการจัดเตรียมเมธอดที่ชื่อ __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 คุณสามารถใช้การเปรียบเทียบทูเพิล แต่คุณอาจพิจารณาเปรียบเทียบจำนวนเต็มด้วย

18.4 สำรับ

ตอนนี้เรามีไพ่แล้ว ขั้นตอนต่อไปคือการประกาศ สำรับ (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

18.5 การพิมพ์สำรับ

นี่คือเมธอด __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 บรรทัด แต่เป็นเพียงหนึ่งสตริงยาวที่มีการขึ้นบรรทัดใหม่แทรกอยู่

18.6 เพิ่ม ลบ สับเปลี่ยน และจัดเรียง

ในการแจกไพ่ เราต้องการวิธีที่จะนำไพ่ออกจากสำรับและส่งคืนค่านั้น เมธอด 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__ ที่เราได้นิยามไว้เพื่อกำหนดลำดับก่อนหลัง

18.7 การสืบทอด

การสืบทอดคือความสามารถในการสร้างคลาสใหม่ที่เป็นรุ่นที่แก้ไขของคลาสที่มีอยู่ ตัวอย่างเช่น สมมติว่าเราต้องการให้คลาสเป็นตัวแทนของ “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.8 แผนภาพคลาส

จนถึงตอนนี้ เราได้เห็นแผนภาพสแต็ก ซึ่งแสดงสถานะของโปรแกรม และแผนภาพของออบเจ๊คต์ ซึ่งแสดงแอตทริบิวต์ของออบเจ๊คต์และค่าของมัน แผนภาพเหล่านี้แสดงสแนปชอตในการทำงานของโปรแกรม ดังนั้นจจึงมีการเปลี่ยนแปลงขณะโปรแกรมทำงาน

นอกจากนี้ยังมีรายละเอียดสูงเพื่อจุดประสงค์บางอย่างที่ละเอียดเกินไป แผนภาพคลาสเป็นตัวแทนของโครงสร้างของโปรแกรมที่เป็นนามธรรมมากขึ้น แทนที่จะแสดงแต่ละออบเจ๊คต์ จะแสดงคลาสและความสัมพันธ์ระหว่างออบเจ๊คต์

มีความสัมพันธ์ระหว่างคลาสหลายประเภท

  • ออบเจ๊คต์ในคลาสหนึ่งอาจมีการอ้างอิงถึงออบเจ๊คต์ในอีกคลาสหนึ่ง ตัวอย่างเช่น สี่เหลี่ยมผืนผ้าแต่ละอันมีการอ้างอิงไปยังจุด และแต่ละสำรับมีการอ้างอิงถึงไพ่หลายใบ ความสัมพันธ์แบบนี้เรียกว่า มี (HAS-A) เช่น “สี่เหลี่ยมผืนผ้ามีจุด”
  • คลาสหนึ่งอาจสืบทอดมาจากอีกคลาสหนึ่ง ความสัมพันธ์นี้เรียกว่า เป็น (IS-A) เช่น “มือเป็นสำรับชนิดหนึ่ง”
  • คลาสหนึ่งอาจขึ้นอยู่กับอีกคลาสหนึ่งในแง่ที่ว่าอ็อบเจ๊คต์ในคลาสหนึ่งใช้อ็อบเจ๊คต์ในคลาสที่สองเป็นพารามิเตอร์ หรือใช้อ็อบเจ๊คต์ในคลาสที่สองเป็นส่วนหนึ่งของการคำนวณ ความสัมพันธ์แบบนี้เรียกว่า การขึ้นต่อกัน (dependency)

แผนภาพคลาสเป็นการแสดงกราฟิกของความสัมพันธ์เหล่านี้ ตัวอย่างเช่น รูปที่ 18.2 แสดงความสัมพันธ์ระหว่าง Card, Deck และ Hand

 แผนภาพคลาส
รูปที่ 18.2 แผนภาพคลาส

ลูกศรที่มีหัวสามเหลี่ยมกลวงแสดงถึงความสัมพันธ์แบบ เป็น (IS-A) ในกรณีนี้แสดงว่ามือนั้นสืบทอดมาจากสำรับ

หัวลูกศรมาตรฐานแสดงถึงความสัมพันธ์แบบ มี (HAS-A) ในกรณีนี้เด็คมีการอ้างอิงถึงอ็อบเจ็คต์ของการ์ด

ดาว (*) ใกล้หัวลูกศรเป็นความหลายหลาก (multiplicity) มันบ่งบอกว่าสำรับมีการ์ดกี่ใบ ความหลายหลากอาจเป็นตัวเลขธรรมดา เช่น 52 หรือเป็นช่วง เช่น 5..7 หรือเป็นดาว ซึ่งบ่งชี้ว่าสำรับสามารถมีการ์ดจำนวนเท่าใดก็ได้

ไม่มีการพึ่งพาในแผนภาพนี้ โดยปกติจะแสดงด้วยลูกศรประ หรือหากมีการพึ่งพากันมากบางครั้งก็ละเว้น

แผนภาพที่มีรายละเอียดมากขึ้นอาจแสดงว่าสำรับมีลิสต์ของไพ่ แต่ชนิดข้อมูลภายใน เช่น ลิสต์และดิกต์ มักจะไม่รวมอยู่ในแผนภาพคลาส

18.9 การดีบัก

การสืบทอดอาจทำให้การดีบักทำได้ยาก เนื่องจากเมื่อคุณเรียกใช้เมธอดบนออบเจ๊คต์ อาจเป็นเรื่องยากที่จะทราบว่าเมธอดใดจะถูกเรียกใช้

สมมติว่าคุณกำลังเขียนฟังก์ชันที่ทำงานกับอ๊อบเจ็คต์ 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” รหัสโปรแกรมของคุณจะพังเหมือน (น่าสลดใจ) บ้านไพ่

18.10 การห่อหุ้มข้อมูล

บทก่อนหน้านี้แสดงให้เห็นถึงแผนการพัฒนาที่เราอาจเรียกว่า “การออกแบบเชิงวัตถุ” เราจำแนกออบเจ๊คต์ที่เราต้องการ เช่น 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)

ตัวอย่างนี้แนะนำแผนการพัฒนาสำหรับการออกแบบออบเจ๊คต์และเมธอด

  1. เริ่มต้นด้วยการเขียนฟังก์ชันที่อ่านและเขียนตัวแปรส่วนกลาง (เมื่อจำเป็น)
  2. เมื่อคุณทำให้โปรแกรมทำงานได้ ให้มองหาความสัมพันธ์ระหว่างตัวแปรส่วนกลางและฟังก์ชันที่ใช้ตัวแปรเหล่านั้น
  3. ห่อหุ้มตัวแปรที่เกี่ยวข้องเป็นแอตทริบิวต์ของออบเจ๊คต์
  4. แปลงฟังก์ชันที่เกี่ยวข้องเป็นเมธอดของคลาสใหม่

เพื่อเป็นการฝึกหัด ให้ดาวน์โหลดโค้ด Markov ของผมจาก http://thinkpython2.com/code/markov.py และทำตามขั้นตอนที่อธิบายไว้ข้างต้นเพื่อห่อหุ้มตัวแปรส่วนกลางเป็นแอตทริบิวต์ของคลาสใหม่ที่เรียกว่า Markov เฉลย: http://thinkpython2.com/code/Markov.py (สังเกตตัวพิมพ์ใหญ่ M)

18.11 อภิธานศัพท์

  • เข้ารหัส (encode): การแทนค่าชุดหนึ่งด้วยชุดค่าอื่นโดยสร้างการแปลงระหว่างค่าเหล่านี้
  • คลาสแอตทริบิวต์ (class attribute): แอตทริบิวต์ที่เกี่ยวข้องกับคลาสออบเจ๊คต์ ซึ่งถูกประกาศไว้ภายในนิยามคลาส แต่อยู่นอกเมธอดใดๆ
  • อินสแตนซ์แอตทริบิวต์ (instance attribute): แอตทริบิวต์ที่เกี่ยวข้องกับอินสแตนซ์ของคลาส
  • วีเนียร์ (veneer): เมธอดหรือฟังก์ชันที่ให้อินเทอร์เฟซที่แตกต่างกันไปยังฟังก์ชันอื่นโดยไม่ต้องคำนวณมาก
  • การสืบทอด(inheritance): ความสามารถในการนิยามคลาสใหม่ที่เป็นรุ่นที่แก้ไขของคลาสที่นิยามไว้ก่อนหน้านี้
  • คลาสพาเรนต์ (parent class): คลาสที่คลาสลูกสืบทอดมา
  • คลาสลูก (child class): คลาสใหม่ที่สร้างขึ้นโดยสืบทอดจากคลาสที่มีอยู่ เรียกอีกอย่างว่า “คลาสย่อย”
  • ความสัมพันธ์แบบเป็น (IS-A relationship): ความสัมพันธ์ระหว่างคลาสลูกกับคลาสพาเรนต์
  • ความสัมพันธ์แบบมี (HAS-A relationship): ความสัมพันธ์ระหว่างสองคลาสโดยที่อินสแตนซ์ของคลาสหนึ่งมีการอ้างอิงถึงอินสแตนซ์ของอีกคลาสหนึ่ง
  • การขึ้นต่อกัน (dependency): ความสัมพันธ์ระหว่างสองคลาสโดยที่อินสแตนซ์ของคลาสหนึ่งใช้อินสแตนซ์ของคลาสอื่น แต่ไม่เก็บไว้เป็นแอตทริบิวต์
  • แผนภาพคลาส (class diagram): แผนภาพที่แสดงคลาสในโปรแกรมและความสัมพันธ์ระหว่างพวกเขา
  • ความหลายหลาก (multiplicity): สัญกรณ์ในแผนภาพคลาสที่แสดงสำหรับความสัมพันธ์แบบมี ว่ามีการอ้างอิงถึงอินสแตนซ์ของคลาสอื่นกี่รายการ
  • การห่อหุ้มข้อมูล (data encapsulation): แผนการพัฒนาโปรแกรมที่เกี่ยวข้องกับต้นแบบโดยใช้ตัวแปรส่วนกลางและเวอร์ชันสุดท้ายที่ทำให้ตัวแปรส่วนกลางเป็นแอตทริบิวต์ของอินสแตนซ์

18.12 แบบฝึกหัด

แบบฝึกหัด 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
ต่อไปนี้เป็นมือที่เป็นไปได้ในโป๊กเกอร์ โดยเรียงตามมูลค่าที่เพิ่มขึ้นและลำดับความน่าจะเป็นที่ลดลง

  • คู่ (pair): ไพ่สองใบที่มีอันดับเดียวกัน
  • สองคู่ (two pair): ไพ่สองคู่ที่มีอันดับเดียวกัน
  • ตอง (three of a kind): ไพ่สามใบที่มีอันดับเดียวกัน
  • สเตรท (straight): ไพ่ห้าใบที่มีอันดับตามลำดับ (เอซสามารถสูงหรือต่ำได้ ดังนั้น เอซ-2-3-4-5 จึงเป็นไพ่สเตรท และ 10-แจ๊ค-แหม่ม-คิง-เอซ ก็เช่นกัน แต่ แหม่ม-คิง-เอซ-2-3 ไม่ใช่.)
  • ฟลัช (flush): ไพ่ห้าใบที่เป็นชุดเดียวกัน
  • ฟูลเฮ้าส์ (full house): มี 1 ตอง และ 1 คู่
  • โฟร์การ์ด (four of a kind): ไพ่ 4 ใบแต้มเหมือนกัน
  • สเตรทฟลัช (straight flush): ไพ่ห้าใบเรียงตามลำดับ (ตามที่กำหนดไว้ข้างต้น) และเป็นชุดเดียวกัน

เป้าหมายของแบบฝึกหัดเหล่านี้คือการประมาณความน่าจะเป็นของการจั่วไพ่แบบต่างๆ เหล่านี้

  1. ดาวน์โหลดไฟล์ต่อไปนี้จาก http://thinkpython2.com/code
    • Card.py เวอร์ชันที่สมบูรณ์ของคลาส Card, Deck และ Hand ในบทนี้
    • PokerHand.py การพัฒนามือของโป๊กเกอร์ที่ยังไม่สมบูรณ์ และโค้ดบางส่วนที่ใช้ทดสอบ
  2. หากคุณใช้ PokerHand.py จะแจกไพ่โป๊กเกอร์ 7 ใบ และตรวจดูว่ามีไพ่ในมือที่เป็นฟลัชหรือไม่ อ่านรหัสนี้อย่างละเอียดก่อนดำเนินการต่อ
  3. เพิ่มเมธอดที่ชื่อว่า has_pair, has_twopair เป็นต้น ไปยัง PokerHand.py ซึ่งคืนค่าเป็น True หรือ False โดยขึ้นอยู่กับว่ามือนั้นตรงตามเกณฑ์ที่เกี่ยวข้องหรือไม่ โค้ดของคุณควรทำงานอย่างถูกต้องสำหรับ “มือ” ที่มีการ์ดจำนวนเท่าใดก็ได้ (แม้ว่า 5 และ 7 จะเป็นขนาดทั่วไป)
  4. เขียนเมธอดที่ชื่อว่า classify ซึ่งคำนวณการจำแนกประเภทที่มีมูลค่าสูงสุดสำหรับมือและตั้งค่าแอตทริบิวต์ label ตามนั้น ตัวอย่างเช่น ไพ่ 7 ใบอาจมีฟลัชและคู่ ควรมีข้อความว่า “flush”
  5. เมื่อคุณมั่นใจว่าวิธีการจำแนกของคุณได้ผล ขั้นตอนต่อไปคือการประมาณความน่าจะเป็นของมือต่างๆ เขียนฟังก์ชันใน PokerHand.py ที่สับไพ่สำรับ แบ่งออกเป็นมือ จำแนกมือ และนับจำนวนครั้งที่การจำแนกประเภทต่างๆ ปรากฏขึ้น
  6. พิมพ์ตารางการจำแนกประเภทและความน่าจะเป็น รันโปรแกรมของคุณด้วยจำนวนมือที่มากขึ้นเรื่อยๆ จนกว่าค่าเอาต์พุตจะบรรจบกันในระดับความแม่นยำที่เหมาะสม เปรียบเทียบผลลัพธ์ของคุณกับค่าต่างๆ ที่ https://en.wikipedia.org/wiki/Hand_rankings และ https://en.wikipedia.org/wiki/Poker_probability

เฉลย: http://thinkpython2.com/code/PokerHandSoln.py

https://greenteapress.com/thinkpython2/html/thinkpython2019.html

python/inheritance.txt · Last modified: 2021/08/30 09:55 (external edit)

Page Tools