Nhập vòng tròn Python?


98

Vì vậy, tôi nhận được lỗi này

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

và bạn có thể thấy rằng tôi sử dụng cùng một câu lệnh nhập và nó hoạt động? Có một số quy tắc bất thành văn về nhập khẩu vòng tròn? Làm cách nào để sử dụng cùng một lớp sâu hơn trong ngăn xếp cuộc gọi?

Câu trả lời:


161

Tôi nghĩ câu trả lời của jpmc26, mặc dù không có nghĩa là sai , nhưng lại đề cập quá nhiều đến nhập khẩu theo vòng tròn. Chúng có thể hoạt động tốt nếu bạn thiết lập chúng đúng cách.

Cách dễ nhất để làm như vậy là sử dụng import my_modulecú pháp, thay vì sử dụng from my_module import some_object. Trước đây hầu như sẽ luôn hoạt động, ngay cả khi my_modulebao gồm nhập khẩu trở lại của chúng tôi. Cái sau chỉ hoạt động nếu my_objectđã được xác định trong my_module, điều này có thể không đúng trong trường hợp nhập vòng.

Để cụ thể cho trường hợp của bạn: Hãy thử thay đổi entities/post.pyđể làm import physicsvà sau đó tham khảo physics.PostBodythay vì chỉ PostBodytrực tiếp. Tương tự, thay đổi physics.pyđể làm import entities.postvà sau đó sử dụng entities.post.Postthay vì chỉ Post.


5
Câu trả lời này có tương thích với nhập khẩu tương đối không?
Joe

17
Lý do tại sao điều này xảy ra?
Juan Pablo Santos

4
Thật sai lầm khi nói rằng phi fromcú pháp sẽ luôn hoạt động. Nếu tôi có class A(object): pass; class C(b.B): passtrong mô-đun a và class B(a.A): passtrong mô-đun b thì nhập vòng tròn vẫn là một vấn đề và điều này sẽ không hoạt động.
CrazyCasta,

1
Bạn nói đúng, bất kỳ phụ thuộc vòng tròn nào trong mã cấp cao nhất của mô-đun (chẳng hạn như các lớp cơ sở của khai báo lớp trong ví dụ của bạn) sẽ là một vấn đề. Đó là loại tình huống mà câu trả lời của jpmc rằng bạn nên cấu trúc lại tổ chức mô-đun có thể đúng 100%. Di chuyển lớp Bvào mô-đun ahoặc chuyển lớp Cvào mô-đun bđể bạn có thể phá vỡ chu kỳ. Cũng cần lưu ý rằng ngay cả khi chỉ một hướng của vòng tròn có liên quan đến mã cấp cao nhất (ví dụ: nếu lớp Ckhông tồn tại), bạn có thể gặp lỗi, tùy thuộc vào mô-đun nào được nhập trước bằng mã khác.
Blckknght

2
@TylerCrompton: Tôi không chắc ý bạn là "nhập mô-đun phải tuyệt đối". Nhập tương đối theo vòng tròn có thể hoạt động, miễn là bạn đang nhập mô-đun, không phải nội dung của chúng (ví dụ: from . import sibling_modulekhông from .sibling_module import SomeClass). Có một số điều tinh tế hơn khi __init__.pytệp của gói tham gia vào quá trình nhập vòng tròn, nhưng vấn đề này rất hiếm và có thể là lỗi trong quá trình importtriển khai. Hãy xem lỗi 23447 của Python mà tôi đã gửi bản vá (lỗi này đã bị mòn).
Blckknght

51

Khi bạn nhập một mô-đun (hoặc một thành viên của nó) lần đầu tiên, mã bên trong mô-đun được thực thi tuần tự như bất kỳ mã nào khác; ví dụ, nó không được đối xử khác biệt với cơ thể của một chức năng. An importchỉ là một lệnh giống như bất kỳ lệnh nào khác (phép gán, lệnh gọi hàm def, class). Giả sử quá trình nhập của bạn xảy ra ở đầu tập lệnh, thì đây là những gì đang xảy ra:

  • Khi bạn cố gắng nhập Worldtừ world, worldtập lệnh sẽ được thực thi.
  • Tập worldlệnh nhập Field, làm cho entities.fieldtập lệnh được thực thi.
  • Quá trình này tiếp tục cho đến khi bạn đến được entities.posttập lệnh vì bạn đã cố nhậpPost
  • Tập entities.postlệnh khiến physicsmô-đun được thực thi vì nó cố gắng nhậpPostBody
  • Cuối cùng, physicscố gắng nhập Posttừentities.post
  • Tôi không chắc liệu entities.postmô-đun có tồn tại trong bộ nhớ hay không, nhưng nó thực sự không quan trọng. Mô-đun không có trong bộ nhớ hoặc mô-đun chưa có Postthành viên vì nó chưa hoàn thành việc thực thi để xác địnhPost
  • Dù bằng cách nào, lỗi xảy ra vì Postkhông có để nhập

Vì vậy, không, nó không "hoạt động thêm trong ngăn xếp cuộc gọi". Đây là một dấu vết ngăn xếp về nơi xảy ra lỗi, có nghĩa là nó đã xảy ra lỗi khi cố gắng nhập Posttrong lớp đó. Bạn không nên sử dụng nhập khẩu vòng tròn. Tốt nhất, nó mang lại lợi ích không đáng kể (thường là không có lợi) và nó gây ra những vấn đề như thế này. Nó tạo gánh nặng cho bất kỳ nhà phát triển nào duy trì nó, buộc họ phải đi trên vỏ trứng để tránh làm vỡ nó. Cơ cấu lại tổ chức mô-đun của bạn.


1
Nên isinstance(userData, Post). Bất kể, bạn không có sự lựa chọn. Nhập theo vòng tròn sẽ không hoạt động. Thực tế là bạn có nhập khẩu vòng tròn là một mùi mã đối với tôi. Nó gợi ý rằng bạn có một số chức năng nên được chuyển sang mô-đun thứ ba. Tôi không thể nói gì nếu không nhìn vào cả hai lớp học.
jpmc26

3
@CpILL Sau một thời gian, một tùy chọn rất khó hiểu đã xảy ra với tôi. Nếu bạn không thể thực hiện việc này ngay bây giờ (do thời gian hạn chế hoặc bạn có gì), thì bạn có thể thực hiện nhập cục bộ bên trong phương thức bạn đang sử dụng. Một thân hàm bên trong defkhông được thực thi cho đến khi hàm được gọi, vì vậy quá trình nhập sẽ không xảy ra cho đến khi bạn thực sự gọi hàm. Đến lúc đó, các imports sẽ hoạt động vì một trong các mô-đun đã được nhập hoàn toàn trước khi gọi. Đó là một vụ hack hoàn toàn kinh tởm và nó không được lưu lại trong cơ sở mã của bạn trong bất kỳ khoảng thời gian đáng kể nào.
jpmc26

15
Tôi nghĩ rằng câu trả lời của bạn quá khó đối với nhập khẩu vòng tròn. Nhập khẩu thông thường thường hoạt động nếu bạn chỉ làm import foothay vì làm from foo import Bar. Đó là bởi vì hầu hết các mô-đun chỉ định nghĩa những thứ (như hàm và lớp) chạy sau. Các mô-đun thực hiện những việc quan trọng khi bạn nhập chúng (như tập lệnh không được bảo vệ bởi if __name__ == "__main__") có thể vẫn gặp sự cố, nhưng điều đó không quá phổ biến.
Blckknght

6
@Blckknght Tôi nghĩ rằng bạn đang tự đặt ra cho mình việc dành thời gian cho những vấn đề kỳ lạ mà người khác sẽ phải điều tra và bối rối nếu bạn sử dụng nhập khẩu vòng tròn. Chúng buộc bạn phải dành thời gian cẩn thận để không đi qua chúng và trên hết là mùi mã mà thiết kế của bạn cần được tái cấu trúc. Tôi có thể đã sai về việc liệu chúng có khả thi về mặt kỹ thuật hay không, nhưng chúng là một lựa chọn thiết kế tồi tệ có thể gây ra vấn đề sớm hay muộn. Sự rõ ràng và đơn giản là những điểm tốt trong lập trình, và việc nhập vòng tròn vi phạm cả hai điều trong sách của tôi.
jpmc 26

6
Ngoài ra; bạn đã chia nhỏ chức năng của mình quá nhiều và đó là nguyên nhân của việc nhập vòng tròn. Nếu bạn có hai điều mà dựa vào nhau tất cả các thời gian ; tốt nhất là chỉ nên đặt chúng trong một tệp. Python không phải là Java; không có lý do gì để không nhóm chức năng / lớp vào một tệp duy nhất để ngăn logic nhập lạ. :-)
Mark Ribau

40

Để hiểu các phụ thuộc vòng tròn, bạn cần nhớ rằng Python về bản chất là một ngôn ngữ kịch bản. Việc thực thi các câu lệnh bên ngoài các phương thức xảy ra tại thời điểm biên dịch. Các câu lệnh nhập được thực thi giống như các lệnh gọi phương thức, và để hiểu chúng, bạn nên nghĩ về chúng giống như các lệnh gọi phương thức.

Khi bạn nhập, điều gì sẽ xảy ra phụ thuộc vào việc tệp bạn đang nhập đã tồn tại trong bảng mô-đun hay chưa. Nếu có, Python sử dụng bất cứ thứ gì hiện có trong bảng ký hiệu. Nếu không, Python sẽ bắt đầu đọc tệp mô-đun, biên dịch / thực thi / nhập bất cứ thứ gì nó tìm thấy ở đó. Các ký hiệu được tham chiếu tại thời điểm biên dịch có được tìm thấy hay không, tùy thuộc vào việc chúng đã được nhìn thấy hay chưa được trình biên dịch nhìn thấy.

Hãy tưởng tượng bạn có hai tệp nguồn:

Tệp X.py

def X1:
    return "x1"

from Y import Y2

def X2:
    return "x2"

Tệp Y.py

def Y1:
    return "y1"

from X import X1

def Y2:
    return "y2"

Bây giờ, giả sử bạn biên dịch tệp X.py. Trình biên dịch bắt đầu bằng cách xác định phương thức X1, sau đó nhấn câu lệnh nhập trong X.py. Điều này khiến trình biên dịch tạm dừng biên dịch X.py và bắt đầu biên dịch Y.py. Ngay sau đó trình biên dịch truy cập vào câu lệnh nhập trong Y.py. Vì X.py đã có trong bảng mô-đun, Python sử dụng bảng ký hiệu X.py chưa hoàn chỉnh hiện có để đáp ứng mọi tham chiếu được yêu cầu. Bất kỳ ký hiệu nào xuất hiện trước câu lệnh nhập trong X.py hiện đều có trong bảng ký hiệu, nhưng bất kỳ ký hiệu nào sau đó thì không. Vì X1 bây giờ xuất hiện trước câu lệnh nhập nên nó đã được nhập thành công. Python sau đó tiếp tục biên dịch Y.py. Khi làm như vậy, nó xác định Y2 và kết thúc quá trình biên dịch Y.py. Sau đó, nó tiếp tục biên dịch X.py và tìm thấy Y2 trong bảng ký hiệu Y.py. Quá trình biên dịch cuối cùng hoàn thành lỗi w / o.

Một điều gì đó rất khác sẽ xảy ra nếu bạn cố gắng biên dịch Y.py từ dòng lệnh. Trong khi biên dịch Y.py, trình biên dịch truy cập câu lệnh nhập trước khi nó định nghĩa Y2. Sau đó, nó bắt đầu biên dịch X.py. Ngay sau đó nó chạm vào câu lệnh nhập trong X.py yêu cầu Y2. Nhưng Y2 là không xác định, vì vậy biên dịch không thành công.

Xin lưu ý rằng nếu bạn sửa đổi X.py để nhập Y1, quá trình biên dịch sẽ luôn thành công, bất kể bạn biên dịch tệp nào. Tuy nhiên, nếu bạn sửa đổi tệp Y.py để nhập ký hiệu X2, thì cả tệp sẽ không biên dịch.

Bất kỳ lúc nào khi mô-đun X hoặc bất kỳ mô-đun nào được nhập bởi X có thể nhập mô-đun hiện tại, KHÔNG sử dụng:

from X import Y

Bất kỳ lúc nào bạn nghĩ rằng có thể có một lần nhập vòng tròn, bạn cũng nên tránh biên dịch tham chiếu thời gian đến các biến trong các mô-đun khác. Hãy xem xét mã trông có vẻ ngây thơ:

import X
z = X.Y

Giả sử mô-đun X nhập mô-đun này trước khi mô-đun này nhập X. Ngoài ra, giả sử Y được định nghĩa trong X sau câu lệnh nhập. Sau đó, Y sẽ không được xác định khi mô-đun này được nhập và bạn sẽ gặp lỗi biên dịch. Nếu mô-đun này nhập Y trước, bạn có thể bỏ qua nó. Nhưng khi một trong những đồng nghiệp của bạn thay đổi thứ tự các định nghĩa trong mô-đun thứ ba một cách vô tội vạ, thì mã sẽ bị hỏng.

Trong một số trường hợp, bạn có thể giải quyết sự phụ thuộc vòng tròn bằng cách di chuyển câu lệnh nhập xuống bên dưới các định nghĩa ký hiệu mà các mô-đun khác cần. Trong các ví dụ trên, các định nghĩa trước câu lệnh nhập không bao giờ bị lỗi. Các định nghĩa sau câu lệnh nhập đôi khi không thành công, tùy thuộc vào thứ tự biên dịch. Bạn thậm chí có thể đặt các câu lệnh nhập ở cuối tệp, miễn là không cần ký hiệu đã nhập nào trong thời gian biên dịch.

Lưu ý rằng việc di chuyển các câu lệnh nhập xuống trong một mô-đun sẽ che khuất những gì bạn đang làm. Hãy bù đắp cho điều này bằng một nhận xét ở đầu mô-đun của bạn như sau:

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

Nói chung đây là một tập tục xấu, nhưng đôi khi rất khó tránh.


2
Tôi không nghĩ rằng có trình biên dịch hoặc thời gian biên dịch trong python ở tất cả
pkqxdd

6
Python không có một trình biên dịch, và được biên soạn @pkqxdd, biên soạn chỉ thường được ẩn đi từ người sử dụng. Điều này có thể hơi khó hiểu, nhưng sẽ rất khó để tác giả đưa ra mô tả rõ ràng đáng ngưỡng mộ này về những gì đang xảy ra nếu không có một số tham chiếu đến "thời gian biên dịch" của Python, hơi bị che khuất.
Hank


Tôi đã tiếp tục thử điều này trên máy của mình và nhận được một kết quả khác. Ran X.py nhưng bị lỗi "không thể nhập tên 'Y2' từ 'Y'". Ran Y.py không vấn đề gì. Tôi đang sử dụng Python 3.7.5, bạn có thể giúp giải thích vấn đề ở đây là gì không?
xuefeng huang

18

Đối với những bạn, những người, như tôi, đến vấn đề này từ Django, bạn nên biết rằng tài liệu cung cấp giải pháp: https://docs.djangoproject.com/en/1.10/ref/models/fields/#foreignkey

"... Để tham chiếu đến các mô hình được xác định trong một ứng dụng khác, bạn có thể chỉ định rõ ràng một mô hình có nhãn ứng dụng đầy đủ. Ví dụ: nếu mô hình Nhà sản xuất ở trên được xác định trong một ứng dụng khác có tên là production, bạn cần sử dụng:

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

Loại tham chiếu này có thể hữu ích khi giải quyết các phụ thuộc nhập vòng giữa hai ứng dụng. ... "


6
Tôi biết tôi không nên dùng bình luận để nói "cảm ơn", nhưng điều này đã khiến tôi khó chịu trong vài giờ. Cảm ơn bạn, cảm ơn bạn, cảm ơn bạn!!!
MikeyE

Tôi đồng ý với @MikeyE. Tôi đã đọc một số blog và Stackoverflows đang cố gắng khắc phục điều này bằng PonyORM. Khi những người khác nói đó là phương pháp không tốt, hoặc tại sao bạn lại viết mã các lớp của mình thành vòng tròn, thì ORM chính xác là nơi điều này xảy ra. Bởi vì rất nhiều ví dụ đặt tất cả các mô hình trong cùng một tệp và chúng tôi làm theo các ví dụ đó, ngoại trừ chúng tôi sử dụng một mô hình cho mỗi tệp, vấn đề không rõ ràng khi Python không biên dịch được. Tuy nhiên, câu trả lời rất đơn giản. Như Mike đã chỉ ra, cảm ơn bạn rất nhiều.
thùng rác 80

3

Tôi đã có thể nhập mô-đun trong (chỉ) hàm yêu cầu các đối tượng từ mô-đun này:

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

làm thế nào tao nhã của trăn
Yaro

2

Nếu bạn gặp phải vấn đề này trong một ứng dụng khá phức tạp, việc cấu trúc lại tất cả các lần nhập của bạn có thể rất phức tạp. PyCharm cung cấp một tiền tố nhanh cho việc này sẽ tự động thay đổi tất cả cách sử dụng các ký hiệu đã nhập.

nhập mô tả hình ảnh ở đây


0

Tôi đã sử dụng như sau:

from module import Foo

foo_instance = Foo()

nhưng để loại bỏ circular referencetôi đã làm như sau và nó hoạt động:

import module.foo

foo_instance = foo.Foo()
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.