Автоматическое создание экземпляра класса после импорта в Python


Я хочу автоматически создавать экземпляры некоторых классов в Python сразу после импорта их модулей,поэтому нет необходимости направлять вызов экземпляра. Мои настройки можно урезать до следующего кода:

class MetaTest(type):

    _instances = {}

    def __init__(cls, name, bases, attrs):
        super(MetaTest, cls).__init__(name, bases, attrs)
        MetaTest._instances[name] = cls()


class Foo(object):

    __metaclass__ = MetaTest

    def __init__(self):
        super(Foo, self).__init__()

class Bar(Foo):

    def __init__(self):
        super(Bar, self).__init__()

Но создание экземпляра завершается неудачей на super с:

    super(Foo, self).__init__()
NameError: global name 'Foo' is not defined

Случайно, не может ли быть способ, как это сделать?

3 2

3 ответа:

Решение 1: Изменить sys.modules

Это версия Python 2. Добавление класса в его модуль работает, потому что он решает проблему, что класс еще не выходит, когда вы используете super():

import sys

class MetaTest(type):

    _instances = {}

    def __init__(cls, name, bases, attrs):
        setattr(sys.modules[cls.__module__], cls.__name__, cls)
        super(MetaTest, cls).__init__(name, bases, attrs)
        MetaTest._instances[name] = cls()


class Foo(object):

    __metaclass__ = MetaTest

    def __init__(self):
        print('init Foo')
        super(Foo, self).__init__()

class Bar(Foo):

    def __init__(self):
        print('init Bar')
        super(Bar, self).__init__()

print(MetaTest._instances)

Вывод:

init Foo
init Bar
init Foo
{'Foo': <__main__.Foo object at 0x10cef90d0>, 'Bar': <__main__.Bar object at 0x10cef9110>}

Решение 2: используйте библиотеку future

Библиотекаfuture предлагает as super() для Python 2, которая работает так же, как и для Python 3, т. е. без аргументов. Это помогает:

from builtins import super

class MetaTest(type):

    _instances = {}

    def __init__(cls, name, bases, attrs):
        super(MetaTest, cls).__init__(name, bases, attrs)
        MetaTest._instances[name] = cls()


class Foo(object):

    __metaclass__ = MetaTest

    def __init__(self):
        print('init Foo')
        super().__init__()

class Bar(Foo):

    def __init__(self):
        print('init Bar')
        super().__init__()

print(MetaTest._instances)

Вывод:

init Foo
init Bar
init Foo
{'Foo': <__main__.Foo object at 0x1066b39d0>, 'Bar': <__main__.Bar object at 0x1066b3b50>}

Первое Примечание:

Для такого рода кода проще использовать Python 3. Подробнее об этом на первом решении и в конце ответа.

Таким образом, имя самого класса Foo, очевидно, не будет доступно внутри тела class Foo: это имя связывается только после того, как экземпляр класса полностью выполнен.

В Python 3 Существует беспараметрическая версия вызова super(), которая может направить его в суперкласс без необходимости явного ссылка на сам класс. Это одно из немногих исключений, которые язык делает для своих очень предсказуемых механизмов. Он связывает данные с вызовом super во время компиляции. Однако даже в Python 3 невозможно вызвать метод, который использует super, пока класс все еще создается - то есть до возвращения из метакласса __new__ и __init__ методов. После этого данные, необходимые параметру без параметров super, автоматически связываются.

Однако в Python 3.6+ есть механизм это даже исключает метакласс: метод класса с именем __init_subclass__ будет вызван на суперклассах после создания класса, и он может нормально использовать (без параметров) super. Это будет работать с Python 3.6:

class Base:

   _instances = {}

   def __init_subclass__(cls):
        __class__._instances[cls.__name__] = cls()

   ...

class Foo(Base):

    def __init__(self):
        super().__init__()

Здесь __class__ (не obj.__class__) - это другое магическое имя, доступное наряду с параметрическим super, которое всегда ссылается на класс, в котором оно используется, а не на подкласс.

Теперь пара соображений о других методах: нет кода внутри метакласс сам по себе может работать для создания экземпляра класса, и методы класса, нуждающиеся в ссылке на класс в его имени, не помогают. Декоратор класса также не будет работать, потому что имя связывается только после возврата от декоратора. Если вы кодируете свою систему в рамках фреймворка, который имеет цикл событий, вы можете на метаклассе __init__ запланировать событие для создания экземпляра класса - это будет работать, но вам нужно кодировать в таком контексте (gobject, Qt, django, sqlalchemy, zope / pyramid, Python asyncIO/tulip-примеры фреймворков, которые имеют систему событий, которая могла бы сделать необходимые обратные вызовы)

Теперь, если вам действительно это нужно в Python 2, я думаю, что самый простой способ-это создать функцию, вызываемую в нижнем колонтитуле каждого из ваших модулей, которая "регистрировала" недавно созданные классы. Что-то вроде:

class MetaTest(type):

    _instances = {}

def register():
    import sys
    module_namespace = sys._getframe(1).f_globals

    for name, obj in f_globals.items():
        if isinstance(obj, MetaTest):
            MetaTest._instances[obj.__name__] = obj()

Еще раз: вызов этой функции "register" должен происходить в последней строке каждого из ваших модулей.

В "мета" вещь здесь-это интроспекция фрейма самой вызывающей функции, чтобы автоматически получить ее глобальные переменные. Вы можете избавиться от него, сделав globals() необходимым явным параметром для функции register и сохранив код более легким для понимания третьими сторонами.

Почему Python 3

На сегодняшний день Python 2 находится в 2 годах от конца строки. И за последние 8 лет в языке произошло много улучшений, включая функции в классе и метаклассе модели. Я думаю, что многие люди, похоже, придерживаются python 2, потому что, поскольку "python" связан с Python 2 в их установках Linux, они думают, что это по умолчанию. Это не так: он просто существует для обратной совместимости-все больше и больше Linux install используют python3 в качестве Python по умолчанию, обычно устанавливаемого вместе с Python 2. А для ваших проектов лучше использовать изолированную среду, созданную с помощью virtuaelnv.

Что сказал jsbueno. Я пытался сделать эту работу в Python 2, но без особого успеха. Вот самое близкое, что у меня есть:

class MetaTest(type):
    _instances = {}

    def __init__(cls, name, bases, attrs):
        super(MetaTest, cls).__init__(name, bases, attrs)
        MetaTest._instances[name] = cls()

class Foo(object):
    __metaclass__ = MetaTest

class Bar(Foo):
    pass

for name, obj in Bar._instances.items():
    print name, obj, type(obj)

Вывод

Foo <__main__.Foo object at 0xb74626ec> <class '__main__.Foo'>
Bar <__main__.Bar object at 0xb746272c> <class '__main__.Bar'>

Однако, если вы попытаетесь дать либо Foo, либо Bar __init__ методы, то все это разваливается на куски, по причинам, приведенным jsbueno.

Вот версия Python 3.6, использующая арглесс-форму super, которая более успешна:

class MetaTest(type):
    _instances = {}

    def __init__(cls, name, bases, attrs):
        print('cls: {}\nname: {}\nbases: {}\nattrs: {}\n'.format(cls, name, bases, attrs))
        super().__init__(name, bases, attrs)
        MetaTest._instances[name] = cls()

class Foo(metaclass=MetaTest):
    def __init__(self):
        super().__init__()

class Bar(Foo):
    def __init__(self):
        super().__init__()

for name, obj in Bar._instances.items():
    print(name, obj, type(obj))

Вывод

cls: <class '__main__.Foo'>
name: Foo
bases: ()
attrs: {'__module__': '__main__', '__qualname__': 'Foo', '__init__': <function Foo.__init__ at 0xb7192dac>, '__classcell__': <cell at 0xb718c2b4: MetaTest object at 0xb718d3cc>}

cls: <class '__main__.Bar'>
name: Bar
bases: (<class '__main__.Foo'>,)
attrs: {'__module__': '__main__', '__qualname__': 'Bar', '__init__': <function Bar.__init__ at 0xb7192d64>, '__classcell__': <cell at 0xb718c2fc: MetaTest object at 0xb718d59c>}

Foo <__main__.Foo object at 0xb712514c> <class '__main__.Foo'>
Bar <__main__.Bar object at 0xb71251ac> <class '__main__.Bar'>

Должен признаться, что я не очень удобно со всей этой концепцией классов, создающих экземпляр себя автоматически: "явное лучше, чем неявное". Я думаю, что предложение jsbueno использовать функцию регистрации-хороший компромисс.