Как MRO, супер работает в Python - PullRequest
0 голосов
/ 25 августа 2018

Может кто-нибудь помочь мне понять, как MRO работает в python? Предположим, у меня есть четыре класса - Персонаж, Вор, Ловкий, Подлый. Характер - супер класс для Вора, Agile и Sneaky - братья и сестры. Пожалуйста, смотрите мой код и вопрос ниже

class Character:
    def __init__(self, name="", **kwargs):
        if not name:
            raise ValueError("'name' is required")
        self.name = name

        for key, value in kwargs.items():
            setattr(self, key, value)


class Agile:
    agile = True

    def __init__(self, agile=True, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.agile = agile


class Sneaky:
    sneaky = True

    def __init__(self, sneaky=True, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.sneaky = sneaky


class Thief(Agile, Sneaky, Character):
    def pickpocket(self):
    return self.sneaky and bool(random.randint(0, 1))


parker = Thief(name="Parker", sneaky=False)

Итак, вот что я думаю, пожалуйста, дайте мне знать, если я правильно понимаю.

Поскольку Agile занимает первое место в списке, все аргументы сначала отправляются в Agile, где аргументы будут иметь перекрестные ссылки с параметрами Agile. Если есть совпадение, то значение будет присвоено, тогда все, у кого нет подходящего ключевого слова, будет упаковано в * kwargs и отправлено в класс Sneaky (через super), где произойдет то же самое - все аргументы будут распакованы с перекрестными ссылками на параметры Sneaky (это когда sneaky = False установлен), затем упаковывается в kwargs и отправляется в Character. Затем все внутри метода Character inint будет запущено и все значения будут установлены (например, name = "Parker").

КАК Я ДУМАЮ, ЧТО МРО РАБОТАЕТ НАЗАД

Теперь, когда все дошло до класса Character и все в методе инициализации Character выполнено, теперь он должен вернуться к классам Agile и Sneaky и завершить выполнение всего в их методах init (или всего, что находится под их супер). Итак, сначала он вернется к классу Sneaky и завершит свой метод init, затем вернется к классу Agile и завершит оставшуюся часть его метода init (соответственно).

Я где-нибудь путаюсь? Уф. Извините, я знаю, что это много, но я действительно застрял здесь и пытаюсь получить четкое представление о том, как работает MRO.

Спасибо всем.

1 Ответ

0 голосов
/ 25 августа 2018

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

Да, у вас все в порядке.

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


Сначала просто распечатайте Thief.mro(). Это должно выглядеть примерно так:

[Thief, Agile, Sneaky, Character, object]

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

>>> [cls for cls in Thief.mro() if '__init__' in cls.__dict__]
[Agile, Sneaky, Character, object]

И, просто чтобы убедиться, что Agile действительно вызывается первым:

>>> Thief.__init__
<function Agile.__init__>

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

Или вы можете просто добавить print операторов вверху и внизу каждого из них, например:

def __init__(self, agile=True, *args, **kwargs):
    print(f'>Agile.__init__(agile={agile}, args={args}, kwargs={kwargs})')
    super().__init__(*args, **kwargs)
    self.agile = agile     
    print(f'<Agile.__init__: agile={agile}')

(Вы могли бы даже написать декоратор, который делает это автоматически, с небольшим количеством inspect волшебства.)

Если вы сделаете это, он напечатает что-то вроде:

> Agile.__init__(agile=True, args=(), kwargs={'name': 'Parker', 'sneaky':False})
> Sneaky.__init__(sneaky=False, args=(), kwargs={'name': 'Parker'})
> Character.__init__(name='Parker', args=(), kwargs={})
< Character.__init__: name: 'Parker'
< Sneaky.__init__: sneaky: False
< Agile.__init__: agile: True

Итак, вы правы насчет порядка, который вызывается через super, и порядок, в котором стек возвращается на обратном пути, явно противоположен.


Но, между тем, вы ошиблись одной деталью:

отправляется в класс Sneaky (через super), где произойдет то же самое - все аргументы распаковываются, с перекрестными ссылками на параметры Sneaky (это когда sneaky = False установлен)

Здесь задается параметр / локальная переменная sneaky, но self.sneaky не устанавливается до после , когда возвращается super. До этого (включая Character.__init__ и аналогично для любых других миксинов, которые вы выберете после Sneaky), sneaky в self.__dict__ не будет, поэтому, если кто-нибудь попытается найти self.sneaky они смогут найти только атрибут класса, который имеет неправильное значение.


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

Если вы хотели, чтобы они указывали значения во время инициализации, то они потенциально неверны, поэтому они хуже, чем бесполезны. Если вам нужно набрать self.sneaky перед вызовом Character.__init__, способ сделать это прост: просто переместите self.sneaky = sneaky вверх до super() вызова.

Фактически, это одна из сильных сторон "явной super" модели Python. В некоторых языках, таких как C ++, конструкторы всегда вызываются автоматически, будь то изнутри или снаружи. Python заставляет вас делать это явно, это менее удобно и труднее ошибиться, но это означает, что вы можете выполнить настройку либо раньше или после того, как базовый класс получит свой шанс (или, конечно, немного от каждого), что иногда полезно.

...