User Tools

Site Tools


python:files

14. ไฟล์

บทนี้แนะนำแนวคิดของ โปรแกรม “คงอยู่” ที่เป็นข้อมูลไว้ใน หน่วยเก็บถาวร พร้อมแสดงวิธีใช้หน่วยเก็บถาวรต่างๆ เช่น ไฟล์ และฐานข้อมูล

14.1 ความคงอยู่

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

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

ตัวอย่างของโปรแกรมคงอยู่ต่างๆ ก็คือ พวกระบบปฏิบัติการ ที่ทำงานเกือบๆ ตลอดเวลาที่คอมพิวเตอร์เปิด และก็พวกเวปเซอร์เวอร์ที่ทำงานตลอดเวลา คอยรับคำร้องที่จะมาจากเครือข่าย

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

นอกจากไฟล์ข้อความแล้ว เราอาจจะเก็บสถานะของโปรแกรมไว้ในฐานข้อมูล (database) ก็ได้ บทนี้ เราจะดูฐานข้อมูลที่เรียบง่ายตัวหนึ่ง และดูโมดูล pickle ที่ช่วยให้เก็บข้อมูลของโปรแกรมได้ง่ายขึ้น

14.2 การอ่าน และการเขียน

ไฟล์ข้อความ (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()

ถ้าเราไม่ได้ปิดไฟล์ มันจะปิดเอง ตอนที่โปรแกรมจบ

14.3 ตัวดำเนินการจัดรูปแบบ

อาร์กิวเมนต์ของ 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

14.4 ชื่อไฟล์และเส้นทาง

ไฟล์ต่างๆ ถูกจัดระเบียบเข้าไปอยู่ใน ไดเรกทอรี ต่างๆ (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

14.5 การจับเอ็กเซ็ปชั่น

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

14.6 ฐานข้อมูล

ฐานข้อมูล (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()

14.7 การทำพิกเกิล

ข้อจำกัดของ 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

14.8 ไปป์

ระบบปฏิบัติการส่วนใหญ่ จะมีส่วนที่ให้ผู้ใช้สามารถสั่งงานได้โดยการพิมพ์คำสั่ง (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

14.9 การเขียนโมดูล

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

14.10 การดีบัก

เวลาที่เราอ่านหรือเขียนไฟล์ เราอาจจะเจอปัญหากับช่องว่าง ปัญหาเหล่านี้จะดีบักได้ยาก เพราะว่า เรามองตัวช่องว่างเองไม่เห็น ไม่ว่าจะเป็นช่องว่างแบบ การเว้น การย่อหน้า หรือการขึ้นบรรทัดใหม่ เรามองไม่เห็นมันโดยตรง เราเห็นมันผ่านตัวอื่นรอบๆ ข้าง

>>> 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 หรือ แน่นอนว่า เราอาจจะลองเขียนโปรแกรมแปลงรูปแบบพวกนี้ขึ้นมาเองก็ได้

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

  • คงอยู่ (persistent): เกี่ยวกับ โปรแกรมที่เก็บข้อมูล (อย่างน้อยก็บางส่วน) ไว้ในแหล่งเก็บข้อมูลถาวร
  • ตัวดำเนินการจัดรูปแบบ (format operator): ตัวดำเนินการ % ที่รับสายอักขระจัดรูปแบบและทูเพิล แล้วสร้างสายอักขระ ที่รวมอิลิเมนต์ต่างๆ ของทูเพิลเข้าไป ในรูปแบบที่ระบุด้วยสายอักขระจัดรูปแบบ
  • สายอักขระจัดรูปแบบ (format string): สายอักขระที่ใช้กับตัวดำเนินการจัดรูปแบบ และมีชุดจัดรูปแบบอยู่
  • ชุดจัดรูปแบบ (format sequence): ชุดลำดับของอักขระ เช่น %d ที่ใช้ระบุว่าค่าตัวเลขควรจะถูกจัดรูปแบบการแสดงผลอย่างไร
  • ไฟล์ข้อความ (text file): เป็นลำดับของตัวอักษรที่เก็บในแหล่งเก็บข้อมูลถาวร เช่น ฮาร์ดดิสก์
  • ไดเรกทอรี (directory): ชื่อของหมวดหมู่ของไฟล์ต่างๆ หรืออาจเรียกว่า โฟลเดอร์ (folder)
  • เส้นทาง (path): สายอักขระที่ใช้ระบุไฟล์ รวมถึงไดเรกทอรีต่างๆ ที่ไฟล์ถูกจัดอยู่
  • เส้นทางสัมพัทธ์ (relative path): เส้นทางที่เริ่มจากไดเรกทอรีปัจจุบันที่ทำงานอยู่
  • เส้นทางสัมบูรณ์ (absolute path): เส้นทางที่เริ่มจากไดเรกทอรีบนสุด ในระบบคอมพิวเตอร์
  • จับเอ็กเซ็ปชั่น (catch exceptions): การใช้คำสั่ง try และ except ช่วย เพื่อป้องกันไม่ให้ เอ็กเซ็ปชั่นหรือข้อผิดพลาดปิดโปรแกรมไปเอง
  • ฐานข้อมูล (database): ไฟล์ที่เนื้อหาถูกจัดระเบียบในลักษณะคล้ายกับดิกชันนารี ที่มีกุญแจคู่กับค่าของกุญแจ
  • ไบต์ออปเจ๊คต์ (bytes object): ออปเจ๊คต์ที่คล้ายกับสายอักขระ
  • เชลล์ (shell): โปรแกรมที่ให้ผู้ใช้สามารถพิมพ์คำสั่งต่างๆ รวมถึงการเรียกรันโปรแกรมอื่นๆ ผ่านการพิมพ์คำสั่ง
  • ไปป์ออปเจ๊คต์ (pipe object): ออปเจ๊คต์ที่เป็นตัวแทนโปรแกรมที่กำลังรันอยู่ และยอมให้โปรแกรมไพธอนรันคำสั่งของเชลล์และอ่านผลลัพธ์ได้

14.12 แบบฝึกหัด

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

  1. จงเขียนโปรแกรมที่ค้นหาไดเรกทอรี และไดเรกทอรีย่อยทั้งหมด ที่มีไฟล์เอ็มพีสามอยู่ (ไฟล์ที่ลงท้ายด้วย .mp3) แล้วรีเทิร์นเส้นทางทั้งหมดออกมาเป็นลิสต์ คำใบ้: โมดูล os.path มีฟังก์ชันหลายๆ อัน ที่มีประโยชน์ในการจัดการชื่อไฟล์และเส้นทาง
  2. เพื่อตรวจสอบว่าไฟล์ซ้ำกัน เราสามารถใช้ md5sum เพื่อคำนวณหาค่า “เช็คซำ” (checksum) ของแต่ละไฟล์ ถ้าสองไฟล์มีค่าเช็คซำเหมือนกัน สองไฟล์นั้นก็น่าจะซ้ำกัน
  3. เพื่อตรวจสอบอีกที เราสามารถใช้คำสั่งขอยูนิกซ์ diff ตรวจไฟล์ที่คิดว่าซ้ำกันอีกทีได้

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

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

1)
ตอนนี้ ฟังก์ชัน popen ถูกประกาศเป็น “deprecated” (เก่าและแนะนำให้เลิกใช้) ซึ่ง เราก็ควรจะหยุดใช้มัน และควรจะใช้โมดูล subprocess ที่ถูกแนะนำให้ใช้แทน แต่สำหรับกรณีง่ายๆ ผม (อัลเลน ดาวนี่) พบว่า subprocess มันซับซ้อนเกินความจำเป็น ดังนั้น ผมจึงยังใช้ popen จนกว่าไพธอนจะไม่มีมันให้ใช้อีก
python/files.txt · Last modified: 2021/08/30 09:55 (external edit)

Page Tools