super () странное поведение в алмазном наследовании в python - PullRequest
0 голосов
/ 27 сентября 2018

Как показывает следующий пример, super() имеет некоторое странное (по крайней мере для меня) поведение при использовании в наследовании алмазов.

class Vehicle:
    def start(self):
        print("engine has been started")

class LandVehicle(Vehicle):
    def start(self):
        super().start()
        print("tires are safe to use")

class WaterCraft(Vehicle):
    def start(self):
        super().start()
        print("anchor has been pulled up")

class Amphibian(LandVehicle, WaterCraft):
    def start(self):
        # we do not want to call WaterCraft.start, amphibious
        # vehicles don't have anchors
        LandVehicle.start(self)
        print("amphibian is ready for travelling on land")

amphibian = Amphibian()
amphibian.start()

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

engine has been started
anchor has been pulled up
tires are safe to use
amphibian is ready for travelling on land

Когда я вызываю super().some_method(), я бы никогда не ожидал, что будет вызван метод класса с тем же уровнем наследования,Так что в моем примере я бы не ожидал, что anchor has been pulled up появится в выводе.

Класс, вызывающий super(), может даже не знать о другом классе, чей метод в конечном итоге вызывается.В моем примере LandVehicle может даже не знать о WaterCraft.

Является ли это поведение нормальным / ожидаемым, и если да, то что за этим стоит?

1 Ответ

0 голосов
/ 27 сентября 2018

Когда вы используете наследование в Python, каждый класс определяет Порядок разрешения методов (MRO), который используется для определения, где искать, когда ищется атрибут класса.Например, для вашего класса Amphibian MRO составляет Amphibian, LandVehicle, WaterCraft, Vehicle и, наконец, object.(Вы можете убедиться в этом сами, позвонив по номеру Amphibian.mro().)

Точные детали того, как происходит MRO, немного сложны (хотя вы можете найти описание того, как это работает, если вам интересно),Важно знать, что любой дочерний класс всегда перечисляется перед его родительскими классами, и если происходит множественное наследование, все родители дочернего класса будут в том же относительном порядке, что и в выражении class (другоеклассы могут появляться между родителями, но они никогда не будут обращены друг относительно друга).

Когда вы используете super для вызова переопределенного метода, он выглядит, как MRO, как для любого атрибутапоиск, но он начинает свой поиск дальше, чем обычно.В частности, он начинает поиск атрибута сразу после «текущего» класса.Под «текущим» я подразумеваю класс, содержащий метод, в котором вызывается super (даже если объект, к которому вызывается метод, имеет какой-то другой, более производный класс).Поэтому, когда LandVehicle.__init__ вызывает super().__init__, он начинает проверять метод __init__ в первом классе после LandVehicle в MRO и находит WaterCraft.__init__.

. Это предполагает один способ, которым вы могли быисправить проблему.Вы можете иметь Amphibian name WaterCraft в качестве первого базового класса, а LandVehicle second:

class Amphibian(Watercraft, LandVehicle):
    ...

Изменение порядка баз также изменит их порядок в MRO.Когда Amphibian.__init__ вызывает LandVehicle.__init__ напрямую по имени (вместо использования super), последующие super вызовы будут пропускать WaterCraft, поскольку класс, из которого они вызываются, уже дальше в MRO.Таким образом, остальные вызовы super будут работать так, как вы хотели.

Но это не очень хорошее решение.Когда вы явно называете базовый класс таким образом, вы можете обнаружить, что он сломает вещи позже, если у вас есть более дочерние классы, которые хотят делать вещи по-другому.Например, класс, производный от переупорядоченной базы Amphibian выше, может в конечном итоге иметь другие базовые классы в диапазоне от WaterCraft до LandVehcle, что также привело бы к случайному пропуску их методов __init__, когда Amphibian.__init__ вызывает LandVehcle.__init__ напрямую.

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

Например, вы можете изменить WaterCraft на:

class WaterCraft(Vehicle):
    def start(self):
        super().start()
        self.weigh_anchor()

    def weigh_anchor(self):
        print("anchor has been pulled up")

Класс Amphibian может переопределить поведение привязки (например, ничего не делать):

class Amphibian(LandVehicle, WaterCraft):
    def start(self):
        super().start(self)
        print("amphibian is ready for travelling on land")

    def weigh_anchor(self):
        pass # no anchor to weigh, so do nothing

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

...