จะเกิดอะไรขึ้นถ้าโมดูลสองโมดูลนำเข้าซึ่งกันและกัน
เพื่อสรุปปัญหาสิ่งที่เกี่ยวกับวงจรนำเข้าในงูหลาม?
จะเกิดอะไรขึ้นถ้าโมดูลสองโมดูลนำเข้าซึ่งกันและกัน
เพื่อสรุปปัญหาสิ่งที่เกี่ยวกับวงจรนำเข้าในงูหลาม?
คำตอบ:
มีการพูดคุยที่ดีในเรื่องนี้ที่comp.lang.pythonเมื่อปีที่แล้ว มันตอบคำถามของคุณอย่างละเอียด
การนำเข้าค่อนข้างตรงไปตรงมาจริงๆ เพียงจำสิ่งต่อไปนี้:
'import' และ 'from xxx import yyy' เป็นคำสั่งที่เรียกใช้งานได้ พวกเขาดำเนินการเมื่อโปรแกรมที่รันอยู่ถึงบรรทัดนั้น
หากโมดูลไม่ได้อยู่ใน sys.modules ดังนั้นการนำเข้าจะสร้างรายการโมดูลใหม่ใน sys.modules แล้วประมวลผลรหัสในโมดูล มันจะไม่ส่งคืนการควบคุมไปยังโมดูลการโทรจนกว่าการดำเนินการเสร็จสิ้น
หากโมดูลมีอยู่ใน sys.modules การอิมพอร์ตจะส่งคืนโมดูลนั้นไม่ว่าจะดำเนินการเสร็จสิ้นหรือไม่ นั่นคือเหตุผลที่การนำเข้าแบบวนกลับอาจส่งคืนโมดูลซึ่งดูเหมือนว่าว่างเปล่าบางส่วน
ในที่สุดสคริปต์เรียกใช้ทำงานในโมดูลชื่อ __main__ การนำเข้าสคริปต์ภายใต้ชื่อของตัวเองจะสร้างโมดูลใหม่ที่ไม่เกี่ยวข้องกับ __main__
นำมารวมกันและคุณไม่ควรประหลาดใจเมื่อนำเข้าโมดูล
ถ้าคุณทำimport fooข้างในbarและimport barข้างในfooมันก็ใช้ได้ดี เมื่อเวลาที่สิ่งใดทำงานจริงโมดูลทั้งสองจะถูกโหลดอย่างเต็มที่และจะมีการอ้างอิงถึงกันและกัน
ปัญหาคือเมื่อแทนคุณทำและfrom foo import abc from bar import xyzเพราะตอนนี้แต่ละโมดูลต้องการโมดูลอื่นที่จะนำเข้าแล้ว (เพื่อให้ชื่อที่เรากำลังนำเข้าอยู่) ก่อนที่จะสามารถนำเข้า
from foo import *และfrom bar import *จะทำงานได้ดี
from x import yและยังได้รับข้อผิดพลาดในการนำเข้าแบบวงกลม
importคำสั่ง ดังนั้นมันจะไม่เกิดข้อผิดพลาด แต่คุณอาจไม่ได้รับตัวแปรทั้งหมดที่คุณคาดหวัง
from foo import *และfrom bar import *ทุกอย่างที่ดำเนินการในfooอยู่ในช่วงเริ่มต้นbarและbarยังไม่ได้กำหนดฟังก์ชั่นจริงใน...
การนำเข้าแบบวงกลมสิ้นสุดลง แต่คุณต้องระวังไม่ให้ใช้โมดูลที่นำเข้าแบบวนซ้ำระหว่างการเริ่มต้นโมดูล
พิจารณาไฟล์ต่อไปนี้:
a.py:
print "a in"
import sys
print "b imported: %s" % ("b" in sys.modules, )
import b
print "a out"
b.py:
print "b in"
import a
print "b out"
x = 3
หากคุณรัน a.py คุณจะได้รับสิ่งต่อไปนี้:
$ python a.py
a in
b imported: False
b in
a in
b imported: True
a out
b out
a out
ในการนำเข้าที่สองของ b.py (ในที่สองa in), Python interpreter ไม่ได้นำเข้าbอีกครั้งเพราะมันมีอยู่แล้วในโมดูล dict
ถ้าคุณพยายามที่จะเข้าถึงb.xจากในระหว่างการเตรียมโมดูลคุณจะได้รับaAttributeError
ต่อท้ายบรรทัดต่อไปนี้เพื่อa.py:
print b.x
จากนั้นผลลัพธ์คือ:
$ python a.py
a in
b imported: False
b in
a in
b imported: True
a out
Traceback (most recent call last):
File "a.py", line 4, in <module>
import b
File "/home/shlomme/tmp/x/b.py", line 2, in <module>
import a
File "/home/shlomme/tmp/x/a.py", line 7, in <module>
print b.x
AttributeError: 'module' object has no attribute 'x'
นี่เป็นเพราะโมดูลถูกเรียกใช้งานเมื่อมีการนำเข้าและในเวลาb.xนั้นบรรทัดx = 3ยังไม่ถูกดำเนินการซึ่งจะเกิดขึ้นหลังจากนั้นb outเท่านั้น
__name__ 'a'ในตอนแรกฉันสับสนอย่างมากว่าทำไมไฟล์จะถูกดำเนินการสองครั้ง
ตามที่คำตอบอื่น ๆ อธิบายถึงรูปแบบนี้เป็นที่ยอมรับในหลาม:
def dostuff(self):
from foo import bar
...
ซึ่งจะหลีกเลี่ยงการดำเนินการคำสั่งนำเข้าเมื่อไฟล์ถูกนำเข้าโดยโมดูลอื่น ๆ เฉพาะในกรณีที่มีการพึ่งพาแบบวงกลมตรรกะนี้จะล้มเหลว
การนำเข้าแบบวงกลมส่วนใหญ่ไม่ใช่การนำเข้าแบบวงกลมแบบลอจิคัล แต่เป็นการเพิ่มImportErrorข้อผิดพลาดเนื่องจากวิธีการimport()ประเมินข้อความสั่งระดับบนสุดของไฟล์ทั้งหมดเมื่อถูกเรียก
ImportErrorsคุณสามารถหลีกเลี่ยงสิ่งเหล่านี้ได้เกือบทุกครั้งหากคุณต้องการนำเข้าของคุณในเชิงบวก :
พิจารณาการนำเข้าแบบวงกลมนี้:
# profiles/serializers.py
from images.serializers import SimplifiedImageSerializer
class SimplifiedProfileSerializer(serializers.Serializer):
name = serializers.CharField()
class ProfileSerializer(SimplifiedProfileSerializer):
recent_images = SimplifiedImageSerializer(many=True)
# images/serializers.py
from profiles.serializers import SimplifiedProfileSerializer
class SimplifiedImageSerializer(serializers.Serializer):
title = serializers.CharField()
class ImageSerializer(SimplifiedImageSerializer):
profile = SimplifiedProfileSerializer()
จาก David Beazleys โมดูลการพูดคุยที่ยอดเยี่ยมและแพ็คเกจ: สดและปล่อยให้ตาย! - PyCon 2015 , 1:54:00นี่คือวิธีการจัดการกับการนำเข้าวงกลมหลาม:
try:
from images.serializers import SimplifiedImageSerializer
except ImportError:
import sys
SimplifiedImageSerializer = sys.modules[__package__ + '.SimplifiedImageSerializer']
การดำเนินการนี้จะพยายามนำเข้าSimplifiedImageSerializerและหากImportErrorมีการยกระดับเนื่องจากมีการนำเข้าแล้วจะเป็นการดึงจาก importcache
PS: คุณต้องอ่านโพสต์ทั้งหมดนี้ด้วยเสียงของ David Beazley
ฉันได้ตัวอย่างที่นี่ที่ทำให้ฉัน!
foo.py
import bar
class gX(object):
g = 10
bar.py
from foo import gX
o = gX()
main.py
import foo
import bar
print "all done"
ที่บรรทัดคำสั่ง: $ python main.py
Traceback (most recent call last):
File "m.py", line 1, in <module>
import foo
File "/home/xolve/foo.py", line 1, in <module>
import bar
File "/home/xolve/bar.py", line 1, in <module>
from foo import gX
ImportError: cannot import name gX
import barในfoo.pyที่ปลาย
barและfooต้องใช้ทั้งสองgXวิธีโซลูชัน 'cleanest' จะต้องใส่gXในโมดูลอื่นและมีทั้งfooและbarนำเข้าโมดูลนั้น (สะอาดที่สุดในแง่ที่ว่าไม่มีการอ้างอิงความหมายแฝง)
barไม่สามารถหาได้gXใน foo การนำเข้าแบบวงกลมนั้นใช้ได้ด้วยตัวเอง แต่ก็gXไม่ได้ถูกกำหนดเมื่อนำเข้า
โมดูล a.py:
import b
print("This is from module a")
โมดูล b.py
import a
print("This is from module b")
กำลังเรียกใช้ "Module a" จะส่งออก:
>>>
'This is from module a'
'This is from module b'
'This is from module a'
>>>
มันออกมา 3 บรรทัดในขณะที่มันควรจะส่งออก infinitival เพราะการนำเข้าแบบวงกลม จะเกิดอะไรขึ้นทีละบรรทัดขณะที่เรียกใช้ "โมดูล a" อยู่ที่นี่
import bบรรทัดแรกคือ ดังนั้นมันจะไปที่โมดูล bimport aบรรทัดแรกที่โมดูลข ดังนั้นมันจะไปที่โมดูลimport bแต่โปรดทราบว่าบรรทัดนี้จะไม่ถูกดำเนินการอีกต่อไปเพราะทุกไฟล์ในไพ ธ อนรันบรรทัดการนำเข้าเพียงครั้งเดียวจึงไม่สำคัญว่าจะดำเนินการที่ไหนหรือเมื่อใด "This is from module a"จึงจะส่งผ่านไปยังบรรทัดถัดไปและพิมพ์"This is from module b""This is from module a"และโปรแกรมจะเสร็จสิ้นฉันเห็นด้วยกับคำตอบของ pythone อย่างสมบูรณ์ แต่ฉันได้สะดุดกับรหัสบางอย่างที่มีข้อบกพร่องด้วยการนำเข้าแบบวงกลมและทำให้เกิดปัญหาเมื่อพยายามเพิ่มการทดสอบหน่วย ดังนั้นการแก้ไขอย่างรวดเร็วโดยไม่เปลี่ยนแปลงทุกสิ่งคุณสามารถแก้ไขปัญหาได้ด้วยการนำเข้าแบบไดนามิก
# Hack to import something without circular import issue
def load_module(name):
"""Load module using imp.find_module"""
names = name.split(".")
path = None
for name in names:
f, path, info = imp.find_module(name, path)
path = [path]
return imp.load_module(name, f, path[0], info)
constants = load_module("app.constants")
นี่ไม่ใช่การแก้ไขแบบถาวร แต่อาจช่วยคนที่ต้องการแก้ไขข้อผิดพลาดในการนำเข้าโดยไม่ต้องเปลี่ยนรหัสมากเกินไป
ไชโย!
มีคำตอบที่ดีมากมายที่นี่ ในขณะที่มักจะมีวิธีแก้ปัญหาอย่างรวดเร็ว แต่บางคนก็รู้สึกว่ามันเป็นระบบที่เรียบง่ายกว่าคนอื่น ๆ หากคุณมีความหรูหราในการปรับโครงสร้างบางส่วนอีกวิธีหนึ่งคือการวิเคราะห์องค์กรของรหัสของคุณและพยายามลบการพึ่งพาแบบวงกลม ตัวอย่างเช่นคุณอาจพบว่าคุณมี:
ไฟล์ a.py
from b import B
class A:
@staticmethod
def save_result(result):
print('save the result')
@staticmethod
def do_something_a_ish(param):
A.save_result(A.use_param_like_a_would(param))
@staticmethod
def do_something_related_to_b(param):
B.do_something_b_ish(param)
ไฟล์ b.py
from a import A
class B:
@staticmethod
def do_something_b_ish(param):
A.save_result(B.use_param_like_b_would(param))
ในกรณีนี้เพียงแค่ย้ายหนึ่งวิธีคงที่ไปยังไฟล์แยกพูดว่าc.py:
ไฟล์ c.py
def save_result(result):
print('save the result')
จะอนุญาตให้ลบsave_resultเมธอดออกจาก A และอนุญาตให้ลบการนำเข้า A จาก a ใน b:
refileored ไฟล์ a.py
from b import B
from c import save_result
class A:
@staticmethod
def do_something_a_ish(param):
A.save_result(A.use_param_like_a_would(param))
@staticmethod
def do_something_related_to_b(param):
B.do_something_b_ish(param)
refactored File b.py
from c import save_result
class B:
@staticmethod
def do_something_b_ish(param):
save_result(B.use_param_like_b_would(param))
โดยสรุปหากคุณมีเครื่องมือ (เช่น pylint หรือ PyCharm) ที่รายงานเกี่ยวกับวิธีการที่สามารถคงที่เพียงแค่โยนstaticmethodมัณฑนากรบนพวกเขาอาจไม่ใช่วิธีที่ดีที่สุดในการปิดเสียงเตือน แม้ว่าวิธีการนั้นจะเกี่ยวข้องกับคลาส แต่มันก็เป็นการดีกว่าถ้าคุณแยกมันออกโดยเฉพาะถ้าคุณมีโมดูลที่เกี่ยวข้องอย่างใกล้ชิดหลายอย่างที่อาจต้องการฟังก์ชั่นเดียวกันและคุณตั้งใจที่จะฝึกหลักการ DRY
การนำเข้าแบบวงกลมอาจสร้างความสับสนเนื่องจากการนำเข้าทำสองสิ่ง:
อดีตทำเพียงครั้งเดียวในขณะที่หลังในแต่ละคำสั่งนำเข้า การนำเข้าแบบวงกลมจะสร้างสถานการณ์เมื่อนำเข้าโมดูลใช้การนำเข้าด้วยรหัสที่ดำเนินการบางส่วน ดังนั้นมันจะไม่เห็นวัตถุที่สร้างขึ้นหลังจากคำสั่งนำเข้า ตัวอย่างรหัสด้านล่างแสดงให้เห็นถึงมัน
การนำเข้าแบบวงกลมไม่ใช่สิ่งชั่วร้ายที่สุดที่ควรหลีกเลี่ยงในทุกกรณี ในบางเฟรมเวิร์กเช่น Flask มันค่อนข้างเป็นธรรมชาติและปรับแต่งโค้ดของคุณเพื่อกำจัดมันไม่ได้ทำให้โค้ดดีขึ้น
main.py
print 'import b'
import b
print 'a in globals() {}'.format('a' in globals())
print 'import a'
import a
print 'a in globals() {}'.format('a' in globals())
if __name__ == '__main__':
print 'imports done'
print 'b has y {}, a is b.a {}'.format(hasattr(b, 'y'), a is b.a)
b.by
print "b in, __name__ = {}".format(__name__)
x = 3
print 'b imports a'
import a
y = 5
print "b out"
a.py
print 'a in, __name__ = {}'.format(__name__)
print 'a imports b'
import b
print 'b has x {}'.format(hasattr(b, 'x'))
print 'b has y {}'.format(hasattr(b, 'y'))
print "a out"
หลาม main.py เอาต์พุตพร้อมความคิดเห็น
import b
b in, __name__ = b # b code execution started
b imports a
a in, __name__ = a # a code execution started
a imports b # b code execution is already in progress
b has x True
b has y False # b defines y after a import,
a out
b out
a in globals() False # import only adds a to main global symbol table
import a
a in globals() True
imports done
b has y True, a is b.a True # all b objects are available
ฉันแก้ไขปัญหาด้วยวิธีต่อไปนี้และทำงานได้ดีโดยไม่มีข้อผิดพลาด พิจารณาสองไฟล์และa.pyb.py
ฉันเพิ่มสิ่งนี้ลงไปa.pyและใช้งานได้
if __name__ == "__main__":
main ()
import b
y = 2
def main():
print ("a out")
print (b.x)
if __name__ == "__main__":
main ()
import a
print ("b out")
x = 3 + a.y
ผลลัพธ์ที่ฉันได้รับคือ
>>> b out
>>> a out
>>> 5
โอเคฉันคิดว่าฉันมีทางออกที่ยอดเยี่ยม สมมติว่าคุณมีไฟล์และแฟ้มa bคุณมีdefหรือclassในไฟล์bที่คุณต้องการที่จะใช้ในโมดูลaแต่คุณมีบางสิ่งบางอย่างอื่นอย่างใดอย่างหนึ่งdef, classหรือตัวแปรจากไฟล์ที่คุณต้องการในความหมายหรือระดับของคุณในแฟ้มa bสิ่งที่คุณสามารถทำได้คือที่ด้านล่างของไฟล์aหลังจากเรียกใช้ฟังก์ชันหรือคลาสในไฟล์aที่จำเป็นต้องใช้ในไฟล์bแต่ก่อนที่จะเรียกใช้ฟังก์ชันหรือคลาสจากไฟล์bที่คุณต้องการสำหรับไฟล์aให้พูดimport b
จากนั้นและนี่คือส่วนสำคัญในคำจำกัดความหรือคลาสทั้งหมดในไฟล์bที่ต้องการdefหรือclassจากไฟล์a(เรียกมันว่าCLASS) คุณพูดfrom a import CLASS
วิธีนี้ใช้งานได้เพราะคุณสามารถนำเข้าไฟล์ได้bโดยไม่ต้องใช้ Python ในการดำเนินการคำสั่งการนำเข้าใด ๆ ในไฟล์bและทำให้คุณหลีกเลี่ยงการนำเข้าแบบวงกลมใด ๆ
ตัวอย่างเช่น:
class A(object):
def __init__(self, name):
self.name = name
CLASS = A("me")
import b
go = B(6)
go.dostuff
class B(object):
def __init__(self, number):
self.number = number
def dostuff(self):
from a import CLASS
print "Hello " + CLASS.name + ", " + str(number) + " is an interesting number."
voila
from a import CLASSไม่ได้ข้ามการเรียกใช้งานโค้ดทั้งหมดใน a.py นี่คือสิ่งที่เกิดขึ้นจริง: (1) โค้ดทั้งหมดใน a.py ถูกเรียกใช้เป็นโมดูลพิเศษ "__main__" (2) ที่import bโค้ดระดับบนสุดใน b.py เริ่มทำงาน (กำหนดคลาส B) จากนั้นการควบคุมจะกลับไปที่ "__main__" (3) "__main__" go.dostuff()ในที่สุดก็ผ่านการควบคุม (4) เมื่อ dostuff () มาถึงimport aมันรันโค้ดทั้งหมดใน a.py อีกครั้งคราวนี้เป็นโมดูล "a"; จากนั้นจะนำเข้าวัตถุคลาสจากโมดูลใหม่ "a" ดังนั้นจริง ๆ แล้วมันจะทำงานได้ดีเท่า ๆ กันหากคุณใช้import aที่ใดก็ได้ใน b.py