ขอบเขตและรายการของคลาสการตั้งค่าหรือความเข้าใจในพจนานุกรมรวมทั้งนิพจน์ตัวสร้างจะไม่รวมกัน
เหตุผล; หรือคำอย่างเป็นทางการเกี่ยวกับเรื่องนี้
ใน Python 3 list comprehensions ได้รับขอบเขตที่เหมาะสม (local namespace) ของตัวเองเพื่อป้องกันตัวแปรโลคอลของมันที่ตกลงไปในขอบเขตโดยรอบ (ดูที่Python list comprehension rebind ชื่อแม้ว่าจะอยู่ในขอบเขตของความเข้าใจแล้ว ที่ดีเมื่อใช้เช่นความเข้าใจรายการในโมดูลหรือในการทำงาน แต่ในชั้นเรียนกำหนดขอบเขตเป็นเล็ก ๆ น้อย ๆ อืมม์, แปลก
มีการบันทึกไว้ในpep 227 :
ชื่อในขอบเขตคลาสไม่สามารถเข้าถึงได้ ชื่อได้รับการแก้ไขในขอบเขตฟังก์ชันที่อยู่ด้านในสุด หากคำจำกัดความของคลาสเกิดขึ้นในสายของขอบเขตที่ซ้อนกันกระบวนการแก้ไขจะข้ามคำจำกัดความของคลาส
และในclass
เอกสารประกอบคำสั่งผสม :
ชุดของชั้นเรียนจะถูกดำเนินการในกรอบการดำเนินการใหม่ (ดูส่วนการตั้งชื่อและการผูก ) โดยใช้เนมสเปซในพื้นที่ที่สร้างขึ้นใหม่และเนมสเปซส่วนกลางดั้งเดิม (ปกติ, ชุดมีคำจำกัดความของฟังก์ชั่นเท่านั้น.) เมื่อดำเนินการเสร็จสิ้นชุดชั้นของกรอบการดำเนินการจะถูกยกเลิก แต่ namespace ท้องถิ่นจะถูกบันทึกไว้ [4]วัตถุคลาสจะถูกสร้างขึ้นโดยใช้รายการการสืบทอดสำหรับคลาสพื้นฐานและเนมสเปซในเครื่องที่บันทึกไว้สำหรับพจนานุกรมแอตทริบิวต์
เน้นการขุด; เฟรมการดำเนินการเป็นขอบเขตชั่วคราว
เนื่องจากขอบเขตถูกนำมาใช้ใหม่เป็นแอตทริบิวต์บนวัตถุคลาสทำให้สามารถใช้เป็นขอบเขต nonlocal และนำไปสู่พฤติกรรมที่ไม่ได้กำหนด จะเกิดอะไรขึ้นถ้าเมธอดคลาสที่เรียกว่าx
ตัวแปรขอบเขตแบบซ้อนจากนั้นจัดการFoo.x
กับตัวอย่างเช่น ที่สำคัญกว่าสิ่งที่จะว่าค่าเฉลี่ยสำหรับคลาสย่อยFoo
? งูหลามมีการรักษาขอบเขตชั้นเรียนแตกต่างกันมันเป็นความแตกต่างจากขอบเขตการทำงาน
สุดท้าย แต่ไม่ท้ายสุดส่วนการตั้งชื่อและการเชื่อมโยงที่เชื่อมโยงในเอกสารประกอบแบบจำลองการดำเนินการกล่าวถึงขอบเขตของคลาสอย่างชัดเจน:
ขอบเขตของชื่อที่กำหนดในคลาสบล็อกนั้น จำกัด เฉพาะคลาสบล็อกเท่านั้น มันไม่ขยายไปถึงบล็อกของวิธีการ - ซึ่งรวมถึงความเข้าใจและการแสดงออกของเครื่องกำเนิดเนื่องจากพวกเขาจะดำเนินการโดยใช้ขอบเขตฟังก์ชั่น ซึ่งหมายความว่าสิ่งต่อไปนี้จะล้มเหลว:
class A:
a = 42
b = list(a + i for i in range(10))
ดังนั้นเพื่อสรุป: คุณไม่สามารถเข้าถึงขอบเขตของคลาสจากฟังก์ชันแสดงรายการความเข้าใจหรือนิพจน์ตัวสร้างที่อยู่ในขอบเขตนั้น มันทำหน้าที่เหมือนไม่มีขอบเขตนั้น ใน Python 2 ความเข้าใจในรายการถูกนำมาใช้โดยใช้ทางลัด แต่ใน Python 3 พวกเขามีขอบเขตฟังก์ชั่นของตัวเอง (ตามที่ควรจะมีทั้งหมด) และทำให้ตัวอย่างของคุณแตก ประเภทความเข้าใจอื่น ๆ มีขอบเขตของตัวเองโดยไม่คำนึงถึงเวอร์ชันของ Python ดังนั้นตัวอย่างที่คล้ายกันกับชุดหรือความเข้าใจแบบ dict จะทำให้แตกใน Python 2
# Same error, in Python 2 or 3
y = {x: x for i in range(1)}
ข้อยกเว้น (เล็ก) หรือเพราะเหตุใดส่วนหนึ่งอาจยังใช้งานได้
มีส่วนหนึ่งของความเข้าใจหรือนิพจน์ตัวสร้างที่ดำเนินการในขอบเขตโดยรอบโดยไม่คำนึงถึงรุ่น Python นั่นจะเป็นการแสดงออกถึงการกระทำที่อยู่นอกสุด ในตัวอย่างของคุณมันคือrange(1)
:
y = [x for i in range(1)]
# ^^^^^^^^
ดังนั้นการใช้x
ในนิพจน์นั้นจะไม่ทำให้เกิดข้อผิดพลาด:
# Runs fine
y = [i for i in range(x)]
สิ่งนี้ใช้กับ iterable นอกสุดเท่านั้น หากความเข้าใจมีหลายส่วนfor
คำสั่ง iterables สำหรับส่วนfor
คำสั่งภายในจะถูกประเมินในขอบเขตของความเข้าใจ:
# NameError
y = [i for i in range(1) for j in range(x)]
การตัดสินใจออกแบบนี้ถูกสร้างขึ้นเพื่อโยนข้อผิดพลาดในเวลาการสร้าง genexp แทนเวลาการวนซ้ำเมื่อสร้าง iterable นอกสุดของนิพจน์ตัวสร้างโยนข้อผิดพลาดหรือเมื่อ iterable ที่อยู่นอกสุดปรากฎว่าไม่สามารถทำซ้ำได้ ความเข้าใจร่วมกันพฤติกรรมนี้เพื่อความมั่นคง
มองใต้ฝากระโปรง หรือให้รายละเอียดมากกว่าที่คุณต้องการ
คุณสามารถดูนี้ทั้งหมดในการดำเนินการโดยใช้โมดูลdis
ฉันใช้ Python 3.3 ในตัวอย่างต่อไปนี้เนื่องจากมันเพิ่มชื่อที่ผ่านการรับรองซึ่งระบุรหัสวัตถุที่เราต้องการตรวจสอบอย่างเรียบร้อย รหัสไบต์ที่ผลิตนั้นจะเหมือนกับ Python 3.2
ในการสร้างคลาส Python ใช้ชุดทั้งหมดที่ประกอบเป็นคลาสของคลาส (ดังนั้นทุกสิ่งที่เยื้องลงไปหนึ่งระดับที่ลึกกว่าclass <name>:
บรรทัด) และดำเนินการราวกับว่ามันเป็นฟังก์ชั่น:
>>> import dis
>>> def foo():
... class Foo:
... x = 5
... y = [x for i in range(1)]
... return Foo
...
>>> dis.dis(foo)
2 0 LOAD_BUILD_CLASS
1 LOAD_CONST 1 (<code object Foo at 0x10a436030, file "<stdin>", line 2>)
4 LOAD_CONST 2 ('Foo')
7 MAKE_FUNCTION 0
10 LOAD_CONST 2 ('Foo')
13 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
16 STORE_FAST 0 (Foo)
5 19 LOAD_FAST 0 (Foo)
22 RETURN_VALUE
ครั้งแรกที่LOAD_CONST
มีการโหลดวัตถุรหัสสำหรับFoo
ร่างกายคลาสแล้วทำให้มันเป็นฟังก์ชั่นและเรียกมัน ผลของการโทรที่ถูกนำมาใช้เพื่อสร้าง namespace __dict__
ของชั้นเรียนของตน จนถึงตอนนี้ดีมาก
สิ่งที่ควรทราบที่นี่คือ bytecode มีวัตถุรหัสซ้อนกัน; ใน Python คำจำกัดความของคลาสฟังก์ชั่นความเข้าใจและเครื่องกำเนิดไฟฟ้าทั้งหมดจะถูกแสดงเป็นวัตถุรหัสที่ไม่เพียง แต่มีไบต์เท่านั้น แต่ยังมีโครงสร้างที่เป็นตัวแทนของตัวแปรท้องถิ่นค่าคงที่ตัวแปรที่นำมาจากกลมและตัวแปรที่นำมาจากขอบเขตซ้อน bytecode ที่คอมไพล์แล้วอ้างถึงโครงสร้างเหล่านั้นและตัวแปลภาษาไพ ธ อนรู้วิธีเข้าถึงสิ่งที่ได้รับจาก bytecode ที่นำเสนอ
สิ่งสำคัญที่ต้องจำที่นี่คือ Python สร้างโครงสร้างเหล่านี้ในเวลารวบรวม class
ชุดเป็นวัตถุรหัส ( <code object Foo at 0x10a436030, file "<stdin>", line 2>
) ที่จะรวบรวมแล้ว
ลองตรวจสอบรหัสวัตถุที่สร้างร่างกายคลาสเอง วัตถุรหัสมีco_consts
โครงสร้าง:
>>> foo.__code__.co_consts
(None, <code object Foo at 0x10a436030, file "<stdin>", line 2>, 'Foo')
>>> dis.dis(foo.__code__.co_consts[1])
2 0 LOAD_FAST 0 (__locals__)
3 STORE_LOCALS
4 LOAD_NAME 0 (__name__)
7 STORE_NAME 1 (__module__)
10 LOAD_CONST 0 ('foo.<locals>.Foo')
13 STORE_NAME 2 (__qualname__)
3 16 LOAD_CONST 1 (5)
19 STORE_NAME 3 (x)
4 22 LOAD_CONST 2 (<code object <listcomp> at 0x10a385420, file "<stdin>", line 4>)
25 LOAD_CONST 3 ('foo.<locals>.Foo.<listcomp>')
28 MAKE_FUNCTION 0
31 LOAD_NAME 4 (range)
34 LOAD_CONST 4 (1)
37 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
40 GET_ITER
41 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
44 STORE_NAME 5 (y)
47 LOAD_CONST 5 (None)
50 RETURN_VALUE
bytecode ข้างต้นสร้างร่างกายคลาส ฟังก์ชันถูกเรียกใช้งานและlocals()
เนมสเปซที่ได้รับซึ่งมีx
และy
ใช้เพื่อสร้างคลาส (ยกเว้นว่าใช้งานไม่ได้เพราะx
ไม่ได้กำหนดเป็นโกลบอล) โปรดทราบว่าหลังจากการจัดเก็บ5
ในx
นั้นมันโหลดวัตถุรหัสอื่น; นั่นคือรายการความเข้าใจ; มันถูกห่อไว้ในวัตถุฟังก์ชั่นเช่นเดียวกับร่างกายของชั้นเรียนเป็น; ฟังก์ชั่นที่สร้างขึ้นใช้อาร์กิวเมนต์ตำแหน่งซึ่งrange(1)
สามารถใช้สำหรับการวนลูปของโค้ดส่งไปยังตัววนซ้ำ ดังที่แสดงใน bytecode range(1)
จะถูกประเมินในขอบเขตของคลาส
จากนี้คุณจะเห็นว่าความแตกต่างเพียงอย่างเดียวระหว่างวัตถุรหัสสำหรับฟังก์ชั่นหรือเครื่องกำเนิดไฟฟ้าและวัตถุรหัสสำหรับความเข้าใจคือว่าหลังจะถูกดำเนินการทันทีเมื่อวัตถุรหัสแม่จะดำเนินการ; bytecode เพียงสร้างฟังก์ชั่นการบินและดำเนินการในไม่กี่ขั้นตอน
Python 2.x ใช้โค้ดอินไลน์แบบอินไลน์ที่นั่นแทนนี่คือผลลัพธ์จาก Python 2.7:
2 0 LOAD_NAME 0 (__name__)
3 STORE_NAME 1 (__module__)
3 6 LOAD_CONST 0 (5)
9 STORE_NAME 2 (x)
4 12 BUILD_LIST 0
15 LOAD_NAME 3 (range)
18 LOAD_CONST 1 (1)
21 CALL_FUNCTION 1
24 GET_ITER
>> 25 FOR_ITER 12 (to 40)
28 STORE_NAME 4 (i)
31 LOAD_NAME 2 (x)
34 LIST_APPEND 2
37 JUMP_ABSOLUTE 25
>> 40 STORE_NAME 5 (y)
43 LOAD_LOCALS
44 RETURN_VALUE
ไม่มีการโหลดวัตถุรหัสแทนการFOR_ITER
วนซ้ำจะทำงานแบบอินไลน์ ดังนั้นใน Python 3.x ตัวสร้างรายการจึงได้รับรหัสวัตถุที่เหมาะสมของตัวเองซึ่งหมายความว่ามันมีขอบเขตของตัวเอง
อย่างไรก็ตามความเข้าใจถูกรวบรวมพร้อมกับส่วนที่เหลือของรหัสที่มาหลามเมื่อโมดูลหรือสคริปต์ถูกโหลดครั้งแรกโดยล่ามและคอมไพเลอร์ไม่พิจารณาขอบเขตชุดที่ถูกต้อง ตัวแปรอ้างอิงใด ๆ ในรายการความเข้าใจต้องดูในขอบเขตรอบคำจำกัดความของคลาสเรียกซ้ำ หากไม่พบตัวแปรโดยคอมไพเลอร์มันจะทำเครื่องหมายว่าเป็นโกลบอล การแยกชิ้นส่วนของวัตถุรหัสรายการความเข้าใจแสดงให้เห็นว่าx
มีการโหลดอย่างแท้จริงในระดับโลก:
>>> foo.__code__.co_consts[1].co_consts
('foo.<locals>.Foo', 5, <code object <listcomp> at 0x10a385420, file "<stdin>", line 4>, 'foo.<locals>.Foo.<listcomp>', 1, None)
>>> dis.dis(foo.__code__.co_consts[1].co_consts[2])
4 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (i)
12 LOAD_GLOBAL 0 (x)
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
ชุดโค้ดไบต์นี้โหลดอาร์กิวเมนต์แรกที่ส่งผ่าน (ตัวrange(1)
วนซ้ำ) และเหมือนกับรุ่น Python 2.x ที่ใช้FOR_ITER
ในการวนรอบมันและสร้างเอาต์พุต
หากเรากำหนดไว้x
ในfoo
ฟังก์ชั่นแทนx
จะเป็นตัวแปรเซลล์ (เซลล์อ้างถึงขอบเขตที่ซ้อนกัน):
>>> def foo():
... x = 2
... class Foo:
... x = 5
... y = [x for i in range(1)]
... return Foo
...
>>> dis.dis(foo.__code__.co_consts[2].co_consts[2])
5 0 BUILD_LIST 0
3 LOAD_FAST 0 (.0)
>> 6 FOR_ITER 12 (to 21)
9 STORE_FAST 1 (i)
12 LOAD_DEREF 0 (x)
15 LIST_APPEND 2
18 JUMP_ABSOLUTE 6
>> 21 RETURN_VALUE
LOAD_DEREF
ทางอ้อมจะโหลดx
จากวัตถุเซลล์รหัสวัตถุ:
>>> foo.__code__.co_cellvars # foo function `x`
('x',)
>>> foo.__code__.co_consts[2].co_cellvars # Foo class, no cell variables
()
>>> foo.__code__.co_consts[2].co_consts[2].co_freevars # Refers to `x` in foo
('x',)
>>> foo().y
[2]
การอ้างอิงที่แท้จริงจะดูค่าขึ้นจากโครงสร้างข้อมูลเฟรมปัจจุบันซึ่งเริ่มต้นได้จาก.__closure__
แอตทริบิวต์ของวัตถุของฟังก์ชัน เนื่องจากฟังก์ชั่นที่สร้างขึ้นสำหรับวัตถุรหัสความเข้าใจถูกยกเลิกอีกครั้งเราจึงไม่ได้ตรวจสอบการปิดฟังก์ชันนั้น หากต้องการดูการปิดการทำงานเราต้องตรวจสอบฟังก์ชันซ้อนกัน:
>>> def spam(x):
... def eggs():
... return x
... return eggs
...
>>> spam(1).__code__.co_freevars
('x',)
>>> spam(1)()
1
>>> spam(1).__closure__
>>> spam(1).__closure__[0].cell_contents
1
>>> spam(5).__closure__[0].cell_contents
5
ดังนั้นเพื่อสรุป:
- รายการความเข้าใจได้รับรหัสวัตถุของตัวเองใน Python 3 และไม่มีความแตกต่างระหว่างวัตถุรหัสสำหรับฟังก์ชั่นเครื่องกำเนิดไฟฟ้าหรือความเข้าใจ; วัตถุรหัสความเข้าใจถูกห่อในวัตถุฟังก์ชั่นชั่วคราวและเรียกได้ทันที
- รหัสวัตถุจะถูกสร้างขึ้นในเวลารวบรวมและตัวแปรที่ไม่ใช่ในท้องถิ่นจะถูกทำเครื่องหมายว่าเป็นโลกหรือเป็นตัวแปรอิสระขึ้นอยู่กับขอบเขตที่ซ้อนกันของรหัส คลาสของร่างกายไม่ถือเป็นขอบเขตสำหรับการค้นหาตัวแปรเหล่านั้น
- เมื่อรันโค้ด Python จะต้องมองเข้าไปใน globals หรือการปิดของอ็อบเจกต์ที่กำลังทำงานอยู่เท่านั้น เนื่องจากคอมไพเลอร์ไม่ได้รวมคลาสของร่างกายเป็นขอบเขตจึงไม่มีการพิจารณา namespace ของฟังก์ชันชั่วคราว
วิธีแก้ปัญหา หรือจะทำอย่างไรกับมัน
หากคุณต้องสร้างขอบเขตที่ชัดเจนสำหรับx
ตัวแปรเช่นในฟังก์ชั่นคุณสามารถใช้ตัวแปรขอบเขตคลาสสำหรับความเข้าใจในรายการ:
>>> class Foo:
... x = 5
... def y(x):
... return [x for i in range(1)]
... y = y(x)
...
>>> Foo.y
[5]
y
สามารถเรียกใช้ฟังก์ชัน'ชั่วคราว' ได้โดยตรง เราแทนที่มันเมื่อเราทำกับค่าตอบแทน ขอบเขตจะถูกพิจารณาเมื่อทำการแก้ไขx
:
>>> foo.__code__.co_consts[1].co_consts[2]
<code object y at 0x10a5df5d0, file "<stdin>", line 4>
>>> foo.__code__.co_consts[1].co_consts[2].co_cellvars
('x',)
แน่นอนว่าคนที่อ่านรหัสของคุณจะเกาหัวเรื่องนี้เล็กน้อย คุณอาจต้องการแสดงความคิดเห็นไขมันขนาดใหญ่ที่นั่นอธิบายว่าทำไมคุณทำเช่นนี้
วิธีแก้ไขที่ดีที่สุดคือใช้__init__
เพื่อสร้างตัวแปรอินสแตนซ์แทน:
def __init__(self):
self.y = [self.x for i in range(1)]
และหลีกเลี่ยงการเกาหัวและคำถามที่จะอธิบายตัวเอง สำหรับตัวอย่างที่เป็นรูปธรรมของคุณเองฉันจะไม่เก็บแม้แต่namedtuple
ในชั้นเรียน อาจใช้เอาต์พุตโดยตรง (ไม่เก็บคลาสที่สร้างเลย) หรือใช้โกลบอล:
from collections import namedtuple
State = namedtuple('State', ['name', 'capital'])
class StateDatabase:
db = [State(*args) for args in [
('Alabama', 'Montgomery'),
('Alaska', 'Juneau'),
# ...
]]
NameError: global name 'x' is not defined
ใช้ Python 3.2 และ 3.3 ซึ่งเป็นสิ่งที่ฉันคาดหวัง