ห้องเรียนรู้ Karinoya

คุณวุฒิ · แล็บสอบผ่าน Cloud / AI / Python

ฟังก์ชัน โมดูล และการรับส่งข้อมูล

อ่านโจทย์และคำอธิบายเป็นไทยได้ ส่วนบทบรรยาย (บทความอธิบาย) มีเฉพาะฉบับภาษาญี่ปุ่นเท่านั้น

ดูฉบับภาษาญี่ปุ่น (มีบทบรรยาย) →

ข้อ 1 | การรับอาร์กิวเมนต์

เรียกฟังก์ชันต่อไปนี้ที่มีพารามิเตอร์ทางการที่มีเครื่องหมายดอกจัน ด้วยอาร์กิวเมนต์ตำแหน่งเท่านั้น สิ่งที่แสดงผลคือข้อใด

def f(a, b=2, *args, **kwargs):    return (a, b, args, kwargs)print(f(1, 3, 4, 5))
  1. (1, 3, (4, 5), None)
  2. (1, 3, (4, 5), {})
  3. (1, 2, (3, 4, 5), {})
  4. (1, 3, [4, 5], {})
คำตอบB. (1, 3, (4, 5), {})

อาร์กิวเมนต์ตำแหน่งจะถูกจับคู่จากซ้ายไปขวาตามลำดับ ดังนั้น a จะได้ 1 และ b จะได้ 3 แม้ b จะมีค่าเริ่มต้น 2 แต่เนื่องจากมีการส่งค่าเข้ามา ค่านั้นจะถูกใช้แทน 4 กับ 5 ที่จับคู่ไม่ทันจะถูกรวมเป็นทูเพิลไว้ใน args ที่มีดอกจันตัวเดียว กลายเป็น (4, 5) เนื่องจากไม่ได้ส่งคีย์เวิร์ดอาร์กิวเมนต์เลย kwargs จึงเป็นพจนานุกรมว่าง {} ไม่ใช่ None หากคิดว่า b ยังคงมีค่าเริ่มต้น 2 เหลืออยู่ จะกลายเป็น (1, 2, (3, 4, 5), {}) แต่อาร์กิวเมนต์ตำแหน่งมีความสำคัญเหนือค่าเริ่มต้น สิ่งที่อยู่ใน args ก็ไม่ใช่ลิสต์แต่เป็นทูเพิล จึงไม่มีทางที่จะแสดงด้วยวงเล็บเหลี่ยม

ข้อ 2 | การระบุคีย์เวิร์ด

การเรียกต่อไปนี้ส่งชื่อที่ไม่มีในนิยามด้วยคีย์เวิร์ด สิ่งที่แสดงผลคือข้อใด

def f(a, b=2, *args, **kwargs):    return (a, b, args, kwargs)print(f(1, x=9))
  1. (1, 9, (), {})
  2. (1, 2, (), {'x': 9})
  3. (1, 2, (9,), {})
  4. (1, 2, {'x': 9}, {})
คำตอบB. (1, 2, (), {'x': 9})

อาร์กิวเมนต์ตำแหน่งมีเพียง 1 ตัว a จึงได้ 1 ส่วน b ไม่ได้ถูกส่ง จึงยังคงเป็นค่าเริ่มต้น 2 พารามิเตอร์ทางการชื่อ x ไม่มีอยู่ในนิยาม คีย์เวิร์ดอาร์กิวเมนต์ x=9 จึงเข้าไปอยู่ใน kwargs ที่มีดอกจัน 2 ตัว กลายเป็น {'x': 9} เนื่องจากไม่มีอาร์กิวเมนต์ตำแหน่งที่เหลือ args จึงเป็นทูเพิลว่าง () หากคิดว่า x=9 จะเข้าไปที่ b จะกลายเป็น (1, 9, (), {}) แต่คีย์เวิร์ดอาร์กิวเมนต์จะเข้าไปเฉพาะพารามิเตอร์ทางการที่ชื่อตรงกันเท่านั้น หากคิดว่า 9 เข้าไปที่ args จะกลายเป็น (1, 2, (9,), {}) แต่ค่าที่ส่งโดยระบุชื่อจะไม่ไหลไปเป็นอาร์กิวเมนต์ตำแหน่งที่เหลือ หากสับสนว่าดอกจันตัวเดียวรับพจนานุกรมและดอกจัน 2 ตัวรับทูเพิล จะดูเหมือนว่า args มี {'x': 9} และ kwargs ว่างเปล่า

ข้อ 3 | ลิสต์ว่างเป็นค่าเริ่มต้น

เรียกฟังก์ชันต่อไปนี้ที่เขียนลิสต์ว่างไว้เป็นค่าเริ่มต้น 3 ครั้ง 3 บรรทัดที่แสดงผลคือข้อใด

def add(x, lst=[]):    lst.append(x)    return lstprint(add(1))print(add(2))print(add(3))
  1. แสดงเรียงจากบนลงล่างคือ None / None / None
  2. แสดงเรียงจากบนลงล่างคือ [1] / [1, 2] / [1, 2, 3]
  3. แสดงเรียงจากบนลงล่างคือ [1] / [2] / [3]
  4. แสดงเรียงจากบนลงล่างคือ [1] / [1, 2] / [2, 3]
คำตอบB. แสดงเรียงจากบนลงล่างคือ [1] / [1, 2] / [1, 2, 3]

ค่าเริ่มต้น [] จะถูกสร้างเพียงครั้งเดียวตอนที่รันบรรทัด def และลิสต์เดียวกันนั้นจะถูกเก็บไว้ในอ็อบเจกต์ฟังก์ชัน ไม่ได้สร้างลิสต์ว่างใหม่ทุกครั้งที่เรียก ผลของ append จึงคงเหลืออยู่ในการเรียกครั้งถัดไป ยืดออกเป็น [1], [1, 2], [1, 2, 3] หากคิดว่าทุกครั้งที่เรียกจะได้ลิสต์ใหม่ จะกลายเป็น [1] / [2] / [3] แต่นั่นคือพฤติกรรมเมื่อกำหนดค่าเริ่มต้นเป็น lst=None แล้วสร้างใหม่ในเนื้อหาฟังก์ชัน หากเขียนเนื้อหาผิดเป็น return lst.append(x) จะได้ None ซึ่งเป็นค่าที่ append คืนกลับเรียงกัน 3 ครั้ง นอกจากนี้ หากไม่แยกเป็น 3 บรรทัดแต่เขียนเป็น print(add(1), add(2), add(3)) ในบรรทัดเดียว เนื่องจากทั้ง 3 ตัวชี้ไปที่ลิสต์เดียวกัน จะแสดง [1, 2, 3] เรียงกัน 3 ครั้งหลังจากประเมินค่าทั้งหมดเสร็จแล้ว

ข้อ 4 | การใช้ค่าเริ่มต้นร่วมกัน

แสดงแอตทริบิวต์ของอ็อบเจกต์ฟังก์ชันหลังจากเรียก 3 ครั้ง สิ่งที่แสดงผลคือข้อใด

def add(x, lst=[]):    lst.append(x)    return lstadd(1)add(2)add(3)print(add.__defaults__)
  1. แสดงผลในรูปแบบ ((1, 2, 3),)
  2. แสดงผลในรูปแบบ ([],)
  3. แสดงผลในรูปแบบ [[1, 2, 3]]
  4. แสดงผลในรูปแบบ ([1, 2, 3],)
คำตอบD. แสดงผลในรูปแบบ ([1, 2, 3],)

อ็อบเจกต์ฟังก์ชันจะเก็บค่าเริ่มต้นไว้ในแอตทริบิวต์ที่ชื่อ __defaults__ ในรูปแบบทูเพิล ในบรรดาพารามิเตอร์ทางการ มีเพียง lst เท่านั้นที่มีค่าเริ่มต้น จึงกลายเป็นทูเพิลที่มี 1 องค์ประกอบและมีจุลภาคติดท้าย เนื้อหาข้างในคือลิสต์ตัวเดียวกันที่ถูกสร้างเพียงครั้งเดียวตอนบรรทัด def ผลของการ append ที่เกิดจาก 3 ครั้งที่เรียกจะคงเหลืออยู่ตามเดิม จึงกลายเป็น ([1, 2, 3],) หากยังไม่เคยเรียกเลยจะเป็น ([],) แต่ผลกระทบจากการเรียกที่ปรากฏในจุดนี้คือความน่ากลัวของอาร์กิวเมนต์ค่าเริ่มต้นที่เปลี่ยนแปลงได้ (Mutable) __defaults__ เองเป็นทูเพิล จึงไม่แสดงผลด้วยวงเล็บเหลี่ยม และเนื้อหาที่เป็นลิสต์ก็ไม่เปลี่ยนเป็นทูเพิลด้วย

ข้อ 5 | การประเมินค่าเริ่มต้น

เขียนตัวแปรไว้เป็นค่าเริ่มต้นแล้วเขียนทับตัวแปรนั้นในภายหลัง 2 บรรทัดที่แสดงผลคือข้อใด

i = 5def h(x=i):    return xi = 10print(h())print(h(i))
  1. 5 แล้วตามด้วย 5
  2. 5 แล้วตามด้วย 10
  3. 10 แล้วตามด้วย 10
  4. 10 แล้วตามด้วย 5
คำตอบB. 5 แล้วตามด้วย 10

นิพจน์ของค่าเริ่มต้นจะไม่ถูกประเมินทุกครั้งที่เรียกฟังก์ชัน แต่จะถูกประเมินเพียงครั้งเดียวตอนที่บรรทัด def ถูกรัน ในเวลานั้น i เป็น 5 ดังนั้น 5 จะถูกเก็บไว้ในอ็อบเจกต์ฟังก์ชัน แม้ภายหลังจะเขียนทับ i เป็น 10 ค่าเริ่มต้นที่เก็บไว้แล้วก็ไม่เปลี่ยน h() จึงคืนค่า 5 ในขณะที่ h(i) จะอ่าน i ณ เวลาที่เรียกแล้วส่งเข้าไป จึงคืนค่า ณ เวลานั้นคือ 10 หากคิดว่าค่าเริ่มต้นถูกประเมินทุกครั้งที่เรียก จะกลายเป็นทั้ง 2 บรรทัดเป็น 10 หากคิดว่าค่าที่ส่งเป็นอาร์กิวเมนต์ก็ถูกแทนที่ด้วยค่าเริ่มต้นด้วย จะกลายเป็นทั้ง 2 บรรทัดเป็น 5 กับดักของอาร์กิวเมนต์ค่าเริ่มต้นที่เปลี่ยนแปลงได้ก็เกิดจากกฎเดียวกันนี้

ข้อ 6 | ลำดับของอาร์กิวเมนต์

บันทึกโค้ดต่อไปนี้ลงไฟล์แล้วรันด้วย python3 จะเกิดอะไรขึ้น

def g(a=1, b):    return a + bprint(g(2, 3))
  1. เกิด SyntaxError ณ จุดนิยาม ไม่มีแม้แต่บรรทัดเดียวถูกรัน
  2. แสดง 4 แล้วรันไปจนจบตามปกติ
  3. เกิด TypeError ณ จุดที่เรียก แล้วหยุดการทำงานที่นั่น
  4. แสดง 5 แล้วรันไปจนจบตามปกติ
คำตอบA. เกิด SyntaxError ณ จุดนิยาม ไม่มีแม้แต่บรรทัดเดียวถูกรัน

ไม่สามารถวางพารามิเตอร์ทางการที่ไม่มีค่าเริ่มต้นไว้หลังพารามิเตอร์ทางการที่มีค่าเริ่มต้นได้ กฎนี้ถูกตรวจสอบตั้งแต่ขั้นตอนไวยากรณ์ ดังนั้นเมื่อรันไฟล์จะเกิด SyntaxError ก่อน พร้อมข้อความ non-default argument follows default argument เนื่องจากการวิเคราะห์ไวยากรณ์ทำกับทั้งไฟล์ก่อนรันจริง จึงไม่มีแม้แต่บรรทัดเดียวถูกรัน รวมถึงบรรทัด print ด้วย นี่เป็นคนละขั้นตอนกับ TypeError ที่เกิดจากการเรียกผิดวิธี g(2, 3) เองมีจำนวนตรงกับพารามิเตอร์ทางการ หากนิยามถูกต้องก็ควรจะแสดง 5 แต่ไปไม่ถึงจุดนั้น หากจะเขียนให้ถูกต้องควรเขียนเป็น def g(b, a=1) โดยวางตัวที่มีค่าเริ่มต้นไว้ด้านหลัง

ข้อ 7 | ค่าที่ฟังก์ชันคืนกลับ

เรียกฟังก์ชัน 2 ตัวที่ต่างกันตรงมีหรือไม่มี return ตามที่กำหนดต่อไปนี้ สิ่งที่แสดงผลคือข้อใด

def one():    passdef two():    return 1, 2print(one(), type(two()).__name__)
  1. None tuple
  2. None list
  3. () list
  4. () tuple
คำตอบA. None tuple

one จบการทำงานโดยไม่ผ่าน return เลยแม้แต่ครั้งเดียว ดังนั้นค่าที่คืนกลับจึงเป็น None ไม่ใช่ทูเพิลว่างหรือสตริงว่าง two เรียง 1 กับ 2 ไว้ด้วยจุลภาคหลัง return แต่นั่นไม่ใช่การคืนค่า 2 ตัว แต่เป็นการคืนทูเพิล 1 ตัวคือ (1, 2) ดังนั้น type(two()).__name__ จึงเป็น tuple หากคิดว่าค่าที่คืนหลายตัวจะถูกรวมเป็นลิสต์ จะกลายเป็น list แต่นิพจน์ที่เรียงด้วยจุลภาคจะสร้างทูเพิล สาเหตุที่มักสะดุดเมื่อพยายามกำหนดค่าจากค่าที่คืนกลับของฟังก์ชันที่แสดงผลไปแล้วนั้น ส่วนใหญ่มาจาก None ตัวนี้

ข้อ 8 | การเรียกแบบแกะแพ็ก

โค้ดต่อไปนี้เรียกโดยใส่เครื่องหมายดอกจันไว้หน้าลิสต์ สิ่งที่แสดงผลคือข้อใด

def f(a, b=2, *args, **kwargs):    return (a, b, args, kwargs)vals = [1, 2, 3]print(f(*vals))
  1. ([1, 2, 3], 2, (), {})
  2. (1, 2, (3,), None)
  3. (1, 2, [3], {})
  4. (1, 2, (3,), {})
คำตอบD. (1, 2, (3,), {})

เมื่อฝั่งเรียกใส่เครื่องหมายดอกจันตัวเดียวไว้หน้าลิสต์ เนื้อหาข้างในจะถูกแจกจ่ายทีละตัวเป็นอาร์กิวเมนต์ตำแหน่ง ดังนั้น a จะได้ 1 และ b จะได้ 2 ส่วน 3 ที่เหลือจะถูกรวมเป็นทูเพิลไว้ใน args ที่มีดอกจันตัวเดียว กลายเป็น (3,) เนื่องจากมีองค์ประกอบเดียวจึงมีจุลภาคติดท้าย เนื่องจากไม่ได้ส่งคีย์เวิร์ดอาร์กิวเมนต์ kwargs จึงเป็นพจนานุกรมว่าง {} ไม่ใช่ None หากลืมใส่ดอกจันแล้วเขียนเป็น f(vals) ตัวลิสต์เองจะเข้าไปที่พารามิเตอร์ทางการตัวแรก กลายเป็น ([1, 2, 3], 2, (), {}) โดยไม่เกิดข้อผิดพลาดแต่ผลลัพธ์เปลี่ยนไป สิ่งที่อยู่ใน args เป็นทูเพิล จึงไม่มีทางที่จะแสดงด้วยวงเล็บเหลี่ยม

ข้อ 9 | การจัดการ Type Annotation

เรียกฟังก์ชันต่อไปนี้ด้วยค่าที่ต่างจากชนิดที่เขียนไว้ในคำอธิบายชนิด (Annotation) จะเกิดอะไรขึ้น

def ann(a: int, b: str = "x") -> bool:    return aprint(ann("文字列", 999))print(ann.__annotations__['a'])
  1. บรรทัดที่ 1 แสดง 文字列 (สตริง) บรรทัดที่ 2 แสดง <class 'int'>
  2. บรรทัดที่ 1 เกิด TypeError แจ้งว่าส่งชนิดที่ต่างจาก Annotation ไม่ได้
  3. บรรทัดที่ 1 แสดง 文字列 (สตริง) บรรทัดที่ 2 แสดง int
  4. บรรทัดที่ 1 เกิด ValueError แจ้งว่าแปลงเป็น int ไม่ได้
คำตอบA. บรรทัดที่ 1 แสดง 文字列 (สตริง) บรรทัดที่ 2 แสดง <class 'int'>

Annotation จะถูกบันทึกไว้ในพจนานุกรมที่ชื่อ __annotations__ ของอ็อบเจกต์ฟังก์ชันเท่านั้น จะไม่มีการตรวจสอบชนิดขณะรันจริง แม้จะเขียน int ไว้ที่ a ก็ยังส่งสตริงได้ แม้จะเขียน str ไว้ที่ b ก็ยังส่งจำนวนเต็มได้ แม้จะเขียน bool ไว้ที่ค่าที่คืนกลับ สิ่งที่คืนกลับจริงก็คือสตริงที่รับเข้ามานั่นเอง ดังนั้นบรรทัดที่ 1 จะแสดง 文字列 (สตริง) ตามเดิม สิ่งที่อยู่ใน __annotations__['a'] คืออ็อบเจกต์ชนิด int ดังนั้นเมื่อ print จะแสดงในรูปแบบ <class 'int'> ไม่ใช่แสดงเพียงตัวอักษร int 3 ตัว หากต้องการตรวจสอบชนิดจริง ๆ ต้องใช้เครื่องมือตรวจสอบแยกต่างหาก หรือตรวจสอบเองในเนื้อหาฟังก์ชัน

ข้อ 10 | วิธีแก้ไขค่าเริ่มต้น

โค้ดต่อไปนี้เขียนใหม่โดยเปลี่ยนค่าเริ่มต้นเป็น None 3 บรรทัดที่แสดงผลคือข้อใด

def add2(x, lst=None):    if lst is None:        lst = []    lst.append(x)    return lstprint(add2(1))print(add2(2))print(add2(3, [9]))
  1. แสดงเรียงจากบนลงล่างคือ [1] / [1, 2] / [9, 3]
  2. แสดงเรียงจากบนลงล่างคือ [1] / [2] / [9, 3]
  3. แสดงเรียงจากบนลงล่างคือ [1] / [2] / [3, 9]
  4. แสดงเรียงจากบนลงล่างคือ [1] / [2] / [3]
คำตอบB. แสดงเรียงจากบนลงล่างคือ [1] / [2] / [9, 3]

วิธีมาตรฐานคือกำหนดค่าเริ่มต้นเป็น None แล้วตรวจสอบว่าเป็น None หรือไม่ที่ต้นเนื้อหาฟังก์ชัน จากนั้นจึงสร้างลิสต์ว่างใหม่ เนื่องจาก None เป็นค่าที่เปลี่ยนแปลงไม่ได้ แม้จะถูกประเมินเพียงครั้งเดียวก็ไม่เกิดปัญหาการใช้ร่วมกัน ครั้งที่ 1 และครั้งที่ 2 ที่ละ lst ไว้ จะสร้างลิสต์ว่างใหม่แยกกันในแต่ละครั้ง จึงได้ [1] และ [2] องค์ประกอบจากการเรียกครั้งก่อนจะไม่คงเหลืออยู่ ครั้งที่ 3 ฝั่งเรียกส่ง [9] เข้ามา lst is None จึงเป็นเท็จ 3 จะถูกเพิ่มต่อท้ายลิสต์ที่ส่งเข้ามา กลายเป็น [9, 3] หากยังคงเป็น lst=[] เหมือนเดิมไม่ได้แก้ไข ครั้งที่ 2 ที่ละไว้จะมีองค์ประกอบจากครั้งก่อนคงเหลืออยู่ กลายเป็น [1, 2] หากละเลยลิสต์ที่ฝั่งเรียกส่งมาแล้วสร้างลิสต์ใหม่แทน จะกลายเป็น [1] / [2] / [3] ซึ่งทำให้ข้อดีของวิธีมาตรฐานนี้ที่ว่า “หากมีการส่งเข้ามาก็ใช้อันนั้น” หายไป หากใช้ insert(0, x) ที่ใส่ไว้ต้นแทนที่จะเป็น append ที่เพิ่มต่อท้าย ครั้งที่ 3 จะกลายเป็น [3, 9]

ข้อ 11 | ลำดับการค้นหาชื่อ

ข้อใดคือลำดับ LEGB ที่ถูกต้องที่ Python ใช้ค้นหาชื่อตัวแปร

  1. ค้นหาตามลำดับ ฟังก์ชันที่กำลังทำงาน, ระดับบนสุดของโมดูล, ฟังก์ชันชั้นนอก, built-in
  2. ค้นหาตามลำดับ ฟังก์ชันที่กำลังทำงาน, ฟังก์ชันชั้นนอก, ระดับบนสุดของโมดูล, built-in
  3. ค้นหาตามลำดับ ระดับบนสุดของโมดูล, built-in, ฟังก์ชันที่กำลังทำงาน, ฟังก์ชันชั้นนอก
  4. ค้นหาตามลำดับ built-in, ระดับบนสุดของโมดูล, ฟังก์ชันชั้นนอก, ฟังก์ชันที่กำลังทำงาน
คำตอบB. ค้นหาตามลำดับ ฟังก์ชันที่กำลังทำงาน, ฟังก์ชันชั้นนอก, ระดับบนสุดของโมดูล, built-in

LEGB คือตัวย่อของ Local, Enclosing, Global, Built-in ซึ่งแสดงถึงการค้นหาจากชั้นในสุดออกไปชั้นนอก โดยจะดูในฟังก์ชันที่กำลังทำงานก่อน ถัดไปคือฟังก์ชันชั้นนอกที่ครอบตัวเองอยู่ ถัดไปคือระดับบนสุดของโมดูล และสุดท้ายคือ built-in อย่าง len หรือ print เมื่อพบแล้วการค้นหาจะจบทันที ดังนั้นหากใช้ชื่อเดียวกันในชั้นใน ชื่อของชั้นนอกจะถูกซ่อนไว้ หากค้นหาจากชั้นนอกไปชั้นใน จะทำให้ตัวแปรท้องถิ่นที่สร้างในฟังก์ชันแพ้ตัวแปรชื่อเดียวกันที่ระดับบนสุดของโมดูล ซึ่งไม่ตรงกับพฤติกรรมจริง การสลับลำดับระหว่าง Global กับ Enclosing ก็เป็นข้อผิดพลาดที่พบบ่อย แต่ฟังก์ชันชั้นนอกอยู่ในตำแหน่งที่ลึกกว่าระดับบนสุดของโมดูล

ข้อ 12 | การกำหนดค่าแบบท้องถิ่น

โค้ดต่อไปนี้กำหนดค่าให้กับชื่อเดียวกับด้านนอกภายในฟังก์ชัน 2 บรรทัดที่แสดงผลคือข้อใด

x = 'global'def f():    x = 'local'    print(x)f()print(x)
  1. local แล้วตามด้วย local
  2. local แล้วตามด้วย global
  3. global แล้วตามด้วย global
  4. global แล้วตามด้วย local
คำตอบB. local แล้วตามด้วย global

เมื่อกำหนดค่าให้กับชื่อภายในฟังก์ชัน ชื่อนั้นจะถูกสร้างเป็นตัวแปรท้องถิ่นใหม่ ตัวแปรชื่อเดียวกันที่ระดับบนสุดของโมดูลจะไม่ได้รับผลกระทบเลย print(x) ที่อยู่ในฟังก์ชันจะอ่าน x ที่เป็นตัวแปรท้องถิ่น จึงแสดง local แต่ print(x) หลังออกจากฟังก์ชันแล้วจะอ่าน x ที่ระดับบนสุดของโมดูล จึงแสดง global หากคิดว่าการกำหนดค่าในฟังก์ชันส่งผลไปถึงด้านนอกด้วย จะกลายเป็นทั้ง 2 บรรทัดเป็น local แต่การทำแบบนั้นต้องประกาศ global x ไว้ที่ต้นฟังก์ชัน ในทางกลับกัน หากคิดว่าการกำหนดค่าในฟังก์ชันถูกละเลยและอ่านค่าด้านนอกแทน จะกลายเป็นทั้ง 2 บรรทัดเป็น global แต่เมื่อกำหนดค่าแล้วตัวแปรท้องถิ่นก็ถูกสร้างขึ้นแน่นอน ความไม่สมมาตรที่ว่า “อ่านจะไปถึงด้านนอกแต่เขียนไปไม่ถึง” นี้คือพื้นฐานของขอบเขต (Scope) ของ Python

ข้อ 13 | nonlocal

โค้ดต่อไปนี้พยายามเขียนทับตัวแปรด้านนอกจากฟังก์ชันด้านใน สิ่งที่แสดงผลคือข้อใด

def outer():    n = 1    def inner():        nonlocal n        n = 2    inner()    return ndef outer2():    n = 1    def inner():        n = 2    inner()    return nprint(outer(), outer2())
  1. 1 2
  2. 2 2
  3. 2 1
  4. 1 1
คำตอบC. 2 1

inner ของ outer มีการเขียน nonlocal n ไว้ ดังนั้นการกำหนดค่าให้ n จะส่งผลถึง n ของฟังก์ชัน outer ที่อยู่ชั้นนอกถัดไป n ที่ outer คืนกลับหลังเรียก inner จึงเป็น 2 ในขณะที่ inner ของ outer2 ไม่มีการประกาศไว้ n = 2 จะเป็นเพียงการสร้างตัวแปรท้องถิ่นใหม่ของ inner เท่านั้น n ของ outer2 จึงยังคงเป็น 1 ไม่เปลี่ยนแปลง ดังนั้นจึงแสดงผล 2 1 หากคิดว่าทั้งคู่ส่งผลถึงกัน จะกลายเป็น 2 2 หากคิดว่าทั้งคู่ไม่ส่งผลถึงกันเลย จะกลายเป็น 1 1 แต่สิ่งที่สร้างความแตกต่างนี้คือบรรทัดเดียวของ nonlocal เท่านั้น นอกจากนี้ nonlocal จะใช้ไม่ได้หากไม่มีตัวแปรชื่อเดียวกันอยู่ในฟังก์ชันชั้นนอก และไม่สามารถชี้ไปที่ตัวแปรระดับบนสุดของโมดูลได้ หากต้องการเขียนทับตัวแปรระดับบนสุดต้องใช้ global

ข้อ 14 | ข้อผิดพลาดตัวแปรที่ยังไม่ถูกผูก

อ่านชื่อที่มีการกำหนดค่าอยู่ในบรรทัดถัดไป จากบรรทัดก่อนหน้านั้น เมื่อรันโปรแกรมต่อไปนี้จะเกิดอะไรขึ้น

z = 100def bad():    print(z)    z = 1bad()
  1. เกิด UnboundLocalError โดยไม่แสดงอะไรเลยแล้วหยุด
  2. แสดง 100 แล้วรันไปจนจบตามปกติ
  3. แสดง None แล้วรันไปจนจบตามปกติ
  4. แสดง 1 แล้วรันไปจนจบตามปกติ
คำตอบA. เกิด UnboundLocalError โดยไม่แสดงอะไรเลยแล้วหยุด

Python จะตัดสินตั้งแต่ตอนคอมไพล์เนื้อหาฟังก์ชันว่า ชื่อใดที่มีการกำหนดค่าอยู่ในฟังก์ชันนั้นจะถือเป็นตัวแปรท้องถิ่นทั้งหมด การตัดสินจะทำกับทั้งฟังก์ชัน ไม่เกี่ยวกับลำดับของบรรทัด ใน bad นี้มีการกำหนดค่า z = 1 อยู่ในบรรทัดถัดไป ดังนั้น z จะถูกจัดการเป็นตัวแปรท้องถิ่นของฟังก์ชันนี้ เมื่อถึง print(z) จึงยังไม่มีค่าใส่อยู่ เกิด UnboundLocalError ข้อความของข้อยกเว้นนี้อาจเปลี่ยนไปตามเวอร์ชันของ Python แต่ชื่อชนิดคือ UnboundLocalError ไม่เปลี่ยน แม้จะมี z = 100 อยู่ที่ระดับบนสุดก็จะไม่ถูกอ้างอิงถึง หากคิดว่าเนื่องจากอยู่ก่อนบรรทัดที่กำหนดค่า จึงอ่านค่าด้านนอก จะกลายเป็น 100 แต่ Python ไม่ทำงานแบบนั้น และก็ไม่มีทางที่ตัวแปรท้องถิ่นที่ยังไม่มีค่าจะกลายเป็น None ด้วย

ข้อ 15 | การประกาศ global

เรียงฟังก์ชันที่มี global กับที่ไม่มี ไว้ในโค้ดต่อไปนี้ สิ่งที่แสดงผลคือข้อใด

n = 10def f():    global n    n = 20    return ndef g():    n = 30    return nprint(f(), g(), n)
  1. 20 30 30
  2. 20 30 20
  3. 20 30 10
  4. 10 30 10
คำตอบB. 20 30 20

f มีการเขียน global n ไว้ ดังนั้น n = 20 จะเขียนทับ n ที่ระดับบนสุดของโมดูล f เองคืนค่า 20 และ n ที่ระดับบนสุดก็เป็น 20 ด้วย g ไม่มีการประกาศไว้ n = 30 จะเป็นเพียงการสร้างตัวแปรท้องถิ่นของ g เท่านั้น จึงไม่ส่งผลถึง n ที่ระดับบนสุด g จะคืนค่าตัวแปรท้องถิ่นของตนคือ 30 print สุดท้ายที่อ่าน n จะเป็นตัวที่ระดับบนสุด ซึ่งถูกเขียนทับโดย f ไปแล้ว จึงเป็น 20 หากคิดว่าการกำหนดค่าของ g ก็ส่งผลไปถึงด้านนอกด้วย จะกลายเป็น 20 30 30 หากคิดว่า global ไม่มีผล จะกลายเป็น 20 30 10 การประกาศ global คือคำสั่งที่ว่า “ให้การกำหนดค่าให้ชื่อนี้ส่งผลไปถึงระดับบนสุด” หากเพียงแค่อ่านค่าอย่างเดียวก็ไม่จำเป็นต้องประกาศ ควรระวังจุดนี้ด้วย

ข้อ 16 | ผลของ __all__

เตรียมไฟล์ mymod.py ต่อไปนี้ แล้วเขียน from mymod import * ในอีกไฟล์หนึ่ง ชื่อใดที่จะถูกนำเข้ามาในฝั่งที่เรียก

# mymod.py__all__ = ['pub']pub = 'P'other = 'O'def hello():    return 'H'
  1. pub, other และ hello ทั้ง 3 ตัวถูกนำเข้ามาทั้งหมด
  2. pub และ hello ถูกนำเข้ามา ส่วน other ไม่ถูกนำเข้ามา
  3. other และ hello ถูกนำเข้ามา ส่วน pub ไม่ถูกนำเข้ามา
  4. มีเพียง pub เท่านั้นที่ถูกนำเข้ามา other และ hello ไม่ถูกนำเข้ามา
คำตอบD. มีเพียง pub เท่านั้นที่ถูกนำเข้ามา other และ hello ไม่ถูกนำเข้ามา

การนำเข้าด้วยเครื่องหมายดอกจันโดยปกติจะนำเข้าชื่อทั้งหมดที่ไม่ได้ขึ้นต้นด้วยขีดล่าง แต่ถ้าฝั่งโมดูลมีลิสต์ของสตริงที่ชื่อ __all__ จะจำกัดเฉพาะชื่อที่ระบุไว้ในนั้นเท่านั้น ในตัวอย่างนี้ __all__ มีเพียง pub เท่านั้น ดังนั้นทั้ง other และ hello จะไม่ถูกนำเข้ามา other ไม่ได้ขึ้นต้นด้วยขีดล่าง แต่เมื่อมี __all__ อยู่แล้ว รายการนั้นจะมีความสำคัญเหนือกว่า เนื่องจากไม่มีการปฏิบัติพิเศษเพราะเป็นฟังก์ชัน hello ก็จะไม่ถูกนำเข้ามาเช่นกัน __all__ จะมีผลเฉพาะตอนนำเข้าด้วยเครื่องหมายดอกจันเท่านั้น การนำเข้าโดยระบุชื่ออย่าง from mymod import hello หรือวิธี import mymod แล้วเขียนเป็น mymod.hello() จะไม่ถูกขัดขวาง

ข้อ 17 | ค่าของ __name__

เกี่ยวกับ mymod2.py ต่อไปนี้ ชุดค่าผสมของค่า __name__ เมื่อรันโดยตรงด้วย python3 mymod2.py และเมื่อ import mymod2 จากไฟล์อื่น คือข้อใด

# mymod2.pyprint('mymod2:', __name__)if __name__ == '__main__':    print('直接実行')
  1. รันโดยตรงจะเป็น mymod2 การ import จะเป็น __main__
  2. ไม่ว่ารันโดยตรงหรือ import ก็เป็น __main__ ทั้งคู่
  3. รันโดยตรงจะเป็น __main__ การ import จะเป็น mymod2
  4. ไม่ว่ารันโดยตรงหรือ import ก็เป็น mymod2 ทั้งคู่
คำตอบC. รันโดยตรงจะเป็น __main__ การ import จะเป็น mymod2

เมื่อรันไฟล์นั้นโดยตรงด้วย python3 __name__ จะมีค่าเป็น '__main__' เมื่อถูก import จากไฟล์อื่น จะมีค่าเป็นชื่อไฟล์ที่ตัดนามสกุลออกคือ 'mymod2' ดังนั้นเมื่อรันโดยตรง จะแสดง 2 บรรทัดคือ mymod2: __main__ และ 直接実行 (รันโดยตรง) เมื่อถูก import จะแสดงเพียง 1 บรรทัดคือ mymod2: mymod2 if __name__ == '__main__': คือวิธีเขียนมาตรฐานที่ใช้ประโยชน์จากความแตกต่างนี้เพื่อแยก “การประมวลผลที่ต้องการให้ทำงานเฉพาะเมื่อรันโดยตรง” หากสลับชุดค่าผสมนี้ จะทำให้การประมวลผลสำหรับตรวจสอบการทำงานทำงานขึ้นมาตอน import ซึ่งไม่ตรงกับจุดประสงค์ของวิธีเขียนนี้ หากทั้งสองกรณีให้ค่าเดียวกัน ก็จะไม่สามารถแยกแยะได้ตั้งแต่แรก

ข้อ 18 | sys.argv

รันไฟล์ args.py ต่อไปนี้ด้วยคำสั่ง python3 args.py foo bar 2 บรรทัดที่แสดงผลคือข้อใด

# args.pyimport sysprint(len(sys.argv))print(sys.argv[1:])
  1. 2 แล้วตามด้วย ['foo', 'bar']
  2. 3 แล้วตามด้วย ['args.py', 'foo']
  3. 3 แล้วตามด้วย ['foo', 'bar']
  4. 2 แล้วตามด้วย ['args.py', 'foo']
คำตอบC. 3 แล้วตามด้วย ['foo', 'bar']

sys.argv เป็นลิสต์ที่เรียงค่าที่ส่งมาจากบรรทัดคำสั่ง โดยตัวแรกสุดจะเป็นชื่อสคริปต์เอง ในตัวอย่างนี้จะกลายเป็นลิสต์ 3 องค์ประกอบคือ ['args.py', 'foo', 'bar'] ดังนั้น len จึงเป็น 3 sys.argv[1:] ที่ตัดออกมาตั้งแต่ดัชนี 1 เป็นต้นไป จะเป็น ['foo', 'bar'] ที่ตัดชื่อสคริปต์ออกแล้ว หากคิดว่าจำนวนอาร์กิวเมนต์ที่ส่งมาจะกลายเป็นความยาวโดยตรง จะได้ 2 ซึ่งลืมนับชื่อสคริปต์ตัวแรก การคิดว่า sys.argv[1:] จะกลายเป็น ['args.py', 'foo'] เป็นกรณีที่อ่านตำแหน่งเริ่มต้นของการแบ่งส่วน (slice) ผิด หากรันโดยไม่ส่งอาร์กิวเมนต์เลย ความยาวจะเป็น 1 และ sys.argv[1:] จะเป็นลิสต์ว่าง

ข้อ 19 | การโหลดโมดูลซ้ำ

ข้อใดคือพฤติกรรมที่ถูกต้องเมื่อ import โมดูลเดียวกัน 2 ครั้งภายในโปรแกรมเดียว

  1. ครั้งที่ 2 ก็ยังคงอ่านเนื้อหาโมดูลใหม่ตั้งแต่ต้นและรันซ้ำอีกครั้ง
  2. ครั้งที่ 2 จะสร้างอ็อบเจกต์อีกตัวขึ้นมา สถานะจะไม่ถูกใช้ร่วมกันอีกต่อไป
  3. ครั้งที่ 2 จะใช้แคช ตัวโมดูลจะไม่ถูกรันซ้ำอีก
  4. ครั้งที่ 2 จะเกิด ImportError เนื่องจากชื่อเดียวกันใช้ได้เพียงครั้งเดียว
คำตอบC. ครั้งที่ 2 จะใช้แคช ตัวโมดูลจะไม่ถูกรันซ้ำอีก

โมดูลที่ถูกอ่านครั้งหนึ่งแล้วจะถูกแคชไว้ในพจนานุกรมที่ชื่อ sys.modules การ import ครั้งที่ 2 จะเป็นเพียงการผูกชื่อเข้ากับอ็อบเจกต์โมดูลตัวเดิมที่ลงทะเบียนไว้แล้วเท่านั้น จึงไม่มีการอ่านไฟล์ใหม่หรือรันเนื้อหาซ้ำอีก หากเขียน print ไว้ที่ระดับบนสุดของโมดูล จะสามารถยืนยันได้ว่าแม้ import 2 ครั้งก็จะแสดงผลเพียงครั้งเดียว แม้จะตั้งชื่อแทนตอน import ก็ยังคงชี้ไปที่อ็อบเจกต์เดียวกัน ดังนั้นหากเขียนทับตัวแปรของโมดูลหนึ่ง ที่อื่นที่ import โมดูลนั้นอยู่ก็จะเห็นค่าที่ถูกเขียนทับแล้วด้วย การ import ชื่อเดียวกันซ้ำ 2 ครั้งจะไม่เกิดข้อผิดพลาด หากต้องการอ่านเนื้อหาใหม่ระหว่างพัฒนาต้องใช้ฟังก์ชันของ importlib อย่างชัดเจน

ข้อ 20 | การตัดสินสภาพแวดล้อมเสมือน

ข้อใดคือวิธีที่เหมาะสมในการตัดสินว่า Python ที่กำลังทำงานอยู่ในสภาพแวดล้อมเสมือนที่สร้างด้วย venv หรือไม่

  1. หาก sys.prefix ไม่ถูกกำหนดไว้ ตัดสินได้ว่าอยู่ในสภาพแวดล้อมเสมือน
  2. หาก sys.prefix กับ sys.base_prefix ตรงกัน ตัดสินได้ว่าอยู่ในสภาพแวดล้อมเสมือน
  3. หาก sys.base_prefix ถูกกำหนดไว้ ตัดสินได้ว่าอยู่ในสภาพแวดล้อมเสมือน
  4. หาก sys.prefix กับ sys.base_prefix ไม่ตรงกัน ตัดสินได้ว่าอยู่ในสภาพแวดล้อมเสมือน
คำตอบD. หาก sys.prefix กับ sys.base_prefix ไม่ตรงกัน ตัดสินได้ว่าอยู่ในสภาพแวดล้อมเสมือน

sys.prefix ชี้ไปที่ตำแหน่งที่ติดตั้ง Python ที่กำลังทำงานอยู่ ส่วน sys.base_prefix ชี้ไปที่ตำแหน่งของ Python ต้นทางที่เป็นที่มา นอกสภาพแวดล้อมเสมือนทั้งสองจะเป็นค่าเดียวกัน แต่ในสภาพแวดล้อมที่สร้างด้วย python3 -m venv sys.prefix จะชี้ไปที่ไดเรกทอรีของสภาพแวดล้อมที่สร้างขึ้น ในขณะที่ sys.base_prefix ยังคงชี้ไปที่ Python ต้นทางเหมือนเดิม ทั้งสองจึงไม่ตรงกัน การไม่ตรงกันนี้เองเป็นเบาะแสของการตัดสิน การที่ทั้งสองตรงกันคือเงื่อนไขตรงข้ามของการอยู่ในสภาพแวดล้อมเสมือน sys.base_prefix จะถูกกำหนดไว้เสมอแม้นอกสภาพแวดล้อมเสมือน จึงตัดสินด้วยการมีอยู่หรือไม่ไม่ได้ sys.prefix ก็ถูกกำหนดไว้เสมอเช่นกัน สตริงพาธที่เฉพาะเจาะจงหรือเวอร์ชันของ pip จะต่างกันไปในแต่ละสภาพแวดล้อม จึงจำไว้เพียงความสัมพันธ์นี้ก็พอ

ข้อ 21 | ค่าที่ write คืนกลับ

รับค่าที่คืนกลับของเมธอดเขียนไฟล์ในโค้ดต่อไปนี้ สิ่งที่แสดงผลคือข้อใด

with open('/tmp/sample.txt', 'w') as f:    n = f.write("one\ntwo\nthree\n")print(n)
  1. แสดง None
  2. แสดง 3
  3. แสดง 14
  4. แสดง 11
คำตอบC. แสดง 14

write จะคืนค่าจำนวนตัวอักษรที่เขียนไปเป็นค่าที่คืนกลับ "one\ntwo\nthree\n" ประกอบด้วย one 3 ตัวอักษร two 3 ตัวอักษร three 5 ตัวอักษร รวมกัน 11 ตัวอักษร บวกกับการขึ้นบรรทัดใหม่ 3 ตัว รวมทั้งหมดเป็น 14 ตัวอักษร หากลืมนับการขึ้นบรรทัดใหม่จะได้ 11 หากคิดว่าจำนวนบรรทัดที่เขียนจะถูกคืนกลับ จะได้ 3 แต่สิ่งที่ write นับคือจำนวนตัวอักษรเท่านั้น การเลือก None เพราะคิดว่าไม่คืนค่าอะไรเลยก็เป็นข้อผิดพลาดที่พบบ่อย มักสับสนกับเมธอดอย่าง append หรือ sort ที่คืนค่า None แต่ write จะคืนค่าจำนวน ทั้งนี้ write จะไม่ใส่การขึ้นบรรทัดใหม่ให้อัตโนมัติ หากต้องการแบ่งบรรทัดต้องเขียนไว้ในสตริงเอง

ข้อ 22 | readlines

อ่านไฟล์ที่เขียนไว้ 3 บรรทัดด้วย readlines สิ่งที่แสดงผลคือข้อใด

with open('/tmp/sample.txt', 'w') as f:    f.write("one\ntwo\nthree\n")with open('/tmp/sample.txt') as f:    print(f.readlines())
  1. แสดง ['one\ntwo\nthree\n']
  2. แสดง 'one\ntwo\nthree\n'
  3. แสดง ['one\n', 'two\n', 'three\n']
  4. แสดง ['one', 'two', 'three']
คำตอบC. แสดง ['one\n', 'two\n', 'three\n']

readlines จะคืนค่าทุกบรรทัดเป็นลิสต์ โดยอักขระขึ้นบรรทัดใหม่ที่ท้ายแต่ละองค์ประกอบจะยังคงติดอยู่ ดังนั้นจึงเป็น ['one\n', 'two\n', 'three\n'] หากคิดว่าการขึ้นบรรทัดใหม่จะถูกตัดออก จะกลายเป็น ['one', 'two', 'three'] แต่หากต้องการผลนั้นต้องใช้ rstrip กับแต่ละองค์ประกอบ สิ่งที่คืนค่าเป็นสตริงเดียวโดยไม่แบ่งคือ read ในขณะที่ readlines จะคืนค่าเป็นลิสต์ หากคิดว่าไม่ได้ใช้การขึ้นบรรทัดใหม่เป็นตัวแบ่ง แต่ห่อทั้งไฟล์ไว้เป็นองค์ประกอบเดียว จะกลายเป็น ['one\ntwo\nthree\n'] แต่ readlines จะแบ่งองค์ประกอบทุกครั้งที่พบการขึ้นบรรทัดใหม่ เมื่อเขียนเป็น for line in f เพื่อดึงทีละบรรทัด บรรทัดที่ส่งมาก็จะมีการขึ้นบรรทัดใหม่ติดมาด้วยเช่นกัน

ข้อ 23 | read ครั้งที่ 2

อ่าน 2 ครั้งต่อเนื่องกันจากอ็อบเจกต์ไฟล์เดียวกัน สิ่งที่แสดงผลคือข้อใด

with open('/tmp/sample.txt') as f:    a = f.read()    b = f.read()print(len(a), b == '')
  1. แสดง 14 False
  2. แสดง 14 True
  3. แสดง 0 False
  4. แสดง 0 True
คำตอบB. แสดง 14 True

อ็อบเจกต์ไฟล์จะจดจำตำแหน่งปัจจุบันไว้ read ครั้งแรกจะอ่านตั้งแต่ต้นจนจบ ดังนั้น a จะได้ 14 ตัวอักษรรวมการขึ้นบรรทัดใหม่ และตำแหน่งจะเลื่อนไปที่ท้ายสุด read ครั้งที่ 2 จะพยายามอ่านจากท้ายสุด เนื่องจากไม่มีส่วนที่เหลือ จึงคืนค่าสตริงว่าง ดังนั้น len(a) จึงเป็น 14 และ b == '' จึงเป็น True หากคิดว่าครั้งที่ 2 จะคืนเนื้อหาเดียวกัน จะกลายเป็น b == '' เป็น False แต่หากต้องการอ่านเนื้อหาเดิมซ้ำต้องใช้ f.seek(0) เพื่อย้ายตำแหน่งกลับไปที่ต้น การที่ len(a) จะเป็น 0 คือกรณีที่ไฟล์ว่างเปล่าหรือตำแหน่งอยู่ที่ท้ายสุดอยู่แล้วตั้งแต่ต้น การไม่แสดงอะไรเลยมักถูกเข้าใจผิดว่าการอ่านล้มเหลว แต่ที่จริงเกิดจากตำแหน่งอยู่ที่ท้ายสุด

ข้อ 24 | with และ close

ยืนยัน closed ทั้งภายในและภายนอกบล็อกของ with 2 บรรทัดที่แสดงผลคือข้อใด

with open('/tmp/sample.txt') as f:    print(f.closed)print(f.closed)
  1. False แล้วตามด้วย False
  2. False แล้วตามด้วย True
  3. True แล้วตามด้วย False
  4. True แล้วตามด้วย True
คำตอบB. False แล้วตามด้วย True

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

ข้อ 25 | โหมดการเขียน

เปิดไฟล์เดียวกัน 3 ครั้งแล้วเขียนตามลำดับก่อนอ่านออกมา สิ่งที่แสดงผลคือข้อใด

p = '/tmp/sample.txt'with open(p, 'w') as f:    f.write('AAA')with open(p, 'a') as f:    f.write('BBB')with open(p, 'w') as f:    f.write('C')with open(p) as f:    print(f.read())
  1. เนื้อหาในไฟล์กลายเป็น AAABBB
  2. เนื้อหาในไฟล์กลายเป็น AAABBBC
  3. เนื้อหาในไฟล์กลายเป็น C
  4. เนื้อหาในไฟล์กลายเป็น CAABBB
คำตอบC. เนื้อหาในไฟล์กลายเป็น C

เมื่อเปิดด้วย 'w' ไฟล์จะถูกตัดให้มีความยาว 0 ณ เวลานั้น ไม่ว่าจะมีเนื้อหาเดิมอยู่หรือไม่ ไล่ตามลำดับ 'w' ครั้งแรกจะใส่ AAA เข้าไป จากนั้น 'a' ถัดมาเป็นการเขียนต่อท้าย จึงกลายเป็น AAABBB แต่พอเปิดด้วย 'w' เป็นครั้งที่ 3 AAABBB จะถูกลบทิ้งทันที แล้วเขียน C เข้าไปแทน สุดท้ายจึงเหลือเพียง C เท่านั้น หากคิดว่าเปิดด้วย 'a' ทั้ง 3 ครั้ง จะกลายเป็น AAABBBC หากคิดว่าการเขียนครั้งสุดท้ายถูกละเลย จะกลายเป็น AAABBB การเขียนทับตั้งแต่ต้นโดยไม่ตัดทิ้งคือพฤติกรรมของ 'r+' ซึ่งในกรณีนั้นจะมีเพียงตัวอักษรแรกเท่านั้นที่ถูกแทนที่ กลายเป็นแบบ CAABBB การตัดทิ้งจะเกิดขึ้น ณ วินาทีที่เปิด ไม่ใช่วินาทีที่เขียน ดังนั้นหากเปิดด้วย 'w' แล้วไม่เขียนอะไรเลย ไฟล์ก็จะกลายเป็นว่างเปล่า หากตั้งใจจะเขียนต่อท้ายให้ใช้ 'a' หากตั้งใจจะสร้างใหม่ให้ใช้ 'w' แสดงเจตนาด้วยโหมด

ข้อ 26 | การดำเนินการหลังปิด

เมื่อพยายามอ่านจากอ็อบเจกต์ไฟล์หลังจาก close แล้ว จะเกิดอะไรขึ้น

f = open('/tmp/sample.txt')f.close()print(f.read())
  1. เกิด ValueError แจ้งว่าเป็นการดำเนินการกับไฟล์ที่ปิดแล้ว
  2. แสดงสตริงว่าง แล้วรันจบตามปกติ
  3. เปิดใหม่โดยอัตโนมัติ แล้วแสดงเนื้อหาทั้งหมดของไฟล์
  4. เกิด OSError แจ้งว่าเปิดไฟล์ใหม่ไม่ได้
คำตอบA. เกิด ValueError แจ้งว่าเป็นการดำเนินการกับไฟล์ที่ปิดแล้ว

เมื่อเรียก read กับอ็อบเจกต์ไฟล์หลังจาก close แล้ว จะเกิด ValueError พร้อมข้อความ I/O operation on closed file. ตัวอ็อบเจกต์จะจดจำไว้ว่าถูกปิดแล้ว จึงไม่มีการเปิดใหม่โดยอัตโนมัติแม้จะจำเป็นต้องใช้ ไม่ได้คืนค่าสตริงว่างในความหมายว่าไม่มีอะไรให้อ่าน นั่นเป็นพฤติกรรมของ read หลังอ่านจนถึงท้ายสุดแล้วต่างหาก ข้อผิดพลาดอย่าง FileNotFoundError ที่เกิดเมื่อหาไฟล์ไม่พบเป็นพวกเดียวกับ OSError แต่ข้อยกเว้นเมื่อดำเนินการกับไฟล์ที่ปิดแล้วเป็นคนละอย่างคือ ValueError หากใช้ประโยค with จะหลีกเลี่ยงการพยายามอ่านหลังออกจากบล็อกได้โดยโครงสร้างเอง

ข้อ 27 | อ่านได้อย่างเดียว

เมื่อพยายามเขียนลงไฟล์ที่เปิดโดยละโหมดไว้ จะเกิดอะไรขึ้น

with open('/tmp/sample.txt', 'w') as f:    f.write('data')with open('/tmp/sample.txt') as f:    f.write('more')
  1. เกิด io.UnsupportedOperation แจ้งว่าเขียนไม่ได้
  2. เกิด ValueError แจ้งว่าการระบุโหมดไม่ถูกต้อง
  3. more ถูกเพิ่มต่อท้ายเนื้อหา แล้วรันจบตามปกติ
  4. เนื้อหาถูกแทนที่ด้วย more แล้วรันจบตามปกติ
คำตอบA. เกิด io.UnsupportedOperation แจ้งว่าเขียนไม่ได้

หากละอาร์กิวเมนต์ตัวที่ 2 ของ open ไว้ จะเป็นโหมดอ่านได้อย่างเดียวคือ 'r' หากเรียก write กับอ็อบเจกต์ไฟล์ที่เปิดด้วยโหมดอ่านได้อย่างเดียว จะเกิด io.UnsupportedOperation พร้อมข้อความ not writable เนื่องจากเป็นวิธีเขียนที่ผ่านไวยากรณ์ได้ จึงจะรู้ตัวว่าเลือกโหมดผิดหลังจากรันจริงแล้ว หากต้องการเขียนทับให้ใช้ 'w' หากต้องการเพิ่มต่อท้ายให้ใช้ 'a' หากต้องการทั้งอ่านและเขียนให้เปิดใหม่ด้วย 'r+' ในทางกลับกัน หากเรียก read กับไฟล์ที่เปิดด้วย 'w' หรือ 'a' ก็จะเกิด io.UnsupportedOperation เช่นเดียวกันพร้อมข้อความ not readable กล่าวได้ว่าโหมดคือที่ที่ประกาศว่าจะอนุญาตให้ทำอะไรกับอ็อบเจกต์ไฟล์นั้นได้บ้าง

ข้อ 28 | seek และ tell

อ่านเพียง 5 ตัวอักษรจากต้นแล้วยืนยันตำแหน่ง โปรแกรมต่อไปนี้ให้ผลลัพธ์ใด

with open('/tmp/sample.txt') as f:    head = f.read(5)    pos = f.tell()    f.seek(0)    print(repr(head), pos, f.tell())
  1. 'one\ntwo\nthree\n' 14 0
  2. 'one\nt' 5 0
  3. 'one\nt' 5 5
  4. 'one\nt' 0 0
คำตอบB. 'one\nt' 5 0

หากส่งจำนวนเข้าไปให้ read จะอ่านเพียงจำนวนตัวอักษรนั้น 5 ตัวอักษรจากต้นคือ one 3 ตัวอักษร การขึ้นบรรทัดใหม่ 1 ตัว และ t ของ two จึงกลายเป็น 'one\nt' เนื่องจากตำแหน่งจะเลื่อนไปตามจำนวนที่อ่าน tell ที่เรียกทันทีหลังจากนั้นจะคืนค่า 5 seek(0) ถัดมาจะย้ายตำแหน่งกลับไปที่ต้น ดังนั้น tell สุดท้ายจึงเป็น 0 หากคิดว่า read ไม่ทำให้ตำแหน่งเคลื่อนที่ จะกลายเป็น tell เป็น 0 ทั้ง 2 ครั้ง หากคิดว่า seek ไม่มีผล จะกลายเป็น tell เป็น 5 ทั้ง 2 ครั้ง หากสับสนว่าอาร์กิวเมนต์ของ read เป็นจำนวนบรรทัดไม่ใช่จำนวนตัวอักษร จะเข้าใจผิดว่าตั้งใจอ่านถึงบรรทัดที่ 5 แล้วเลือกทั้งไฟล์ 'one\ntwo\nthree\n' กับ 14 ผิดไป หากใช้ seek(0) สามารถอ่านจากต้นซ้ำได้โดยใช้อ็อบเจกต์ไฟล์เดิม

ข้อ 29 | JSON และทูเพิล

แปลงพจนานุกรมที่มีทูเพิลอยู่ข้างในเป็น JSON แล้วอ่านกลับ โปรแกรมต่อไปนี้ให้ 2 บรรทัดใด

import jsons = json.dumps({'t': (1, 2)})back = json.loads(s)print(s)print(back, type(back['t']).__name__)
  1. {"t": "(1, 2)"} แล้วตามด้วย {'t': '(1, 2)'} str
  2. {"t": [1, 2]} แล้วตามด้วย {'t': (1, 2)} tuple
  3. {"t": (1, 2)} แล้วตามด้วย {'t': (1, 2)} tuple
  4. {"t": [1, 2]} แล้วตามด้วย {'t': [1, 2]} list
คำตอบD. {"t": [1, 2]} แล้วตามด้วย {'t': [1, 2]} list

ใน JSON มีแต่อาร์เรย์เท่านั้น ไม่มีชนิดที่เรียกว่าทูเพิลอยู่ ดังนั้น ณ เวลาที่เรียก dumps ทูเพิลจะถูกแปลงเป็นอาร์เรย์ เอาต์พุตจึงเป็น {"t": [1, 2]} เมื่ออ่านกลับ อาร์เรย์ก็จะถูกคืนสภาพเป็นลิสต์ ดังนั้นชนิดของ back['t'] จึงเป็น list ไม่กลับไปเป็นทูเพิล ข้อมูลที่ว่าเดิมเป็นทูเพิลจะไม่หลงเหลืออยู่ที่ใดเลย ดังนั้นเมื่อผ่านไปกลับชนิดจึงเปลี่ยน หากต้องการให้ยังคงเป็นทูเพิล ต้องเรียก tuple() เองหลังจากอ่านกลับมาแล้ว วงเล็บกลมจะไม่ปรากฏในเอาต์พุตของ JSON และทูเพิลก็จะไม่ถูกแปลงเป็นสตริงด้วย ด้วยเหตุผลเดียวกัน หากใช้จำนวนเต็มเป็นคีย์ของพจนานุกรม จะถูกแปลงเป็นสตริงตอน dumps และเมื่ออ่านกลับก็จะไม่กลับไปเป็นจำนวนเต็ม

ข้อ 30 | คีย์ของ JSON

เมื่อพยายามแปลงพจนานุกรมที่มีคีย์เป็นจำนวนเต็มและเซตเป็น JSON จะเกิดอะไรขึ้น

import jsonprint(json.dumps({1: 'a'}))print(json.dumps({1, 2}))
  1. บรรทัดที่ 1 แสดง {1: "a"} บรรทัดที่ 2 แสดง [1, 2]
  2. บรรทัดที่ 1 เกิด TypeError บรรทัดที่ 2 ไม่ถูกรัน
  3. บรรทัดที่ 1 แสดง {"1": "a"} บรรทัดที่ 2 เกิด TypeError
  4. บรรทัดที่ 1 แสดง {"1": "a"} บรรทัดที่ 2 แสดง [1, 2]
คำตอบC. บรรทัดที่ 1 แสดง {"1": "a"} บรรทัดที่ 2 เกิด TypeError

คีย์ของออบเจกต์ใน JSON ต้องเป็นสตริงเท่านั้น ดังนั้นเมื่อ dumps พจนานุกรมที่มีจำนวนเต็มเป็นคีย์ คีย์นั้นจะถูกแปลงเป็นสตริงแล้วแสดงผลเป็น {"1": "a"} นี่คือการดำเนินการที่สำเร็จตามปกติ ไม่เกิดข้อยกเว้น ณ จุดนี้ แต่ควรระวังว่าเมื่ออ่านกลับมา คีย์จะยังคงเป็นสตริงตามเดิม ไม่กลับไปเป็นจำนวนเต็ม เซตในบรรทัดที่ 2 ไม่มีชนิดที่สอดคล้องใน JSON ดังนั้นเมื่อพยายาม dumps จะเกิด TypeError พร้อมข้อความ Object of type set is not JSON serializable หากคิดว่าเซตจะถูกเขียนออกมาเป็นอาร์เรย์ จะกลายเป็น [1, 2] แต่หากต้องการให้แปลงแบบนั้นต้องเรียก list() เองก่อนแล้วจึงส่งเข้าไป สำหรับค่าที่มีภาษาญี่ปุ่นปนอยู่ ตามค่าเริ่มต้นจะถูก escape แล้วแสดงผล หากส่งค่าเท็จให้กับ ensure_ascii จะแสดงผลเป็นตัวอักษรตามเดิม

ฝึกทำ: ทำโจทย์ในหน้านี้

เครื่องมือฝึกทำโจทย์แบบสุ่มคำถาม (ทำงานเมื่อเปิดใช้ JavaScript) โจทย์และคำอธิบายด้านบนสามารถอ่านได้ทั้งหมดตามปกติ

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

หน้านี้แปลจากต้นฉบับภาษาญี่ปุ่น หากเนื้อหาของคำแปลและต้นฉบับไม่ตรงกัน ให้ถือฉบับภาษาญี่ปุ่นเป็นหลัก ดูต้นฉบับภาษาญี่ปุ่น