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

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

ข้อยกเว้น คลาส และไลบรารีมาตรฐาน

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

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

ข้อ 1 | ลำดับของประโยค try

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

def calc(a, b):    try:        print("try")        r = a / b    except ZeroDivisionError:        print("except")    else:        print("else")    finally:        print("finally")calc(9, 3)
  1. try, else, except
  2. try, else, finally
  3. try, except, else
  4. try, except, finally
คำตอบB. try, else, finally

9 / 3 ไม่ส่งข้อยกเว้นออกมา จึงไม่เข้า except ส่วน else จะทำงานเฉพาะเมื่อไม่เกิดข้อยกเว้นเท่านั้น จึงทำงานหลัง try ส่วน finally จะทำงานเสมอไม่ว่าจะมีข้อยกเว้นหรือไม่ จึงทำงานเป็นลำดับสุดท้าย ดังนั้นจึงได้ 3 บรรทัดคือ try, else, finally ตัวเลือกที่มี except ปรากฏอยู่คือลำดับที่เกิดจากการเข้าใจผิดว่าเกิดข้อยกเว้น แต่การเรียกนี้ไม่เกิด ZeroDivisionError ตัวเลือกที่ขาด finally ไปคือลำดับที่เกิดจากการสับสนว่า finally คือ “การจัดการหลังบ้านเมื่อเกิดข้อยกเว้น”

ข้อ 2 | ส่วน else

ข้อใดคือเงื่อนไขที่ส่วน else ของประโยค try จะทำงาน

  1. ทำงานเป็นลำดับสุดท้ายเสมอ ไม่ว่าจะเกิดข้อยกเว้นหรือไม่
  2. ทำงานเฉพาะเมื่อเกิดข้อยกเว้นในส่วน try เท่านั้น
  3. ทำงานเมื่อไม่ตรงกับ except ข้อใดเลย
  4. ทำงานเฉพาะเมื่อไม่เกิดข้อยกเว้นในส่วน try เท่านั้น
คำตอบD. ทำงานเฉพาะเมื่อไม่เกิดข้อยกเว้นในส่วน try เท่านั้น

ส่วน else ของประโยค try จะทำงานเฉพาะเมื่อส่วน try จบลงโดยไม่ส่งข้อยกเว้นออกมาเลยเท่านั้น ในขณะที่ else ของประโยค if คือ “เมื่อเงื่อนไขเป็นเท็จ” else ของประโยค try จะอยู่ฝั่ง “เมื่อสำเร็จ” จึงควรจำไว้ว่าทิศทางตรงข้ามกัน สิ่งที่ทำงานเมื่อเกิดข้อยกเว้นคือส่วน except สิ่งที่ทำงานเสมอไม่ว่าจะมีข้อยกเว้นหรือไม่คือส่วน finally หากไม่ตรงกับ except ข้อใดเลย จะไม่ตกไปที่ else แต่ข้อยกเว้นนั้นจะถูกส่งต่อไปยังฝั่งที่เรียก

ข้อ 3 | finally มาก่อน

เมื่อรันโค้ดต่อไปนี้ ผลลัพธ์คือข้อใด

def f():    try:        return "T"    finally:        print("F")def g():    try:        return "T"    finally:        return "N"print(f(), g())
  1. แสดง F แล้วตามด้วย T T รวม 2 บรรทัด
  2. แสดง F แล้วตามด้วย T N รวม 2 บรรทัด
  3. แสดง T T แล้วตามด้วย F รวม 2 บรรทัด
  4. แสดง T N แล้วตามด้วย F รวม 2 บรรทัด
คำตอบB. แสดง F แล้วตามด้วย T N รวม 2 บรรทัด

f() จะเตรียมค่าที่จะ return ไว้ก่อน แล้วรันส่วน finally ก่อนที่จะคืนค่าจริง ดังนั้น F จะแสดงก่อน และค่าที่คืนกลับยังคงเป็น T ตามเดิม g() มี return อยู่ในส่วน finally ซึ่งจะเขียนทับ return ของส่วน try ค่าที่คืนกลับจึงเป็น N print จะประเมิน f() และ g() เสร็จก่อนแล้วจึงแสดงรวมเป็นบรรทัดเดียว ดังนั้นบรรทัด F จะมาก่อน บรรทัด T N จะตามมาทีหลัง ตัวเลือกที่เป็น T T คือคำตอบเมื่อคิดว่า return ของ finally ถูกละเลย ตัวเลือกที่ F มาทีหลังคือคำตอบเมื่อคิดว่า finally ทำงานหลัง return

ข้อ 4 | ข้อยกเว้นที่ดักจับไม่ได้

ข้อใดคือข้อยกเว้นที่ส่วน except ที่เขียนว่า except Exception ดักจับไม่ได้

  1. FileNotFoundError
  2. ZeroDivisionError
  3. KeyboardInterrupt
  4. ModuleNotFoundError
คำตอบC. KeyboardInterrupt

KeyboardInterrupt อยู่ใต้ BaseException โดยตรง ไม่ได้สืบทอดมาจาก Exception issubclass(KeyboardInterrupt, Exception) จะเป็น False เนื่องจากเป็นสิ่งที่เกิดขึ้นเมื่อผู้ใช้พยายามหยุดด้วย Ctrl-C จึงถูกแยกลำดับชั้นไว้เพื่อไม่ให้ except Exception ที่เขียนไว้กว้าง ๆ กลืนไปโดยไม่ตั้งใจ SystemExit ก็อยู่ในตำแหน่งเดียวกันนี้ ส่วนอีก 3 ตัวที่เหลือล้วนเป็นลูกหลานของ Exception ทั้งหมด ZeroDivisionError อยู่ใต้ ArithmeticError, FileNotFoundError อยู่ใต้ OSError, ModuleNotFoundError อยู่ใต้ ImportError จึงถูก except Exception ดักจับได้

ข้อ 5 | คลาสแม่ของข้อยกเว้น

คลาสแม่ที่ใกล้ที่สุดที่ IndexError และ KeyError มีร่วมกันคือข้อใด

  1. ReferenceError
  2. ArithmeticError
  3. AttributeError
  4. LookupError
คำตอบD. LookupError

ทั้ง IndexError และ KeyError มีลักษณะร่วมกันคือใช้ดัชนีหรือคีย์ค้นหาแล้วไม่พบ จึงถูกวางไว้ใต้คลาสแม่ร่วมกันคือ LookupError หากเขียน except LookupError จะสามารถรับทั้งการเข้าถึงลิสต์นอกขอบเขตและคีย์ที่ไม่มีอยู่ในพจนานุกรมได้พร้อมกัน ArithmeticError เป็นคลาสแม่ของ ZeroDivisionError เป็นต้น ใช้ในกรณีที่การคำนวณเองไม่สำเร็จ AttributeError เป็นข้อยกเว้นเมื่อหาแอตทริบิวต์ไม่พบ ReferenceError เป็นข้อยกเว้นเมื่อสิ่งที่ weak reference ชี้ไปหายไปแล้ว ทั้งสองอยู่ใต้ Exception โดยตรงและเป็นคนละสายกับ LookupError

ข้อ 6 | ลำดับของ except

ข้อใดคือผลลัพธ์ที่ถูกต้องเมื่อรันโค้ดต่อไปนี้

try:    x = 8 / 0except ArithmeticError:    print("A")except ZeroDivisionError:    print("Z")
  1. ไม่แสดงอะไรเลยแล้วหยุด
  2. แสดงเฉพาะ Z
  3. แสดงทั้ง A และ Z
  4. แสดงเฉพาะ A
คำตอบD. แสดงเฉพาะ A

8 / 0 จะส่ง ZeroDivisionError ออกมา แต่ ZeroDivisionError เป็นคลาสลูกของ ArithmeticError ส่วน except จะตรวจสอบเรียงจากบนลงล่างและหยุดที่จุดแรกที่ตรงกัน ดังนั้น except ArithmeticError ที่เขียนไว้ก่อนจะดักจับไปก่อน แสดงเฉพาะ A เท่านั้น ส่วน except ZeroDivisionError ที่อยู่ถัดมาจะไม่มีโอกาสทำงาน เนื่องจาก except ที่ตรงกันจะทำงานเพียงตัวเดียว จึงไม่มีทางที่ทั้ง A และ Z จะปรากฏพร้อมกัน หลักการคือให้เรียงตัวที่แคบกว่า (คลาสลูก) ไว้ก่อน แล้วเรียงตัวที่กว้างกว่า (คลาสแม่) ไว้ทีหลัง โค้ดในโจทย์นี้เขียนสลับลำดับนี้

ข้อ 7 | ข้อยกเว้นหลายชนิด

บรรทัดที่แสดงผลเมื่อรันโค้ดต่อไปนี้คือข้อใด

def conv(s):    try:        return int(s) * 2    except (ValueError, TypeError) as e:        return type(e).__name__print(conv("21"), conv("abc"), conv(None))
  1. 42 ValueError TypeError
  2. 42 TypeError TypeError
  3. 42 ValueError ValueError
  4. 42 TypeError ValueError
คำตอบA. 42 ValueError TypeError

หากเขียนทูเพิลที่ล้อมด้วยวงเล็บกลมไว้หลัง except จะทำงานเมื่อตรงกับตัวใดตัวหนึ่งในนั้น conv("21") จะได้ 42 เพราะ int("21") เป็น 21 conv("abc") ส่งเป็นสตริงได้แต่ตีความเป็นตัวเลขไม่ได้ จึงเกิด ValueError conv(None) ไม่ใช่ชนิดที่ส่งให้ int() ได้ จึงเกิด TypeError การแยกแยะว่าชนิดถูกต้องแต่ค่าไม่เหมาะสมจะเป็น ValueError ส่วนถ้าชนิดไม่ตรงตั้งแต่แรกจะเป็น TypeError นี่คือลำดับของคำตอบโดยตรง

ข้อ 8 | การส่งต่อข้อยกเว้นอีกครั้ง

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

class LoadError(Exception):    passdef load():    try:        raise LoadError("bad")    except LoadError:        print("inner")        raisetry:    load()except Exception as e:    print("outer", type(e).__name__)
  1. แสดงเฉพาะ inner โดยไม่แสดง outer
  2. แสดง inner และ outer Exception รวม 2 บรรทัด
  3. แสดง inner และ outer LoadError รวม 2 บรรทัด
  4. แสดง inner แล้วหยุดที่ LoadError
คำตอบC. แสดง inner และ outer LoadError รวม 2 บรรทัด

หากเขียน raise เฉย ๆ โดยไม่มีอาร์กิวเมนต์ในส่วน except จะเป็นการส่งข้อยกเว้นที่กำลังจัดการอยู่กลับไปยังฝั่งที่เรียกอีกครั้งตามเดิม ดังนั้นหลังแสดง inner แล้ว LoadError จะแพร่ออกไปนอก load() แล้ว except Exception ชั้นนอกจะรับไว้ เนื่องจาก LoadError สืบทอดมาจาก Exception จึงถูก except Exception ดักจับได้ ไม่หยุดการทำงาน type(e).__name__ จะคืนชื่อคลาสข้อยกเว้นที่ถูกส่งออกมาจริง ดังนั้นจะแสดง LoadError ไม่ใช่ชื่อชนิดของฝั่งที่ดักจับคือ Exception การที่แสดงเฉพาะ inner คือผลลัพธ์เมื่อกลืนข้อยกเว้นไว้ ณ จุดนั้นโดยไม่ส่งต่ออีกครั้ง

ข้อ 9 | ข้อยกเว้นที่ดักจับไม่ได้

เมื่อรันโค้ดต่อไปนี้ จะได้ผลลัพธ์แบบใด

def get(seq, key):    try:        return seq[key]    except IndexError:        return "not found"print(get([1, 2, 3], 1))print(get({"a": 1}, "z"))
  1. แสดง 2 แล้วหยุดที่ KeyError
  2. แสดง 2 และ not found รวม 2 บรรทัด
  3. แสดง 2 และ KeyError รวม 2 บรรทัด
  4. แสดง 2 และ None รวม 2 บรรทัด
คำตอบA. แสดง 2 แล้วหยุดที่ KeyError

การเรียกครั้งที่ 1 ใช้ดัชนี 1 ของลิสต์ จึงได้ 2 กลับมา ครั้งที่ 2 ดึงคีย์ที่ไม่มีในพจนานุกรม จึงเกิด KeyError แต่สิ่งที่เขียนไว้ใน except มีเพียง IndexError เท่านั้น แม้ทั้ง IndexError และ KeyError จะเป็นลูกของ LookupError เหมือนกัน แต่ไม่ได้เป็นพ่อลูกกันเอง except ของ IndexError จึงดักจับ KeyError ไม่ได้ ข้อยกเว้นที่ดักจับไม่ได้จะแพร่ไปยังฝั่งที่เรียก เมื่อไม่มีผู้รับไว้ โปรแกรมจึงหยุดทำงาน หากต้องการรับทั้งคู่ ให้เขียนเป็น except LookupError หรือเรียงทั้งสองไว้ในทูเพิล

ข้อ 10 | assert

ข้อใดคือคำอธิบายที่ถูกต้องของประโยค assert

  1. หากเงื่อนไขเป็นเท็จจะส่ง AssertionError และถูกปิดการทำงานเมื่อรันด้วย python3 -O
  2. หากเงื่อนไขเป็นเท็จจะส่ง AssertionError และยังคงทำงานเสมอแม้รันด้วย python3 -O
  3. หากเงื่อนไขเป็นจริงจะส่ง AssertionError และถูกปิดการทำงานเมื่อรันด้วย python3 -O
  4. หากเงื่อนไขเป็นเท็จจะส่ง ValueError และยังคงทำงานเสมอแม้รันด้วย python3 -O
คำตอบA. หากเงื่อนไขเป็นเท็จจะส่ง AssertionError และถูกปิดการทำงานเมื่อรันด้วย python3 -O

assert เงื่อนไข, ข้อความ จะส่ง AssertionError เมื่อเงื่อนไขเป็นเท็จ หากเงื่อนไขเป็นจริงจะไม่เกิดอะไรขึ้นเลย สิ่งสำคัญคือเมื่อรันในโหมดปรับให้เหมาะสมอย่าง python3 -O ประโยค assert ทั้งหมดจะถูกตัดออกไป ไม่มีแม้แต่บรรทัดเดียวถูกรัน ดังนั้นห้ามใช้ assert เพื่อจุดประสงค์ในการตรวจสอบข้อมูลนำเข้าจากผู้ใช้ สำหรับจุดประสงค์นั้นควรตรวจสอบเงื่อนไขด้วย if แล้วส่ง raise ValueError(...) แทน ควรจำไว้แยกบทบาทว่า assert เป็นเครื่องมือสำหรับบันทึกสมมติฐานภายในไว้ระหว่างการพัฒนา

ข้อ 11 | self

ข้อใดคือคำอธิบายที่ถูกต้องเกี่ยวกับอาร์กิวเมนต์ตัวที่ 1 ของเมธอดคือ self

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

เมื่อเขียน d.speak() Python จะแปลงให้เป็น Dog.speak(d) แล้วเรียก ในตำแหน่งอาร์กิวเมนต์ตัวที่ 1 นั้นเองที่ตัวอินสแตนซ์ที่เรียกจะถูกส่งเข้ามา การสะกดว่า self เป็นเพียงธรรมเนียมปฏิบัติ ไม่ใช่ชื่อที่ภาษาสงวนไว้ สิ่งที่กำหนดไว้ในข้อกำหนดมีเพียงตำแหน่งที่อาร์กิวเมนต์ตัวที่ 1 จะรับตัวเองเท่านั้น ดังนั้นแม้จะเขียนเองว่า Dog.speak(d) ก็ยังทำงานเหมือนเดิม การสร้างอินสแตนซ์แล้วคืนกลับเป็นหน้าที่ของ __new__ และกลไกการเรียกคลาส ส่วน __init__ เป็นเพียงการกำหนดค่าเริ่มต้นให้กับอินสแตนซ์ที่ถูกสร้างขึ้นแล้วเท่านั้น การอ้างอิงคลาสแม่ใช้ super()

ข้อ 12 | ตัวแปรของคลาส

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

class Dog:    kind = "canine"    def __init__(self, name):        self.name = namea = Dog("Rex")b = Dog("Mia")Dog.kind = "wolf"print(a.kind, b.kind)
  1. wolf wolf
  2. canine wolf
  3. canine canine
  4. wolf canine
คำตอบA. wolf wolf

kind เป็นตัวแปรของคลาสที่เขียนไว้ใต้ class โดยตรง อินสแตนซ์ทั้งหมดที่สร้างจากคลาสนี้จะใช้ตัวเดียวกันร่วมกัน เนื่องจากทั้ง a และ b ไม่มี kind อยู่ใน __dict__ ของตนเอง เมื่ออ่านแอตทริบิวต์จึงย้อนไปดูที่ฝั่งคลาสและเห็นค่าเดียวกัน การกำหนดค่าให้ Dog.kind คือการเขียนทับตัวที่ใช้ร่วมกันนี้ ดังนั้นทั้ง a และ b ที่สร้างไว้แล้วจะเห็น wolf ทั้งคู่ เนื่องจากค่าไม่ได้ถูกคัดลอกตอนสร้างอินสแตนซ์ จึงไม่มีทางที่ canine จะยังคงเหลืออยู่

ข้อ 13 | เงาของแอตทริบิวต์

โค้ดต่อไปนี้กำหนดค่าที่ฝั่งอินสแตนซ์ ผลลัพธ์คือข้อใด

class Dog:    kind = "canine"    def __init__(self, name):        self.name = namea = Dog("Rex")b = Dog("Mia")a.kind = "husky"print(a.kind, b.kind, Dog.kind)print(sorted(a.__dict__))
  1. husky husky husky แล้วตามด้วย ['name']
  2. husky canine canine แล้วตามด้วย ['kind', 'name']
  3. husky canine canine แล้วตามด้วย ['name']
  4. husky husky husky แล้วตามด้วย ['kind', 'name']
คำตอบB. husky canine canine แล้วตามด้วย ['kind', 'name']

การกำหนดค่าให้ a.kind จะไม่เขียนทับตัวแปรของคลาส แต่จะสร้างรายการใหม่ชื่อ kind ไว้ในพจนานุกรมของอินสแตนซ์ a นับจากนั้นเมื่ออ่าน a.kind จะพบรายการใหม่นี้ก่อน กลายเป็นการทอดเงาทับตัวแปรของคลาส ส่วน b และ Dog.kind ยังคงเป็น canine ตามเดิม ดังนั้นใน a.__dict__ จะมีทั้ง name ที่ใส่ไว้ใน __init__ และ kind เรียงอยู่ด้วย เมื่อ sorted จะได้ ['kind', 'name'] ตัวเลือกที่ทั้งหมดเป็น husky คือคำตอบเมื่อเข้าใจผิดว่าการกำหนดค่าที่อินสแตนซ์เขียนทับตัวแปรของคลาส ตัวเลือกที่มีเพียง ['name'] คือคำตอบเมื่อคิดว่าไม่เกิดเงาขึ้น

ข้อ 14 | ถูกใช้ร่วมกัน

โค้ดต่อไปนี้ทำให้ลิสต์เป็นตัวแปรของคลาส ผลลัพธ์คือข้อใด

class Basket:    items = []    def add(self, x):        self.items.append(x)p = Basket()q = Basket()p.add(1)q.add(2)print(p.items, q.items)
  1. p.items เป็น [1], q.items เป็น [1, 2]
  2. p.items เป็น [1, 2], q.items เป็น [2]
  3. p.items เป็น [1, 2], q.items เป็น [1, 2]
  4. p.items เป็น [1], q.items เป็น [2]
คำตอบC. p.items เป็น [1, 2], q.items เป็น [1, 2]

self.items.append(x) ไม่ใช่การกำหนดค่าให้ self.items แต่เป็นเพียงการเรียกอ็อบเจกต์ที่ self.items ไปถึงเท่านั้น เนื่องจากทั้ง p และ q ไม่มี items เป็นของตัวเอง ปลายทางที่ไปถึงจึงเป็นลิสต์ตัวเดียวกันของตัวแปรคลาส ผลคือทั้ง 1 ที่เพิ่มจาก p และ 2 ที่เพิ่มจาก q จะเข้าไปอยู่ในลิสต์เดียวกัน มองจากฝั่งไหนก็จะเห็นเป็น [1, 2] เหมือนกัน หากต้องการให้แต่ละอินสแตนซ์มีภาชนะแยกกัน ต้องกำหนดค่า self.items = [] ไว้ใน __init__ เพื่อสร้างขึ้นใหม่ เมื่อนั้นจะแยกเป็น [1] และ [2]

ข้อ 15 | super()

โค้ดต่อไปนี้ใช้การสืบทอดและ super() ผลลัพธ์คือข้อใด

class Animal:    def __init__(self, name):        self.name = name    def cry(self):        return self.name + " ?"class Cat(Animal):    def __init__(self, name, color):        super().__init__(name)        self.color = color    def cry(self):        return self.name + " meow"c = Cat("Tama", "black")print(c.cry(), c.color, isinstance(c, Animal))
  1. Tama meow None True
  2. Tama ? black True
  3. Tama meow black False
  4. Tama meow black True
คำตอบD. Tama meow black True

__init__ ของ Cat เรียก super().__init__(name) จึงให้ Animal ซึ่งเป็นคลาสแม่กำหนดค่า self.name ให้ จากนั้นจะใส่ black ให้ self.color ทำให้มีแอตทริบิวต์ทั้งสองครบ เนื่องจาก cry ถูกนิยามใหม่ในคลาสลูก การทำงานของคลาสแม่จึงถูกเขียนทับ และจะเรียก meow แทน isinstance(c, Animal) จะเป็น True เนื่องจาก Cat สืบทอดมาจาก Animal หากลืมเขียน super() name จะไม่ถูกกำหนดค่าและจะเกิด AttributeError จึงไม่มีทางที่จะกลายเป็นความล้มเหลวแบบเงียบ ๆ อย่างตัวเลือกที่เป็น None

ข้อ 16 | MRO

โค้ดต่อไปนี้ใช้การสืบทอดหลายสาย (Multiple Inheritance) ผลลัพธ์คือข้อใด

class A:    def who(self):        return "A"class B(A):    def who(self):        return "B"class C(A):    def who(self):        return "C"class D(B, C):    passprint(D().who())print([k.__name__ for k in D.__mro__])
  1. แสดง B และ ['D', 'B', 'C', 'A', 'object']
  2. แสดง C และ ['D', 'C', 'B', 'A', 'object']
  3. แสดง B และ ['D', 'B', 'A', 'C', 'object']
  4. แสดง A และ ['D', 'B', 'C', 'A', 'object']
คำตอบA. แสดง B และ ['D', 'B', 'C', 'A', 'object']

ลำดับการค้นหาเมธอด (MRO) ถูกกำหนดด้วยกฎ C3 Linearization ไม่ใช่การค้นหาแบบเจาะลึกก่อนอย่างง่าย ๆ คลาสแม่ร่วม A จะถูกวางไว้หลังคลาสลูกทั้งหมดที่สืบทอดจาก A ดังนั้นจึงเรียงเป็น D, B, C, A, object หากเป็นการค้นหาแบบเจาะลึกก่อนที่ลงไปที่ B แล้วไปที่ A ตรง ๆ จะได้ D, B, A, C, object แต่ไม่ได้เป็นเช่นนั้น จุดนี้ต้องระวังเป็นพิเศษ who() จะค้นหาจากต้น MRO แล้วใช้ตัวแรกที่พบ จึงได้ B หากเขียนเป็น class E(C, B) จะได้ E, C, B, A, object ตัวที่เขียนไว้ก่อนจะมีความสำคัญเหนือกว่า

ข้อ 17 | ความขัดแย้งของ MRO

เมื่อ class B(A) ถูกนิยามไว้แล้ว หากเขียน class F(A, B): pass จะเกิดอะไรขึ้น

  1. นิยามได้สำเร็จ F().who() จะเรียกใช้ของ A
  2. นิยามได้สำเร็จ __mro__ จะเป็น F, A, B, object
  3. นิยามได้สำเร็จ F().who() จะเรียกใช้ของ B
  4. เกิด TypeError ตั้งแต่ตอนนิยามคลาส
คำตอบD. เกิด TypeError ตั้งแต่ตอนนิยามคลาส

C3 Linearization มีกฎ 2 ข้อคือ ต้องรักษาลำดับที่เรียงคลาสฐานไว้ และคลาสแม่ต้องอยู่หลังคลาสลูก F(A, B) เป็นการระบุให้วาง A ไว้ก่อน B แต่เนื่องจาก B เป็นลูกของ A คลาสแม่ A จึงต้องอยู่หลังคลาสลูก B เงื่อนไขทั้งสองนี้ไม่สามารถสอดคล้องกันได้พร้อมกัน จึงถือว่าไม่สามารถสร้าง MRO ที่สอดคล้องกันได้ และจะเกิด TypeError ณ เวลาที่ประเมินนิยามคลาส ไม่ใช่ว่าต้องรอลองรันดูก่อนถึงจะรู้ แต่จะล้มเหลวทันทีที่ผ่านประโยค class หากเขียนให้ถูกต้องต้องเขียนเป็น class F(B, A) โดยวางคลาสลูกไว้ก่อน

ข้อ 18 | การปรับเปลี่ยนชื่อ

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

class P:    def __init__(self):        self._one = 1        self.__two = 2p = P()print(p._one, p._P__two)print(sorted(p.__dict__))
  1. แสดง 1 2 และ ['_one', '_two']
  2. แสดง 1 2 และ ['_P__one', '_P__two']
  3. แสดง 1 2 และ ['__two', '_one']
  4. แสดง 1 2 และ ['_P__two', '_one']
คำตอบD. แสดง 1 2 และ ['_P__two', '_one']

_one ที่ขึ้นต้นด้วยขีดล่าง 1 ตัว เป็นเพียงธรรมเนียมที่บ่งบอกว่าใช้ภายในเท่านั้น ชื่อจะไม่เปลี่ยนแปลงและอ่านจากภายนอกได้ตามปกติ ในขณะที่ __two ที่ขึ้นต้นด้วยขีดล่าง 2 ตัว จะเกิดการปรับเปลี่ยนชื่อ (Name Mangling) ชื่อแอตทริบิวต์จริงจะถูกเขียนใหม่เป็น _P__two กล่าวคือ _ชื่อคลาส__ชื่อ ดังนั้นหากเขียน p.__two จะเกิด AttributeError แต่หากเขียน p._P__two จะอ่านได้ ใน __dict__ ก็จะเรียงเป็นชื่อหลังปรับเปลี่ยนแล้วเช่นกัน เมื่อ sorted จะได้ ['_P__two', '_one'] สิ่งที่ถูกปรับเปลี่ยนคือเฉพาะที่ขึ้นต้นด้วยขีดล่าง 2 ตัวเท่านั้น _one ไม่ได้เป็นเป้าหมาย

ข้อ 19 | เมธอดพิเศษ

โค้ดต่อไปนี้นิยามเมธอดพิเศษไว้ ผลลัพธ์คือข้อใด

class Vec:    def __init__(self, x, y):        self.x = x        self.y = y    def __str__(self):        return "V({}, {})".format(self.x, self.y)    def __repr__(self):        return "Vec(x={}, y={})".format(self.x, self.y)    def __len__(self):        return 2    def __add__(self, o):        return Vec(self.x + o.x, self.y + o.y)v = Vec(1, 2)print(v)print([v])print(len(v), v + Vec(3, 4))
  1. Vec(x=1, y=2) และ [Vec(x=1, y=2)] และ 2 V(4, 6)
  2. V(1, 2) และ [Vec(x=1, y=2)] และ 2 V(4, 6)
  3. V(1, 2) และ [Vec(x=1, y=2)] และ 2 Vec(x=4, y=6)
  4. V(1, 2) และ [V(1, 2)] และ 2 V(4, 6)
คำตอบB. V(1, 2) และ [Vec(x=1, y=2)] และ 2 V(4, 6)

print(v) และ str(v) จะใช้ __str__ จึงได้ V(1, 2) ในขณะที่หากใส่ไว้ในคอนเทนเนอร์อย่างลิสต์หรือพจนานุกรมแล้วแสดงผล แต่ละองค์ประกอบจะใช้ __repr__ แทน จึงได้ [Vec(x=1, y=2)] การแยกใช้แบบนี้คือจุดสำคัญของข้อนี้ len(v) คือค่าที่ __len__ คืนกลับคือ 2 ส่วน v + Vec(3, 4) คือ Vec ตัวใหม่ที่ __add__ คืนกลับ เมื่อส่งเข้า print จะใช้ __str__ อีกครั้งจึงแสดงเป็น V(4, 6) ในคลาสที่ไม่ได้นิยาม __str__ ไว้ __repr__ จะถูกใช้แทน แต่ในทางกลับกัน __str__ จะไม่ถูกใช้แทน __repr__

ข้อ 20 | __eq__

โค้ดต่อไปนี้นิยามเฉพาะ __eq__ ผลลัพธ์คือข้อใด

class Point:    def __init__(self, x):        self.x = x    def __eq__(self, other):        return self.x == other.xp = Point(1)q = Point(1)print(p == q, p is q)print(Point.__hash__ is None)try:    print(len({p, q}))except TypeError:    print("TypeError")
  1. True False และ False และ 2
  2. True False และ True และ TypeError
  3. True False และ False และ 1
  4. True True และ True และ TypeError
คำตอบB. True False และ True และ TypeError

เนื่องจากนิยาม __eq__ ไว้ p == q จะเปรียบเทียบแอตทริบิวต์ x แล้วได้ True แต่ p กับ q เป็นอ็อบเจกต์ที่ถูกสร้างแยกกัน p is q จึงยังคงเป็น False สิ่งที่สำคัญในที่นี้คือ คลาสที่นิยาม __eq__ ไว้จะทำให้ __hash__ ถูกตั้งเป็น None โดยอัตโนมัติ Point.__hash__ is None จึงเป็น True อ็อบเจกต์ที่ไม่มีแฮชจะใช้เป็นสมาชิกของเซตหรือคีย์ของพจนานุกรมไม่ได้ ดังนั้นเมื่อพยายามสร้าง {p, q} จะเกิด TypeError หากต้องการใส่ในเซตด้วย ต้องนิยาม __hash__ เองให้คืนค่าเดียวกันเมื่อเท่ากัน

ข้อ 21 | math

โค้ดต่อไปนี้ใช้โมดูล math ผลลัพธ์คือข้อใด

import mathprint(math.sqrt(16))print(math.floor(-1.5), math.ceil(-1.5))
  1. แสดง 4.0 และ -2 -1
  2. แสดง 4.0 และ -1 -2
  3. แสดง 4 และ -2 -1
  4. แสดง 4 และ -1 -1
คำตอบA. แสดง 4.0 และ -2 -1

math.sqrt จะคืนค่า float เสมอแม้จะหารลงตัวก็ตาม ดังนั้นรากที่สองของ 16 จึงแสดงเป็น 4.0 ไม่ใช่ 4 math.floor จะปัดทิ้งในทิศทางลบบนเส้นจำนวน ดังนั้น -1.5 จึงเป็น -2 ส่วน math.ceil เป็นทิศทางขึ้น ดังนั้น -1.5 จึงเป็น -1 สำหรับจำนวนลบ ควรระวังว่า int(-1.5) ที่ปัดทิ้งเข้าหา 0 จะได้ -1 ซึ่งต่างจากผลของ floor หากสับสน floor กับ ceil จะกลายเป็นเรียง -1 -2 หากเข้าใจผิดว่าค่าที่ sqrt คืนกลับเป็นชนิด int จะได้ 4

ข้อ 22 | os.path

ชุดค่าผสมของค่าที่ os.path.splitext('/tmp/z.txt') และ os.path.basename('/tmp/z.txt') คืนกลับคือข้อใด

  1. ('/tmp/z', '.txt') และ 'z.txt'
  2. ('/tmp', 'z.txt') และ '/tmp'
  3. ('/tmp/z', 'txt') และ 'z.txt'
  4. ('/tmp/z.txt', '') และ 'z.txt'
คำตอบA. ('/tmp/z', '.txt') และ 'z.txt'

os.path.splitext จะแยกเฉพาะส่วนนามสกุลออก โดยจุดจะติดไปกับฝั่งนามสกุลด้วย แล้วคืนค่าเป็นทูเพิล 2 องค์ประกอบ ดังนั้นจึงได้ ('/tmp/z', '.txt') จุดจะไม่หลุดหายไปกลายเป็น 'txt' os.path.basename จะคืนชื่อที่ท้ายสุดของพาธ จึงเป็น 'z.txt' ส่วนที่คืนไดเรกทอรีฝั่ง '/tmp' คือ os.path.dirname หากต้องการทูเพิลที่รวมทั้งสองไว้ด้วยกันให้ใช้ os.path.split ควรจำแยกบทบาทไว้ว่า splitext จะตัดที่นามสกุล ไม่ใช่ที่ตัวแบ่งไดเรกทอรี

ข้อ 23 | การจับคู่ของ re

โค้ดต่อไปนี้ใช้โมดูล re ผลลัพธ์คือข้อใด

import reprint(re.match("b", "abc"))print(re.search("b", "abc").group())print(re.findall(r"\d+", "a1b22c333"))
  1. แสดง b และ None และ ['1', '22', '333']
  2. แสดง None และ b และ ['1', '22', '333']
  3. แสดง b และ b และ ['1', '22', '333']
  4. แสดง None และ b และ ['a1', 'b22', 'c333']
คำตอบB. แสดง None และ b และ ['1', '22', '333']

re.match จะจับคู่จากต้นสตริงเท่านั้น ต้นของ 'abc' คือ 'a' จึงไม่ตรงกับ 'b' และคืนค่า None re.search จะค้นหาแม้ในระหว่างทาง จึงตรงกับ 'b' และ .group() จะดึงสตริงที่ตรงกันคือ b ออกมาได้ ความแตกต่างนี้คือสิ่งที่ถูกถามบ่อยที่สุดในเรื่อง re re.findall จะคืนลิสต์ที่รวมส่วนที่ตรงกับแพตเทิร์นทั้งหมด \d+ คือตัวเลขที่ต่อเนื่องกัน จึงได้ ['1', '22', '333'] จะไม่มีอักขระที่ใช้คั่นรวมอยู่ด้วย หากไม่ตรงเลยจะคืนลิสต์ว่าง

ข้อ 24 | datetime

โค้ดต่อไปนี้ใช้ datetime ผลลัพธ์คือข้อใด

import datetimed = datetime.date(2026, 8, 22)print(d)print(d.strftime("%Y/%m/%d"))print(d + datetime.timedelta(days=10))
  1. 2026/08/22 และ 2026/08/22 และ 2026/09/01
  2. 2026-08-22 และ 2026/08/22 และ 2026-09-01
  3. 2026-08-22 และ 2026/08/22 และ 10 days, 0:00:00
  4. 2026-08-22 และ 2026/08/22 และ 2026-08-32
คำตอบB. 2026-08-22 และ 2026/08/22 และ 2026-09-01

เมื่อ print อ็อบเจกต์ date ตรง ๆ จะแสดงในรูปแบบ ISO ที่คั่นด้วยยัติภังค์คือ 2026-08-22 เมธอดที่ใช้เปลี่ยนรูปแบบคือ strftime หากส่ง '%Y/%m/%d' เข้าไปจะได้สตริงที่คั่นด้วยเครื่องหมายทับคือ 2026/08/22 เมื่อบวก timedelta(days=10) เข้ากับ date สิ่งที่คืนกลับไม่ใช่ timedelta แต่เป็น date ใหม่ที่ชี้ไปที่ 10 วันถัดไป ดังนั้นเมื่อ print จะแสดงเป็น 2026-09-01 การคำนวณที่ข้ามเดือนจะถูกจัดการให้โดยฝั่ง datetime เอง จึงไม่กลายเป็นวันที่ไม่มีอยู่จริงอย่าง 2026-08-32 ที่เพียงบวกตัวเลขของวันด้วย 10 หากคิดว่า print(d) จะมีรูปแบบเดียวกับ strftime จะกลายเป็นคั่นด้วยเครื่องหมายทับเรียงกัน แต่การแสดงผลตามค่าเริ่มต้นคือคั่นด้วยยัติภังค์ หากลบวันที่ทั้งสองออกจากกัน จะได้ timedelta ไม่ใช่ date และการแสดงผลจะอยู่ในรูปแบบเช่น 10 days, 0:00:00

ข้อ 25 | Counter

โค้ดต่อไปนี้ใช้ collections.Counter ผลลัพธ์คือข้อใด

from collections import Counterc = Counter("abracadabra")print(c.most_common(2))print(c["a"], c["z"])
  1. แสดง [('a', 5), ('b', 2)] และ 5 และ KeyError
  2. แสดง [('a', 5), ('r', 2)] และ 5 0
  3. แสดง [('a', 5), ('b', 2)] และ 5 0
  4. แสดง [('a', 5), ('b', 2), ('r', 2)] และ 5 0
คำตอบC. แสดง [('a', 5), ('b', 2)] และ 5 0

'abracadabra' มี a อยู่ 5 ตัว b และ r อยู่ตัวละ 2 ตัว c และ d อยู่ตัวละ 1 ตัว most_common(2) จะคืนเฉพาะ 2 รายการที่มากที่สุดในรูปทูเพิลของ (องค์ประกอบ, จำนวน) จึงได้ [('a', 5), ('b', 2)] b กับ r ที่มีจำนวนเท่ากัน ตัวที่ปรากฏก่อนจะถูกเรียงไว้ก่อน จึงเลือก b Counter เป็นชนิดหนึ่งของพจนานุกรม แต่ต่างจากพจนานุกรมทั่วไปตรงที่แม้จะดึงองค์ประกอบที่ไม่มีอยู่ก็จะไม่เกิด KeyError แต่จะคืนค่า 0 แทน ออกแบบมาเพื่อไม่ต้องตรวจสอบว่ามีอยู่หรือไม่ก่อนนับ

ข้อ 26 | namedtuple

โค้ดต่อไปนี้ใช้ชนิดที่สร้างด้วย namedtuple ผลลัพธ์คือข้อใด

from collections import namedtuplePt = namedtuple("Pt", "x y")p = Pt(1, 2)print(p)print(p.x, p[0], isinstance(p, tuple))
  1. แสดง (1, 2) และ 1 1 True
  2. แสดง Pt(x=1, y=2) และ 1 1 False
  3. แสดง Pt(x=1, y=2) และ 1 1 True
  4. แสดง Pt(1, 2) และ 1 1 False
คำตอบC. แสดง Pt(x=1, y=2) และ 1 1 True

ชนิดที่สร้างด้วย namedtuple เมื่อแสดงผลจะอยู่ในรูปแบบที่มีชื่อชนิดและชื่อฟิลด์ติดมาด้วยคือ Pt(x=1, y=2) ค่าสามารถอ่านได้ทั้งด้วยชื่อและด้วยดัชนี ดังนั้น p.x และ p[0] จึงเป็น 1 ทั้งคู่ และเนื่องจาก namedtuple เป็นชนิดที่สืบทอดมาจากทูเพิล isinstance(p, tuple) จึงเป็น True กล่าวคือเป็นทูเพิลชนิดหนึ่ง จึงไม่สามารถเขียนทับองค์ประกอบในภายหลังได้ หากต้องการค่าที่แก้ไขได้ ให้ใช้ _replace เพื่อสร้างอินสแตนซ์ใหม่ที่แทนที่ค่าแล้ว หรือนิยามเป็นคลาสธรรมดาแทน

ข้อ 27 | ใช้ครั้งเดียวหมด

โค้ดต่อไปนี้ส่งอ็อบเจกต์ map เดียวกันเข้า list() 2 ครั้ง ผลลัพธ์คือข้อใด

m = map(str, [1, 2, 3])print(list(m))print(list(m))
  1. บรรทัดที่ 1 เป็น ['1', '2', '3'] บรรทัดที่ 2 เป็น None
  2. บรรทัดที่ 1 เป็น ['1', '2', '3'] บรรทัดที่ 2 เป็น []
  3. บรรทัดที่ 1 เป็น [1, 2, 3] บรรทัดที่ 2 เป็น []
  4. ทั้งบรรทัดที่ 1 และ 2 เป็น ['1', '2', '3']
คำตอบB. บรรทัดที่ 1 เป็น ['1', '2', '3'] บรรทัดที่ 2 เป็น []

สิ่งที่ map คืนกลับไม่ใช่ลิสต์ แต่เป็น Iterator ซึ่งจะส่งค่าถัดไปทีละตัวทุกครั้งที่เรียก next() และเมื่อหมดแล้วก็จะหมดไปเลย list() ครั้งแรกจะดึงออกมาทั้ง 3 ตัว list() ครั้งที่สองจึงไม่มีอะไรเหลือ กลายเป็นลิสต์ว่าง ไม่กลายเป็นข้อผิดพลาดหรือ None แต่จะว่างเปล่าอย่างเงียบ ๆ จึงสังเกตเห็นได้ยาก map(str, ...) จะทำให้แต่ละองค์ประกอบกลายเป็นสตริง ดังนั้นเนื้อหาครั้งแรกจึงไม่ใช่ตัวเลข แต่เป็น ['1', '2', '3'] เรื่องเดียวกันนี้เกิดขึ้นกับ filter zip enumerate และเจเนอเรเตอร์ด้วย หากจะวนซ้ำหลายครั้งควรใช้ list() ห่อไว้ทันทีที่ได้รับมา

ข้อ 28 | filter และ all

โค้ดต่อไปนี้ใช้ filter และ all และ any ผลลัพธ์คือข้อใด

print(list(filter(None, [0, 1, "", 2, None, "a"])))print(all([]), any([]))
  1. แสดง [1, 2, 'a'] และ False False
  2. แสดง [0, '', None] และ True False
  3. แสดง [1, 2, 'a'] และ True False
  4. แสดง [1, 2, 'a'] และ False True
คำตอบC. แสดง [1, 2, 'a'] และ True False

หากส่ง None เป็นอาร์กิวเมนต์ตัวที่ 1 ของ filter จะคัดกรองตามค่าความจริงเทียม-เท็จขององค์ประกอบเอง 0 สตริงว่าง และ None ถูกประเมินเป็นเท็จ จึงตกไป เหลืออยู่คือ [1, 2, 'a'] ไม่ใช่ว่าตัวที่ตกไปจะเหลืออยู่ all มีข้อกำหนดว่าจะคืนค่า True เมื่อไม่มีองค์ประกอบที่เป็นเท็จอยู่เลยแม้แต่ตัวเดียว ดังนั้นในลิสต์ว่างที่ไม่มีองค์ประกอบเลยจะได้ True any จะคืนค่า True เมื่อมีองค์ประกอบที่เป็นจริงอยู่อย่างน้อย 1 ตัว ดังนั้นถ้าว่างเปล่าจะได้ False จุดที่ทั้งสองให้คำตอบตรงข้ามกันเมื่อส่งลำดับว่างเข้าไปคือจุดที่มักถูกถาม

ข้อ 29 | เจเนอเรเตอร์

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

def gen():    print("start")    yield 1    print("mid")    yield 2g = gen()print("created")print(next(g))print(next(g))
  1. ลำดับ start mid created 1 2
  2. ลำดับ created start mid 1 2
  3. ลำดับ created start 1 mid 2
  4. ลำดับ start created 1 mid 2
คำตอบC. ลำดับ created start 1 mid 2

แม้จะเรียกฟังก์ชันที่มี yield อยู่ เนื้อหาจะยังไม่ถูกรันแม้แต่บรรทัดเดียว สิ่งที่คืนกลับมามีเพียงอ็อบเจกต์เจเนอเรเตอร์เท่านั้น จึงแสดง created ก่อน เมื่อเรียก next(g) ครั้งแรกจึงจะเริ่มทำงานจากต้น แสดง start แล้วหยุดที่ yield 1 คืนค่า 1 next(g) ครั้งถัดไปจะกลับมาทำงานต่อจากจุดที่หยุดไว้ แสดง mid แล้วหยุดที่ yield 2 คืนค่า 2 การทำงานแบบล่าช้านี้คือความแตกต่างที่ชัดเจนจากฟังก์ชันปกติ หากเรียก next(g) ต่ออีกครั้ง ฟังก์ชันจะจบและเกิด StopIteration

ข้อ 30 | is และ ==

โค้ดต่อไปนี้เรียงตัวดำเนินการ is และ == ไว้ ผลลัพธ์คือข้อใด

a = [1, 2]b = [1, 2]c = aprint(a == b, a is b)print(a == c, a is c)
  1. True False และ True False
  2. True True และ True True
  3. False False และ True True
  4. True False และ True True
คำตอบD. True False และ True True

== เป็นตัวดำเนินการที่ถามว่าค่าเท่ากันหรือไม่ ดังนั้น a กับ b ที่มีเนื้อหาเหมือนกันจะได้ True is เป็นตัวดำเนินการที่ถามว่าเป็นอ็อบเจกต์เดียวกันหรือไม่ a กับ b เป็นลิสต์ 2 ตัวที่ถูกสร้างแยกกัน จึงได้ False ในขณะที่ c = a ไม่ได้สร้างลิสต์ใหม่ แต่เพียงตั้งชื่ออื่นให้กับอ็อบเจกต์เดียวกัน ดังนั้น a is c จึงเป็น True กล่าวคือหากเขียนทับลิสต์ผ่าน c จะเห็นการเปลี่ยนแปลงจาก a ด้วย None เป็นอ็อบเจกต์พิเศษที่มีอยู่เพียงตัวเดียว จึงเป็นธรรมเนียมที่จะเขียนว่า x is None แต่หากต้องการเปรียบเทียบค่า ให้ใช้ == เสมอ

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

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

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

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