การเข้าถึงตัวแปรคลาสจากรายการความเข้าใจในการกำหนดคลาส


174

คุณเข้าถึงตัวแปรคลาสอื่น ๆ ได้อย่างไรจากรายการความเข้าใจภายในนิยามคลาส? การทำงานต่อไปนี้ใน Python 2 แต่ล้มเหลวใน Python 3:

class Foo:
    x = 5
    y = [x for i in range(1)]

Python 3.2 ให้ข้อผิดพลาด:

NameError: global name 'x' is not defined

การพยายามFoo.xไม่ทำงานเช่นกัน ความคิดเห็นเกี่ยวกับวิธีการทำเช่นนี้ใน Python 3

ตัวอย่างแรงจูงใจที่ซับซ้อนกว่าเล็กน้อย:

from collections import namedtuple
class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for args in [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ]]

ในตัวอย่างนี้apply()จะเป็นการแก้ปัญหาที่เหมาะสม แต่มันถูกลบออกจาก Python 3 อย่างน่าเศร้า


ข้อความแสดงข้อผิดพลาดของคุณไม่ถูกต้อง ฉันNameError: global name 'x' is not definedใช้ Python 3.2 และ 3.3 ซึ่งเป็นสิ่งที่ฉันคาดหวัง
Martijn Pieters

สิ่งที่น่าสนใจ ... วิธีแก้ปัญหาหนึ่งที่ชัดเจนคือการกำหนด y หลังจากคุณออกจากการกำหนดคลาส Foo.y = [Foo.x สำหรับฉันอยู่ในช่วง (1)]
gps

3
+ ลิงค์ martijn-pieters เพื่อทำสำเนาถูกต้องมีข้อคิดเห็นจาก + matt-b ในคำอธิบาย: Python 2.7 list comprehensions ไม่มี namespace ของตัวเอง (ต่างจากชุดหรือ dict comprehensions หรือ expressions generator ... แทนที่ [ ] ด้วย {} เพื่อดูว่าใช้งานจริง) พวกเขาทั้งหมดมี namespace ของตัวเองใน 3
gps

@gps: หรือใช้ขอบเขตที่ซ้อนกันโดยการแทรกฟังก์ชัน (ชั่วคราว) ในชุดคำจำกัดความของคลาส
Martijn Pieters

ฉันเพิ่งทดสอบใน 2.7.11 มีข้อผิดพลาดของชื่อ
Junchao Gu

คำตอบ:


244

ขอบเขตและรายการของคลาสการตั้งค่าหรือความเข้าใจในพจนานุกรมรวมทั้งนิพจน์ตัวสร้างจะไม่รวมกัน

เหตุผล; หรือคำอย่างเป็นทางการเกี่ยวกับเรื่องนี้

ใน 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'),
       # ...
    ]]

21
นอกจากนี้คุณยังสามารถใช้แลมบ์ดาเพื่อแก้ไขการเชื่อมโยง:y = (lambda x=x: [x for i in range(1)])()
ecatmur

3
@ecatmur: แน่นอนว่าlambdaเป็นเพียงฟังก์ชั่นนิรนาม
Martijn Pieters

2
สำหรับเร็กคอร์ดการหลีกเลี่ยงที่ใช้อาร์กิวเมนต์เริ่มต้น (ไปยังแลมบ์ดาหรือฟังก์ชัน) เพื่อส่งผ่านตัวแปรคลาสมี gotcha กล่าวคือมันผ่านค่าปัจจุบันของตัวแปร ดังนั้นหากตัวแปรเปลี่ยนแปลงในภายหลังและเรียกแลมบ์ดาหรือฟังก์ชั่นแลมบ์ดาหรือฟังก์ชั่นจะใช้ค่าเก่า พฤติกรรมนี้แตกต่างจากพฤติกรรมของการปิด (ซึ่งจะจับการอ้างอิงถึงตัวแปรมากกว่าค่าของมัน) ดังนั้นอาจไม่คาดคิด
Neal Young

9
หากต้องการหน้าข้อมูลทางเทคนิคเพื่ออธิบายว่าทำไมบางสิ่งบางอย่างไม่ทำงานอย่างสังหรณ์ใจฉันเรียกว่าข้อบกพร่อง
โจนาธา

5
@JonathanLeaders: อย่าเรียกว่าข้อผิดพลาด , เรียกมันว่าการถ่วงดุลอำนาจ หากคุณต้องการ A และ B แต่สามารถรับหนึ่งในนั้นได้ไม่ว่าคุณจะตัดสินใจอย่างไรในบางสถานการณ์คุณจะไม่ชอบผลลัพธ์ นั่นคือชีวิต.
Lutz Prechelt

15

ในความคิดของฉันมันเป็นข้อบกพร่องใน Python 3 ฉันหวังว่าพวกเขาจะเปลี่ยนมัน

Old Way (ทำงานใน 2.7, พ่นNameError: name 'x' is not definedใน 3+):

class A:
    x = 4
    y = [x+i for i in range(1)]

หมายเหตุ: เพียงแค่กำหนดขอบเขตด้วยA.xจะไม่แก้ปัญหา

วิธีใหม่ (ใช้งานได้ใน 3+):

class A:
    x = 4
    y = (lambda x=x: [x+i for i in range(1)])()

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


6
ปัญหามีอยู่ใน Python 2 เช่นกันเมื่อใช้นิพจน์ตัวสร้างรวมถึงการตั้งค่าและความเข้าใจในพจนานุกรม ไม่ใช่ข้อผิดพลาด แต่เป็นผลมาจากการทำงานของคลาสเนมสเปซ มันจะไม่เปลี่ยน
Martijn Pieters

4
และฉันทราบว่าวิธีแก้ปัญหาของคุณทำในสิ่งที่คำตอบของฉันระบุไว้แล้ว: สร้างขอบเขตใหม่ (แลมบ์ดาไม่แตกต่างจากที่นี่defเพื่อใช้สร้างฟังก์ชัน)
Martijn Pieters

1
อ๋อ ในขณะที่มันเป็นเรื่องดีที่จะได้รับคำตอบด้วยการหลีกเลี่ยงอย่างรวดเร็วสิ่งนี้เป็นการระบุพฤติกรรมที่เป็นข้อผิดพลาดเมื่อมันเป็นผลข้างเคียงของวิธีการทำงานของภาษา (และดังนั้นจะไม่เปลี่ยนแปลง)
jsbueno

ปัญหานี้เป็นปัญหาที่แตกต่างกันที่จริงไม่ได้เป็นปัญหาในหลาม 3. มันเกิดขึ้นเฉพาะใน IPython python -c "import IPython;IPython.embed()"เมื่อคุณเรียกมันฝังอยู่ในโหมดที่ใช้พูด เรียกใช้ IPython โดยตรงโดยใช้คำพูดipythonและปัญหาจะหายไป
Riaz Rizvi

6

คำตอบที่ได้รับการยอมรับให้ข้อมูลที่ดีเยี่ยม แต่ดูเหมือนว่าจะมีรอยย่นอื่น ๆ ที่นี่ - ความแตกต่างระหว่างรายการความเข้าใจและการแสดงออกของเครื่องกำเนิด ตัวอย่างที่ฉันเล่นด้วย:

class Foo:

    # A class-level variable.
    X = 10

    # I can use that variable to define another class-level variable.
    Y = sum((X, X))

    # Works in Python 2, but not 3.
    # In Python 3, list comprehensions were given their own scope.
    try:
        Z1 = sum([X for _ in range(3)])
    except NameError:
        Z1 = None

    # Fails in both.
    # Apparently, generator expressions (that's what the entire argument
    # to sum() is) did have their own scope even in Python 2.
    try:
        Z2 = sum(X for _ in range(3))
    except NameError:
        Z2 = None

    # Workaround: put the computation in lambda or def.
    compute_z3 = lambda val: sum(val for _ in range(3))

    # Then use that function.
    Z3 = compute_z3(X)

    # Also worth noting: here I can refer to XS in the for-part of the
    # generator expression (Z4 works), but I cannot refer to XS in the
    # inner-part of the generator expression (Z5 fails).
    XS = [15, 15, 15, 15]
    Z4 = sum(val for val in XS)
    try:
        Z5 = sum(XS[i] for i in range(len(XS)))
    except NameError:
        Z5 = None

print(Foo.Z1, Foo.Z2, Foo.Z3, Foo.Z4, Foo.Z5)

2

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

เพื่อแสดงให้เห็นว่าเหตุใดข้อผิดพลาดนี้ให้กลับไปเป็นตัวอย่างดั้งเดิม สิ่งนี้ล้มเหลว:

class Foo:
    x = 5
    y = [x for i in range(1)]

แต่งานนี้:

def Foo():
    x = 5
    y = [x for i in range(1)]

ข้อ จำกัด ระบุไว้ที่ท้ายส่วนนี้ในคู่มืออ้างอิง


1

เนื่องจากตัววนซ้ำตัวนอกสุดได้รับการประเมินในขอบเขตโดยรอบเราจึงสามารถใช้zipร่วมกับitertools.repeatเพื่อดำเนินการอ้างอิงไปยังขอบเขตของความเข้าใจ:

import itertools as it

class Foo:
    x = 5
    y = [j for i, j in zip(range(3), it.repeat(x))]

ท่านสามารถใช้forลูปซ้อนในความเข้าใจและรวมถึงการพึ่งพาในการทำซ้ำนอกสุด:

class Foo:
    x = 5
    y = [j for j in (x,) for i in range(3)]

สำหรับตัวอย่างที่เฉพาะเจาะจงของ OP:

from collections import namedtuple
import itertools as it

class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for State, args in zip(it.repeat(State), [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ])]
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.