Що буде, якщо два модулі імпортують один одного?
Щоб узагальнити проблему, як щодо циклічного імпорту в Python?
Що буде, якщо два модулі імпортують один одного?
Щоб узагальнити проблему, як щодо циклічного імпорту в Python?
Відповіді:
Про це була справді хороша дискусія на 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 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'
. На початку мене зовсім збентежило, чому файл буде виконуватися двічі.
Оскільки інші відповіді описують, ця модель прийнятна в python:
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
ось спосіб вирішити круговий імпорт в python:
try:
from images.serializers import SimplifiedImageSerializer
except ImportError:
import sys
SimplifiedImageSerializer = sys.modules[__package__ + '.SimplifiedImageSerializer']
Це намагається імпортувати, SimplifiedImageSerializer
і якщо ImportError
його підняти, оскільки він вже імпортується, він витягне його з імпорту кеша.
PS: Ви повинні прочитати весь цей пост у голосі Девіда Бізлі.
Тут я отримав приклад, який мене вразив!
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
, "найчистішим" рішенням є розміщення gX
в іншому модулі, і те, і інше, foo
та bar
імпорт цього модуля. (найчистіший в тому сенсі, що немає прихованих семантичних залежностей.)
bar
навіть не можна знайти gX
в підніжжі. круговий імпорт сам по собі добре, але це просто gX
не визначено, коли він імпортується.
Модуль a.py:
import b
print("This is from module a")
Модуль b.py
import a
print("This is from module b")
Запуск "Модуля a" виведе:
>>>
'This is from module a'
'This is from module b'
'This is from module a'
>>>
Він виводить ці 3 рядки, тоді як передбачалося виводити нескінченність через круговий імпорт. Що відбувається за рядком під час запуску "Модуля a", перераховано тут:
import b
. тому він відвідає модуль bimport a
. тому він відвідає модуль aimport b
але зауважте, що цей рядок більше не буде виконуватись , оскільки кожен файл у python виконує імпортну рядок лише один раз, неважливо, де і коли він виконується. тому він перейде до наступного рядка та друкується "This is from module a"
."This is from module b"
"This is from module a"
і програма буде завершена.Я повністю згоден з відповіддю pythoneer тут. Але я натрапив на якийсь код, який був помилковим круговим імпортом і викликав проблеми при спробі додати одиничні тести. Тож швидко виправити його, не змінюючи все, що ви зможете вирішити проблему, виконавши динамічний імпорт.
# 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:
Повторно налаштований файл 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)
Відновлений файл 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"
Вихід python 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."
Вуаля.
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"; тоді він імпортує об’єкт CLASS з нового модуля "a". Так що насправді це буде однаково добре, якби ви використовували import a
будь-де в b.py.