2.34. โค้ดแบบ dynamic

built-in สามตัวรับ string ของ Python source และรันมัน ได้แก่ eval(), exec() และ compile() รวมกันแล้วช่วยให้โค้ดสร้างและรันโค้ด เพิ่มเติม ในขณะ runtime ซึ่งบางครั้งเป็นเครื่องมือที่เหมาะสมอย่างยิ่ง แต่บ่อยกว่านั้นเป็นแหล่งที่มาของบั๊กและช่องโหว่ด้านความปลอดภัย

Warning

ฟังก์ชันเหล่านี้รัน Python ใดๆ ก็ได้ การส่ง input ของผู้ใช้ให้กับ eval หรือ exec ไม่ว่าจะเป็น string จากไฟล์ที่ผู้ใช้แก้ไขได้, payload ที่รับผ่านเครือข่าย, หรือค่าที่พิมพ์ที่ REPL prompt ช่วยให้ input นั้นทำสิ่งใดก็ได้ที่ script ที่เรียกใช้ทำได้ รวมถึงการลบทุกไฟล์บนอุปกรณ์ด้วย ใช้พวกมันอย่างตั้งใจ ไม่ใช้ภายในโค้ดที่ทำงานอัตโนมัติ และไม่ใช้กับข้อมูลที่คุณไม่ได้ควบคุม

2.34.1. eval

eval() รัน expression เดี่ยวและคืนค่าของมัน:

>>> eval("3 * 7")
21
>>> name = "OpenMV"
>>> eval("name.lower()")
'openmv'

expression จะเห็น globals และ locals ของผู้เรียกโดยค่าเริ่มต้น ซึ่งเป็นเหตุผลที่ name แก้ไขได้ในตัวอย่างที่สอง การส่ง dictionary ที่ชัดเจนช่วยให้คุณ sandbox การประเมินได้:

eval("a + b", {"__builtins__": None}, {"a": 1, "b": 2})

แม้จะอยู่ใน sandbox แต่ eval ก็อันตราย มีเทคนิคที่รู้จักกันดีในการหลบหนี sandbox เหล่านี้ อย่าพึ่งพาเพียงแค่กลอุบาย __builtins__ ว่างสำหรับ input ที่ไม่น่าเชื่อถือ

2.34.2. exec

exec() รัน block ของโค้ดแทนที่จะเป็น expression เดี่ยว ไม่ว่าจะเป็น statement, การนิยามฟังก์ชัน, ลูป และคืนค่า None:

exec("for i in range(3): print(i)")

Output:

0
1
2

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

2.34.3. compile

compile() แปลง source string เป็น code object ที่สามารถส่งให้กับ eval() หรือ exec() ในภายหลัง ใช้มันเมื่อ source เดียวกันจะรันหลายครั้ง การ parse จะเกิดครั้งเดียวและการรันจะเร็วขึ้น:

expr = compile("x * x", "<expr>", "eval")
for x in range(5):
    print(eval(expr))

Output:

0
1
4
9
16

อาร์กิวเมนต์กลางคือป้ายกำกับที่ปรากฏใน traceback หากโค้ด raise อาร์กิวเมนต์ที่สามคือ "eval" สำหรับ expression เดี่ยว, "exec" สำหรับ block, หรือ "single" สำหรับ statement แบบ interactive ที่พิมพ์ผลลัพธ์ของมัน

2.34.4. เมื่อไหร่ควรใช้สิ่งเหล่านี้

แทบไม่ควรใช้เลย กรณีการใช้งานที่ผู้เริ่มต้นส่วนใหญ่นึกถึงมีทางเลือกที่ปลอดภัยกว่า:

  • การอ่านไฟล์ config ใช้ json ซึ่งเป็นข้อมูลที่มีโครงสร้าง ไม่ต้องรัน

  • การประเมินค่าตัวเลขที่ผู้ใช้พิมพ์ ใช้ int() / float() เพื่อ parse แล้วค่อยคำนวณ หากผู้ใช้ต้องการป้อนสูตรจริงๆ ให้ใช้ expression parser ขนาดเล็ก ไม่ใช่ eval

เมื่อคุณต้องการ eval / exec / compile จริงๆ ให้แยก call site ออก บันทึก string ที่กำลังจะรัน และถือว่า source นั้นเป็นสิ่งที่น่าสงสัยที่สุดในโค้ดของคุณ