Неявный вызов инициализатора родительского класса

class A(object):
    def __init__(self, a, b, c):
        #super(A, self).__init__()
        super(self.__class__, self).__init__()


class B(A):
    def __init__(self, b, c):
        print super(B, self)
        print super(self.__class__, self)
        #super(B, self).__init__(1, b, c)
        super(self.__class__, self).__init__(1, b, c)

class C(B):
    def __init__(self, c):
        #super(C, self).__init__(2, c)
        super(self.__class__, self).__init__(2, c)
C(3)

В приведенном выше коде закомментированные вызовы __init__ кажутся общепринятым «умным» способом инициализации суперкласса. Однако на случай, если иерархия классов может измениться, я до недавнего времени использовал форму без комментариев.

Похоже, что при вызове суперконструктора для B в приведенной выше иерархии снова вызывается B.__init__, self.__class__ на самом деле является C, а не B, как я всегда предполагал.

Есть ли какой-нибудь способ в Python-2.x, чтобы я мог поддерживать правильный MRO (в отношении инициализации всех родительских классов в правильном порядке) при вызове суперконструкторов, не называя текущий класс (B в super(B, self).__init__(1, b, c))?


person Matt Joiner    schedule 01.03.2010    source источник


Ответы (4)


Краткий ответ: нет, в Python 2.x невозможно неявно вызвать правильный __init__ с правильными аргументами правильного родительского класса.

Между прочим, приведенный здесь код неверен: если вы используете super().__init__, то все классы в вашей иерархии должны иметь одинаковую сигнатуру в своих __init__ методах. В противном случае ваш код может перестать работать, если вы введете новый подкласс, использующий множественное наследование.

Подробное описание проблемы см. на http://fuhm.net/super-harmful/ ( с картинками).

person Marius Gedminas    schedule 03.05.2010
comment
я начал задаваться вопросом о сигнатурах инициализации и множественном наследовании - person Matt Joiner; 04.05.2010

Ваш код не имеет ничего общего с порядком разрешения методов. Разрешение метода происходит в случае множественного наследования, что не относится к вашему примеру. Ваш код просто неверен, потому что вы предполагаете, что self.__class__ на самом деле является тем же классом, в котором определен метод, и это неправильно:

>>> class A(object):
...     def __init__(self):
...         print self.__class__
... 
>>> 
>>> class B(A):
...     def __init__(self):
...         A.__init__(self)
... 
>>> B()
<class '__main__.B'>
<__main__.B object at 0x1bcfed0>
>>> A()
<class '__main__.A'>
<__main__.A object at 0x1bcff90>
>>> 

поэтому, когда вы должны позвонить:

super(B, self).__init__(1, b, c)

вы действительно звоните:

# super(self.__class__, self).__init__(1, b, c)
super(C, self).__init__(1, b, c)

EDIT: попытка лучше ответить на вопрос.

class A(object):
    def __init__(self, a):
        for cls in self.__class__.mro():
            if cls is not object:
                cls._init(self, a)
    def _init(self, a):
        print 'A._init'
        self.a = a

class B(A):
    def _init(self, a):
        print 'B._init'

class C(A):
    def _init(self, a):
        print 'C._init'

class D(B, C):
    def _init(self, a):
        print 'D._init'


d = D(3)
print d.a

печатает:

D._init
B._init
C._init
A._init
3

(Измененная версия шаблона).

Теперь методы родителей действительно вызываются неявно, но я должен согласиться с python zen, где явное лучше, чем неявное, потому что код менее читаем, а выигрыш плохой. Но имейте в виду, что все методы _init имеют одинаковые параметры, полностью забыть о родителях нельзя, и я не советую этого делать.

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

Хорошие чтения: правильная вещь и ссылки, предложенные в этом вопросе, и, в частности, Python's Super отлично, но вы можете не использовать

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

ИЗМЕНИТЬ 2

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

Ex:

>>> class MetaSuper(type):
...     """adding .__super"""
...     def __init__(cls, name, bases, d):
...         super(MetaSuper, cls).__init__(name, bases, d)
...         if hasattr(cls, "_%s__super" % name):
...             raise AttributeError, "Class has same name as one of its super classes"
...         setattr(cls, "_%s__super" % name, super(cls))
... 
>>> class A:
...  __metaclass__ = MetaSuper
...  def __init__(self, a):
...   self.a = a
...   print 'A.__init__'
... 
>>> class B(A):
...  def __init__(self, a):
...   print 'B.__init__'
...   self.__super.__init__(a)
... 
>>> b = B(42)
B.__init__
A.__init__
>>> b.a
42
>>> 
person mg.    schedule 30.04.2010

Возможно, вы ищете метаклассы?

class metawrap(type):
    def __new__(mcs,name, bases, dict):
        dict['bases'] = bases
        return type.__new__(mcs,name,bases,dict)

class A(object):
    def __init__(self):
        pass
    def test(self):
        print "I am class A"

class B(A):
    __metaclass__ = metawrap
    def __init__(self):
        pass
    def test(self):
        par = super(self.bases[0],self)
        par.__thisclass__.test(self)
foo = B()
foo.test()

Принты «Я класс А»

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

person Kristoffer Sall-Storgaard    schedule 01.03.2010
comment
Я только что понял, что стреляю по воробьям из пушек (датская пословица), за это прошу прощения, однако я оставлю это, если кто-то не решит это удалить. - person Kristoffer Sall-Storgaard; 01.03.2010
comment
я бы предпочел не прибегать к метаклассам для чего-то тривиального, но спасибо - person Matt Joiner; 01.05.2010

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

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

Пример хранения ссылок на самом объекте путем реализации __new__ в базовом классе:

def mangle(cls, name):
    if not name.startswith('__'):
        raise ValueError('name must start with double underscore')
    return '_%s%s' % (cls.__name__, name)

class ClassStasher(object):
    def __new__(cls, *args, **kwargs):
        obj = object.__new__(cls)
        for c in cls.mro():
            setattr(obj, mangle(c, '__class'), c)
        return obj

class A(ClassStasher):
    def __init__(self):
        print 'init in A', self.__class
        super(self.__class, self).__init__()

class B(A):
    def __init__(self):
        print 'init in B', self.__class
        super(self.__class, self).__init__()

class C(A):
    def __init__(self):
        print 'init in C', self.__class
        super(self.__class, self).__init__()

class D(B, C):
    def __init__(self):
        print 'init in D', self.__class
        super(self.__class, self).__init__()


d = D()    
print d

И, делая то же самое, но используя метакласс и пряча ссылки __class на сами объекты класса:

class ClassStasherType(type):
    def __init__(cls, name, bases, attributes):
        setattr(cls, mangle(cls, '__class'), cls)

class ClassStasher(object):
    __metaclass__ = ClassStasherType

class A_meta(ClassStasher):
    def __init__(self):
        print 'init in A_meta', self.__class
        super(self.__class, self).__init__()

class B_meta(A_meta):
    def __init__(self):
        print 'init in B_meta', self.__class
        super(self.__class, self).__init__()

class C_meta(A_meta):
    def __init__(self):
        print 'init in C_meta', self.__class
        super(self.__class, self).__init__()

class D_meta(B_meta, C_meta):
    def __init__(self):
        print 'init in D_meta', self.__class
        super(self.__class, self).__init__()


d = D_meta()    
print d

Запускаем все это вместе, как один исходный файл:

% python /tmp/junk.py
init in D <class '__main__.D'>
init in B <class '__main__.B'>
init in C <class '__main__.C'>
init in A <class '__main__.A'>
<__main__.D object at 0x1004a4a50>
init in D_meta <class '__main__.D_meta'>
init in B_meta <class '__main__.B_meta'>
init in C_meta <class '__main__.C_meta'>
init in A_meta <class '__main__.A_meta'>
<__main__.D_meta object at 0x1004a4bd0>
person Matt Anderson    schedule 04.05.2010