บทนี้แนะนำแนวคิดของ โปรแกรม “คงอยู่” ที่เป็นข้อมูลไว้ใน หน่วยเก็บถาวร พร้อมแสดงวิธีใช้หน่วยเก็บถาวรต่างๆ เช่น ไฟล์ และฐานข้อมูล
โปรแกรมเกือบทั้งหมดที่เราได้ดูกันไป เป็นลักษณะชั่วคราว คือ มันรันแล้วก็ให้เอาต์พุตออกมา แต่พอทำงานเสร็จ ข้อมูลก็หายไป ถ้าเรารันโปรแกรมใหม่ มันก็เริ่มต้นใหม่ทุกอย่าง
โปรแกรมบางอย่างเป็นลักษณะคงอยู่ (persistent) นั่นคือ มันทำงานนานมาก (หรืออาจจะทำงานตลอดเวลา) และมันเก็บข้อมูล หรืออย่างน้อย ก็บางส่วนของข้อมูล ลงในหน่วยเก็บถาวร (เช่น ฮาร์ดดิสก์) แล้วถ้าปิดหรือเริ่มโปรแกรมพวกนี้ใหม่ มันจะไปเริ่มจากสถานะที่เก็บไว้ได้
ตัวอย่างของโปรแกรมคงอยู่ต่างๆ ก็คือ พวกระบบปฏิบัติการ ที่ทำงานเกือบๆ ตลอดเวลาที่คอมพิวเตอร์เปิด และก็พวกเวปเซอร์เวอร์ที่ทำงานตลอดเวลา คอยรับคำร้องที่จะมาจากเครือข่าย
วิธีหนึ่งที่ง่ายที่สุดที่โปรแกรม จะเก็บรักษาข้อมูลของมันได้ คือ อ่านและเขียนไฟล์ข้อความ เราได้เห็นโปรแกรมที่อ่านไฟล์ข้อความไปแล้ว บทนี้ เราจะได้เห็นโปรแกรมที่เขียนไฟล์ข้อความด้วย
นอกจากไฟล์ข้อความแล้ว เราอาจจะเก็บสถานะของโปรแกรมไว้ในฐานข้อมูล (database) ก็ได้ บทนี้ เราจะดูฐานข้อมูลที่เรียบง่ายตัวหนึ่ง และดูโมดูล pickle
ที่ช่วยให้เก็บข้อมูลของโปรแกรมได้ง่ายขึ้น
ไฟล์ข้อความ (text file) เป็นลำดับของอักขระต่างๆ ที่เก็บในสื่อถาวระ เช่น ฮาร์ดดิสก์ หน่วยความจำแฟลช หรือซีดีรอม เราได้เห็นวิธีเปิดและอ่านไฟล์มาแล้ว จากหัวข้อ 9.1
เพื่อจะเขียนไฟล์ เราต้องเปิดมัน ด้วยโหมด 'w'
ที่ระบุด้วยพารามิเตอร์ที่สอง:
>>> fout = open('output.txt', 'w')
ถ้าไฟล์ที่เปิดมีอยู่แล้ว การไปเปิดมันในโหลดเขียน จะเท่าไปลบข้อมูลเก่าออกทั้งหมด และเริ่มต้นใหม่ เพราะฉะนั้นก็ระวังด้วย! ถ้าไฟล์ที่เปิดยังไม่มีอยู่ ไฟล์ใหม่จะถูกสร้างขึ้นมา
ฟังก์ชัน open
ให้ไฟล์ออบเจ๊คต์ (file object) ออกมา โดย ไฟล์ออบเจ๊คต์นี้จะมีเมธอดต่างๆ ที่เอาไว้ใช้ทำงานกับไฟล์ เมธอด write
ใช้เขียนข้อมูลเข้าไปในไฟล์
>>> line1 = "This here's the wattle,\n" >>> fout.write(line1) 24
ค่าที่คืนออกมา เป็นจำนวนอักขระที่ได้เขียนเข้าไป ไฟล์ออบเจ๊คต์ จะเก็บตำแหน่งว่ามันทำงานถึงตรงไหนในไฟล์ ดังนั้น ถ้าเราเรียก write
อีกครั้ง มันจะเพิ่มข้อมูลใหม่ต่อเข้าไปที่ท้ายไฟล์
>>> line2 = "the emblem of our land.\n" >>> fout.write(line2) 24
พอเขียนข้อมูลเข้าไปเสร็จหมดแล้ว ควรที่จะปิดไฟล์
>>> fout.close()
ถ้าเราไม่ได้ปิดไฟล์ มันจะปิดเอง ตอนที่โปรแกรมจบ
อาร์กิวเมนต์ของ write
ต้องเป็นข้อมูลแบบสายอักขระ ดังนั้น ถ้าเราต้องการใส่ค่าชนิดอื่นๆ เข้าไปในไฟล์ เราต้องแปลงมันเป็นชนิดสายอักขระก่อน วิธีที่ง่ายที่สุดคือ ใช้ str
:
>>> x = 52 >>> fout.write(str(x))
อีกวิธีหนึ่งก็คือ การใช้ ตัวดำเนินการจัดรูปแบบ (format operator) คือ %
เมื่อใช้กับตัวเลขจำนวนจริง ตัวดำเนินการ %
ทำหน้าที่เป็นตัวดำเนินการมอดุลัส แต่ถ้าตัวถูกดำเนินการ ตัวแรก เป็นสายอักขระ ตัวดำเนินการ %
จะทำหน้าที่เป็นตัวดำเนินการจัดรูปแบบ
ตัวถูกดำเนินการ ตัวแรก เป็นสายอักขระจัดรูปแบบ (format string) ที่มีชุดจัดรูปแบบ (format sequence) ตั้งแต่หนึ่งหรือมากกว่า โดย ชุดจัดรูปแบบ จะระบุว่า ตัวถูกดำเนินการตัวที่สอง จะถูกนำไปจัดรูปแบบเป็นสายอักขระอย่างไร ผลลัพธ์ของการดำเนินการ คือสายอักขระ
ตัวอย่าง ชุดจัดรูปแบบ '%d'
หมายถึง ตัวถูกดำเนินการ ตัวที่สอง ควรจะถูกจัดรูปแบบ สายอักขระเป็นเลขจำนวนเต็มฐานสิบ:
>>> camels = 42 >>> '%d' % camels '42'
ผลลัพธ์คือ สายอักขระ '42'
ที่ไม่ใช่ค่าจำนวนเต็ม 42
ชุดจัดรูปแบบ จะอยู่ตรงไหนในสายอักขระก็ได้ ดังนั้น เราสามารถฝั่งค่าเข้าไปในประโยคได้:
>>> 'I have spotted %d camels.' % camels 'I have spotted 42 camels.'
ถ้ามีชุดจัดรูปแบบ อยู่ในสายอักขระ มากกว่าหนึ่งชุด ตัวถูกดำเนินการที่สอง ต้องเป็นทูเพิล แต่ละชุดจัดรูปแบบ จะคู่กับแต่ละอิลิเมนต์ของทูเพิล ตามลำดับ
ตัวอย่างต่อไปนี้ ใช้ชุด '%d'
เพื่อจัดรูปแบบสายอักขระ เป็นตัวเลขจำนวนเต็ม ชุด '%g'
ใช้จัดรูปแบบ เป็นเลขทศนิยม และชุด '%s'
ใช้จัดรูปแบบ เป็นสายอักขระ:
>>> 'In %d years I have spotted %g %s.' % (3, 0.1, 'camels') 'In 3 years I have spotted 0.1 camels.'
จำนวนอิลิเมนต์ในทูเพิล ต้องเท่ากับ จำนวนชุดจัดรูปแบบในสายอักขระจัดรูปแบบ และชนิดของอิลิเมนต์ ก็ยังต้องถูกชนิดกับชุดจัดรูปแบบที่คู่กันด้วย
>>> '%d %d %d' % (1, 2) TypeError: not enough arguments for format string >>> '%d' % 'dollars' TypeError: %d format: a number is required, not str
ในตัวอย่างแรก จำนวนอิลิเมนต์ของทูเพิลมีไม่พอ ในตัวอย่างที่สอง อิลิเมนต์ผิดชนิด
สำหรับรายละเอียดเพิ่มเติม ของตัวดำเนินการจัดรูปแบบ ดู
https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting วิธีจัดรูปแบบที่ดีกว่านี้ คือ การใช้เมธอด format
ของสายอักขระ ซึ่งสามารถศึกษาได้จาก https://docs.python.org/3/library/stdtypes.html#str.format
ไฟล์ต่างๆ ถูกจัดระเบียบเข้าไปอยู่ใน ไดเรกทอรี ต่างๆ (directories) ซึ่งจะเรียก “โฟลเดอร์” (folder) ก็ได้ ทุกๆ โปรแกรมที่ทำงานอยู่ จะมี “ไดเรกทอรีปัจจุบัน” (current directory) ที่เป็นไดเรกทอรีดีฟอลท์ สำหรับการดำเนินการเกือบทั้งหมด ตัวอย่าง เวลาที่เราเปิดไฟล์เพื่ออ่าน ไพธอนจะหาไฟล์ในไดเรกทอรีปัจจุบัน
โมดูล os
มีฟังก์ชันต่างๆ ที่ใช้ทำงานกับไฟล์และไดเรกทอรีได้ (“os” ย่อมาจาก “operating system” ซึ่งหมายถึง ระบบปฏิบัติการ) ฟังก์ชัน os.getcwd
จะให้ชื่อของไดเรกทอรีปัจจุบันออกมา:
>>> import os >>> cwd = os.getcwd() >>> cwd '/home/dinsdale'
ส่วนของชื่อ cwd
ย่อจาก “current working directory” หมายถึง ไดเรกทอรีที่ทำงานอยู่ปัจจุบัน ผลลัพธ์ของตัวอย่างนี้คือ /home/dinsdale
ที่เป็นไดเรกทอรี บ้านของผู้ใช้ ชื่อ dinsdale
สายอักขระ เช่น '/home/dinsdale'
ที่ระบุไฟล์ หรือไดเรกทอรี จะเรียกว่า เส้นทาง (path).
ชื่อไฟล์ง่ายๆ เช่น memo.txt
ก็ถือว่าเป็นเส้นทางด้วยเหมือนกัน แต่อันนี้เรียกว่าเป็น เส้นทางสัมพัทธ์ (relative path) เพราะว่า มันสัมพันธ์กับไดเรกทอรีปัจจุบัน ถ้าไดเรกทอรีปัจจุบัน คือ /home/dinsdale
ชื่อไฟล์ memo.txt
จะหมายถึง /home/dinsdale/memo.txt
สำหรับบางระบบปฏิบัติการ เช่น ยูนิกซ์ เส้นทางที่เริ่มด้วย /
จะไม่ขึ้นกับไดเรกทอรีปัจจุบัน เส้นทางแบบนี้ จะเรียกว่า เส้นทางสัมบูรณ์ (absolute path) สำหรับระบบปฎิบัติการวินโดวส์ เส้นทางสัมบูรณ์ จะเริ่มต้นด้วยตัวอักษรหนึ่งตัว และตามด้วยเครื่องหมาย :
เช่น D:\\Users\\dinsdale\\memo.txt
เพื่อหาเส้นทางสัมบูรณ์ของไฟล์ เราสามารถใช้ os.path.abspath
:
>>> os.path.abspath('memo.txt') '/home/dinsdale/memo.txt'
โมดูล os.path
มีฟังก์ชันอื่นๆ ที่ใช้ทำงานกับชื่อไฟล์และเส้นทางต่างๆ ตัวอย่าง ฟังก์ชัน os.path.exists
ใช้ตรวจสอบว่า ไฟล์ หรือไดเรกทอรี มีอยู่หรือไม่:
>>> os.path.exists('memo.txt') True
ถ้ามีอยู่ ลองใช้ os.path.isdir
ตรวจสอบดูว่า มันเป็นไดเรกทอรีหรือไม่:
>>> os.path.isdir('memo.txt') False >>> os.path.isdir('/home/dinsdale') True
คล้ายๆ กัน ฟังก์ชัน os.path.isfile
ใช้ตรวจสอบว่า มันเป็นไฟล์หรือไม่
ฟังก์ชัน os.listdir
จะให้ ลิสต์ของชื่อไฟล์ และไดเรกทอรีต่างๆ ที่อยู่ภายใต้ไดเรกทอรีที่เป็นอาร์กิวเมนต์ ออกมา:
>>> os.listdir(cwd) ['music', 'photos', 'memo.txt']
เพื่อสาธิตการใช้งานฟังก์ชันเหล่านี้ ตัวอย่างต่อไปนี้ ท่องเข้าไปในไดเรกทอรี แล้วพิมพ์ชื่อของทุกไฟล์ออกมา และเวียนเรียกตัวเองซ้ำสำหรับทุกๆ ไดเรกทอรีภายใน
def walk(dirname): for name in os.listdir(dirname): path = os.path.join(dirname, name) if os.path.isfile(path): print(path) else: walk(path)
ฟังก์ชัน os.path.join
รับชื่อไดเรกทอรี และชื่อไฟล์ แล้วนำไปต่อกัน เพื่อเป็นเส้นทางที่สมบูรณ์
โมดูล os
มีฟังก์ชันชื่อ walk
ที่คล้ายๆ กับ ฟังก์ชันนี้ แต่ทำงานได้หลากหลายกว่า เพื่อเป็นการฝึก ลองอ่านเอกสาร และใช้ฟังก์ชัน os.walk
พิมพ์ ชื่อของไฟล์และไดเรกทอรีย่อยต่างๆ ของไดเรกทอรีที่ระบุ เฉลย:สามารถดาวน์โหลดได้จาก http://thinkpython2.com/code/walk.py
เวลาที่เราอ่านหรือเขียนไฟล์ อาจมีปัญหาเกิดขึ้นได้หลายอย่าง ถ้าเราพยายามจะเปิดไฟล์ที่ไม่มีอยู่ เราจะได้ IOError
:
>>> fin = open('bad_file') IOError: [Errno 2] No such file or directory: 'bad_file'
ถ้าเราไม่มีสิทธิหรือไม่ได้รับอนุญาต (permission) ในการเข้าถึงไฟล์ เราจะเห็น
>>> fout = open('/etc/passwd', 'w') PermissionError: [Errno 13] Permission denied: '/etc/passwd'
หรือ ถ้าเราไปเปิดไดเรกทอรี (directory) เพื่อจะอ่าน เราจะได้
>>> fin = open('/home') IsADirectoryError: [Errno 21] Is a directory: '/home'
เพื่อไม่ให้เกิดข้อผิดพลาดแบบนี้ เราควรใช้ฟังก์ชัน เช่น os.path.exists
และ os.path.isfile
แต่มันก็ใช้ทั้งเวลาและโค้ดจำนวนมากในการตรวจสอบทุกอย่างที่อาจจะผิดพลาดได้ (ถ้า Errno 21
จะบอกอะไรสักอย่าง มันบอกว่า มีอย่างน้อย 21 อย่างที่อาจจะผิดพลาดได้)
มันดีกว่าที่เราจะลองทำไปเลย (และค่อยไปจัดการกับปัญหา ถ้ามันเกิด) ซึ่งนึ่งคือสิ่งที่ข้อความคำสั่ง try
ไวยากรณ์ของ try
จะคล้ายๆ กับไวยากรณ์ของ if...else
เช่น
try: fin = open('bad_file') except: print('Something went wrong.')
ไพธอนจะเริ่มด้วยการรันคำสั่งต่างๆ ในส่วนของ try
ถ้าทุกอย่างๆ ไปได้ดี ไพธอนจะข้ามส่วนของคำสั่ง except
ไปทำคำสั่งหลังจากนั้น แต่ถ้ามีข้อผิดพลาด หรือมีเอ็กเซ็ปชั่น (exception) เกิดขึ้น ไพธอนจะออกจากส่วนของ try
และไปรันคำสั่งในส่วนของ except
การจัดการกับเอ็กเซ็ปชั่น ด้วยคำสั่ง try
จะเรียกว่า การจับเอ็กเซ็ปชั่น (catching exception) ในตัวอย่างนี้ ตัวคำสั่งในส่วน except
ทำการพิมพ์ข้อความแจ้งข้อผิดพลาด ที่ไม่มีรายละเอียดอะไรมาก โดยทั่วไปแล้ว การจับเอ็กเซ็ปชั่นจะให้โอกาสเราในการแก้ปัญหา หรือจะลองทำอีกครั้ง หรืออย่างน้อยก็ให้โอกาสเราได้จบโปรแกรมไปอย่างเรียบร้อย
ฐานข้อมูล (database) เป็นไฟล์ที่ได้รับการจัดระเบียบไว้สำหรับการเก็บข้อมูล ฐานข้อมูลหลายๆ ชนิดถูกจัดในลักษณะคล้ายๆ กับดิกชันนารี ในแง่ที่ว่า มันจะแปลงจากกุญแจไปสู่ค่าที่คู่กับกุญแจ จุดต่างที่สำคัญที่สุดระหว่างฐานข้อมูลกับดิกชันนารี อยู่ที่ฮาร์ดดิสก์ (หรืออาจจะเป็นแหล่งเก็บข้อมูลถาวรอื่นๆ) นั่นคือ ข้อมูลในฐานข้อมูลจะยังคงอยู่ได้ แม้หลังจากโปรแกรมจบไปแล้ว
โมดูล dbm
มีฟังก์ชันสำหรับสร้างและแก้ไขไฟล์ฐานข้อมูลให้ ตัวอย่างเช่น เราจะสร้างฐานข้อมูลเพื่อเก็บคำบรรยายไฟล์รูปภาพต่างๆ
การเปิดฐานข้อมูล ก็คล้ายกับการเปิดไฟล์อื่นๆ:
>>> import dbm >>> db = dbm.open('captions', 'c')
อาร์กิวเมนต์ 'c'
บอกภาวะ (mode) ว่า ฐานข้อมูลควรจะถูกสร้างขึ้นมา ถ้ามันยังไม่มีอยู่ ผลลัพธ์คือ ออปเจ๊คต์ฐานข้อมูล ที่สามารถจะถูกใช้ได้คล้ายกับดิกชันนารี (สำหรับการดำเนินการต่างๆ ส่วนใหญ่)
เมื่อเราสร้างรายการขึ้นมาใหม่ dbm
ก็จะปรับปรุงไฟล์ฐานข้อมูลไปด้วย
>>> db['cleese.png'] = 'Photo of John Cleese.'
เมื่อเราเข้าถึงรายการใดก็ตาม dbm
ก็ต้องอ่านไฟล์:
>>> db['cleese.png'] b'Photo of John Cleese.'
ผลลัพธ์จะออกมาเป็น ไบต์ออปเจ๊คต์ (bytes object) ที่ระบุโดยมี b
ขึ้นต้น ไบต์ออปเจ๊คต์ จะคล้ายกับสายอักขระในหลายๆ แง่ เวลาที่เราศึกษาไพธอนลึกลงไป ความแตกต่างระหว่างไบต์ออปเจ๊คต์กับสายอักขระจะสำคัญมาก แต่ตอนนี้ เรายังไม่ต้องไปสนใจมันก่อน
ถ้าเรากำหนดค่าใหม่ให้กับกุญแจเดิม dbm
ก็จะเอาค่าใหม่ไปเขียนทับค่าเดิม:
>>> db['cleese.png'] = 'Photo of John Cleese doing a silly walk.' >>> db['cleese.png'] b'Photo of John Cleese doing a silly walk.'
เมธอดของดิกชันนารีบางเมธอด เช่น keys
และ items
ใช้ไม่ได้กับออปเจ๊คต์ฐานข้อมูล แต่การวนซ้ำด้วยลูป for
ใช้ได้อยู่:
for key in db: print(key, db[key])
เหมือนกับไฟล์อื่นๆ เราควรจะปิดฐานข้อมูลเมื่อเราใช้งานเสร็จ:
>>> db.close()
ข้อจำกัดของ dbm
คือ ทั้งกุญแจและค่าคู่กุญแจ จะต้องเป็นสายอักขระ หรือไบต์ออปเจ๊คต์ ถ้าเราลองใช้ข้อมูลชนิดอื่น เราจะได้เห็นข้อความแจ้งข้อผิดพลาดออกมา
โมดูล pickle
สามารถช่วยได้ มันจะแปลงข้อมูลชนิดอื่นๆ เกือบทุกชนิด ไปเป็นสายอักขระ เพื่อให้เหมาะกับการเก็บในฐานข้อมูล (เรียกว่า เป็นการทำพิกเกิล ซึ่งมาจาก pickling ในภาษาอังกฤษที่หมายถึง การดอง) และ โมดูล pickle
ก็สามารถช่วยแปลงจากสายอักขระเหล่านั้นกลับมาเป็นข้อมูลเดิม เวลาเรียกใช้ภายหลัง
ฟังก์ชัน pickle.dumps
รับออปเจ๊คต์ชนิดใดๆ เป็นพารามิเตอร์ และรีเทิร์นสายอักขระที่เป็นตัวแทนออกมาให้ (dumps
ย่อมาจาก “dump string”):
>>> import pickle >>> t = [1, 2, 3] >>> pickle.dumps(t) b'\x80\x03]q\x00(K\x01K\x02K\x03e.'
รูปแบบที่แปลงออกมา อาจจะดูยากสำหรับคนอ่าน แต่มันออกแบบมาให้ง่ายสำหรับ pickle
ที่จะอ่านและตีความ ฟังก์ชัน pickle.loads
(“load string”) สร้างออปเจ๊คต์ขึ้นมาใหม่:
>>> t1 = [1, 2, 3] >>> s = pickle.dumps(t1) >>> t2 = pickle.loads(s) >>> t2 [1, 2, 3]
ออปเจ๊คต์ใหม่ที่ได้ ถึงแม้จะมีค่าเหมือนกับออปเจ๊คต์เดิม แต่ มันเป็นออปเจ๊คต์คนละตัวกัน:
>>> t1 == t2 True >>> t1 is t2 False
พูดอีกอย่างก็คือ การทำพิกเกิล (ใช้ pickle.dump
) แล้วทำย้อนพิกเกิล (ใช้ pickle.load
) จะให้ผลเหมือนกับการทำสำเนาของออปเจ๊คต์
เราสามารถใช้ pickle
เพื่อเก็บข้อมูลที่ไม่ใช่สายอักขระ เข้าไปในฐานข้อมูลได้. จริงๆ แล้ว การใช้งาน pickle
ร่วมกับ dbm
นี้ เป็นที่นิยมมาก จนกระทั่งมีการรวมกันเป็นโมดูล ชื่อ shelve
ระบบปฏิบัติการส่วนใหญ่ จะมีส่วนที่ให้ผู้ใช้สามารถสั่งงานได้โดยการพิมพ์คำสั่ง (command-line interface) ซึ่งมักจะเรียกว่า เชลล์ (shell) เชลล์ จะมีคำสั่งต่างๆ ที่สามารถใช้ท่องระบบไฟล์ และสั่งเปิดโปรแกรมต่างๆ ได้ ตัวอย่างเช่น ในระบบปฎิบัติการยูนิกซ์ เราสามารถเปลี่ยนไดเรกทอรีได้ด้วย คำสั่ง cd
เราสามารถแสดงสิ่งที่อยู่ในไดเรกทอรีได้ด้วย คำสั่ง ls
และเราสามารถที่จะสั่งเปิดเว็บเบราว์เซอร์ โดยพิมพ์ (ตัวอย่างเช่น) firefox
โปรแกรมที่เราสามารถเปิดได้ด้วยเชลล์ ก็สามารถเปิดได้ด้วยไพธอน โดยใช้ ไปป์ออปเจ็คต์ (pipe object).
ตัวอย่างเช่น คำสั่งยูนิกซ์ ls -l
โดยทั่วไปจะแสดงไฟล์ต่างๆ ภายใต้ไดเรกทอรีปัจจุบัน โดยแสดงในแบบยาว (มีรายละเอียด) เราสามารถสั่ง ls
ได้จาก os.open
1)
>>> cmd = 'ls -l' >>> fp = os.popen(cmd)
อาร์กิวเมนต์ เป็นสายอักขระที่มีคำสั่งของเชลล์อยู่ ค่าที่ให้กลับออกมาเป็นออปเจ๊คต์ ที่คล้ายๆ กับการเปิดไฟล์ เราสามารถอ่านเอาต์พุตจาก ls
แบบทีละบรรทัดได้ด้วย readline
หรือจะอ่านทีเดียวทั้งหมดด้วย read
:
>>> res = fp.read()
ถ้าทำงานเสร็จแล้ว ก็ปิดไปป์เหมือนกับที่ปิดไฟล์:
>>> stat = fp.close() >>> print(stat) None
ค่าที่รีเทิร์นออกมา จะบอกสถานะสุดท้ายของการทำคำสั่ง ls
และ None
หมายถึงว่า มันจบการทำคำสั่งแบบปกติ คือไม่มีข้อผิดพลาด
ตัวอย่าง ระบบยูนิกซ์ส่วนใหญ่จะมีคำสั่ง md5sum
มาให้ด้วย คำสั่ง md5sum
อ่านเนื้อหาในไฟล์ และคำนวณค่า “เช็คซัม” (checksum) แบบ เอมดีห้า (MD5) ออกมา สามารถอ่านเรื่องเอมดีห้า ได้จาก http://en.wikipedia.org/wiki/Md5 คำสั่งนี้เป็นวิธีที่สะดวกในการตรวจสอบว่าไฟล์สองไฟล์มีเนื้อหาเหมือนกัน ความน่าจะเป็นที่เนื้อหาที่ต่างกันจะให้ค่าเช็คซัม ออกมาเหมือนกัน จะมีค่าน้อยมาก (นั่นคือ โอกาสที่จะซ้ำไม่น่าจะเกิดขึ้นก่อนที่เอกภพจะล่มสลาย)
เราสามารถใช้ไปป์เพื่อรัน md5sum
จากไพธอนได้ และเราจะได้ผลลัพธ์เป็น:
>>> filename = 'book.tex' >>> cmd = 'md5sum ' + filename >>> fp = os.popen(cmd) >>> res = fp.read() >>> stat = fp.close() >>> print(res) 1e0033f0ed0656636de0d75144ba32e0 book.tex >>> print(stat) None
ไฟล์ที่มีโค้ดของไพธอน สามารถที่จะนำเข้ามาเป็นโมดูลได้ ตัวอย่าง เช่น ถ้าเรามีไฟล์ชื่อ wc.py
ที่มีโค้ดดังนี้:
def linecount(filename): count = 0 for line in open(filename): count += 1 return count print(linecount('wc.py'))
ถ้าเรารันโปรแกรมนี้ มันจะอ่านเนื้อหาของตัวมันเอง แล้วพิมพ์จำนวนบรรทัดในไฟล์ เป็น 7 ออกมา เราสามารถนำเข้ามันได้แบบนี้:
>>> import wc 7
ตอนนี้เรามีโมดูลออปเจ๊คต์ (module object) wc
:
>>> wc <module 'wc' from 'wc.py'>
โมดูลออปเจ๊คต์จะมี linecount
:
>>> wc.linecount('wc.py') 7
ตอนนี้เราก็รู้วิธีเขียนโมดูลของไพธอนแล้ว
ปัญหาเดียวที่เห็นในตัวอย่างนี้คือ เมื่อเรานำเข้าโมดูล มันจะรันโค้ดทดสอบที่เขียนอยู่ไว้ด้วย เหมือนที่เราเห็น 7 พิมพ์ออกมา โดยทั่วไปแล้ว ตอนที่เรานำเข้าโมดูล มันจะแค่นิยามฟังก์ชันใหม่ แต่ยังไม่รันมัน
โปรแกรมที่เขียนเพื่อที่จะถูกนำเข้าเป็นโมดูล ส่วนใหญ่จะเขียนด้วยรูปแบบต่อไปนี้:
if __name__ == '__main__': print(linecount('wc.py'))
ตัวแปร __name__
เป็นตัวแปรสำเร็จรูป ที่ไพธอนจะสร้างขึ้น เมื่อเริ่มโปรแกรม ถ้าโปรแกรมถูกรันโดยตรง ตัวแปร __name__
จะมีค่าเป็น '__main__'
ซึ่งในกรณีนั้น โค้ดทดสอบจะถูกรัน ในกรณีที่โมดูลถูกนำเข้า ค่าของตัวแปร __name__
จะไม่เท่ากับ '__main__'
และโค้ดทดสอบจะไม่ถูกรัน
เพื่อเป็นแบบฝึกหัด พิมพ์ตัวอย่างนี้ในไฟล์ ชื่อ wc.py
แล้วลองรันมันโดยตรง (รันสคริปต์ ได้แก่ สั่ง python wc.py
ที่เชลล์) ลองนำเข้ามัน แล้วตรวจสอบค่าของตัวแปร __name__
ของโมดูล wc
คำเตือน ถ้าเรานำเข้าโมดูล ที่ได้นำเข้ามาก่อนแล้ว (โมดูล ชื่อเดียวกัน) ไพธอนจะไม่ทำอะไร มันจะไม่อ่านไฟล์มาใหม่ ถึงแม้ว่าเราได้แก้ไขไฟล์นั้นไปแล้ว
ถ้าเราต้องการนำเข้าโมดูลใหม่ เราสามารถใช้ฟังก์ชันสำเร็จรูป reload
ได้ แต่มันอาจจะใช้ยากนิดหน่อย วิธีที่ง่ายที่สุด คือ ปิดแล้วเปิดอินเตอร์พรีเตอร์ใหม่ แล้วนำเข้าโมดูลอีกครั้ง
เวลาที่เราอ่านหรือเขียนไฟล์ เราอาจจะเจอปัญหากับช่องว่าง ปัญหาเหล่านี้จะดีบักได้ยาก เพราะว่า เรามองตัวช่องว่างเองไม่เห็น ไม่ว่าจะเป็นช่องว่างแบบ การเว้น การย่อหน้า หรือการขึ้นบรรทัดใหม่ เรามองไม่เห็นมันโดยตรง เราเห็นมันผ่านตัวอื่นรอบๆ ข้าง
>>> s = '1 2\t 3\n 4' >>> print(s) 1 2 3 4
ฟังก์ชันสำเร็จรูป repr
อาจช่วยได้ มันจะรับออปเจ๊คต์เป็นอาร์กิวเมนต์ และรีเทิร์นตัวแทนสายอักขระของออปเจ๊คต์นั้นออกมา สำหรับสายอักขระ มันแทนช่องว่างต่างๆ ด้วยชุดแบกสแลช (backslash sequences):
>>> print(repr(s)) '1 2\t 3\n 4'
อันนี้อาจจะเป็นมีประโยชน์ตอนดีบัก
อีกปัญหาที่อาจจะเจอ คือ คอมพิวเตอร์ที่ต่างระบบกัน อาจจะใช้อักขระที่ระบุการขึ้นบรรทัดใหม่ต่างกัน บางระบบใช้การขึ้นบรรทัดใหม่ แทนด้วย \n
บางระบบใช้ \r
บางระบบใช้ทั้งคู่ ถ้าเราย้ายไฟล์ข้ามระบบ ความต่างนี้อาจจะสร้างปัญหาให้ได้
ระบบคอมพิวเตอร์ส่วนใหญ่ มีเครื่องมือช่วยแปลงจากรูปแบบหนึ่งไปแบบอื่นได้ ลองหาเครื่องมือพวกนี้ (และอ่านเพิ่มเติมเกี่ยวกับประเด็นนี้) ที่ http://en.wikipedia.org/wiki/Newline หรือ แน่นอนว่า เราอาจจะลองเขียนโปรแกรมแปลงรูปแบบพวกนี้ขึ้นมาเองก็ได้
%
ที่รับสายอักขระจัดรูปแบบและทูเพิล แล้วสร้างสายอักขระ ที่รวมอิลิเมนต์ต่างๆ ของทูเพิลเข้าไป ในรูปแบบที่ระบุด้วยสายอักขระจัดรูปแบบ%d
ที่ใช้ระบุว่าค่าตัวเลขควรจะถูกจัดรูปแบบการแสดงผลอย่างไรtry
และ except
ช่วย เพื่อป้องกันไม่ให้ เอ็กเซ็ปชั่นหรือข้อผิดพลาดปิดโปรแกรมไปเอง
แบบฝึกหัด 1
จงเขียนฟังก์ชัน ชื่อ sed
ที่รับอาร์กิวเมนต์สี่ตัว คือ สายอักขระรูปแบบ สายอักขระแทนที่ และชื่อไฟล์สองชื่อ. ฟังก์ชันอ่านไฟล์แรก และเขียนเนื้อหาของไฟล์แรกลงในไฟล์ที่สอง (อาจจะสร้างไฟล์ที่สอง ถ้าจำเป็น) ถ้ามีสายอักขระรูปแบบปรากฎอยู่ในเนื้อหาของไฟล์ แทนมันด้วยสายอักขระแทนที่
ถ้ามีข้อผิดพลาดเกิดขึ้นระหว่าง เปิดไฟล์ อ่านไฟล์ เขียนไฟล์ หรือปิดไฟล์ โปรแกรมควรจะสามารถจับเอ็กเซ็ปชั่น แล้วพิมพ์ข้อความแจ้งข้อผิดพลาดและปิดโปรแกรม เฉลย: http://thinkpython2.com/code/sed.py
แบบฝึกหัด 2
ถ้าคุณดาวน์โหลดเฉลยของแบบฝึกหัด แบบฝึกหัด 12.2 จาก http://thinkpython2.com/code/anagram_sets.py คุณจะเห็นว่า มันสร้างดิกชันนารีที่แปลงจาก สายอักขระของตัวอักษรที่เรียงตามลำดับ ไปหาลิสต์ของคำต่างๆ ที่สามารถสะกดได้ด้วยตัวอักษรเหล่านั้น ตัวอย่างเช่น 'opst'
แปลงไปเป็นลิสต์ ['opts', 'post', 'pots', 'spot', 'stop', 'tops']
จงเขียนโมดูลที่นำเข้า anagram_sets
และมีสองฟังก์ชันใหม่ ได้แก่ ฟังก์ชัน store_anagrams
ควรจะมีดิกชันนารีของคำสลับอักษรเก็บไว้ และฟังก์ชัน read_anagrams
เมื่อรับคำมา ควรจะค้นหาคำนั้น และรีเทิร์นลิสต์ของคำสลับอักษรของคำๆ นั้น เฉลย: http://thinkpython2.com/code/anagram_db.py.
แบบฝึกหัด 3
ในการเก็บไฟล์เอ็มพีสาม (MP3 files) จำนวนมากๆ มันอาจจะมีไฟล์ของเพลงซ้ำกัน โดยเก็บที่ไดเรกทอรีต่างกัน หรือใช้ชื่อต่างกัน จุดประสงค์หลัก คือค้นหาเนื้อหาที่ซ้ำกัน
.mp3
) แล้วรีเทิร์นเส้นทางทั้งหมดออกมาเป็นลิสต์ คำใบ้: โมดูล os.path
มีฟังก์ชันหลายๆ อัน ที่มีประโยชน์ในการจัดการชื่อไฟล์และเส้นทางmd5sum
เพื่อคำนวณหาค่า “เช็คซำ” (checksum) ของแต่ละไฟล์ ถ้าสองไฟล์มีค่าเช็คซำเหมือนกัน สองไฟล์นั้นก็น่าจะซ้ำกันdiff
ตรวจไฟล์ที่คิดว่าซ้ำกันอีกทีได้เฉลย: http://thinkpython2.com/code/find_duplicates.py
https://greenteapress.com/thinkpython2/html/thinkpython2015.html
popen
ถูกประกาศเป็น “deprecated” (เก่าและแนะนำให้เลิกใช้) ซึ่ง เราก็ควรจะหยุดใช้มัน และควรจะใช้โมดูล subprocess
ที่ถูกแนะนำให้ใช้แทน แต่สำหรับกรณีง่ายๆ ผม (อัลเลน ดาวนี่) พบว่า subprocess
มันซับซ้อนเกินความจำเป็น ดังนั้น ผมจึงยังใช้ popen
จนกว่าไพธอนจะไม่มีมันให้ใช้อีก