TypeError при использовании супер-метода с декоратором класса для производного класса

Во-первых, извинения за долгое объяснение.

Версия # 1 - Код: декоратор класса для класса

class A(object):
    def __init__(self, klass):
        print "A::__init__()"
        self._klass = klass

    def __call__(self):
        print "A::__call__()"
        return self._klass()

    def __del__(self):
        print "A::__del__()"

@A
class B(object):
    def __init__(self):
        print "B::__init__()"

def main():
    b = B()

if __name__ == "__main__":
    main()

Версия №1 - Вывод:

A::__init__()
A::__call__()
B::__init__()
A::__del__()

Версия №2 - Код: декоратор класса для производного класса, который явно инициализирует базовые классы.

class A(object):
    def __init__(self, klass):
        print "A::__init__()"
        self._klass = klass

    def __call__(self):
        print "A::__call__()"
        return self._klass()

    def __del__(self):
        print "A::__del__()"

class Parent1(object):
    def __init__(self):
        print "Parent1:: __init__()"
        super(Parent1, self).__init__()

class Parent2(object):
    def __init__(self):
        print "Parent2:: __init__()"
        super(Parent2, self).__init__()    

@A
class B(Parent1, Parent2):
    def __init__(self):
        print "B::__init__()"
#        super(B, self).__init__()
        Parent1.__init__(self)
        Parent2.__init__(self)

def main():
    b = B()

if __name__ == "__main__":
    main()

Версия №2 - Вывод:

A::__init__()
A::__call__()
B::__init__()
Parent1:: __init__()
Parent2:: __init__()
Parent2:: __init__()
A::__del__()

Версия №3 - Код: декоратор класса для производного класса с super()

class A(object):
    def __init__(self, klass):
        print "A::__init__()"
        self._klass = klass

    def __call__(self):
        print "A::__call__()"
        return self._klass()

    def __del__(self):
        print "A::__del__()"   

class Parent1(object):
    def __init__(self):
        print "Parent1:: __init__()"
        super(Parent1, self).__init__()

class Parent2(object):
    def __init__(self):
        print "Parent2:: __init__()"
        super(Parent2, self).__init__()

@A
class B(Parent1, Parent2):
    def __init__(self):
        print "B::__init__()"
        super(B, self).__init__()

def main():
    b = B()

if __name__ == "__main__":
    main()

Версия №3 - Вывод:

A::__init__()
A::__call__()
B::__init__()
Traceback (most recent call last):
  File "so.py", line 40, in <module>
    main()
  File "so.py", line 36, in main
    b = B()
  File "so.py", line 10, in __call__
    return self._klass()
  File "so.py", line 32, in __init__
    super(B, self).__init__()
TypeError: must be type, not A
A::__del__()

Вопрос:

Версия №1 предназначена только для справки. Он объясняет, что я пытаюсь сделать, то есть захватить creation и deletion объектов class B.

В версии № 2 я пробовал то же самое для объектов class B, которые являются производными от Parent1 и Parent2, которые инициализируются явно с использованием Parent1.__init__(self) и Parent2.__init__(self), которые отлично работают, как и ожидалось.

Но в версии №3 я пробовал то же самое с методом super(). Но получаю следующую ошибку - TypeError: must be type, not A. Я думаю, это потому, что метод __init__() для всех родительских классов в цепочке MRO не вызывается должным образом. Почему? И как мне это исправить?


person Sangeeth Saravanaraj    schedule 17.02.2014    source источник
comment
Здесь уже ответил: stackoverflow.com/a/2543256/705086 это Алекс Мартелли, этому парню можно доверять! :)   -  person Dima Tisnek    schedule 26.02.2014


Ответы (2)


Основная проблема заключается в том, что первым аргументом super должен быть фактический класс, но в версии 3 в

super(B, self)

B - это не тот класс, который вы создали. Это A экземпляр, оборачивающий класс. Вам нужно будет сделать что-то вроде

class _B(Parent1, Parent2):
    def __init__(self):
        print "B::__init__()"
        super(_B, self).__init__()
B = A(_B)

или вместо упаковки B в экземпляр A используйте декоратор, который заменяет методы __init__ и __del__ B оболочками без замены всего класса B.

Кроме того, если вы хотите отслеживать удаление B экземпляров, метод __del__ на A этого не сделает. Он будет отслеживать удаление класса, а не отдельных экземпляров.


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

def track_creation_and_deletion(klass):
    original_init = klass.__init__
    try:
        original_del = klass.__del__
    except AttributeError:
        def original_del(self):
            pass

    def new_init(self, *args, **kwargs):
        print '{}.{}.__init__'.format(klass.__module__, klass.__name__)
        return original_init(self, *args, **kwargs)
    def new_del(self):
        print '{}.{}.__del__'.format(klass.__module__, klass.__name__)
        return original_del(self)

    # functools.wraps doesn't play nicely with built-in methods,
    # so we handle it ourselves
    new_init.__name__ = '__init__'
    new_init.__doc__ = original_init.__doc__
    new_init.__module__ = klass.__module__
    new_init.__dict__.update(getattr(original_init, '__dict__', {}))

    new_del.__name__ = '__del__'
    new_del.__doc__ = original_del.__doc__
    new_del.__module__ = klass.__module__
    new_del.__dict__.update(getattr(original_del, '__dict__', {}))

    klass.__init__ = new_init
    klass.__del__ = new_del

    return klass

Примерно половина из них - это обработка ошибок и копирование некоторых метаданных, чтобы новые методы выглядели так, как будто они были определены вызывающей стороной. Ключевым моментом является то, что мы определяем новые __init__ и __del__ методы, обертывающие и заменяющие старые методы класса. Когда создается экземпляр декорированного класса, метод __init__, который мы ему дали, вызовет код регистрации по нашему выбору. Когда экземпляр декорированного класса собирается сборщиком мусора, метод __del__, который мы ему предоставили, вызовет другой код ведения журнала. Поскольку мы не заменяли сам объект класса, ссылки на класс по имени в super вызовах будут ссылаться на класс, на который они должны ссылаться.

Одним из ограничений этого подхода является то, что трудно проверить сам экземпляр в нашем __init__, потому что он может быть не полностью сконструирован даже после возврата завернутого __init__. Например, если мы попытаемся print экземпляр, мы можем запустить метод __str__ подкласса, полагающийся на атрибуты подкласса, которые еще не готовы, что вызовет AttributeError.

person user2357112 supports Monica    schedule 17.02.2014
comment
+1 Я думал передать self._klass вызову __init__: self._klass(self._klass), а затем использовать это в вызове super вместо B. Но так выглядит лучше. - person Ashwini Chaudhary; 17.02.2014

Мне потребовалось время, чтобы понять, почему трудно зафиксировать создание и удаление объекта с помощью методов __call__ и __del__ соответственно. Ниже приведены некоторые из полезных ссылок.

Для этого есть отличные хаки с использованием __del__ метода, но они имеют побочные эффекты! Например, ответ @ user2357112 - хороший прием, но он не работает, когда мы делаем циклическую ссылку, потому что сборщик мусора не может определить, какой __del__ из циклической ссылки вызвать первым! Однако этого можно избежать, используя слабую ссылку; но это все же взлом!

Одно из предложений было создать диспетчер контекста, который может создавать и удалять объекты определенного класса.

У меня есть следующий пример, который имитирует это. Пожалуйста, присмотритесь к декоратору Controller.

class Parent1(object):
    def __init__(self):
        #print "Parent1::__init__()"
        super(Parent1, self).__init__()

class Parent2(object):
    def __init__(self):
        #print "Parent2::__init__()"
        super(Parent2, self).__init__()

def Controller(_cls):
    class Wrapper(_cls):
        def create(self, name):
            ret = _cls.create(self, name)
            print "Added to Database! :: ", name
            # Database add here!
            return ret

        def remove(self, name):
            ret = _cls.remove(self, name)
            print "Deleted from Database! :: ", name
            # Database delete here!
            return ret
    return Wrapper

@Controller
class Manager(object):
    def __init__(self):
        #print "Manager::__init__()"
        self._repo = []

    def create(self, name):
        a = A(name)
        print "Object created :: ", name
        self._repo.append(a)

    def remove(self, name):
        for i, item in enumerate(self._repo):
            if item._name == name:
                del self._repo[i]
                print "Object removed :: ", name

    def display(self):
        for item in self._repo:
            print item

class A(Parent1, Parent2):
    def __init__(self, name):
        #print "A::__init__()"
        self._name = name
        super(A, self).__init__()

    def __repr__(self):
        return self._name

def main():
    m1 = Manager()
    m1.create("apples")
    m1.create("oranges")
    m1.create("grapes")
    #m1.display()
    m1.remove("apples")
    #m1.display()

if __name__ == "__main__":
    main()

При выполнении он дает следующий результат:

Object created ::  apples
Added to Database! ::  apples
Object created ::  oranges
Added to Database! ::  oranges
Object created ::  grapes
Added to Database! ::  grapes
Object removed ::  apples
Deleted from Database! ::  apples

Это самое безопасное решение, которое я мог придумать для своей проблемы. Предложения приветствуются!

person Sangeeth Saravanaraj    schedule 03.03.2014