__subclasses__() и импорт

Есть два файла с цепочкой команд и словарем, который динамически собирает все неабстрактные команды.

логика.py

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

com.py

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__(), пуст.

Скажите пожалуйста, чего мне не хватает?

Где именно создается экземпляр Commands? Очевидно, это тот момент, когда comm.py еще не достиг точки определения своего подкласса.

jasonharper 24.05.2024 16:57

Этот код протестирован с помощью python -i logic.py. Итак, я полагаю, экземпляр Commands создается из пространства имен модуля logic (как __main__). Также обратите внимание, что я импортирую comm внутри Commands конструктора.

Gennadiy 24.05.2024 17:05

это может быть что-то особенное для ABC... если вы сделаете Command регулярным занятием, это сработает?

Anentropic 24.05.2024 17:17

@Анэнтропик, нет. Я также пытался импортировать comm между определением классов Command и Commands. Тоже не работает.

Gennadiy 24.05.2024 17:19
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
4
82
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я думаю, проблема в том, что, как ни странно, объект класса 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 референсов! Попробую покопаться в этой механике.

Gennadiy 25.05.2024 08:48
Ответ принят как подходящий

С ответом @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 класс.

логика.py

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__() = }')

com.py

from logic import Command

class TestCommand(Command):
    ...
$ python logic.py
Command.__subclasses__() = []
comm.Command.__subclasses__() = [<class 'comm.TestCommand'>]

Как видите, вторая ссылка — это именно та, которая вам нужна, чтобы получить все Command подклассы.


Кстати, есть и третий способ. Я использовал его, когда столкнулся с проблемой импорта. Идея состоит в том, что файл comm.py должен содержать только подклассы абстрактного базового класса Command. Пока вы сохраняете это, вы можете использовать инструменты модуля inspect.

логика.py

from inspect import getmembers, isclass

class Commands(dict):
    def __init__(self):
        import comm
        print(getmembers(comm, isclass))

com.py

import logic

class TestCommand(logic.Command):
    ...
$ python logic.py
[('TestCommand', <class 'comm.TestCommand'>)]

Другие вопросы по теме