บทนี้เป็นเรื่องของชนิดข้อมูล ชนิดหนึ่งที่มีประโยชน์มากที่สุดในภาษาไพธอน นั่นคือ ลิสต์ (list) เราจะเรียนเพิ่มเติมในเรื่องออบเจ๊คต์ (object) และสิ่งที่จะเกิดขึ้น ถ้าเรามีมากกว่าหนึ่งชื่อสำหรับออบเจ๊คต์ เดียวกัน
ในลักษณะเดียวกับสายอักขระ (string) ลิสต์เป็นลำดับของค่าต่างๆ ในสายอักขระ ค่าต่างๆ ที่ว่า คือ ตัวอักษร ในลิสต์ ค่าต่างๆ นั้นอาจจะเป็นอะไรก็ได้ ค่าต่างๆ ในลิสต์นั้น จะเรียกว่า อิลิเมนต์ (elements) หรือบางครั้งก็เรียกว่า ไอเท็ม (items)
มีหลายวิธีที่ใช้สร้างลิสต์ได้ วิธีที่ง่ายที่สุดคือ ล้อมอิลิเมนต์ต่างๆ ไว้ในวงเล็มสี่เหลี่ยม ([
และ ]
) เช่น
[10, 20, 30, 40] ['crunchy frog', 'ram bladder', 'lark vomit']
ตัวอย่างแรก เป็นลิสต์ของเลขจำนวนเต็มสี่ตัว ตัวอย่างที่สอง เป็นลิสต์ของสายอักขระสามตัว อิลิเมนต์ต่างๆ ในลิสต์ไม่จำเป็นต้องเป็นชนิดเดียวกัน ลิสต์ข้างล่างนี้ มีสายอักขระ เลขทศนิยม เลขจำนวนเต็ม และลิสต์อีกอัน
['spam', 2.0, 5, [10, 20]]
ลิสต์ที่อยู่ในอีกลิสต์หนึ่ง จะเรียกว่า ลิสต์ซ้อนใน (nested list).
ลิสต์ที่ไม่มีอิลิเมนต์อยู่เลย จะเรียกว่า ลิสต์ว่าง (empty list). เราสามารถสร้างลิสต์ว่างได้ด้วยวงเล็บสี่เหลี่ยมว่างๆ []
ซึ่งก็น่าจะเดาได้ว่า เราสามารถกำหนดค่าที่เป็นลิสต์ให้กับตัวแปรได้
>>> cheeses = ['Cheddar', 'Edam', 'Gouda'] >>> numbers = [42, 123] >>> empty = [] >>> print(cheeses, numbers, empty) ['Cheddar', 'Edam', 'Gouda'] [42, 123] []
วิธีการเข้าถึงอิลิเมนต์ของลิสต์คล้ายกับ วิธีการเข้าถึงตัวอักษรในสายอักขระ นั่นคือ ใช้ตัวดำเนินการวงเล็บสี่เหลี่ยม นิพจน์ภายในวงเล็บสี่เหลี่ยมจะระบุ ดัชนี (index หรือเลขลำดับ) ของอิลิเมนต์ ทบทวนว่า ดัชนีแรกเริ่มต้นที่ 0
จากตัวอย่างที่แล้ว
>>> cheeses[0] 'Cheddar'
สิ่งที่ต่างจากสายอักขระ (string) ก็คือ ลิสต์เป็นชนิดข้อมูลที่เปลี่ยนแปลงได้ (mutable) ถ้าตัวดำเนินการวงเล็บสี่เหลี่ยมอยู่ทางซ้ายมือของ การกำหนดค่า (assignment) มันจะระบุอิลิเมนต์ของลิสต์ที่จะกำหนดค่าให้ใหม่
>>> numbers = [42, 123] >>> numbers[1] = 5 >>> numbers [42, 5]
อิลิเมนต์ดัชนี 1 ของตัวแปร numbers
ที่เคยเป็น 123 ตอนนี้กลายเป็น 5
รูปที่ 10.1 แสดงแผนภาพสถานะ(state diagram) ของตัวแปร cheeses
, numbers
และ empty
ลิสต์แสดงเป็นกล่องที่มีคำว่า “list” กำกับอยู่ข้างบน และมีอิลิเมนต์ต่างๆ อยู่ภายใน ตัวแปร cheeses
อ้างถึงลิสต์ที่มีสามอิลิเมนต์ ใช้ดัชนี 0, 1, และ 2 ตัวแปร numbers
มีสองอิลิเมนต์ แผนภาพแสดงค่าของอิลิเมนต์ที่สอง (ดัชนี 1) ถูกเปลี่ยนจาก 123 เป็น 5 ตัวแปร empty
อ้างถึงลิสต์ที่ไม่มีอิลิเมนต์
ดัชนีของลิสต์ทำงานแบบเดียวกับดัชนีของสายอักขระ
>>> cheeses[3 - 2] 'Edam'
IndexError
ออกมา เช่น>>> cheeses[3] Traceback (most recent call last): File "<stdin>", line 1, in <module> IndexError: list index out of range
>>> cheeses[-1] 'Gouda'
ตัวดำเนินการ in
ก็ทำงานกับลิสต์ได้เหมือนกับในสายอักขระ
>>> cheeses = ['Cheddar', 'Edam', 'Gouda'] >>> 'Edam' in cheeses True >>> 'Brie' in cheeses False
วิธีที่นิยมที่สุดในการท่องสำรวจอิลิเมนต์ต่างๆ ของลิสต์ คือ การใช้ for
ลูป ไวยากรณ์ก็จะคล้ายกับตอนที่ใช้กับสายอักขระ
for cheese in cheeses: print(cheese)
วิธีนี้ใช้ได้ดี ถ้าเราต้องการแค่อ่านอิลิเมนต์ของลิสต์ แต่ถ้าเราต้องการเขียนหรือเปลี่ยนค่าของอิลิเมนต์ เราต้องใช้ดัชนี วิธีง่ายๆ คือ ใช้ ฟังก์ชันที่มีอยู่ในตัว (built-in functions) ได้แก่ range
และ len
for i in range(len(numbers)): numbers[i] = numbers[i] * 2
ลูปนี้ท่องสำรวจลิสต์ และแก้ค่าของอิลิเมนต์แต่ละตัว ฟังก์ชัน len
ส่งค่าจำนวนของอิลิเมนต์ในลิสต์ออกมา ฟังก์ชัน range
ส่งค่าดัชนีจาก $0$ ถึง $n-1$ ออกมา โดย $n$ เป็นความยาว1)ของลิสต์. แต่ละครั้งของลูป ตัวแปร i
จะรับดัชนีของอิลิเมนต์มา การกำหนดค่าในตัวลูป (loop body) จะใช้ตัวแปร i
เพื่ออ่านค่าเดิมของอิลิเมนต์ออกมา แล้วค่อยกำหนดค่าใหม่เข้าไป
ถ้าใช้ลูป for
กับลิสต์ว่าง (empty list) ตัวลูป จะไม่ถูกดำเนินการ เช่น
for x in []: print('This never happens.')
ถึงแม้ลิสต์สามารถจะมีสมาชิกเป็นลิสต์อีกอันได้ แต่ ลิสต์ซ้อนใน จะนับเป็นแค่หนึ่งอิลิเมนต์ของลิสต์แม่ ตัวอย่างข้างล่างความยาวของลิสต์จึงเป็นแค่สี่
['spam', 1, ['Brie', 'Roquefort', 'Pol le Veq'], [1, 2, 3]]
ตัวดำเนินการ +
ทำการต่อลิสต์เข้าด้วยกัน เช่น
>>> a = [1, 2, 3] >>> b = [4, 5, 6] >>> c = a + b >>> c [1, 2, 3, 4, 5, 6]
ตัวดำเนินการ *
ให้ลิสต์ซ้ำเท่ากับจำนวนตัวเลขที่ระบุ
>>> [0] * 4 [0, 0, 0, 0] >>> [1, 2, 3] * 3 [1, 2, 3, 1, 2, 3, 1, 2, 3]
ตัวอย่างแรกให้ลิสต์ซ้ำของ ลิสต์ [0]
สี่ครั้ง ตัวอย่างที่สองให้ลิสต์ซ้ำของ ลิสต์ [1, 2, 3]
สามครั้ง
ตัวดำเนินการตัด (slice operator) ก็ทำงานกับลิสต์ได้เช่นเดียวกับสายอักขระ เช่น
>>> t = ['a', 'b', 'c', 'd', 'e', 'f'] >>> t[1:3] ['b', 'c'] >>> t[:4] ['a', 'b', 'c', 'd'] >>> t[3:] ['d', 'e', 'f']
ถ้าเราไม่ใส่ดัชนีตัวแรก การตัดจะเริ่มที่ดัชนีเริ่มต้น ถ้าเราไม่ใส่ดัชนีตัวที่สอง การตัดจะไปจนถึงตัวสุดท้าย ถ้าไม่ใส่ดัชนีเลย การตัดจะทำสำเนาลิสต์ทั้งลิสต์ออกมา
>>> t[:] ['a', 'b', 'c', 'd', 'e', 'f']
เพราะว่าลิสต์เปลี่ยนแปลงแก้ไขได้ ดังนั้นส่วนใหญ่แล้ว จะมีประโยชน์ที่เราจะคัดลอกลิสต์ออกมาก่อนที่จะแก้ไขเปลี่ยนแปลงมัน
การใช้ตัวดำเนินการตัดทางด้านซ้ายของการกำหนดค่า สามารถใช้เพื่อแก้ไขค่าอิลิเมนต์หลายๆ ตัวพร้อมๆ กันได้
>>> t = ['a', 'b', 'c', 'd', 'e', 'f'] >>> t[1:3] = ['x', 'y'] >>> t ['a', 'x', 'y', 'd', 'e', 'f']
ไพธอนมีเมธอดของลิสต์อยู่หลายตัว ตัวอย่างเช่น append
เพิ่มอิลิเมนต์ใหม่เข้าไปท้ายลิสต์
>>> t = ['a', 'b', 'c'] >>> t.append('d') >>> t ['a', 'b', 'c', 'd']
เมธอด extend
รับลิสต์เป็นอาร์กิวเมนต์ และต่ออิลิเมนต์ทั้งหมดเข้าไป
>>> t1 = ['a', 'b', 'c'] >>> t2 = ['d', 'e'] >>> t1.extend(t2) >>> t1 ['a', 'b', 'c', 'd', 'e']
ในตัวอย่างนี้ ลิสต์ t2
จะเหมือนเดิม
เมธอด sort
จะเรียงอิลิเมนต์ต่างๆ ในลิสต์จากน้อยไปมาก
>>> t = ['d', 'c', 'e', 'b', 'a'] >>> t.sort() >>> t ['a', 'b', 'c', 'd', 'e']
เมธอดของลิสต์ส่วนใหญ่ไม่ได้ให้ค่าออกมา นั่นคือ มันแก้สมาชิกของลิสต์ตามหน้าที่ และให้ค่า None
ออกมา ถ้าบังเอิญไปเขียน t = t.sort()
ก็อาจจะผิดหวังได้
ถ้าต้องการบวกเลขต่างๆ ที่อยู่ในลิสต์ เราอาจทำเป็นลูปแบบนี้
def add_all(t): total = 0 for x in t: total += x return total
ตัวแปร total
มีค่าเริ่มต้นเป็น 0 แต่ละครั้งของลูป ตัวแปร x
รับอิลิเมนต์ทีละตัวมาจากลิสต์ ตัวดำเนินการ +=
เป็นวิธีเขียนสั้นๆ เพื่อเปลี่ยนค่าตัวแปร คำสั่งเสริมสำหรับกำหนดค่า (augmented assignment statement) ข้างล่างนี้
total += x
เทียบเท่ากับ
total = total + x
ขณะที่ลูปทำงานไป ตัวแปร total
ก็จะสะสมผลรวมของอิลิเมนต์ ตัวแปรที่ใช้งานในลักษณะนี้ บางครั้ง จะเรียกว่า ตัวสะสม (accumulator).
การบวกอิลิเมนต์ทุกตัวในลิสต์เป็นสิ่งที่ใช้บ่อยมาก จนไพธอนมีฟังก์ชันในตัวให้ คือ sum
>>> t = [1, 2, 3] >>> sum(t) 6
การทำลักษณะนี้ ที่รวบเอาอิลิเมนต์ต่างๆ มาเป็นค่าๆ เดียว บางครั้งจะเรียกว่า การยุบ (reduce)
บางครั้ง เราอาจต้องการท่องสำรวจลิสต์หนึ่ง เพื่อสร้างอีกลิสต์หนึ่ง ตัวอย่างเช่น ฟังก์ชันต่อไปนี้รับลิสต์ของสายอักขระ (string) และสร้างลิสต์ใหม่ออกมา โดยลิสต์ใหม่จะเป็นลิสต์ของสายอักขระ ที่ทุกคำขึ้นต้นด้วยตัวใหญ่
def capitalize_all(t): res = [] for s in t: res.append(s.capitalize()) return res
ตัวแปร res
ถูกกำหนดค่าเริ่มต้นเป็น ลิสต์ว่าง แต่ละครั้งของลูป เราจะเติมอิลิเมนต์เข้าไปทีละอิลิเมนต์ ดังนั้น res
ก็เป็นตัวสะสมอีกแบบหนึ่ง
ลักษณะการทำแบบฟังก์ชัน capitalize_all
บางครั้งจะเรียกว่า การแปลง (map) เพราะว่า มัน“แปลง”แต่ละอิลิเมนต์ (ด้วยฟังก์ชัน หรือในทีนี้ ด้วยเมธอด capitalize
) ในลิสต์
ลักษณะงานอีกอย่างที่มักเจอคือ การเลือกบางอิลิเมนต์มาจากลิสต์ และสร้างลิสต์ย่อย ขึ้นมาใหม่ ตัวอย่างเช่น ฟังก์ชันต่อไปนี้รับลิสต์ของสายอักขระ เข้าไป และให้ลิสต์ที่มีเฉพาะคำที่เป็นอักษรตัวใหญ่ออกมา
def only_upper(t): res = [] for s in t: if s.isupper(): res.append(s) return res
isupper
เป็นเมธอดของสายอักขระ ที่ให้ค่า True
ถ้าสายอักขระมีแต่อักษรตัวใหญ่
ลักษณะการทำแบบฟังก์ชัน only_upper
จะเรียกว่า การกรอง (filter) เพราะว่า มันเลือกเฉพาะบางอิลิเมนต์ และกรองอิลิเมนต์อื่นๆ ออกไป
การทำงานกับลิสต์ส่วนใหญ่ มักจะสามารถแสดงอยู่ในรูปแบบผสมกันของ การแปลง การกรอง และการยุบได้
มีหลายๆ วิธีที่จะลบอิลิเมนต์ออกจากลิสต์ ถ้าเรารู้ดัชนีของอิลิเมนต์ที่เราต้องการลบ เราก็สามารถใช้ pop
:
>>> t = ['a', 'b', 'c'] >>> x = t.pop(1) >>> t ['a', 'c'] >>> x 'b'
เมธอด pop
แก้ไขค่าของลิสต์ และให้อิลิเมนต์ที่ถูกถอดออกมา ถ้าเราเรียกใช้ เมธอด pop
โดยไม่ระบุดัชนี มันจะถอดอิลิเมนต์สุดท้ายออกมาให้
หรือถ้าเราไม่ต้องการได้อิลิเมนต์ที่ถอดออกมา เราสามารถใช้ตัวดำเนินการdel
ได้:
>>> t = ['a', 'b', 'c'] >>> del t[1] >>> t ['a', 'c']
ถ้าเรารู้อิลิเมนต์ที่ต้องการลบ แต่ไม่รู้ดัชนี เราก็สามารถใช้ remove
ได้:
>>> t = ['a', 'b', 'c'] >>> t.remove('b') >>> t ['a', 'c']
เมธอด remove
ไม่ได้ให้ค่าออกมา (ให้ None
ออกมา)
เราสามารถลบหลายๆ อิลิเมนต์พร้อมๆ กันได้ โดยใช้ del
กับดัชนีตัดช่วง (slice index):
>>> t = ['a', 'b', 'c', 'd', 'e', 'f'] >>> del t[1:5] >>> t ['a', 'f']
เช่นเคย การตัดเลือกทุกๆ อิลิเมนต์ไปจนถึง แต่ไม่รวมอิลิเมนต์ที่ดัชนีที่สอง
สายอักขระ (string) เป็นลำดับของอักขระ และลิสต์เป็นลำดับของค่าต่างๆ แต่ลิสต์ของอักขระไม่เหมือนกับสายอักขระ
เราสามารถแปลงจากสายอักขระเป็นลิสต์ของอักขระได้ โดยใช้ list
:
>>> s = 'spam' >>> t = list(s) >>> t ['s', 'p', 'a', 'm']
เพราะว่า list
เป็นชื่อของฟังก์ชันที่มีอยู่ในตัว ดังนั้นเราควรจะหลีกเลี่ยงการใช้คำว่า list
เป็นชื่อตัวแปร นอกจากนั้น แนะนำว่าควรหลีกเลี่ยงที่จะใช้ตัวอักษรแอลเดี่ยว l
เพราะว่ามันดูคล้ายกับเลขหนึ่งมาก 1
ฟังก์ชัน list
แยกสายอักขระออกมาเป็นอักขระแต่ละตัว (ดังแสดงในตัวอย่างข้างต้น) ถ้าเราต้องการแยกสายอักขระออกมาเป็นคำๆ เราควรจะใช้เมธอด split
:
>>> s = 'pining for the fjords' >>> t = s.split() >>> t ['pining', 'for', 'the', 'fjords']
นอกจากนั้น เมธอด split
ยังมีอาร์กิวเมนต์ทางเลือก delimiter ที่ใช้ระบุ ตัวอักษรที่ใช้เป็นตัวแบ่งคำได้ ตัวอย่างต่อไปนี้ใช้เครื่องหมายยัติภังค์ (hyphen) เป็นตัวแบ่งคำ:
>>> s = 'spam-spam-spam' >>> delimiter = '-' >>> t = s.split(delimiter) >>> t ['spam', 'spam', 'spam']
เมธอด join
เป็นการทำตรงข้ามกับ split
เมธอด join
รับลิสต์ของสายอักขระ และเชื่อมต่ออิลิเมนต์เข้าด้วยกัน เมธอด join
เป็นเมธอดของสายอักขระ ดังนั้น เราต้องเรียกใช้จากตัวแปร delimiter
ที่เป็นชนิดสายอักขระ และส่งลิสต์เข้าไปเป็นพารามิเตอร์:
>>> t = ['pining', 'for', 'the', 'fjords'] >>> delimiter = ' ' >>> s = delimiter.join(t) >>> s 'pining for the fjords'
กรณีนี้ ตัวแปร delimiter
เป็นช่องว่าง ดังนั้น join
ใส่ช่องว่างระหว่างคำ ถ้าหากต้องการต่อสายอักขระ โดยไม่มีช่องว่างระหว่างคำ เราสามารถใช้สายอักขระว่าง ''
เป็นตัวแบ่งคำได้
ถ้าเรารันข้อความคำสั่งกำหนดค่า:
a = 'banana' b = 'banana'
เรารู้ว่าทั้ง a
และ b
อ้างถึงสายอักขระ แต่เราไม่รู้ว่ามันอ้างถึงสายอักขระเดียวกันหรือไม่ มีความเป็นไปได้อยู่สองอย่าง ดังแสดงในรูปที่ 10.2
ในกรณีที่หนึ่ง a
และ b
อ้างถึงออบเจ๊คต์ที่ต่างกันสองออบเจ๊คต์ที่มีค่าเหมือนกัน ในกรณีที่สอง ทั้งa
และ b
อ้างถึงออบเจ๊คต์เดียวกัน
เพื่อจะตรวจดูว่าตัวแปรสองตัวอ้างถึงออบเจ๊คต์เดียวกันหรือไม่ เราสามารถใช้ตัวดำเนินการ is
ได้
>>> a = 'banana' >>> b = 'banana' >>> a is b True
ในตัวอย่างนี้ ไพธอนแค่สร้างออบเจ๊คต์สายอักขระขึ้นมาแค่หนึ่งออบเจ๊คต์ และทั้งตัวแปร a
และ b
ก็อ้างถึงมัน แต่ถ้าเราสร้างลิสต์ออกมา เราจะได้สองออบเจ๊คต์:
>>> a = [1, 2, 3] >>> b = [1, 2, 3] >>> a is b False
ดังนั้นแผนภาพสถานะแสดงได้ดังรูปที่ 10.3.
ในกรณีนี้ เราพูดได้ว่าลิสต์ทั้งสองเทียบเท่ากัน (equivalent) เพราะว่า มันมีอิลิเมนต์ต่างๆ เหมือนกัน แต่ไม่ใช่เป็นอันเดียวกัน (identical) เพราะว่ามันไม่ใช่ออบเจ๊คต์เดียวกัน ถ้าสองออบเจ๊คต์เป็นอันเดียวกันแล้ว มันจะเทียบเท่ากันด้วย แต่ถ้ามันเทียบเท่ากัน มันไม่จำเป็นต้องเป็นอันเดียวกัน
จนถึงตอนนี้ เราใช้คำว่า “ออบเจ๊คต์” และ “ค่า” สลับกันไปมาได้ แต่มันจะถูกต้องมากกว่าที่จะพูดว่าออบเจ๊คต์มีค่า ถ้าเราประเมินค่า [1, 2, 3]
เราจะได้ลิสต์ของออบเจ๊คต์ที่มีค่าเป็นลำดับของเลขจำนวนเต็มออกมา ถ้าอีกลิสต์หนึ่งมีอิลิเมนต์เหมือนๆ กัน เราพูดได้ว่ามันมีค่าเหมือนกัน แต่มันไม่ใช่ออบเจ๊คต์เดียวกัน
ถ้าตัวแปร a
อ้างอิงออบเจ๊คต์ และเรากำหนดให้ $\texttt{b = a}$ แล้วตัวแปรทั้งคู่จะอ้างอิงถึงออบเจ๊คต์เดียวกัน:
>>> a = [1, 2, 3] >>> b = a >>> b is a True
แผนภาพสถานะจะเป็นดังแสดงในรูปที่ 10.4
ความเกี่ยวข้องของตัวแปรกับออบเจ๊คต์จะเรียกว่า การอ้างอิง (reference) ในตัวอย่างนี้ มีสองการอ้างอิงไปที่ออบเจ๊คต์เดียวกัน
ออบเจ๊คต์ที่มีมากกว่าหนึ่งการอ้างอิง จะมีมากกว่าหนึ่งชื่อ ดังนั้น เราจะเรียกว่า ออบเจ๊คต์ถูกทำสมนาม (aliased)
ถ้าออบเจ๊คต์ที่ถูกทำสมนาม (อ้างถึงได้จากหลายชื่อ) สามารถเปลี่ยนแปลงค่าได้ (mutable) การเปลี่ยนแปลงที่ทำกับชื่อหนึ่ง จะมีผลไปที่ชื่ออื่นๆ ด้วย:
>>> b[0] = 42 >>> a [42, 2, 3]
แม้ว่าพฤติกรรมนี้จะมีประโยชน์ แต่มันก็มีแนวโน้มจะสร้างปัญหาอยู่มาก โดยทั่วไป มันจะปลอดภัยกว่าที่จะหลีกเลี่ยงการทำสมนามกับออบเจ๊คต์ที่เปลี่ยนแปลงค่าได้ (mutable objects)
สำหรับออบเจ๊คต์ที่เปลี่ยนแปลงค่าไม่ได้ เช่น สายอักขระ การทำสมนามจะไม่ได้เป็นปัญหาอะไรมาก ในตัวอย่างนี้:
a = 'banana' b = 'banana'
มันแทบจะไม่ต่างเลยว่า a
และ b
อ้างถึงสายอักขระเดียวกันหรือไม่
เมื่อเราส่งลิสต์เข้าไปให้กับฟังก์ชัน ฟังก์ชันจะได้การอ้างอิงถึงลิสต์ (เป็นการอ้างอิงถึงลิสต์เดิมด้วยชื่อใหม่ ไม่ใช่ได้ลิสต์ใหม่. ดังนั้นถ้าภายในฟังก์ชันมีการแก้ไขค่าของลิสต์ โปรแกรมที่เรียกฟังก์ชันจะเห็นการเปลี่ยนแปลงค่าของลิสต์นี้ด้วย ตัวอย่างเช่น delete_head
ลบอิลิเมนต์แรกออกจากลิสต์:
def delete_head(t): del t[0]
และนี่คือตัวอย่างการเรียกใช้ฟังก์ชัน:
>>> letters = ['a', 'b', 'c'] >>> delete_head(letters) >>> letters ['b', 'c']
พารามิเตอร์ t
(ในฟังก์ชัน) กับตัวแปร letters
(ในโปรแกรมที่เรียกฟังก์ชัน เช่น __main__
) เป็นสมนามของออบเจ๊คต์เดียวกัน แผนภาพกองซ้อนแสดงในรูปที่ 10.5
เนื่องจากลิสต์ถูกอ้างถึงจากทั้งสองตัวแปร ในภาพจึงวาดให้ลิสต์อยู่ระหว่างทั้งสองตัว
มันสำคัญที่จะรู้ความแตกต่างระหว่าง การดำเนินการที่แก้ไขลิสต์ และ การดำเนินการที่สร้างลิสต์ใหม่ ตัวอย่าง เมธอด append
แก้ไขลิสต์ แต่ตัวดำเนินการ +
สร้างลิสต์ใหม่
ตัวอย่างการใช้ append
:
>>> t1 = [1, 2] >>> t2 = t1.append(3) >>> t1 [1, 2, 3] >>> t2 None
ค่าที่ให้ออกมาจาก append
คือ None
ตัวอย่างการใช้ตัวดำเนินการ +
:
>>> t3 = t1 + [4] >>> t1 [1, 2, 3] >>> t3 [1, 2, 3, 4]
ผลลัพธ์จากตัวดำเนินการจะเป็นลิสต์ใหม่ และลิสต์เดิมไม่ได้เปลี่ยนอะไรไป
ความต่างนี้สำคัญมาก ถ้าเราเขียนฟังก์ชันที่อาจมีการแก้ไขลิสต์ ตัวอย่าง ฟังก์ชันข้างล่างนี้ไม่ได้ ลบหัวของลิสต์ออก:
def bad_delete_head(t): t = t[1:] # WRONG!
ตัวดำเนินการตัดลิสต์ (slice operator) สร้างลิสต์ใหม่ขึ้นมา และการกำหนดค่าได้กำหนดให้ตัวแปร t
อ้างถึงลิสต์ใหม่นี้ แต่ทั้งหมดนี้ไม่ได้มีผลกับโปรแกรมที่เรียกฟังก์ชันนี้เลย
>>> t4 = [1, 2, 3] >>> bad_delete_head(t4) >>> t4 [1, 2, 3]
ในตอนเริ่มต้นฟังก์ชัน bad_delete_head
ตัวแปร t
(ในฟังก์ชัน) และตัวแปร t4
(ในโปรแกรมหลัก) อ้างถึงลิสต์เดียวกัน แต่ตอนท้าย ตัวแปร t
อ้างถึงลิสต์ใหม่ ในขณะที่ตัวแปร t4
ยังอ้างถึงลิสต์เดิมอยู่ ลิสต์เดิมที่ไม่ได้ถูกแก้ไข
วิธีที่ดีกว่า คือ เขียนฟังก์ชันที่สร้างและให้ค่าของลิสต์ใหม่ออกมา ตัวอย่างเช่น ฟังก์ชัน tail
ให้ค่าของลิสต์ออกมาทั้งหมด ยกเว้นอิลิเมนต์แรกสุด:
def tail(t): return t[1:]
ฟังก์ชันนี้ก็ไม่ได้เปลี่ยนลิสต์ดั่งเดิม วิธีเรียกใช้มันคือ:
>>> letters = ['a', 'b', 'c'] >>> rest = tail(letters) >>> rest ['b', 'c']
การใช้ลิสต์อย่างไม่ระวัง (รวมถึงออบเจ๊คต์ที่สามารถแก้ไขค่าได้ชนิดอื่นๆ ด้วย) อาจนำไปสู่ปัญหาที่ใช้เวลานานมากในการดีบัก ต่อไปนี้เป็นตัวอย่างของความผิดพลาดที่พบบ่อย และวิธีที่จะหลีกเลี่ยง:
1. เมธอดของลิสต์เกือบทั้งหมดแก้ค่าในอาร์กิวเมนต์ และมักส่งค่า None
ออกมา (return None
) พฤติกรรมนี้จะต่างจากเมธอดของสายอักขระ ที่มักส่งสายอักขระใหม่ออกมา โดยไม่ไปยุ่งกับสายอักขระเดิม
ถ้าเคยเขียนโปรแกรมแบบนี้:
word = word.strip()
มันอาจจะมีแนวโน้มที่จะเขียนโปรแกรมกับลิสต์แบบนี้:
t = t.sort() # WRONG!
แต่เมธอด sort
ส่งค่า None
ออกมา หลังจากนั้น ไม่ว่าเราจะทำอะไรกับตัวแปร t
ก็ไม่น่าจะได้เรื่องอะไร
ก่อนจะใช้เมธอดหรือตัวดำเนินการใดๆ ของลิสต์ ให้อ่านเอกสารให้ถี่ถ้วน และทดสอบเมธอดหรือตัวดำเนินการเหล่านั้น ในการทำงานแบบโต้ตอบ (interactive mode) ก่อน
2. เลือกรูปแบบการเขียนและยึดติดกับมัน
ส่วนหนึ่งของปัญหาของการทำงานกับลิสต์ คือ มีวิธีที่จะทำงานหลายวิธีมาก ตัวอย่างเช่น การลบอิลิเมนต์จากลิสต์ เราสามารถใช้ pop
หรือ remove
หรือ del
หรือแม้แต่จะใช้การกำหนดค่าและการตัดลิสต์ (slice assignment)
การเพิ่มอิลิเมนต์เอง เราก็สามารถใช้เมธอด append
หรือตัวดำเนินการ +
สมมติว่า t
เป็นลิสต์และ x
เป็นสมาชิกของลิสต์ วิธีเพิ่มอิลิเมนต์ข้างล่างนี้ถูกต้อง:
t.append(x) t = t + [x] t += [x]
แต่วิธีข้างล่างนี้ผิด:
t.append([x]) # WRONG! t = t.append(x) # WRONG! t + [x] # WRONG! t = t + x # WRONG!
ลองตัวอย่างแต่ละอันในการทำงานแบบโต้ตอบ เพื่อให้แน่ใจว่าเข้าใจการทำงานของมันก่อน สังเกตว่า มีเฉพาะตัวอย่างสุดท้าย (t = t + x
) ที่ให้ runtime error
ออกมา อีกสามตัวอย่างข้างต้น แม้ว่าไม่ได้ให้ runtime error
ออกมา แต่มันทำงานผิดจากที่เราต้องการ
3. คัดลอก (copy) เพื่อเลี่ยงปัญหาจากการทำสมนาม
เช่น ถ้าหากเราต้องการใช้เมธอดอย่าง sort
ที่แก้ไขข้อมูลของลิสต์ แต่ถ้าเราต้องการเก็บข้อมูลเดิมของลิสต์ไว้ด้วย เราสามารถใช้การคัดลอกทำสำเนาไว้ได้
>>> t = [3, 1, 2] >>> t2 = t[:] >>> t2.sort() >>> t [3, 1, 2] >>> t2 [1, 2, 3]
ตัวอย่างนี้ เราสามารถใช้ฟังก์ชันสำเร็จที่มีอยู่ในตัว sorted
ก็ได้ ซึ่งฟังก์ชันนี้ให้ค่าลิสต์ใหม่ที่จัดเรียงแล้วออกมา โดยไม่ไปเปลี่ยนแปลงลิสต์เดิม
>>> t2 = sorted(t) >>> t [3, 1, 2] >>> t2 [1, 2, 3]
+=
ผู้อ่านสามารถดาวน์โหลดเฉลยของแบบฝึกหัดเหล่านี้ได้จาก http://thinkpython2.com/code/list_exercises.py
แบบฝึกหัด 1
จงเขียนฟังก์ชัน ชื่อ nested_sum
ที่รับลิสต์ของลิสต์ของเลขจำนวนเต็ม และบวกค่าอิลิเมนต์จากทุกลิสต์ซ้อนในทั้งหมด ตัวอย่าง:
>>> t = [[1, 2], [3], [4, 5, 6]] >>> nested_sum(t) 21
แบบฝึกหัด 2
จงเขียนฟังก์ชัน ชื่อ cumsum
ที่รับลิสต์ของตัวเลข และให้ค่าผลบวกสะสมออกมา นั่นคือ ลิสต์ใหม่ที่เป็นผลลัพธ์มีอิลิเมนต์ที่ $i$ เป็นผลรวมของอิลิเมนต์ $i+1$ ตัวแรกของลิสต์ต้นฉบับ ตัวอย่าง:
>>> t = [1, 2, 3] >>> cumsum(t) [1, 3, 6]
แบบฝึกหัด 3
จงเขียนฟังก์ชัน ชื่อ middle
ที่รับลิสต์ และให้ค่าลิสต์ใหม่ออกมา โดยที่ลิสต์ใหม่นั้นมีอิลิเมนต์อื่นๆ เหมือนกับลิสต์ที่ใส่เข้าไป แต่ไม่มีอิลิเมนต์แรก ไม่มีอิลิเมนต์สุดท้าย ตัวอย่าง:
>>> t = [1, 2, 3, 4] >>> middle(t) [2, 3]
แบบฝึกหัด 4
จงเขียนฟังก์ชัน ชื่อ chop
ที่รับลิสต์ แล้วไปแก้ไขค่าของมัน โดยลบอิลิเมนต์แรกสุด และลบอิลิเมนต์ท้ายสุดออก ฟังก์ชันนี้ให้ค่า None
ออกมา ตัวอย่าง:
>>> t = [1, 2, 3, 4] >>> chop(t) >>> t [2, 3]
แบบฝึกหัด 5
จงเขียนฟังก์ชัน ชื่อ is_sorted
ที่รับลิสต์ และส่งค่า True
ออกมา ถ้าลิสต์ถูกเรียงลำดับจากน้อยไปมาก และส่งค่า False
ออกมา ถ้าไม่ใช่ ตัวอย่าง:
>>> is_sorted([1, 2, 2]) True >>> is_sorted(['b', 'a']) False
แบบฝึกหัด 6
คำสองคำจะเรียกว่าเป็น อนาแกรม (anagrams) ถ้าเราสามารถเรียงตัวอักษรในคำหนึ่งให้สะกดเป็นอีกคำได้ จงเขียนฟังก์ชัน ชื่อ is_anagram
ที่รับสายอักขระสองสาย และให้ค่า True
ออกมา ถ้าสายอักขระทั้งสองเป็นอนาแกรม
แบบฝึกหัด 7
จงเขียนฟังก์ชัน ชื่อ has_duplicates
ที่รับลิสต์ และให้ค่า True
ออกมา ถ้ามีอิลิเมนต์ในลิสต์ที่ปรากฏมากกว่าหนึ่งครั้ง ฟังก์ชันนี้ไม่เปลี่ยนแปลงค่าลิสต์ต้นฉบับ
แบบฝึกหัด 8
แบบฝึกหัดนี้เกี่ยวกับ ปฏิทรรศน์วันเกิด (Birthday Paradox ซึ่งศึกษาเพิ่มเติมได้จาก http://en.wikipedia.org/wiki/Birthday_paradox)
ถ้ามีนักเรียนในห้อง 23 คน มีโอกาสที่จะมีนักเรียนสองคนที่วันเกิดตรงกันเป็นเท่าไร จงประมาณความน่าจะเป็น โดยสร้างตัวอย่างสุ่มของวันเกิด 23 วัน และตรวจสอบว่ามีวันตรงกันหรือไม่ คำใบ้: เราสามารถสร้างตัวอย่างวันเกิดสุ่มได้ด้วยฟังก์ชัน randint
ในโมดูล random
เฉลยดาวน์โหลดได้จาก http://thinkpython2.com/code/birthday.py
แบบฝึกหัด 9
จงเขียนฟังก์ชันที่อ่านไฟล์ words.txt
(ดาวน์โหลดไฟล์ได้จาก http://greenteapress.com/thinkpython2/code/words.txt) และสร้างลิสต์ที่แต่ละอิลิเมนต์เป็นแต่ละคำในไฟล์ เขียนฟังก์ชันเป็นสองแบบ แบบแรกใช้เมธอด append
และอีกแบบใช้ t = t + [x]
แบบไหนใช้เวลารันนานกว่า? ทำไม?
เฉลย: http://thinkpython2.com/code/wordlist.py
แบบฝึกหัด 10
เพื่อตรวจสอบว่าคำอยู่ในลิสต์ของคำหรือไม่ เราควรจะใช้ตัวดำเนินการ in
แต่มันจะทำงานช้า เพราะว่ามันตรวจโดยการค้นหาทีละอิลิเมนต์ตามลำดับ
ถ้าเรารู้ว่าคำในลิสต์เรียงตามลำดับตัวอักษระอยู่แล้ว เราสามารถทำให้การค้นหาเร็วขึ้นได้ ด้วยวิธีค้นหาแบ่งสองส่วน (bisection search หรืออีกชื่อ binary search) วิธีค้นหาแบ่งสองส่วน คล้ายกับวิธีที่เราทำ เวลาที่เราหาคำในพจนานุกรม เราเริ่มที่ตรงกลาง และดูว่าคำที่เราหามาก่อนหรือหลังคำที่ตรงกลางลิสต์ ถ้าคำที่หามาก่อน ก็ให้ไปหาในครึ่งหน้าด้วยแนวทางนี้อีก ถ้าคำที่หามาหลัง ก็ให้ไปหาในครึ่งหลัง ไม่ว่าอย่างไร เราก็ลด ปริภูมิค้นหา (search space)ลงไปครึ่งหนึ่ง ถ้าในลิสต์ของคำมีคำอยู่ 113,809 คำ มันจะใช้แค่ประมาณ 17 ขั้น เพื่อหาคำให้เจอ หรือเพื่อบอกว่าคำนั้นไม่อยู่ในลิสต์
จงเขียนฟังก์ชัน in_bisect
ที่รับลิสต์ที่เรียงลำดับไว้แล้ว กับรับค่าที่ต้องการค้นหา แล้วส่งดัชนีของค่าที่หาออกมาถ้าลิสต์มีค่านั้นอยู่ หรือส่งค่า None
ออกมา ถ้าลิสต์ไม่มีค่าที่หา
หรือ อ่านเอกสารของโมดูล bisect
และเรียกใช้! เฉลย: http://thinkpython2.com/code/inlist.py
แบบฝึกหัด 11
คำสองคำเป็น “คู่กลับ” (reverse pair) ถ้าคำหนึ่งเป็นคำเรียงกลับของอีกคำหนึ่ง จงเขียนโปรแกรมที่หาคู่กลับทั้งหมดในลิสต์ของคำ
เฉลย: http://thinkpython2.com/code/reverse_pair.py
แบบฝึกหัด 12
คำสองคำจะเรียกว่า “เกี่ยวติดกัน” (interlock) ถ้านำอักษรจากแต่ละคำมาเรียงสลับกันแล้วได้คำใหม่ ตัวอย่างเช่น “shoe” และ “cold” เกี่ยวติดกันแล้วได้คำใหม่คือ “schooled”
เฉลย: http://thinkpython2.com/code/interlock.py ขอบคุณ:แบบฝึกหัดนี้ได้รับแรงบันดาลใจจากตัวอย่างใน http://puzzlers.org
words.txt
ได้) จงเขียนโปรแกรมที่หาทุกคู่ของคำในลิสต์ ที่เกี่ยวติดกันเป็นคำใหม่ ที่ก็อยู่ในลิสต์ คำใบ้: ไม่ต้องนับทุกคู่ (เราอาจหาคำที่เกี่ยวติดกันจากคำสองคู่ว่าอยู่ในลิสต์หรือไม่ หรืออาจดูว่าคำในลิสต์ว่าแยกออกมาเป็นคำสองคำอะไร)https://greenteapress.com/thinkpython2/html/thinkpython2011.html