การนำเข้าแบบวงกลม Python?


103

ฉันได้รับข้อผิดพลาดนี้

Traceback (most recent call last):
  File "/Users/alex/dev/runswift/utils/sim2014/simulator.py", line 3, in <module>
    from world import World
  File "/Users/alex/dev/runswift/utils/sim2014/world.py", line 2, in <module>
    from entities.field import Field
  File "/Users/alex/dev/runswift/utils/sim2014/entities/field.py", line 2, in <module>
    from entities.goal import Goal
  File "/Users/alex/dev/runswift/utils/sim2014/entities/goal.py", line 2, in <module>
    from entities.post import Post
  File "/Users/alex/dev/runswift/utils/sim2014/entities/post.py", line 4, in <module>
    from physics import PostBody
  File "/Users/alex/dev/runswift/utils/sim2014/physics.py", line 21, in <module>
    from entities.post import Post
ImportError: cannot import name Post

และคุณจะเห็นว่าฉันใช้คำสั่งนำเข้าเดียวกันเพิ่มเติมและได้ผล? มีกฎที่ไม่ได้เขียนไว้เกี่ยวกับการนำเข้าแบบวงกลมหรือไม่? ฉันจะใช้คลาสเดียวกันใน call stack ได้อย่างไร

คำตอบ:


172

ฉันคิดว่าคำตอบของ jpmc26 ในขณะที่ไม่ผิดแต่ก็มีการนำเข้าแบบวงกลมมากเกินไป สามารถทำงานได้ดีหากคุณตั้งค่าอย่างถูกต้อง

วิธีที่ง่ายที่สุดที่จะทำคือการใช้ไวยากรณ์มากกว่าimport my_module from my_module import some_objectอดีตเกือบตลอดเวลาแม้ว่าจะmy_moduleรวมการนำเข้าเรากลับ ส่วนหลังจะใช้งานได้my_objectก็ต่อเมื่อมีการกำหนดไว้แล้วmy_moduleซึ่งในการนำเข้าแบบวงกลมอาจไม่เป็นเช่นนั้น

จะเฉพาะเจาะจงกับกรณีของคุณ: ลองเปลี่ยนentities/post.pyที่จะทำimport physicsแล้วอ้างถึงphysics.PostBodyมากกว่าแค่PostBodyโดยตรง ในทำนองเดียวกันการเปลี่ยนแปลงphysics.pyที่จะทำimport entities.postแล้วใช้มากกว่าแค่entities.post.PostPost


5
คำตอบนี้เข้ากันได้กับการนำเข้าแบบสัมพัทธ์หรือไม่
โจ

19
ทำไมสิ่งนี้ถึงเกิดขึ้น?
Juan Pablo Santos

6
เป็นเรื่องผิดที่จะกล่าวว่าการไม่fromใช้ไวยากรณ์จะใช้ได้ผลเสมอไป หากฉันมีclass A(object): pass; class C(b.B): passในโมดูล a และclass B(a.A): passในโมดูล b การนำเข้าแบบวงกลมยังคงเป็นปัญหาและจะใช้ไม่ได้
CrazyCasta

1
คุณถูกต้องการอ้างอิงแบบวงกลมใด ๆ ในโค้ดระดับบนสุดของโมดูล (เช่นคลาสพื้นฐานของการประกาศคลาสในตัวอย่างของคุณ) จะเป็นปัญหา นั่นคือสถานการณ์ที่คำตอบของ jpmc ที่คุณควร refactor องค์กรโมดูลนั้นน่าจะถูกต้อง 100% ย้ายคลาสBไปไว้ในโมดูลaหรือย้ายคลาสCไปไว้ในโมดูลbเพื่อให้คุณสามารถตัดวงจรได้ นอกจากนี้ยังเป็นที่น่าสังเกตว่าแม้ว่าจะมีเพียงทิศทางเดียวของวงกลมที่มีรหัสระดับบนสุดที่เกี่ยวข้อง (เช่นถ้าไม่มีคลาสC) คุณอาจได้รับข้อผิดพลาดขึ้นอยู่กับว่าโมดูลใดที่นำเข้ามาก่อนโดยรหัสอื่น
Blckknght

2
@TylerCrompton: ฉันไม่แน่ใจว่า "การนำเข้าโมดูลต้องเป็นแบบสัมบูรณ์" หมายถึงอะไร การนำเข้าแบบสัมพัทธ์แบบวงกลมสามารถทำงานได้ตราบเท่าที่คุณกำลังนำเข้าโมดูลไม่ใช่เนื้อหา (เช่นfrom . import sibling_moduleไม่ใช่from .sibling_module import SomeClass) มีความละเอียดอ่อนมากขึ้นเมื่อ__init__.pyไฟล์ของแพ็คเกจมีส่วนเกี่ยวข้องกับการนำเข้าแบบวงกลม แต่ปัญหานี้เกิดขึ้นได้ยากและอาจเป็นข้อบกพร่องในการimportใช้งาน ดูPython bug 23447ซึ่งฉันส่งแพทช์ให้ (ซึ่งอนิจจาดูอิดโรย)
Blckknght

52

เมื่อคุณนำเข้าโมดูล (หรือสมาชิกของโมดูล) เป็นครั้งแรกรหัสภายในโมดูลจะถูกดำเนินการตามลำดับเช่นเดียวกับรหัสอื่น ๆ เช่นมันไม่ได้รับการปฏิบัติที่แตกต่างจากร่างกายของฟังก์ชัน An importเป็นเพียงคำสั่งอื่น ๆ (การกำหนด, การเรียกใช้ฟังก์ชันdef, class) สมมติว่าการนำเข้าของคุณเกิดขึ้นที่ด้านบนของสคริปต์นี่คือสิ่งที่เกิดขึ้น:

  • เมื่อคุณพยายามที่จะนำเข้าWorldจากworldที่worldสคริปต์ได้รับการดำเนินการ
  • worldการนำเข้าสคริปต์Fieldซึ่งเป็นสาเหตุที่ทำให้entities.fieldสคริปต์จะได้รับการดำเนินการ
  • กระบวนการนี้จะดำเนินต่อไปจนกว่าคุณจะไปถึงentities.postสคริปต์เนื่องจากคุณพยายามนำเข้าPost
  • entities.postสาเหตุสคริปต์physicsโมดูลจะต้องถูกประหารชีวิตเพราะมันพยายามที่จะนำเข้าPostBody
  • สุดท้ายphysicsพยายามนำเข้าPostจากentities.post
  • ฉันไม่แน่ใจว่ามีentities.postโมดูลอยู่ในหน่วยความจำหรือยัง แต่มันไม่สำคัญจริงๆ โมดูลไม่ได้อยู่ในหน่วยความจำหรือโมดูลยังไม่มีPostสมาชิกเนื่องจากยังไม่เสร็จสิ้นการดำเนินการเพื่อกำหนดPost
  • ไม่ว่าจะด้วยวิธีใดข้อผิดพลาดเกิดขึ้นเนื่องจากPostไม่มีการนำเข้า

ไม่เลยมันไม่ "ทำงานต่อไปในกลุ่มการโทร" นี่คือการติดตามสแต็กที่เกิดข้อผิดพลาดซึ่งหมายความว่าเกิดความผิดพลาดขณะพยายามอิมพอร์ตPostในคลาสนั้น คุณไม่ควรใช้การนำเข้าแบบวงกลม อย่างดีที่สุดก็มีประโยชน์เล็กน้อย (โดยทั่วไปคือไม่มีประโยชน์) และทำให้เกิดปัญหาเช่นนี้ มันสร้างภาระให้กับนักพัฒนาทุกคนที่ดูแลรักษามันโดยบังคับให้พวกเขาเดินบนเปลือกไข่เพื่อหลีกเลี่ยงการทำลาย ปรับโครงสร้างองค์กรโมดูลของคุณใหม่


1
isinstance(userData, Post)ควรจะเป็น ไม่ว่าคุณจะไม่มีทางเลือก การนำเข้าแบบวงกลมจะไม่ทำงาน ความจริงที่คุณมีการนำเข้าแบบวงกลมเป็นกลิ่นรหัสสำหรับฉัน แนะนำว่าคุณมีฟังก์ชันบางอย่างที่ควรย้ายออกไปยังโมดูลที่สาม ฉันไม่สามารถพูดอะไรได้โดยไม่ต้องดูทั้งชั้นเรียน
jpmc26

3
@CpILL หลังจากนั้นไม่นานตัวเลือกที่แฮ็คมากก็เกิดขึ้นกับฉัน หากคุณไม่สามารถดำเนินการได้ในตอนนี้ (เนื่องจากข้อ จำกัด ด้านเวลาหรือสิ่งที่คุณมี) คุณสามารถนำเข้าภายในเครื่องภายในวิธีที่คุณใช้ ร่างกายของฟังก์ชันภายในdefจะไม่ถูกเรียกใช้จนกว่าจะมีการเรียกใช้ฟังก์ชันดังนั้นการนำเข้าจะไม่เกิดขึ้นจนกว่าคุณจะเรียกฟังก์ชันนั้นจริงๆ จากนั้นระบบimportควรใช้งานได้เนื่องจากโมดูลใดโมดูลหนึ่งจะได้รับการนำเข้าอย่างสมบูรณ์ก่อนการโทร นั่นเป็นการแฮ็กที่น่ารังเกียจอย่างยิ่งและไม่ควรอยู่ในฐานรหัสของคุณเป็นระยะเวลานาน
jpmc26

15
ฉันคิดว่าคำตอบของคุณยากเกินไปสำหรับการนำเข้าแบบวงกลม การนำเข้า circularly มักจะทำงานถ้าคุณทำแค่มากกว่าimport foo from foo import Barนั่นเป็นเพราะโมดูลส่วนใหญ่กำหนดสิ่งต่างๆ (เช่นฟังก์ชันและคลาส) ที่ทำงานในภายหลัง โมดูลที่ทำสิ่งสำคัญเมื่อคุณนำเข้า (เช่นสคริปต์ที่ไม่ได้รับการป้องกันif __name__ == "__main__") อาจยังคงมีปัญหา แต่ก็ไม่บ่อยนัก
Blckknght

6
@Blckknght ฉันคิดว่าคุณกำลังตั้งค่าตัวเองเพื่อใช้เวลากับปัญหาแปลก ๆ ที่คนอื่นจะต้องตรวจสอบและสับสนหากคุณใช้การนำเข้าแบบวงกลม พวกเขาบังคับให้คุณใช้เวลาในการระมัดระวังที่จะไม่ข้ามผ่านพวกเขาและยิ่งไปกว่านั้นคือกลิ่นรหัสที่การออกแบบของคุณต้องการการปรับโครงสร้างใหม่ ฉันอาจจะคิดผิดว่ามันเป็นไปได้ในทางเทคนิคหรือไม่ แต่มันเป็นตัวเลือกการออกแบบที่แย่มากที่กำหนดให้เกิดปัญหาไม่ช้าก็เร็ว ความชัดเจนและความเรียบง่ายเป็นสิ่งศักดิ์สิทธิ์ในการเขียนโปรแกรมและการนำเข้าแบบวงกลมละเมิดทั้งในหนังสือของฉัน
jpmc26

6
หรือ; คุณแยกฟังก์ชันการทำงานของคุณมากเกินไปและนั่นคือสาเหตุของการนำเข้าแบบวงกลม หากคุณมีสองสิ่งที่ต้องพึ่งพากันและกันตลอดเวลา ; อาจเป็นการดีที่สุดที่จะใส่ไว้ในไฟล์เดียว Python ไม่ใช่ Java ไม่มีเหตุผลที่จะไม่จัดกลุ่มฟังก์ชัน / คลาสเป็นไฟล์เดียวเพื่อป้องกันตรรกะการนำเข้าแปลก ๆ :-)
Mark Ribau

41

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

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

ลองนึกภาพคุณมีไฟล์ต้นฉบับสองไฟล์:

ไฟล์ X.py

def X1:
    return "x1"

from Y import Y2

def X2:
    return "x2"

ไฟล์ Y.py

def Y1:
    return "y1"

from X import X1

def Y2:
    return "y2"

ตอนนี้สมมติว่าคุณรวบรวมไฟล์ X.py. คอมไพเลอร์เริ่มต้นด้วยการกำหนดเมธอด X1 จากนั้นเข้าสู่คำสั่ง import ใน X.py สิ่งนี้ทำให้คอมไพเลอร์หยุดการคอมไพล์ X.py ชั่วคราวและเริ่มคอมไพล์ Y.py หลังจากนั้นไม่นานคอมไพเลอร์ก็เข้าสู่คำสั่งนำเข้าใน Y.py. เนื่องจาก X.py อยู่ในตารางโมดูลแล้ว Python จึงใช้ตารางสัญลักษณ์ X.py ที่ไม่สมบูรณ์ที่มีอยู่เพื่อตอบสนองการอ้างอิงที่ร้องขอ สัญลักษณ์ใด ๆ ที่ปรากฏก่อนคำสั่งนำเข้าใน X.py ขณะนี้อยู่ในตารางสัญลักษณ์ แต่สัญลักษณ์ใด ๆ ที่ตามมาจะไม่ปรากฏ เนื่องจาก X1 ปรากฏก่อนคำสั่งนำเข้าจึงนำเข้าได้สำเร็จ จากนั้น Python จะดำเนินการรวบรวม Y.py. ในการดำเนินการดังกล่าวจะกำหนด Y2 และทำการรวบรวม Y.py จากนั้นจะดำเนินการรวบรวม X.py ต่อและค้นหา Y2 ในตารางสัญลักษณ์ Y.py ในที่สุดการคอมไพล์ก็เสร็จสมบูรณ์โดยไม่มีข้อผิดพลาด

จะมีบางอย่างที่แตกต่างออกไปมากหากคุณพยายามรวบรวม Y.py จากบรรทัดคำสั่ง ขณะรวบรวม Y.py คอมไพลเลอร์จะเข้าสู่คำสั่งนำเข้าก่อนที่จะกำหนด Y2 จากนั้นจะเริ่มรวบรวม X.py. ในไม่ช้าก็จะเข้าสู่คำสั่งนำเข้าใน X.py ที่ต้องใช้ Y2 แต่ Y2 ไม่ได้กำหนดไว้ดังนั้นการคอมไพล์จึงล้มเหลว

โปรดทราบว่าหากคุณแก้ไข X.py เพื่อนำเข้า Y1 การคอมไพล์จะสำเร็จเสมอไม่ว่าคุณจะคอมไพล์ไฟล์ใดก็ตาม อย่างไรก็ตามหากคุณแก้ไขไฟล์ Y.py เพื่อนำเข้าสัญลักษณ์ X2 ไฟล์ทั้งสองจะไม่คอมไพล์

เมื่อใดก็ตามที่โมดูล X หรือโมดูลใด ๆ ที่ X นำเข้าอาจนำเข้าโมดูลปัจจุบันห้ามใช้:

from X import Y

เมื่อใดก็ตามที่คุณคิดว่าอาจมีการนำเข้าแบบวงกลมคุณควรหลีกเลี่ยงการรวบรวมการอ้างอิงเวลาไปยังตัวแปรในโมดูลอื่น ๆ พิจารณารหัสที่ดูไร้เดียงสา:

import X
z = X.Y

สมมติว่าโมดูล X นำเข้าโมดูลนี้ก่อนที่โมดูลนี้จะนำเข้า X อีกสมมติว่า Y ถูกกำหนดไว้ใน X หลังคำสั่งนำเข้า จากนั้น Y จะไม่ถูกกำหนดเมื่อนำเข้าโมดูลนี้และคุณจะได้รับข้อผิดพลาดในการคอมไพล์ หากโมดูลนี้นำเข้า Y ก่อนคุณสามารถหลีกเลี่ยงได้ แต่เมื่อเพื่อนร่วมงานคนหนึ่งของคุณเปลี่ยนลำดับของคำจำกัดความในโมดูลที่สามโดยบริสุทธิ์ใจโค้ดจะแตก

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

โปรดทราบว่าการย้ายคำสั่งการนำเข้าลงในโมดูลจะบดบังสิ่งที่คุณกำลังทำอยู่ ชดเชยสิ่งนี้ด้วยความคิดเห็นที่ด้านบนของโมดูลของคุณดังต่อไปนี้:

#import X   (actual import moved down to avoid circular dependency)

โดยทั่วไปนี่เป็นแนวทางปฏิบัติที่ไม่ดี แต่บางครั้งก็ยากที่จะหลีกเลี่ยง


2
ฉันไม่คิดว่าจะมีคอมไพเลอร์หรือคอมไพล์เวลาใน python เลย
Jerie Wang

6
งูหลามจะมีคอมไพเลอร์และจะรวบรวม @pkqxdd รวบรวมเพียงมักจะถูกซ่อนอยู่ห่างจากผู้ใช้ สิ่งนี้อาจทำให้สับสนเล็กน้อย แต่คงเป็นเรื่องยากสำหรับผู้เขียนที่จะให้คำอธิบายที่ชัดเจนอย่างน่าชื่นชมนี้ว่าเกิดอะไรขึ้นโดยไม่ต้องอ้างอิงถึง Python ซึ่งค่อนข้างคลุมเครือและ "เวลารวบรวม"
Hank


ฉันไปลองใช้บนเครื่องและได้ผลลัพธ์ที่แตกต่างออกไป เรียกใช้ X.py แต่มีข้อผิดพลาด "ไม่สามารถนำเข้าชื่อ" Y2 "จาก" Y "" วิ่ง Y.py โดยไม่มีปัญหาแม้ว่า ฉันใช้ Python 3.7.5 ช่วยอธิบายได้ไหมว่าปัญหาคืออะไร
xuefeng huang

19

สำหรับพวกคุณที่มาเจอปัญหานี้จาก Django คุณควรรู้ว่าเอกสารมีวิธีแก้ปัญหา: https://docs.djangoproject.com/en/1.10/ref/models/fields/#foreignkey

"... หากต้องการอ้างถึงรุ่นที่กำหนดไว้ในแอปพลิเคชันอื่นคุณสามารถระบุรุ่นที่มีป้ายกำกับแอปพลิเคชันแบบเต็มได้อย่างชัดเจนตัวอย่างเช่นหากมีการกำหนดรูปแบบผู้ผลิตข้างต้นในแอปพลิเคชันอื่นที่เรียกว่าการผลิตคุณจะต้องใช้:

class Car(models.Model):
    manufacturer = models.ForeignKey(
        'production.Manufacturer',
        on_delete=models.CASCADE,
)

การอ้างอิงประเภทนี้มีประโยชน์เมื่อแก้ไขการอ้างอิงการนำเข้าแบบวงกลมระหว่างสองแอปพลิเคชัน ... ”


7
ฉันรู้ว่าฉันไม่ควรใช้ความคิดเห็นเพื่อกล่าวว่า "ขอบคุณ" แต่สิ่งนี้ทำให้ฉันหงุดหงิดไปสองสามชั่วโมงแล้ว ขอบคุณขอบคุณขอบคุณ!!!
MikeyE

ฉันเห็นด้วยกับ @MikeyE ฉันได้อ่านบล็อกและ Stackoverflow หลายบล็อกที่พยายามแก้ไขปัญหานี้ด้วย PonyORM ในกรณีที่คนอื่นบอกว่าเป็นการปฏิบัติที่ไม่ดีหรือทำไมคุณต้องเขียนโค้ดชั้นเรียนของคุณให้เป็นแบบวงกลม ORM ก็คือสิ่งที่เกิดขึ้น เนื่องจากตัวอย่างจำนวนมากทำให้โมเดลทั้งหมดอยู่ในไฟล์เดียวกันและเราทำตามตัวอย่างเหล่านั้นยกเว้นเราใช้โมเดลต่อไฟล์ปัญหาจึงไม่ชัดเจนเมื่อ Python ไม่สามารถคอมไพล์ แต่คำตอบนั้นง่ายมาก ขณะที่ไมค์ชี้ขอบคุณมาก
trash80

6

ฉันสามารถนำเข้าโมดูลภายในฟังก์ชัน (เท่านั้น) ที่ต้องการวัตถุจากโมดูลนี้:

def my_func():
    import Foo
    foo_instance = Foo()

1
งูหลามสง่างามแค่ไหน
Yaro

3

หากคุณพบปัญหานี้ในแอปที่ค่อนข้างซับซ้อนอาจเป็นเรื่องยุ่งยากในการ refactor การนำเข้าทั้งหมดของคุณ PyCharm เสนอ Quickfix สำหรับสิ่งนี้ซึ่งจะเปลี่ยนการใช้สัญลักษณ์ที่นำเข้าทั้งหมดโดยอัตโนมัติเช่นกัน

ป้อนคำอธิบายภาพที่นี่


0

ฉันใช้สิ่งต่อไปนี้:

from module import Foo

foo_instance = Foo()

แต่เพื่อกำจัดcircular referenceฉันทำสิ่งต่อไปนี้และได้ผล:

import module.foo

foo_instance = foo.Foo()
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.