Есть два файла с цепочкой команд и словарем, который динамически собирает все неабстрактные команды.
from abc import ABC, abstractmethod
class Command(ABC):
def __init__(self):
...
@abstractmethod
def run():
...
class Commands(dict):
def __init__(self):
import comm
breakpoint()
for cls in Command.__subclasses__():
names = self.split_camel_case(cls.__name__)
self[names] = cls
from logic import Command
class TestCommand(Command):
...
> logic.py(28) __init__()
-> for cls in Command.__subclasses__():
(Pdb) comm.TestCommand.__mro__
(<class 'comm.TestCommand'>, <class 'logic.Command'>, <class 'abc.ABC'>, <class 'object'>)
(Pdb) Command.__subclasses__()
[]
К моменту вызова Commands.__init__()
базовый класс Command
уже определен. Модуль comm
импортирован, а также определен подкласс TestCommand
. Но список, возвращаемый Command.__subclass__()
, пуст.
Скажите пожалуйста, чего мне не хватает?
Этот код протестирован с помощью python -i logic.py
. Итак, я полагаю, экземпляр Commands
создается из пространства имен модуля logic
(как __main__
). Также обратите внимание, что я импортирую comm
внутри Commands
конструктора.
это может быть что-то особенное для ABC
... если вы сделаете Command
регулярным занятием, это сработает?
@Анэнтропик, нет. Я также пытался импортировать comm
между определением классов Command
и Commands
. Тоже не работает.
Я думаю, проблема в том, что, как ни странно, объект класса Command
, который вы видите изнутри Commands.__init__
, не тот, от которого TestCommand
наследуется.
На каком-то уровне да, они одинаковы, но для системы импорта Python они разные.
Попробуйте этот код и посмотрите, что произойдет:
logic.py
from abc import ABC, abstractmethod
class Command(ABC):
def __init__(self):
...
@abstractmethod
def run():
...
class Commands(dict):
def __init__(self):
import comm
print(Command.__module__)
print(Command.__subclasses__())
if __name__ == "__main__":
Commands()
comm.py
from logic import Command
class TestCommand(Command):
...
if __name__ == "__main__":
print(Command.__module__)
print(Command.__subclasses__())
$ python comm.py
logic
[<class '__main__.TestCommand'>]
$ python logic.py
__main__
[]
Обратите внимание, что две ссылки на класс Command
имеют разное __module__
значение.
Commands.__init__
имеет ссылку на класс Command
, который был определен до того, как модуль, к которому он принадлежит, был полностью определен.
Мы можем это исправить, переместив логику в третий файл:
base.py
from abc import ABC, abstractmethod
class Command(ABC):
def __init__(self):
...
@abstractmethod
def run():
...
comm.py
from base import Command
class TestCommand(Command):
...
logic.py
from base import Command
import comm
class Commands(dict):
def __init__(self):
print(Command.__module__)
print(Command.__subclasses__())
if __name__ == "__main__":
Commands()
Теперь, когда мы его запустим, мы получим желаемый результат:
$ python logic.py
base
[<class 'comm.TestCommand'>]
...потому что logic.py
и comm.py
относятся к одному и тому же классу Command
, импортированному из одного и того же места.
Спасибо, что показали разницу Command
референсов! Попробую покопаться в этой механике.
С ответом @Anentropic стало ясно, какой вопрос задавать. И я обнаружил, что это уже обсуждалось здесь.
Короче говоря, модуль __main__
всегда создается по умолчанию и без системы импорта Python. Но если вы импортируете файл точки входа из другого модуля, интерпретатор второй раз обработает этот файл своей системой импорта. В результате второй объект модуля ссылается на тот же файл точки входа.
Это легко найти. Для моего кода из вопроса:
$ python -i logic.py
>>> from sys import modules
>>> import comm
>>>
>>> modules['__main__'].__loader__.path
'...\\tests\\prototypes\\logic.py'
>>>
>>> modules['logic'].__loader__.path
'...\\tests\\prototypes\\logic.py'
Кстати о проблеме со ссылкой на класс Command
. Первый способ — переместить этот класс в другой файл, как предложил @Anentropic.
Однако я хотел бы сохранить свой абстрактный базовый класс Command
вместе с другими классами логики в одном файле. К счастью, в моем случае есть второй простой способ сослаться на нужный Command
класс.
from abc import ABC, abstractmethod
class Command(ABC):
def __init__(self):
...
@abstractmethod
def run():
...
class Commands(dict):
def __init__(self):
import comm
print(f'{Command.__subclasses__() = }')
print(f'{comm.Command.__subclasses__() = }')
from logic import Command
class TestCommand(Command):
...
$ python logic.py
Command.__subclasses__() = []
comm.Command.__subclasses__() = [<class 'comm.TestCommand'>]
Как видите, вторая ссылка — это именно та, которая вам нужна, чтобы получить все Command
подклассы.
Кстати, есть и третий способ. Я использовал его, когда столкнулся с проблемой импорта. Идея состоит в том, что файл comm.py
должен содержать только подклассы абстрактного базового класса Command
. Пока вы сохраняете это, вы можете использовать инструменты модуля inspect
.
from inspect import getmembers, isclass
class Commands(dict):
def __init__(self):
import comm
print(getmembers(comm, isclass))
import logic
class TestCommand(logic.Command):
...
$ python logic.py
[('TestCommand', <class 'comm.TestCommand'>)]
Где именно создается экземпляр
Commands
? Очевидно, это тот момент, когда comm.py еще не достиг точки определения своего подкласса.