จะเกิดอะไรขึ้นถ้าโมดูลสองโมดูลนำเข้าซึ่งกันและกัน
เพื่อสรุปปัญหาสิ่งที่เกี่ยวกับวงจรนำเข้าในงูหลาม?
จะเกิดอะไรขึ้นถ้าโมดูลสองโมดูลนำเข้าซึ่งกันและกัน
เพื่อสรุปปัญหาสิ่งที่เกี่ยวกับวงจรนำเข้าในงูหลาม?
คำตอบ:
มีการพูดคุยที่ดีในเรื่องนี้ที่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
จากในระหว่างการเตรียมโมดูลคุณจะได้รับa
AttributeError
ต่อท้ายบรรทัดต่อไปนี้เพื่อ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.py
b.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