Подтвердить что ты не робот

Как я могу отложить вызов __init__ до тех пор, пока не будет получен доступ к атрибуту?

У меня есть тестовая структура, которая требует, чтобы тестовые примеры определялись с использованием следующих шаблонов классов:

class TestBase:
    def __init__(self, params):
        self.name = str(self.__class__)
        print('initializing test: {} with params: {}'.format(self.name, params))

class TestCase1(TestBase):
    def run(self):
        print('running test: ' + self.name)

Когда я создаю и запускаю тест, я получаю следующее:

>>> test1 = TestCase1('test 1 params')
initializing test: <class '__main__.TestCase1'> with params: test 1 params
>>> test1.run()
running test: <class '__main__.TestCase1'>

Рамка тестов ищет и загружает все классы TestCase, которые она может найти, создает экземпляр каждого, затем вызывает метод run для каждого теста.

load_test(TestCase1(test_params1))
load_test(TestCase2(test_params2))
...
load_test(TestCaseN(test_params3))

...

for test in loaded_tests:
    test.run()

Однако теперь у меня есть несколько тестовых примеров, для которых я не хочу, чтобы метод __init__ вызывался до тех пор, пока не будет вызван метод run, но я мало контролирую структуру или методы структуры. Как я могу отсрочить вызов __init__ без переопределения методов __init__ или run?


Update

Предположения о том, что это возникло как проблема XY, верны. Сотрудник спросил меня этот вопрос некоторое время назад, когда я поддерживал указанную тестовую структуру. Я больше интересовался тем, чего он действительно пытался достичь, и мы выяснили более простой способ обхода, который не включал изменение структуры или введение метаклассов и т.д.

Тем не менее, я все еще думаю, что это вопрос, который стоит изучить: если бы я хотел создать новые объекты с "ленивой" инициализацией ( "ленивый", как в ленивых генераторах оценки, таких как range и т.д.), что было бы лучше способ его достижения? Моя лучшая попытка пока приведена ниже, мне интересно узнать, есть ли что-нибудь более простое или менее подробное.

4b9b3361

Ответ 1

Первое решение: используйте свойство. элегантный способ setter/getter в python.

class Bars(object):
    def __init__(self):
        self._foo = None

    @property
    def foo(self):
        if not self._foo:
            print("lazy initialization")
            self._foo =  [1,2,3]
        return self._foo

if __name__ == "__main__":
    f = Bars()
    print(f.foo)
    print(f.foo)

Второе решение: прокси-решение и всегда реализуется декоратором.

Короче говоря, Proxy - это оболочка, которая обертывает необходимый вам объект. Прокси может предоставить дополнительные функции объекту, который он обертывает и не изменяет объектный код. Это суррогат, который обеспечивает доступность элемента управления к объекту. Появляется код пользователя Cyclone.

class LazyProperty:
    def __init__(self, method):
        self.method = method
        self.method_name = method.__name__

    def __get__(self, obj, cls):
        if not obj:
            return None
        value = self.method(obj)
        print('value {}'.format(value))
        setattr(obj, self.method_name, value)
        return value

class test:
    def __init__(self):
        self._resource = None

    @LazyProperty
    def resource(self):
        print("lazy")
        self._resource = tuple(range(5))
        return self._resource
if __name__ == '__main__':
    t = test()
    print(t.resource)
    print(t.resource)
    print(t.resource)

Используется для истинных одноразовых рассчитанных ленивых свойств. Мне это нравится, потому что это позволяет избежать наложения дополнительных атрибутов на объекты, и после активации не тратит время на проверку наличия атрибута

Ответ 2

Вариант метакласса

Вы можете перехватить вызов __init__ с помощью метакласса. Создайте объект с помощью __new__ и перезапишите метод __getattribute__, чтобы проверить, был ли вызван __init__ или нет, и вызовите его, если он этого не сделал.

class DelayInit(type):

    def __call__(cls, *args, **kwargs):

        def init_before_get(obj, attr):
            if not object.__getattribute__(obj, '_initialized'):
                obj.__init__(*args, **kwargs)
                obj._initialized = True
            return object.__getattribute__(obj, attr)

        cls.__getattribute__ = init_before_get

        new_obj = cls.__new__(cls, *args, **kwargs)
        new_obj._initialized = False
        return new_obj

class TestDelayed(TestCase1, metaclass=DelayInit):
    pass

В приведенном ниже примере вы увидите, что печать инициализации не будет выполняться до тех пор, пока не будет выполнен метод run.

>>> new_test = TestDelayed('delayed test params')
>>> new_test.run()
initializing test: <class '__main__.TestDelayed'> with params: delayed test params
running test: <class '__main__.TestDelayed'>

Вариант декоратора

Вы также можете использовать декоратор, который имеет аналогичный шаблон для метакласса выше:

def delayinit(cls):

    def init_before_get(obj, attr):
        if not object.__getattribute__(obj, '_initialized'):
            obj.__init__(*obj._init_args, **obj._init_kwargs)
            obj._initialized = True
        return object.__getattribute__(obj, attr)

    cls.__getattribute__ = init_before_get

    def construct(*args, **kwargs):
        obj = cls.__new__(cls, *args, **kwargs)
        obj._init_args = args
        obj._init_kwargs = kwargs
        obj._initialized = False
        return obj

    return construct

@delayinit
class TestDelayed(TestCase1):
    pass

Это будет вести себя одинаково с приведенным выше примером.

Ответ 3

В Python нет способа избежать вызова __init__ при создании класса cls. Если вызов cls(args) возвращает экземпляр cls, тогда язык гарантирует, что cls.__init__ будет вызван.

Таким образом, единственный способ добиться чего-то похожего на то, что вы просите, - это ввести еще один класс, который отложит вызов __init__ в исходном классе до тех пор, пока не будет доступен атрибут экземпляра класса.

Вот один из способов:

def delay_init(cls):
    class Delay(cls):
        def __init__(self, *arg, **kwarg):
            self._arg = arg
            self._kwarg = kwarg
        def __getattribute__(self, name):
            self.__class__ = cls
            arg = self._arg
            kwarg = self._kwarg
            del self._arg
            del self._kwarg
            self.__init__(*arg, **kwarg)
            return getattr(self, name)
    return Delay

Эта функция-обложка работает, захватывая любую попытку доступа к атрибуту созданного класса. Когда такая попытка выполнена, она меняет экземпляр __class__ на исходный класс, вызывает исходный метод __init__ с аргументами, которые были использованы при создании экземпляра, а затем возвращает правильный атрибут. Эта функция может использоваться в качестве декоратора для вашего класса TestCase1:

class TestBase:
    def __init__(self, params):
        self.name = str(self.__class__)
        print('initializing test: {} with params: {}'.format(self.name, params))


class TestCase1(TestBase):
    def run(self):
        print('running test: ' + self.name)


>>> t1 = TestCase1("No delay")
initializing test: <class '__main__.TestCase1'> with params: No delay
>>> t2 = delay_init(TestCase1)("Delayed init")
>>> t1.run()
running test: <class '__main__.TestCase1'>
>>> t2.run()
initializing test: <class '__main__.TestCase1'> with params: Delayed init
running test: <class '__main__.TestCase1'>
>>> 

Будьте осторожны, когда вы применяете эту функцию. Если вы украшаете TestBase с помощью delay_init, это не сработает, потому что оно вернет экземпляры TestCase1 в экземпляры TestBase.

Ответ 4

В моем ответе я хотел бы сосредоточиться на случаях, когда нужно создать экземпляр класса, инициатор которого (dunder init) имеет побочные эффекты. Например, pysftp.Connection, создает соединение SSH, которое может быть нежелательным до тех пор, пока оно фактически не будет использовано.

В большой серии блога о создании wrapt package (nit-picky decorator implementationaion) автор описывает Прозрачный объектный прокси. Этот код может быть настроен для данного объекта.

class LazyObject:

    _factory = None
    '''Callable responsible for creation of target object'''

    _object = None
    '''Target object created lazily'''

    def __init__(self, factory):
        self._factory = factory

    def __getattr__(self, name):
        if not self._object:
            self._object = self._factory()

        return getattr(self._object, name)

Затем его можно использовать как:

obj = LazyObject(lambda: dict(foo = 'bar'))
obj.keys()  # dict_keys(['foo'])

Но len(obj), obj['foo'] и другие языковые конструкции, которые вызывают протоколы объектов Python (методы dunder, такие как __len__ и __getitem__), не будут работать. Однако для многих случаев, которые ограничены регулярными методами, это решение.

Для реализаций протокола прокси-объекта нельзя использовать ни __getattr__, ни __getattribute__ (сделать это в общем виде). Последняя документация примечания:

Этот метод все же может быть обойден при поиске специальных методов в результате неявного вызова посредством синтаксиса языка или встроенных функций. См. Специальный поиск метода.

В качестве полного решения требуются примеры ручных реализаций, таких как werkzeug LocalProxy и django SimpleLazyObject. Однако умное обходное решение возможно.

К счастью, имеется специальный пакет (основанный на wrapt) для точного варианта использования lazy-object-proxy, который описан в этот пост в блоге.

from lazy_object_proxy import Proxy

obj = Proxy(labmda: dict(foo = 'bar'))
obj.keys()     # dict_keys(['foo'])
len(len(obj))  # 1
obj['foo']     # 'bar'

Ответ 5

Одним из альтернатив было бы написать оболочку, которая принимает класс как ввод и возвращает класс с задержкой инициализации до тех пор, пока не будет доступ к любому члену. Это можно сделать, например, следующим образом:

def lazy_init(cls):
    class LazyInit(cls):
        def __init__(self, *args, **kwargs):
            self.args = args
            self.kwargs = kwargs
            self._initialized = False

        def __getattr__(self, attr):
            if not self.__dict__['_initialized']:
                cls.__init__(self,
                             *self.__dict__['args'], **self.__dict__['kwargs'])
                self._initialized = True

            return self.__dict__[attr]

    return LazyInit

Затем это можно было бы использовать как таковое

load_test(lazy_init(TestCase1)(test_params1))
load_test(lazy_init(TestCase2)(test_params2))
...
load_test(lazy_init(TestCaseN)(test_params3))

...

for test in loaded_tests:
    test.run()

Ответ 6

Отвечая на ваш первоначальный вопрос (и проблема, которую, как я думаю, вы пытаетесь решить), "Как я могу задержать вызов init до тех пор, пока не будет получен доступ к атрибуту?": не вызывайте init, пока вы не получите доступ к этому атрибуту.

Сказан по-другому: вы можете инициализировать класс одновременно с вызовом атрибута. Кажется, что вам действительно хочется: 1) создать коллекцию классов TestCase# вместе с их связанными параметрами; 2) запускайте каждый тестовый пример.

Вероятно, ваша оригинальная проблема возникла из-за того, что вы должны были инициализировать все ваши классы TestCase, чтобы создать список из них, который вы могли бы перебрать. Но на самом деле вы можете хранить объекты класса в lists, dicts и т.д. Это означает, что вы можете сделать любой метод, который у вас есть, для поиска всех классов TestCase и сохранить эти объекты класса в dict с их соответствующими параметрами. Затем просто повторите этот dict и вызовите каждый класс с помощью метода run().

Это может выглядеть так:

tests = {TestCase1: 'test 1 params', TestCase2: 'test 2 params', TestCase3: 'test 3 params'}

for test_case, param in tests.items():
    test_case(param).run()

Ответ 7

Переопределение __new__

Вы можете сделать это, переопределив метод __new__ и заменив метод __init__ на пользовательскую функцию.

def init(cls, real_init):
    def wrapped(self, *args, **kwargs):
        # This will run during the first call to `__init__`
        # made after `__new__`. Here we re-assign the original
        # __init__ back to class and assign a custom function
        # to `instances.__init__`.
        cls.__init__ = real_init
        def new_init():
            if new_init.called is False:
                real_init(self, *args, **kwargs)
                new_init.called = True
        new_init.called = False
        self.__init__ = new_init
    return wrapped


class DelayInitMixin(object):
    def __new__(cls, *args, **kwargs):
        cls.__init__ = init(cls, cls.__init__)
        return object.__new__(cls)


class A(DelayInitMixin):
    def __init__(self, a, b):
        print('inside __init__')
        self.a = sum(a)
        self.b = sum(b)

    def __getattribute__(self, attr):
        init = object.__getattribute__(self, '__init__')
        if not init.called:
            init()
        return object.__getattribute__(self, attr)

    def run(self):
        pass

    def fun(self):
        pass

Demo:

>>> a = A(range(1000), range(10000))    
>>> a.run()
inside __init__    
>>> a.a, a.b
(499500, 49995000)    
>>> a.run(), a.__init__()
(None, None)    
>>> b = A(range(100), range(10000))    
>>> b.a, b.b
inside __init__
(4950, 49995000)    
>>> b.run(), b.__init__()
(None, None)

Использование кешированных свойств

Идея состоит в том, чтобы выполнить тяжелый расчет только один раз путем кеширования результатов. Такой подход приведет к значительно более читаемому коду, если все время задержки инициализации улучшит производительность.

Django поставляется с красивым декоратором под названием @cached_property. Я часто использую его как в коде, так и в модульных тестах для кэширования результатов тяжелых свойств.

A cached_property является дескриптор без данных. Следовательно, как только ключ задан в словаре экземпляра, доступ к свойству всегда будет получать значение отсюда.

class cached_property(object):
    """
    Decorator that converts a method with a single self argument into a
    property cached on the instance.

    Optional ``name`` argument allows you to make cached properties of other
    methods. (e.g.  url = cached_property(get_absolute_url, name='url') )
    """
    def __init__(self, func, name=None):
        self.func = func
        self.__doc__ = getattr(func, '__doc__')
        self.name = name or func.__name__

    def __get__(self, instance, cls=None):
        if instance is None:
            return self
        res = instance.__dict__[self.name] = self.func(instance)
        return res

Применение:

class A:
    @cached_property
    def a(self):
        print('calculating a')
        return sum(range(1000))

    @cached_property
    def b(self):
        print('calculating b')
        return sum(range(10000))

Demo:

>>> a = A()
>>> a.a
calculating a
499500
>>> a.b
calculating b
49995000
>>> a.a, a.b
(499500, 49995000)

Ответ 8

Я думаю, вы можете использовать класс-оболочку для хранения реального класса, который вы хотите использовать, и использовать call __init__ самостоятельно в своем коде, например (код Python 3):

class Wrapper:
    def __init__(self, cls):
        self.cls = cls
        self.instance = None

    def your_method(self, *args, **kwargs):
        if not self.instance:
            self.instnace = cls()
        return self.instance(*args, **kwargs)

class YourClass:
    def __init__(self):
        print("calling __init__")

но это свалка, но без всякого трюка.