Nhập khẩu tròn (hoặc tuần hoàn) trong Python


352

Điều gì sẽ xảy ra nếu hai mô-đun nhập khẩu lẫn nhau?

Để khái quát vấn đề, còn việc nhập theo chu kỳ trong Python thì sao?



1
cũng như một tài liệu tham khảo, có vẻ như nhập khẩu tròn được phép trên python 3.5 (và có thể vượt ra ngoài) nhưng không phải là 3,4 (và có thể dưới đây).
Charlie Parker

4
Tôi đang sử dụng python 3.7.2 và vẫn gặp lỗi thời gian chạy do phụ thuộc vòng tròn.
Richard Whitehead

Câu trả lời:


281

Có một cuộc thảo luận thực sự tốt về vấn đề này tại comp.lang.python năm ngoái. Nó trả lời câu hỏi của bạn khá kỹ lưỡng.

Nhập khẩu là khá đơn giản thực sự. Chỉ cần nhớ những điều sau đây:

'Nhập' và 'từ xxx nhập yyy' là các câu lệnh thực thi. Họ thực thi khi chương trình đang chạy đến dòng đó.

Nếu một mô-đun không có trong sys.modules, thì việc nhập sẽ tạo ra mục nhập mô-đun mới trong sys.modules và sau đó thực thi mã trong mô-đun. Nó không trả lại điều khiển cho mô-đun gọi cho đến khi thực hiện xong.

Nếu một mô-đun tồn tại trong sys.modules thì việc nhập chỉ đơn giản trả về mô-đun đó cho dù nó có hoàn thành việc thực thi hay không. Đó là lý do tại sao nhập khẩu theo chu kỳ có thể trả về các mô-đun dường như trống một phần.

Cuối cùng, tập lệnh thực thi chạy trong một mô-đun có tên __main__, nhập tập lệnh dưới tên riêng của nó sẽ tạo ra một mô-đun mới không liên quan đến __main__.

Kết hợp nhiều thứ đó lại với nhau và bạn sẽ không gặp phải bất ngờ nào khi nhập mô-đun.


13
@meawoppl Bạn có thể mở rộng nhận xét này được không? Cụ thể họ đã thay đổi như thế nào?
Dan Schien

3
Đến bây giờ, tài liệu tham khảo duy nhất về nhập khẩu tròn trong python3 "Có gì mới?" các trang nằm trong 3,5 một . Nó nói "Nhập khẩu tròn liên quan đến nhập khẩu tương đối hiện được hỗ trợ". @meawoppl bạn có tìm thấy gì khác không được liệt kê trong các trang này không?
zezoche

4
Họ là def. không được hỗ trợ trong 3.0-3.4. Hoặc ít nhất là ngữ nghĩa cho thành công là khác nhau. Dưới đây là tóm tắt tôi thấy rằng không đề cập đến các thay đổi 3.5. gist.github.com/datagrok / 40bf84d5870c41a77dc6
meawoppl

Xin vui lòng bạn có thể mở rộng về điều này "Cuối cùng, tập lệnh thực thi chạy trong một mô-đun có tên chính , nhập tập lệnh dưới tên riêng của nó sẽ tạo ra một mô-đun mới không liên quan đến chính ." Vì vậy, giả sử tập tin là a.py và khi nó chạy như là điểm nhập chính, bây giờ sẽ là chính nếu nó có mã như từ một biến nhập. Sau đó, cùng một tệp 'a.py' sẽ được tải trong bảng mô-đun sys? Vì vậy, nó có nghĩa là nếu nó có câu lệnh in thì nó sẽ chạy hai lần? Một lần cho tập tin chính và một lần nữa khi nhập khẩu gặp phải?
biến

Câu trả lời này đã được 10 năm và tôi muốn một bản cập nhật được hiện đại hóa để đảm bảo rằng nó vẫn đúng trong các phiên bản khác nhau của Python, 2.x hoặc 3.x
Fallenreaper

296

Nếu bạn làm import foobên trong barimport barbên trong foo, nó sẽ hoạt động tốt. Vào thời điểm mọi thứ thực sự chạy, cả hai mô-đun sẽ được tải đầy đủ và sẽ có các tham chiếu cho nhau.

Vấn đề là khi thay vì bạn làm from foo import abcfrom bar import xyz. Bởi vì bây giờ mỗi mô-đun yêu cầu mô-đun khác đã được nhập (để tên chúng tôi đang nhập tồn tại) trước khi có thể được nhập.


27
Có vẻ như from foo import *from bar import *cũng sẽ làm việc tốt.
Akavall

1
Kiểm tra chỉnh sửa bài đăng ở trên bằng cách sử dụng a.py/b.py. Anh ta không sử dụng from x import y, nhưng vẫn nhận được lỗi nhập vòng tròn
Greg Enni

2
Điều này không hoàn toàn đúng. Giống như nhập * từ, nếu bạn cố truy cập một phần tử trong nhập vòng tròn, ở cấp cao nhất, vì vậy trước khi tập lệnh hoàn thành việc chạy, thì bạn sẽ gặp vấn đề tương tự. Chẳng hạn, nếu bạn đang thiết lập một gói toàn cầu trong một gói từ một gói khác và cả hai đều bao gồm nhau. Tôi đã làm điều này để tạo ra một nhà máy cẩu thả cho một đối tượng trong lớp cơ sở nơi đối tượng đó có thể là một trong số các lớp con và mã sử dụng không cần phải biết nó đang tạo ra cái gì.
AaronM

3
@Akavall Không hẳn. Điều đó sẽ chỉ nhập tên có sẵn khi importcâu lệnh được thực thi. Vì vậy, nó sẽ không lỗi nhưng bạn có thể không nhận được tất cả các biến bạn mong đợi.
tám

3
Lưu ý, nếu bạn làm from foo import *from bar import *, mọi thứ được thực hiện trong foogiai đoạn khởi tạo barvà các chức năng thực tế trong barvẫn chưa được xác định ...
Martian2049

100

Nhập theo chu kỳ chấm dứt, nhưng bạn cần cẩn thận không sử dụng các mô-đun nhập theo chu kỳ trong quá trình khởi tạo mô-đun.

Hãy xem xét các tệp sau:

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

Nếu bạn thực hiện a.py, bạn sẽ nhận được những điều sau đây:

$ python a.py
a in
b imported: False
b in
a in
b imported: True
a out
b out
a out

Trong lần nhập thứ hai của b.py (trong lần thứ hai a in), trình thông dịch Python không nhập blại, vì nó đã tồn tại trong mô-đun chính tả.

Nếu bạn cố gắng truy cập b.xtừ atrong mô-đun khởi tạo, bạn sẽ nhận được một AttributeError.

Nối dòng sau vào a.py:

print b.x

Sau đó, đầu ra là:

$ 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'

Điều này là do các mô-đun được thực thi khi nhập và tại thời điểm b.xđược truy cập, dòng x = 3chưa được thực thi, điều này sẽ chỉ xảy ra sau đó b out.


14
Điều này giải thích rất nhiều vấn đề, nhưng làm thế nào về giải pháp? Làm thế nào chúng ta có thể nhập và in đúng x? giải pháp khác ở trên không hiệu quả với tôi
mehmet

Tôi nghĩ rằng câu trả lời này sẽ có lợi rất nhiều nếu bạn sử dụng __name__thay vì 'a'. Lúc đầu, tôi hoàn toàn bối rối tại sao một tập tin sẽ được thực thi hai lần.
Bergi

30

Như các câu trả lời khác mô tả mô hình này được chấp nhận ở python:

def dostuff(self):
     from foo import bar
     ...

Điều này sẽ tránh việc thực thi câu lệnh nhập khi tệp được nhập bởi các mô-đun khác. Chỉ khi có một phụ thuộc vòng tròn logic, điều này sẽ thất bại.

Hầu hết các Nhập khẩu Thông tư không thực sự nhập khẩu vòng tròn logic mà chỉ gây ra ImportErrorlỗi, do cách import()đánh giá các báo cáo cấp cao nhất của toàn bộ tệp khi được gọi.

Những thứ này ImportErrorshầu như luôn có thể tránh được nếu bạn tích cực muốn hàng nhập khẩu của mình lên hàng đầu :

Xem xét nhập khẩu tròn này:

Ứng dụng A

# profiles/serializers.py

from images.serializers import SimplifiedImageSerializer

class SimplifiedProfileSerializer(serializers.Serializer):
    name = serializers.CharField()

class ProfileSerializer(SimplifiedProfileSerializer):
    recent_images = SimplifiedImageSerializer(many=True)

Ứng dụng B

# images/serializers.py

from profiles.serializers import SimplifiedProfileSerializer

class SimplifiedImageSerializer(serializers.Serializer):
    title = serializers.CharField()

class ImageSerializer(SimplifiedImageSerializer):
    profile = SimplifiedProfileSerializer()

Từ David Beazley Các mô-đun và gói nói chuyện tuyệt vời : Sống và để chết! - PyCon 2015 , 1:54:00đây là một cách để đối phó với nhập khẩu tròn trong python:

try:
    from images.serializers import SimplifiedImageSerializer
except ImportError:
    import sys
    SimplifiedImageSerializer = sys.modules[__package__ + '.SimplifiedImageSerializer']

Điều này cố gắng nhập SimplifiedImageSerializervà nếu ImportErrorđược nâng lên, vì nó đã được nhập, nó sẽ kéo nó từ bộ nhập.

Tái bút: Bạn phải đọc toàn bộ bài viết này bằng giọng của David Beazley.


9
ImportError không được nâng lên nếu mô-đun đã được nhập. Các mô-đun có thể được nhập nhiều lần nếu bạn muốn "nhập a; nhập a;" vẫn ổn
Yura

9

Tôi có một ví dụ ở đây mà đánh tôi!

foo.py

import bar

class gX(object):
    g = 10

bar.py

from foo import gX

o = gX()

chính

import foo
import bar

print "all done"

Tại dòng lệnh: $ 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

2
Làm thế nào bạn sửa cái này? Tôi đang cố gắng hiểu nhập vòng tròn để khắc phục sự cố của chính mình trông rất giống với những gì bạn đang làm ...
c089

12
Errm ... Tôi nghĩ rằng tôi đã khắc phục vấn đề của mình với bản hack cực kỳ xấu xí này. {{{nếu không phải là 'foo.bar' trong sys.modules: từ thanh nhập foo khác: bar = sys.modules ['foo.bar']}}} Cá nhân, tôi nghĩ rằng nhập khẩu tròn là dấu hiệu cảnh báo HUGE trên mã xấu thiết kế ...
c089

5
@ c089, hoặc bạn chỉ có thể di chuyển import bartrong foo.pyđến cuối
warvariuc

5
Nếu barfoocả hai phải sử dụng gX, giải pháp 'sạch nhất' là đưa gXvào một mô-đun khác và có cả hai foobarnhập mô-đun đó. (sạch nhất theo nghĩa là không có phụ thuộc ngữ nghĩa ẩn.)
Tim Wilder

2
Tim có một điểm tốt. Về cơ bản là bởi vì barthậm chí không thể tìm thấy gXtrong foo. nhập khẩu tròn là tốt, nhưng nó gXkhông được xác định khi nhập.
Sao Hỏa2049

9

Mô-đun a.py:

import b
print("This is from module a")

Mô-đun b.py

import a
print("This is from module b")

Chạy "Module a" sẽ xuất ra:

>>> 
'This is from module a'
'This is from module b'
'This is from module a'
>>> 

Nó xuất ra 3 dòng này trong khi nó được cho là xuất vô hạn vì nhập vòng tròn. Điều gì xảy ra từng dòng trong khi chạy "Mô-đun a" được liệt kê ở đây:

  1. Dòng đầu tiên là import b. vì vậy nó sẽ ghé thăm mô-đun b
  2. Dòng đầu tiên tại mô-đun b là import a. vì vậy nó sẽ ghé thăm mô-đun a
  3. Dòng đầu tiên tại mô-đun a là import bnhưng lưu ý rằng dòng này sẽ không được thực hiện nữa , bởi vì mọi tệp trong python thực thi một dòng nhập chỉ một lần, không quan trọng là nó được thực thi ở đâu hoặc khi nào. Vì vậy, nó sẽ vượt qua dòng tiếp theo và in "This is from module a".
  4. Sau khi kết thúc việc truy cập toàn bộ mô-đun a từ mô-đun b, chúng tôi vẫn ở mô-đun b. vì vậy dòng tiếp theo sẽ in"This is from module b"
  5. Dòng b mô-đun được thực hiện hoàn toàn. vì vậy chúng tôi sẽ quay trở lại mô-đun nơi chúng tôi bắt đầu mô-đun b.
  6. dòng nhập b đã được thực thi và sẽ không được thực hiện lại. dòng tiếp theo sẽ in "This is from module a"và chương trình sẽ kết thúc.

4

Tôi hoàn toàn đồng ý với câu trả lời của pythoneer ở đây. Nhưng tôi đã vấp phải một số mã bị lỗi khi nhập vòng tròn và gây ra sự cố khi cố gắng thêm các bài kiểm tra đơn vị. Vì vậy, để nhanh chóng vá nó mà không thay đổi mọi thứ bạn có thể giải quyết vấn đề bằng cách nhập động.

# 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")

Một lần nữa, đây không phải là bản sửa lỗi vĩnh viễn nhưng có thể giúp người nào đó muốn sửa lỗi nhập mà không thay đổi quá nhiều mã.

Chúc mừng!


3

Có rất nhiều câu trả lời tuyệt vời ở đây. Mặc dù thường có các giải pháp nhanh chóng cho vấn đề, một số trong số đó cảm thấy pythonic hơn các giải pháp khác, nếu bạn có thể thực hiện tái cấu trúc, một cách tiếp cận khác là phân tích tổ chức mã của bạn và cố gắng loại bỏ sự phụ thuộc vòng tròn. Bạn có thể tìm thấy, ví dụ, bạn có:

Tập tin 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)

Tập tin 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))

Trong trường hợp này, chỉ cần di chuyển một phương thức tĩnh sang một tệp riêng, giả sử c.py:

Tập tin c.py

def save_result(result):
    print('save the result')

sẽ cho phép xóa save_resultphương thức khỏi A và do đó cho phép xóa nhập A từ a trong b:

Tập tin tái cấu trúc 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)

Tập tin được cấu trúc lại

from c import save_result

class B:
    @staticmethod
    def do_something_b_ish(param):
        save_result(B.use_param_like_b_would(param))

Tóm lại, nếu bạn có một công cụ (ví dụ như pylint hoặc PyCharm) báo cáo về các phương thức có thể tĩnh, chỉ cần ném một vật staticmethodtrang trí lên chúng có thể không phải là cách tốt nhất để tắt tiếng cảnh báo. Mặc dù phương thức có vẻ liên quan đến lớp, nhưng tốt hơn là nên tách nó ra, đặc biệt nếu bạn có một số mô-đun liên quan chặt chẽ có thể cần cùng chức năng và bạn có ý định thực hành các nguyên tắc DRY.


2

Nhập khẩu tròn có thể gây nhầm lẫn vì nhập khẩu có hai điều:

  1. nó thực thi mã mô-đun nhập khẩu
  2. thêm mô-đun nhập vào mô-đun nhập biểu tượng toàn cầu

Cái trước chỉ được thực hiện một lần, trong khi cái sau ở mỗi câu lệnh nhập. Nhập khẩu tròn tạo ra tình huống khi nhập mô-đun sử dụng mô-đun nhập với mã được thực thi một phần. Do đó, nó sẽ không nhìn thấy các đối tượng được tạo sau câu lệnh nhập. Dưới đây mẫu mã chứng minh điều đó.

Nhập khẩu thông tư không phải là cái ác cuối cùng cần phải tránh bằng mọi giá. Trong một số khung như Flask, chúng khá tự nhiên và điều chỉnh mã của bạn để loại bỏ chúng không làm cho mã tốt hơn.

chính

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"

đầu ra chính của python với ý kiến

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

1

Tôi đã giải quyết vấn đề theo cách sau, và nó hoạt động tốt mà không có lỗi. Hãy xem xét hai tập tin a.pyb.py.

Tôi đã thêm nó vào a.pyvà nó hoạt động.

if __name__ == "__main__":
        main ()

a.py:

import b
y = 2
def main():
    print ("a out")
    print (b.x)

if __name__ == "__main__":
    main ()

b.py:

import a
print ("b out")
x = 3 + a.y

Đầu ra tôi nhận được là

>>> b out 
>>> a out 
>>> 5

0

Ok, tôi nghĩ rằng tôi có một giải pháp khá tuyệt vời. Giả sử bạn có tập tin avà tập tin b. Bạn có một defhoặc một classtrong tập tin bmà bạn muốn sử dụng trong mô-đun a, nhưng bạn có cái gì khác, hoặc một def, classhoặc biến từ tập tin amà bạn cần trong định nghĩa hoặc lớp học của bạn trong tập tin b. Những gì bạn có thể làm là, ở dưới cùng của tệp a, sau khi gọi hàm hoặc lớp trong tệp acần trong tệp b, nhưng trước khi gọi hàm hoặc lớp từ tệp bmà bạn cần cho tệp a, hãy nói import b Sau đó, và đây là phần chính , trong tất cả các định nghĩa hoặc các lớp trong tệp bcần defhoặcclass từ tệpa(hãy gọi nó CLASS), bạn nóifrom a import CLASS

Điều này hoạt động vì bạn có thể nhập tệp bmà không cần Python thực hiện bất kỳ câu lệnh nhập nào trong tệp bvà do đó bạn bỏ qua mọi nhập khẩu tròn.

Ví dụ:

Tập tin:

class A(object):

     def __init__(self, name):

         self.name = name

CLASS = A("me")

import b

go = B(6)

go.dostuff

Tệp b:

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 CLASSkhông thực sự bỏ qua việc thực thi tất cả các mã trong a.py. Đây là những gì thực sự xảy ra: (1) Tất cả mã trong a.py được chạy dưới dạng một mô-đun đặc biệt "__main__". (2) Tại import b, mã cấp cao nhất trong b.py được chạy (xác định lớp B) và sau đó kiểm soát trả về "__main__". (3) "__main__" cuối cùng vượt qua sự kiểm soát go.dostuff(). (4) khi do do () đến import a, nó sẽ chạy lại tất cả mã trong a.py , lần này là mô-đun "a"; sau đó nó nhập đối tượng LỚP từ mô-đun mới "a". Vì vậy, trên thực tế, điều này sẽ hoạt động tốt như nhau nếu bạn sử dụng import abất cứ nơi nào trong b.py.
Matthias Fripp
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.