วิธีคิดแบบนักวิทยาการคอมพิวเตอร์/Sets of objects

บทที่ 15 15.1 composition แก้ไข

15.1 composition แก้ไข

ตอนนี้คุณได้เห็นตัวอย่างมากมายของ composition หนึ่งในตัวอย่างแรกที่ใช้ method invocation ซึ่งเป็นส่วนหนึ่งของ expression อีกตัวอย่างคือ nested structure statement คุณสามารถใส่ if statement ภายใน while loop และภายในอาจมี statement อีกอัน เป็นต้น

ได้เห็นรูปแบบนี้และได้เห็นเกี่ยวกับ list และ object คุณไม่ต้องแปลกใจเพื่อที่เรียนสิ่งที่คุณสามารถสร้าง list ของ object ที่บรรจุ list ได้ด้วย (เช่น attributes) คุณสามารถสร้าง list ที่บรรจุ list คุณสามารถสร้าง object ที่บรรจุ object เป็นต้น

ในบทนี้และบทถัดไป เราจะดูบางตัวอย่างที่รวมเข้าด้วยกัน การใช้ตัวอย่าง Card objects

15.2 Card objects แก้ไข

ถ้าคุณคุ้นเคยกับการเล่นไพ่ทั่วไป ตอนนี้เป็นเวลาที่ดีที่จะใช้ไพ่ ในหนึ่งสำรับมีไพ่ 52 ใบ ซึ่งแต่ละสำรับมี 4 suit และ 13 rank โดยที่ suit คือ ดอกจิก โพธิ์แดง ข้าวหลามตัด และ โพธิ์ดำ ส่วน rank คือ Ace 1,2,3,4,5,6,7,8,9,10,jack,queen,king ขึ้นอยู่กับว่าเกมที่คุณเล่น Ace อาจมีค่าสูงกว่า king หรือต่ำกว่า 2

ถ้าคุณต้องการกำหนด object ใหม่เพื่อแสดงการเล่นไพ่ attributes ที่ควรมีอย่างแน่นอนคือ rank และ suit มันไม่มีชนิดที่ชัดเจนที่ควรเป็น attributes สิ่งที่เป็นไปได้คือ ใช้ strings บรรจุคำเช่น ดอกจิก สำหรับ suit และ Queen สำหรับ rank ปัญหาหนึ่งของการ implementation นี้คือ มันไม่ง่ายที่จะเปรียบเทียบไพ่ที่เห็นซึ่งมีค่าสูงกว่า rank หรือ suit

ทางเลือกคือ ใช้integer ในการ encode rank และ suit โดย encode ไม่ได้หมายถึงอะไรที่คนส่วนใหญ่คิดว่าคือ การแปลงให้เป็นโค้ดลับ นักคอมพิวเตอร์ได้ให้ความหมายไว้ว่า การกำหนดโดยการจับคู่ระหว่างลำดับตัวเลขกับ item ที่เราต้องการแสดง ตัวอย่างเช่น

Spades  3

Hearts 7  2

Diamonds  1

Clubs  0

รูปร่างหน้าตาของการจับคู่คือ suit จับคู่กับ integer ในคำสั่ง ดังนั้นเราสามารถเปรียบเทียบ suit โดยการเปรียบเทียบกับ integer การจับคู่ของ rank ที่ค่อนข้างชัดเจน แต่ละตัวเลขของ rank จะสอดคล้องกับ integer และหน้าไพ่

Jack  11

Queen  12

King  13

เหตุผลที่เราใช้เครื่องหมายทางคณิตศาสตร์ สำหรับการจับคู่เหล่านี้ คือ พวกมันไม่ได้เป็นส่วนหนึ่งของโปรแกรม python พวกมันเป็นส่วนหนึ่งของโปรแกรมที่ออกแบบขึ้นแต่พวกมันไม่ปรากฏแน่ชัดในโค้ด class definition สำหรับชนิดของ Card มีลักษณะดังนี้

class Card:

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

self.suit = suit

self.rank = rank

ตามปกติพวกเราให้ initialization method รับค่า optional พารามิเตอร์ สำหรับแต่ละ attributes ค่าเริ่มต้นของ suit คือ 0 ซึ่งแสดงถึงโพธิ์ดำ

การสร้าง Card เราสร้างตัวสร้าง Card ซึ่งมีพารามิเตอร์ของไพ่ที่เราต้องการ

threeOfClubs = Card(3, 1)

15.3 Class attributes and the __str__ method แก้ไข

เพื่อที่จะ print Card object ในวิธีที่คนทั่วไปอ่านได้ง่าย เราต้องการจับคู่โค้ด integer กับตัวอักษร วิธีปกติที่จะทำ คือ มี lists ของ strings เรากำหนด class attribute ของ list เหล่านี้ด้านบนสุดของ class definition

class Card:

suitList = ["Clubs", "Diamonds", "Hearts", "Spades"]

rankList = ["narf", "Ace", "2", "3", "4", "5", "6", "7",

"8", "9", "10", "Jack", "Queen", "King"]

#init method omitted

def __str__(self):

return (self.rankList[self.rank] + " of " +

self.suitList[self.suit])

class attribute คือ ระบุภายนอก method ใดๆ และมันไม่สามารถถูกเข้าจาก method ใดๆ ใน class นั้น

ภายใน __str__ เราสามารถใช้ suitList และ rankList เพื่อที่จับคู่ตัวเลขของ suit และ rank กับ string ตัวอย่างเช่น exoression self.suitList[self.suit] หมายถึงใช้ attribute suit จาก object self ขณะที่ index เข้าไปยัง class attribute ชื่อ suitList และเลือก string ที่เหมาะสม

เหตุผลสำหรับ “narf” ใน element แรกใน rankList คือ การเก็บ element ลำดับที่ 0 ซึ่งไม่ควรใช้ rank ที่ถูกต้องคือ 1 ถึง 13 item เหล่านี้ไม่มีประโยชน์ไม่จำเป็นต้องใส่เข้าไป เราสามารถเริ่มต้นที่ 0 แต่มันอาจสับสนเล็กน้อยในการ encode 2 เท่ากับ 2 ,3 เท่ากับ 3 เป็นต้น มี method ที่พวกเรามี จนกระทั่ง เราสามารถสร้างและ print ไพ่

>>> card1 = Card(1, 11)

>>> print card1

Jack of Diamonds

Class attributes เช่น suitList ถูกแบ่งโดย Card object ทั้งหมด ข้อดีของสิ่งนี้ คือ เราสามารถใช้ Card object ใดๆ เพื่อที่จะเข้าไปยัง class attributes

>>> card2 = Card(1, 3)

>>> print card2

3 of Diamonds

>>> print card2.suitList[1]

Diamonds

ข้อเสีย คือ ถ้าเราแก้ไข class attribute มันส่งผลต่อ instance ของ class ตัวอย่างเช่น ถ้าเราตัดสินใจว่า Jack of Diamonds ควรถูกเรียกว่า Jack of Swirly Whales เราสามารถทำดังนี้

>>> card1.suitList[1] = "Swirly Whales"

>>> print card1

Jack of Swirly Whales

ปัญหา คือ ทังหมดของ Diamonds จะเปลี่ยนเป็น Swirly Whales

>>> print card2

3 of Swirly Whales

มันจึงไม่ใช่ความคิดที่ดีที่จะแก้ไข class attributes

15.4 Comparing cards แก้ไข

สำหรับ primitive types มีเงื่อนไขในการเปรียบเทียบค่าและกำหนดเมื่ออันหนึ่งใหญ่กว่า น้อยกว่า หรือเท่ากับอีกอันหนึ่ง สำหรับ user-defined types เราสามารถยกเลิกพฤติกรรมของ built-in operators โดยให้ method ชื่อ __cmp__ รับค่าพารามิเตอร์ 2 ตัว คือself และ other และ return ค่า 1 ถ้า object แรกใหญ่กว่า เป็น -1 ถ้า object ที่สองใหญ่กว่า และเป็น 0 ถ้ามันเท่ากัน

บาง types ถูกจัดเรียงอย่างสมบูรณ์ หมายความว่า คุณสามารถเปรียบเทียบสอง elements และบอกว่าอันไหนใหญ่กว่า ตัวอย่างเช่น จำนวน integer และ float-point ถูกจัดเรียงโดยสมบูรณ์ บางชุดไม่ถูกจัดเรียง หมายความว่า ไม่มีความหมายที่จะพูดว่า element หนึ่งใหญ่กว่าอีกอันหนึ่ง ตัวอย่างเช่น ผลไม้ ไม่สามารถเรียงลำดับ ซึ่งคุณไม่สามารถเปรียบเทียบ แอปเปิ้ลกับส้ม

ในการเล่นไพ่บางครั้งสามารถเปรียบเทียบได้ บางครั้งไม่สามารถเปรียบเทียบได้ ตัวอย่างเช่น คุณรู้ว่า 3 โพธิ์ดำ สูงกว่า 2 โพธิ์ดำ และ 3 ข้าวหลามตัด สูงกว่า 3 โพธิ์ดำ แต่อันไหนสูงกว่ากันระหว่าง 3 โพธิ์ดำหรือ 2 ข้าวหลามตัด อันหนึ่ง rank สูงกว่า แต่อีกอัน suit สูงกว่า

เพื่อที่จะทำการเปรียบเทียบไพ่ คุณต้องตัดสินใจว่าอันไหนสำคัญมากกว่า ระหว่าง rank หรือ suit ด้วยความซื่อสัตย์ เลือกโดยการสุ่ม สำหรับวัตถุประสงค์ของการเลือกเราจะพูดว่า suit สำคัญมากกว่า เพราะในสำรับใหม่ของไพ่เรียงมาจากโพธิ์ดำทั้งหมด ตามด้วยข้าวหลามตัดทั้งหมด เป็นต้น

เมื่อตัดสินใจแล้ว เราสามารถเขียน __cmp__

def __cmp__(self, other):

# check the suits

if self.suit > other.suit: return 1

if self.suit < other.suit: return -1

# suits are the same... check ranks

if self.rank > other.rank: return 1

if self.rank < other.rank: return -1

# ranks are the same... it's a tie

return 0

ในการเรียงนี้ Ace ปรากฏค่าต่ำกว่า 2

15.5 Deck แก้ไข

ตอนนี้เรามี object แสดงถึง Cards ขั้นตอนต่อไประบุ class ที่แทน Deck แน่นอนที่สำรับไพ่ถูกสร้างขึ้นจากไพ่ ดังนั้นแต่ละ Deck object จะบรรจุ list ของไพ่เป็น attribute

ต่อมา คือ class defination เพื่อ Deck class โดย initialization method สร้าง attribute cards และสร้างไพ่พื้นฐาน 52 ใบ

class Deck:

def __init__(self):

self.cards = []

for suit in range(4):

for rank in range(1, 14):

self.cards.append(Card(suit, rank))

วิธีง่ายที่สุดเพื่อที่ให้ deck อยู่ใน nested loop loop ภายนอกนับจำนวน suit จาก 0 ถึง 3 loop ภายในนับจำนวน rank จาก 1 ถึง 13 ตั้งแต่ loop ภายนอก 4 ครั้ง และ loop ภายใจ 13 ครั้ง จำนวนครั้งของ body ที่ถูก execute คือ 52 แต่ละการสร้างตัวอย่างของไพ่ซ้ำๆ มี suit และ rank ปัจจุบัน และเพิ่มไพ่เข้าไปใน card list

15.6 Printing the deck แก้ไข

เมื่อเราระบุชนิดใหม่ของ object เราต้องการ method ที่ print หัวข้อของ object การ print Deck เราตรวจ list และ print แต่ละ Card

class Deck:

...

def printDeck(self):

for card in self.cards:

print card

ที่นี่และจากตอนนี้ การละไว้ ( . . . ) บ่งชี้ว่าเราละเลย methods ต่างๆ ใน class

ทางเลือกที่จะ print Deck เราสามารถเขียน __str__ method สำหรับ Deck class ข้อดีของ __str__ คือ มันสามารถปรับตัวได้ดีกว่า แต่การ print หัวข้อของ object มันสร้าง string ที่แสดงส่วนอื่นๆ ของโปรแกรมสามารถปรับให้เหมาะสมก่อนการ print หรือ เก็บค่าหลังการใช้

นี้คือ เวอร์ชันของ __str__ ที่ return การแสดง string ของ Deck มันจัดเรียงไพ่ในลักษณะเดียวกัน ซึ่งไพ่แต่ละใบจะเว้นวรรคเข้ามามากกว่าไพ่ก่อนหน้านี้

class Deck:

...

def __str__(self):

s = ""

for i in range(len(self.cards)):

s = s + " "*i + str(self.cards[i]) + "\n"

return s

ตัวอย่างนี้อธิบายหลายรูปแบบ สิ่งแรก คือ แทนที่โดยการเข้าไป self.cards และกำหนดตัวแปรของแต่ละไพ่ เราใช้ i เป็น loop variable และ index เข้าไปใน list ของไพ่

ขั้นที่สอง เราใช้การคูณ string เพื่อเว้นไพ่แต่ละใบ โดยให้มีที่ว่างมากกว่าใบสุดท้าย expression “ ” *i ให้จำนวนที่ว่างเท่ากับค่าปัจจุบันของ i

ขั้นที่สาม แทนที่โดยการใช้คำสั่ง print เพื่อ print ไพ่ เราใช้ฟังก์ชัน str การผ่าน object ไปยัง argument เพื่อที่ str เท่ากัน เพื่อให้เกิด __str__ บน object

สุดท้าย เราใช้ตัวแปร s เป็น accumulator ในขั้นต้น s คือ string ที่ว่างเปล่า แต่ละครั้งที่ผ่าน loop จะมี string ใหม่ที่ถูกสร้างขึ้นและได้เชื่อมเข้าด้วยกันกับค่าเก่าของ s ที่ได้ค่าใหม่ เมื่อจบ loop s จะบรรจุการแสดงค่า string ที่สมบูรณ์ของ Deck ซึ่งเป็นดังนี้

>>> deck = Deck()

>>> print deck

Ace of Clubs

2 of Clubs

3 of Clubs

4 of Clubs

5 of Clubs

6 of Clubs

7 of Clubs

8 of Clubs

9 of Clubs

10 of Clubs

Jack of Clubs

Queen of Clubs

King of Clubs

Ace of Diamonds

15.7 Shuffling the deck แก้ไข

ถ้าสำรับไพ่ถูกสับอย่างดี แล้วไพ่ใดๆ มีความเท่าเทียมกัน ลักษณะที่ปรากฏที่ใดก็ตามในสำรับ และ ตำแหน่งใดๆ ในสำรับเท่าเทียมกันเช่นเดียวกับการบรรจุไพ่ใดๆ

การสับไพ่เราใช้ฟังก์ชัน randrange จาก ramdom module มี 2 integer argument คือ a และ b randrange เลือก random integer ในขอบเขต a < = x < b ตั้งแต่ข้อจำกัดนี้เข้มงวดน้อยกว่า b เราสามารถใช้ความยาวของ list เป็น argument ทีสอง และเรารับประกันได้ว่า index ถูกต้อง ตัวอย่างเช่น expression นี้เลือก index ของการสุ่มไพ่ในสำรับ

random.randrange(0, len(self.cards))

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

class Deck:

...

def shuffle(self):

import random

nCards = len(self.cards)

for i in range(nCards):

j = random.randrange(i, nCards)

self.cards[i], self.cards[j] = self.cards[j], self.cards[i]

สมมติว่า มีไพ่ 52 ใบ ในหนึ่งสำรับ เราได้ความยาวของ list และเก็บค่ามันใน nCards

ไพ่แต่ละใบในสำรับ เราสุ่มเลือกไพ่ขึ้นมาจากกองไพ่ซึ่งยังไม่มีการสับไพ่ แล้วเปลี่ยนไพ่ปัจจุบันกับการเลือกไพ่ การเปลี่ยนไพ่ เราใช้งาน tuple เช่นใน section 9.2

self.cards[i], self.cards[j] = self.cards[j], self.cards[i]

15.8 Removing and dealing cards แก้ไข

อีก method ที่มีประโยชน์กับ Deck class คือ removeCard ซึ่งรับค่าพารามิเตอร์ของไพ่ ลบมันออกและ return ค่า True ถ้าไพ่อยู่ในสำรับ หรือ False ถ้าเป็นอย่างอื่น

class Deck:

...

def removeCard(self, card):

if card in self.cards:

self.cards.remove(card)

return True

else:

return False

ใน operation returns true ถ้า operand อยู่ในลำดับที่สอง ซึ่งต้องเป็น list หรือ tuple ถ้า operand แรกคือ object python ใช้ method __cmp__ ของ object กำหนดความเท่าเทียมกันของ item ใน list ตั้งแต่ cmp ใน Cardclass ตรวจสอบความเท่ากัน removeCard method ก็ตรวจสอบความเท่ากัน

การแจกไพ่เราต้องลบและ return ไพ่ด้านบนสุด list method pop ให้วิธีง่ายที่จะทำ class Deck:

...

def popCard(self):

return self.cards.pop()

ตามความเป็นจริง pop ลบไพ่ใบสุดท้ายใน list ดังนั้นเรามีผลกระทบต่อการแจกไพ่จากด้านล่างของสำรับ การดำเนินการที่มากกว่า เรามีลักษณะที่ต้องการให้เป็นฟังก์ชัน boolean isEmpty ซึ่ง return ค่า True ถ้าในสำรับไม่มีไพ่


class Deck:

...

def isEmpty(self):

return (len(self.cards) == 0)

15.9 Glossary แก้ไข

encode: To represent one set of values using another set of values by constructing a mapping between them.

class attribute: A variable that is defined inside a class definition but outside any method. Class attributes are accessible from any method in the class and are shared by all instances of the class.

accumulator: A variable used in a loop to accumulate a series of values, such as by concatenating them onto a string or adding them to a running sum.