Круговой импорт Python: как справиться с ним?
Существует приложение, использующее PyQt5, которое состоит из главного скрипта AsQamm.py и собственной библиотеки _asQamm, в которой находятся все функции, необходимые для его работы.
Функции находятся в классах, унаследованных от класса MainWindow, внутри соответствующих модулей. Соответственно, каждый модуль для своей работы нуждается в импорте класса MainWindow, который находится в AsQamm.py.
В то же время, в конструкторе класса MainWindow есть методы, для вызова которых необходимы модули из _asQamm. В итоге - получается порочный круг кругового импорта, при попытке с ним справиться вылетают самые разные ошибки.
Дисклеймер
Я знаю, что по-хорошему, нужно делать МВП, однако я не понимаю, как мне изложить суть проблемы, не рассказав всю суть.
Структура приложения
Текущая структура приложения и комментарий представлена ниже. Вы также можете спустится ниже к списку ссылок на файлы и смотреть их код через pastebin:
_asQamm #Библиотека со всеми модулями, которые необходимы для осуществления функций приложения.
|________
|__init__.py #На данный момент здесь отсутствуют какие-либо команды. Я пробовал перенести сюда
| (пустой) #команды импорта из AsQamm.py, но это породило другую проблему — классы из
| #модулей, находящихся в библиотеке (см. далее), не могли
| #инициализироваться, так как имя MainWindow при
| #'определении классов не определяется (ImportError:
| # name ‘MainWindow’ is not defined').
|________
|asQammUI.py #Второй по важности модуль приложения. Его код является результатом работы
| (ссылка ниже) #pyuic5, здесь находится интерфейс. От класса Ui_MainWindow здесь наследуется
| #главный класс приложения MainWindow.
|________
|asQammFunctions.py #Внутри находится класс UIFunctions, предназначенный для осуществления действий с
| (ссылка ниже) #интерфейсом приложения без создания экземпляра, по этой причине класс не имеет
| #конструктора. Все методы этого класса работают напрямую с родительским классом
| #MainWindow. Фактически, в модуле functions я планировал размещать классы с
| #методами, для осуществления которых создавать экземпляр класса не нужно.
|________
|asQammResources.py #RCC, и так понятно, зачем он нужен. Не содержит никакого дополнительно
| (ссылка ниже) #написанного мной кода, подгружается в начале AsQamm.py. Прикрепил в неконверт.
| #виде, конвертируйте командой
| # `pyrcc5 resources.qrc -o _asQamm\asQammResources.py`.
|________
|asQammUsers.py #Модуль, после тщетной попытки создания класса в котором возникла проблема.
| (ссылка ниже) #Планировался как код модуля пользователей программы, в котором должен был быть
| #центральный класс AQUserSystem (род. MainWindow) (с экземпляром в основном коде)
| #и другие классы, наследуемые от него.
| #Однако я обнаружил, что я не могу создать экземпляр этого класса (ошибка
| #ImportError: name 'MainWindow is not defined' при инициализации модуля
| #asQammUsers). Если я изменю в коде последнего `import AsQamm` на `from AsQamm
| #import MainWindow`, интерпретатор скажет, что круговой импорт.
| # ~~Да, я в ООП у Python что-то всегда не понимал. Надо ещё подучить это~~
|
AsQamm.py # Ядро приложения. Конструктор класса MainWindow создаёт экземпляр класса Ui_MainWindow,
(ссылка ниже) # затем должен был создавать экземпляр AQUserSystem, затем - выполнять несколько методов
# по первоначальной настройке интерфейса (привязка кнопок и инициализатор анимации меню
# (UIFunctions.toggleSimpleWidgetInteraction и UIFunctions.createLabels)).
Ссылки на файлы
_asQamm\asQammUI.py - тык. Осторожно! 1249 строк!
_asQamm\asQammFunctions.py - тык
_asQamm\asQammUsers.py - тык
AsQamm.py - тык
resources.qrc - тык
Также для запуска приложения потребуeтся папка images: скачать архив. разархивируйте в корень программы.
Стактрейсы
В текущем состоянии
File "G:\Work\Code\Hyrex AsQamm\AsQamm.py", line 9, in <module>
import _asQamm.asQammUsers as aqUsers
File "G:\Work\Code\Hyrex AsQamm\_asQamm\asQammUsers.py", line 1, in <module>
import AsQamm
File "G:\Work\Code\Hyrex AsQamm\AsQamm.py", line 10, in <module>
import _asQamm.asQammFunctions as aqFunctions
File "G:\Work\Code\Hyrex AsQamm\_asQamm\asQammFunctions.py", line 5, in <module>
class UIFunctions(MainWindow):
NameError: name 'MainWindow' is not defined.
Если строку 1 в модуле asQammUsers заменить на from AsQamm import MainWindow
File "G:\Work\Code\Hyrex AsQamm\AsQamm.py", line 9, in <module>
from _asQamm.asQammUsers import *
File "G:\Work\Code\Hyrex AsQamm\_asQamm\asQammUsers.py", line 1, in <module>
import AsQamm
File "G:\Work\Code\Hyrex AsQamm\AsQamm.py", line 10, in <module>
from _asQamm.asQammFunctions import *
File "G:\Work\Code\Hyrex AsQamm\_asQamm\asQammFunctions.py", line 1, in <module>
from AsQamm import MainWindow
ImportError: cannot import name 'MainWindow' from partially initialized module 'AsQamm' (most likely due to a circular import) (G:\Work\Code\Hyrex AsQamm\AsQamm.py)
Ответы (2 шт):
Перестройте компонент
AsQamm.py. Сейчас все методы, которые осуществляются сMainWindow, вызываются в конструкторе этого класса. Для выполнения этих методов необходимы модули, отсылающие при инициализации обратно к классуMainWindow. таким, образом, образовалась парадоксальная ситуация: для того, чтобы инициализировать классMainWindow, необходима функциональность из внешних модулей, однако для инициализации этих модулей необходимо инициализировать родительский класс.Наследование класса
UIFunctionsдействительно не имеет смысла. Переопределите атрибуты методов сselfнаroot.
Решение:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from _asQamm.asQammUI import *
from _asQamm.asQammResources import *
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
from _asQamm.asQammUsers import *
from _asQamm.asQammFunctions import *
if __name__ == "__main__":
app = QApplication(sys.argv)
root = MainWindow()
UIFunctions.createLabelsAtMainMenu(root)
# Добавляем привязки клавиш к анимациям
root.ui.btn_Toggle.clicked.connect(lambda: UIFunctions.toggleSimpleWidgetInteraction(root, 190, 1))
root.ui.btn_DefnToggleCamList.toggled.connect(lambda: UIFunctions.toggleSimpleWidgetInteraction(root, 320, 2))
# Cкин Дом
root.ui.btn_page1.clicked.connect( lambda: UIFunctions.selectSkin(1) )
# Скин Защита
root.ui.btn_page2.clicked.connect( lambda: UIFunctions.selectSkin(2) )
# Скин Растения
root.ui.btn_page3.clicked.connect( lambda: UIFunctions.selectSkin(3) )
# Скин Управление
root.ui.btn_page4.clicked.connect( lambda: UIFunctions.selectSkin(4) )
# Скин Конфигурации
root.ui.btn_page5.clicked.connect( lambda: UIFunctions.selectSkin(5) )
root.show()
sys.exit(app.exec_())
Я отметил строки которые поменял
AsQamm.py
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import *
from _asQamm.asQammUI import *
#from _asQamm.asQammResources import *
# добавил в asQammUI.py -> import resources_rc # !!!
import _asQamm.asQammUsers as aqUsers
#import _asQamm.asQammFunctions as aqFunctions
from _asQamm.asQammFunctions import UIFunctions # !!!
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
###
self.uiFunctions = UIFunctions(self) # +++
# aqFunctions.UIFunctions.createLabelsAtMainMenu(self)
self.uiFunctions.createLabelsAtMainMenu() # ? - self
# Добавляем привязки клавиш к анимациям vvvvvvvvvvvvvvvv
self.ui.btn_Toggle.clicked.connect(lambda: self.uiFunctions.toggleSimpleWidgetInteraction(190, 1)) # - self,
# vvvvvvvvvvvvvvvv
self.ui.btn_DefnToggleCamList.toggled.connect(lambda: self.uiFunctions.toggleSimpleWidgetInteraction(320, 2)) # - self,
# Cкин Дом selectSkin () отсутствует 1 обязательный позиционный аргумент: 'self
# self.ui.btn_page1.clicked.connect( lambda: aqFunctions.UIFunctions.selectSkin(1) )
self.ui.btn_page1.clicked.connect( lambda: self.uiFunctions.selectSkin(1) )
# Скин Защита
# self.ui.btn_page2.clicked.connect( lambda: aqFunctions.UIFunctions.selectSkin(2) )
self.ui.btn_page2.clicked.connect( lambda: self.uiFunctions.selectSkin(2) )
# Скин Растения
# self.ui.btn_page3.clicked.connect( lambda: aqFunctions.UIFunctions.selectSkin(3) )
self.ui.btn_page3.clicked.connect( lambda: self.uiFunctions.selectSkin(3) )
# Скин Управление vvvvvvvvvvvvvvvv
self.ui.btn_page4.clicked.connect( lambda: self.uiFunctions.selectSkin(4) ) # !
# Скин Конфигурации vvvvvvvvvvvvvvvv
self.ui.btn_page5.clicked.connect( lambda: self.uiFunctions.selectSkin(5) ) # !
if __name__ == "__main__":
app = QApplication(sys.argv)
root = MainWindow()
root.show()
sys.exit(app.exec_())
asQammFunctions.py
from PyQt5 import QtCore, QtGui, QtWidgets # +++
from PyQt5.Qt import * # +++ QPropertyAnimation
#from AsQamm import MainWindow # ---
import AsQamm # +++
import json
#class UIFunctions(MainWindow):
class UIFunctions(QtWidgets.QMainWindow):
def __init__(self, parent=None): # +++
super(UIFunctions, self).__init__(parent) # +++
self.parent = parent # +++
def createLabelsAtMainMenu(self):
# self.parent.ui. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
self.parent.ui.hintsSizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
self.parent.ui.styleSheet = str('''QLabel { font: 12pt "Segoe UI Semibold"; color: white; }
QLabel:hover { color: rgb(47, 105, 23); }''')
# self.parent.ui.
self.parent.ui.lbl_MenuHint1 = QtWidgets.QLabel(parent = self.parent.ui.frame_top_menus)
self.parent.ui.lbl_MenuHint1.setText('Наш дом')
self.parent.ui.lbl_MenuHint1.setStyleSheet(self.parent.ui.styleSheet)
self.parent.ui.lbl_MenuHint1.setSizePolicy(self.parent.ui.hintsSizePolicy)
self.parent.ui.cel_TopMenuBtn1.addWidget(self.parent.ui.lbl_MenuHint1)
self.parent.ui.lbl_MenuHint1.hide()
self.parent.ui.lbl_MenuHint2 = QtWidgets.QLabel(parent = self.parent.ui.frame_top_menus)
self.parent.ui.lbl_MenuHint2.setText('Защита')
self.parent.ui.lbl_MenuHint2.setStyleSheet(self.parent.ui.styleSheet)
self.parent.ui.lbl_MenuHint2.setSizePolicy(self.parent.ui.hintsSizePolicy)
self.parent.ui.cel_TopMenuBtn2.addWidget(self.parent.ui.lbl_MenuHint2)
self.parent.ui.lbl_MenuHint2.hide()
self.parent.ui.lbl_MenuHint3 = QtWidgets.QLabel(parent = self.parent.ui.frame_top_menus)
self.parent.ui.lbl_MenuHint3.setText('Растения')
self.parent.ui.lbl_MenuHint3.setStyleSheet(self.parent.ui.styleSheet)
self.parent.ui.lbl_MenuHint3.setSizePolicy(self.parent.ui.hintsSizePolicy)
self.parent.ui.cel_TopMenuBtn3.addWidget(self.parent.ui.lbl_MenuHint3)
self.parent.ui.lbl_MenuHint3.hide()
self.parent.ui.lbl_MenuHint4 = QtWidgets.QLabel(parent = self.parent.ui.frame_top_menus)
self.parent.ui.lbl_MenuHint4.setText('Оборудование')
self.parent.ui.lbl_MenuHint4.setStyleSheet(self.parent.ui.styleSheet)
self.parent.ui.lbl_MenuHint4.setSizePolicy(self.parent.ui.hintsSizePolicy)
self.parent.ui.cel_TopMenuBtn4.addWidget(self.parent.ui.lbl_MenuHint4)
self.parent.ui.lbl_MenuHint4.hide()
self.parent.ui.lbl_MenuHint5 = QtWidgets.QLabel(parent = self.parent.ui.frame_top_menus)
self.parent.ui.lbl_MenuHint5.setText('Конфигурация')
self.parent.ui.lbl_MenuHint5.setStyleSheet(self.parent.ui.styleSheet)
self.parent.ui.lbl_MenuHint5.setSizePolicy(self.parent.ui.hintsSizePolicy)
self.parent.ui.cel_TopMenuBtn5.addWidget(self.parent.ui.lbl_MenuHint5)
self.parent.ui.lbl_MenuHint5.hide()
# Функция toggleSimpleWidgetInteraction может быть использована для анимирования виджетов простой
# линейной анимацией. Ей необходимо иметь следующие аргументы: желаемый максимальный размер
# виджета (ширина/высота в зависимости от типа анимации) и ID виджета, с которым она будет
# работать.
def toggleSimpleWidgetInteraction(self, maxLength, widgetId):
maxExtend = int()
if widgetId == 1: # проверяем аргумент на 1 вариант
# получаем ширину фрейма
width = self.parent.ui.frame_left_menu.width()
maxExtend = maxLength
standard = 60
# установим максимальное значение ширины фрейма
if width == 60:
lengthExtended = maxExtend
self.parent.ui.lbl_MenuHint1.show()
self.parent.ui.lbl_MenuHint2.show()
self.parent.ui.lbl_MenuHint3.show()
self.parent.ui.lbl_MenuHint4.show()
self.parent.ui.lbl_MenuHint5.show()
else:
lengthExtended = standard
self.parent.ui.lbl_MenuHint1.hide()
self.parent.ui.lbl_MenuHint2.hide()
self.parent.ui.lbl_MenuHint3.hide()
self.parent.ui.lbl_MenuHint4.hide()
self.parent.ui.lbl_MenuHint5.hide()
# выполним горизонтальную анимацию
self.animation = QPropertyAnimation(self.parent.ui.frame_left_menu, b"minimumWidth")
self.animation.setDuration(200)
self.animation.setStartValue(width)
self.animation.setEndValue(lengthExtended)
self.animation.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
self.animation.start()
self.animation2 = QPropertyAnimation(self.parent.ui.frame_top_menus, b"minimumWidth")
self.animation2.setDuration(200)
self.animation2.setStartValue(width)
self.animation2.setEndValue(lengthExtended)
self.animation2.setEasingCurve(QtCore.QEasingCurve.InOutQuart)
self.animation2.start()
elif widgetId == 2: # проверяем второй вариант
height = self.parent.ui.tbw_DefnCamList.height()
maxExtend == maxLength
standard = 43
# установим максимальное значение высоты фрейма и скроем/отобразим мешающие
# элементы
if height == 43:
self.parent.ui.lbl_DefnCurrSetCamId.hide()
self.parent.ui.lbl_ReadOnly10.hide()
self.parent.ui.gvf_DefnCamView.hide()
else:
self.parent.ui.lbl_DefnCurrSetCamId.show()
self.parent.ui.lbl_ReadOnly10.show()
self.parent.ui.gvf_DefnCamView.show()
# def selectSkin(id, self):
def selectSkin(self, id): ###################################
print(f'def selectSkin(self, id): {id}')
if id == 1:
self.parent.ui.stack.setCurrentWidget(self.parent.ui.page_1)
self.parent.ui.lbl_SkinName.setText('Наш дом')
elif id == 2:
self.parent.ui.stack.setCurrentWidget(self.parent.ui.page_2)
self.parent.ui.lbl_SkinName.setText('Защита')
elif id == 3:
self.parent.ui.stack.setCurrentWidget(self.parent.ui.page_3)
self.parent.ui.lbl_SkinName.setText('Растения')
elif id == 4:
self.parent.ui.stack.setCurrentWidget(self.parent.ui.page_4)
self.parent.ui.lbl_SkinName.setText('Управление')
elif id == 5:
self.parent.ui.stack.setCurrentWidget(self.parent.ui.page_5)
self.parent.ui.lbl_SkinName.setText('Конфигурация')
asQammUI.py
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
...
# +++
import resources_rc # +++
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MainWindow = QtWidgets.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())
