จะเปลี่ยนตัวแปรโมดูลจากโมดูลอื่นได้อย่างไร?


111

สมมติว่าฉันมีชื่อแพ็คเกจbarและประกอบด้วยbar.py:

a = None

def foobar():
    print a

และ__init__.py:

from bar import a, foobar

จากนั้นฉันเรียกใช้สคริปต์นี้:

import bar

print bar.a
bar.a = 1
print bar.a
bar.foobar()

นี่คือสิ่งที่ฉันคาดหวัง:

None
1
1

นี่คือสิ่งที่ฉันได้รับ:

None
1
None

ใครสามารถอธิบายความเข้าใจผิดของฉัน?

คำตอบ:


109

คุณกำลังใช้from bar import a. aกลายเป็นสัญลักษณ์ในขอบเขตส่วนกลางของโมดูลการนำเข้า (หรือขอบเขตใดก็ตามที่คำสั่งการนำเข้าเกิดขึ้น)

เมื่อคุณกำหนดค่าใหม่ให้aคุณเพียงแค่เปลี่ยนaจุดค่าใดด้วยไม่ใช่ค่าจริง พยายามที่จะนำเข้าbar.pyโดยตรงกับimport barในและดำเนินการทดสอบของคุณมีโดยการตั้งค่า__init__.py bar.a = 1ด้วยวิธีนี้คุณจะต้องแก้ไขbar.__dict__['a']ซึ่งเป็นค่า 'ของจริง' aในบริบทนี้

มันเป็นเรื่องเล็ก ๆ น้อย ๆ ที่ซับซ้อนที่มีสามชั้น แต่bar.a = 1เปลี่ยนค่าของaในโมดูลที่เรียกว่าที่มาจริงจากbar __init__.pyมันไม่ได้เปลี่ยนค่าของaที่foobarเห็นเพราะชีวิตในแฟ้มที่เกิดขึ้นจริงfoobar bar.pyคุณสามารถตั้งค่าได้bar.bar.aว่าต้องการเปลี่ยนแปลงหรือไม่

นี่เป็นอันตรายอย่างหนึ่งของการใช้from foo import barรูปแบบของimportคำสั่ง: มันจะแยกออกbarเป็นสองสัญลักษณ์โดยสัญลักษณ์หนึ่งที่มองเห็นได้ทั่วโลกจากภายในfooซึ่งเริ่มจากการชี้ไปที่ค่าดั้งเดิมและสัญลักษณ์อื่นที่มองเห็นได้ในขอบเขตที่importมีการดำเนินการคำสั่ง การเปลี่ยนตำแหน่งที่สัญลักษณ์ชี้จะไม่เปลี่ยนค่าที่ชี้ไปด้วย

สิ่งประเภทนี้เป็นตัวฆ่าเมื่อพยายามreloadใช้โมดูลจากล่ามโต้ตอบ


1
ขอบคุณ! คำตอบของคุณอาจช่วยฉันได้หลายชั่วโมงในการมองหาผู้ร้าย
jnns

ดูเหมือนbar.bar.aวิธีการนี้จะไม่ช่วยได้มากนักในกรณีการใช้งานส่วนใหญ่ อาจเป็นความคิดที่ไม่ดีในการผสมผสานการคอมไพล์หรือการโหลดโมดูลและการกำหนดรันไทม์เพราะคุณจะสับสนในตัวเอง (หรือคนอื่น ๆ ที่ใช้โค้ดของคุณ) โดยเฉพาะอย่างยิ่งในภาษาที่มีพฤติกรรมไม่สอดคล้องกับเรื่องนี้เช่น python มีเนื้อหา
matanster

26

ที่มาของปัญหาอย่างหนึ่งของคำถามนี้คือคุณมีโปรแกรมชื่อbar/bar.py: import barimports อย่างใดอย่างหนึ่งbar/__init__.pyหรือbar/bar.pyขึ้นอยู่กับที่มันจะทำซึ่งจะทำให้มันยุ่งยากเล็ก ๆ น้อย ๆ ในการติดตามซึ่งเป็นabar.a

นี่คือวิธีการทำงาน:

กุญแจสำคัญในการทำความเข้าใจสิ่งที่เกิดขึ้นคือการตระหนักว่าในตัวคุณ __init__.py ,

from bar import a

ในผลบางอย่างเช่น

a = bar.a
# … where bar = bar/bar.py (as if bar were imported locally from __init__.py)

และกำหนดตัวแปรใหม่ ( bar/__init__.py:aหากคุณต้องการ) ดังนั้นคุณfrom bar import aในการ__init__.pyผูกชื่อbar/__init__.py:aกับbar.py:aวัตถุดั้งเดิม( None) นี่คือเหตุผลที่คุณสามารถทำได้from bar import a as a2ใน__init__.py: ในกรณีนี้มันเป็นที่ชัดเจนว่าคุณมีทั้งbar/bar.py:aและที่แตกต่างกันชื่อตัวแปรbar/__init__.py:a2(ในกรณีของคุณชื่อของทั้งสองตัวแปรเพียงเกิดขึ้นกับทั้งสองจะสามารถaแต่พวกเขายังคงอาศัยอยู่ใน namespaces ที่แตกต่างกันใน__init__.pyพวกเขาbar.aและa)

ตอนนี้เมื่อคุณทำ

import bar

print bar.a

คุณกำลังเข้าถึงตัวแปรbar/__init__.py:a(เนื่องจากimport barนำเข้าของคุณbar/__init__.py) นี่คือตัวแปรที่คุณแก้ไข (ถึง 1) bar/bar.py:aคุณยังไม่ได้สัมผัสเนื้อหาของตัวแปร ดังนั้นเมื่อคุณทำในภายหลัง

bar.foobar()

คุณเรียกbar/bar.py:foobar()ซึ่งเข้าถึงตัวแปรaจากbar/bar.pyซึ่งยังคงอยู่None(เมื่อfoobar()ถูกกำหนดมันผูกชื่อตัวแปรทุกครั้งดังนั้นaอินbar.pyคือbar.py:aไม่ใช่อื่นใดaตัวแปรกำหนดไว้ในโมดูลอื่นเนื่องจากอาจมีaตัวแปรจำนวนมากในโมดูลที่นำเข้าทั้งหมด ). ดังนั้นNoneผลลัพธ์สุดท้าย

สรุป: เป็นการดีที่สุดที่จะหลีกเลี่ยงความคลุมเครือimport barโดยไม่มีbar/bar.pyโมดูลใด ๆ(เนื่องจากbar.__init__.pyทำให้ไดเรกทอรีbar/เป็นแพ็คเกจอยู่แล้วซึ่งคุณสามารถนำเข้าได้ด้วยimport bar)


12

วิธีอื่น: ปรากฎว่าความเข้าใจผิดนี้ทำได้ง่ายมาก มันถูกกำหนดลอบในการอ้างอิงภาษา Python:การใช้วัตถุแทนสัญลักษณ์ ฉันขอแนะนำว่าการอ้างอิงภาษา Python ทำให้สิ่งนี้ชัดเจนขึ้นและเบาบางน้อยลง ..

fromรูปแบบไม่ผูกชื่อโมดูล: มันจะไปผ่านรายการของตัวระบุลักษณะหนึ่งของพวกเขาแต่ละคนขึ้นมาในโมดูลที่พบในขั้นตอนที่ (1) และผูกชื่อใน namespace ท้องถิ่นเพื่อวัตถุที่พบจึง

อย่างไรก็ตาม:

เมื่อคุณนำเข้าคุณจะนำเข้าค่าปัจจุบันของสัญลักษณ์ที่นำเข้าและเพิ่มลงในเนมสเปซของคุณตามที่กำหนดไว้ คุณไม่ได้นำเข้าข้อมูลอ้างอิงคุณกำลังนำเข้าค่าอย่างมีประสิทธิภาพ

ดังนั้นในการรับค่าที่อัปเดตiคุณต้องนำเข้าตัวแปรที่มีการอ้างอิงถึงสัญลักษณ์นั้น

กล่าวอีกนัยหนึ่งการนำเข้าไม่เหมือนimportใน JAVA externalการประกาศใน C / C ++ หรือแม้แต่useประโยคใน PERL

แต่คำสั่งต่อไปนี้ใน Python:

from some_other_module import a as x

เป็นเหมือนโค้ดต่อไปนี้ใน K&R C:

extern int a; /* import from the EXTERN file */

int x = a;

(ข้อแม้: ในกรณี Python "a" และ "x" เป็นการอ้างอิงถึงค่าจริง: คุณไม่ได้คัดลอก INT คุณกำลังคัดลอกที่อยู่อ้างอิง)


อันที่จริงฉันพบว่าวิธีของ Python นั้นimportสะอาดกว่า Java มากเนื่องจากเนมสเปซ / ขอบเขตจะถูกแยกออกจากกันอย่างเหมาะสมและไม่รบกวนซึ่งกันและกันด้วยวิธีที่ไม่คาดคิด เช่นเดียวกับที่นี่: การแก้ไขการเชื่อมโยงของอ็อบเจ็กต์กับชื่อในเนมสเปซ (อ่าน: การรวมบางอย่างกับคุณสมบัติส่วนกลางของโมดูล) จะไม่ส่งผลกระทบต่อเนมสเปซอื่น ๆ (อ่าน: การอ้างอิงที่ถูกอิมพอร์ต) ใน Python แต่ทำได้ใน Java เป็นต้นใน Python คุณจะต้องเข้าใจสิ่งที่นำเข้าเท่านั้นในขณะที่ใน Java คุณต้องเข้าใจโมดูลอื่นด้วยในกรณีที่มันเปลี่ยนแปลงค่าที่นำเข้านี้ในภายหลัง
Tino

2
ฉันต้องไม่เห็นด้วยอย่างมาก การนำเข้า / รวม / การใช้มีประวัติอันยาวนานในการใช้รูปแบบการอ้างอิงและไม่ใช่รูปแบบค่าที่เกี่ยวกับภาษาอื่น ๆ
Mark Gerolimatos

4
บ้าฉันตีกลับ ... นี่คือความคาดหวังของการนำเข้าโดยการอ้างอิง (ตาม @OP) ในความเป็นจริงโปรแกรมเมอร์ Python มือใหม่ไม่ว่าจะมีประสบการณ์แค่ไหนก็ต้องบอกเรื่องนี้ในลักษณะ "ระวัง" สิ่งนี้ไม่ควรเกิดขึ้น: จากการใช้งานทั่วไป Python ใช้เส้นทางที่ผิด สร้าง "ค่าการนำเข้า" หากจำเป็น แต่อย่ารวมสัญลักษณ์กับค่าเมื่อนำเข้า
Mark Gerolimatos
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.