Заряжай свои классы с помощью Python super ()

Заряжай свои классы с помощью Python super ()

Хотя Python не является чисто объектно-ориентированным языком, он достаточно гибкий и достаточно мощный, чтобы позволить вам создавать приложения с использованием объектно-ориентированной парадигмы. Один из способов, которыми Python достигает этого, - это поддержкаinheritance, что он делает сsuper().

В этом руководстве вы узнаете о следующем:

  • Концепция наследования в Python

  • Множественное наследование в Python

  • Как работает функцияsuper()

  • Как работает функцияsuper() при одиночном наследовании

  • Как работает функцияsuper() при множественном наследовании

Free Bonus:5 Thoughts On Python Mastery, бесплатный курс для разработчиков Python, который показывает вам план действий и образ мышления, который вам понадобится, чтобы вывести свои навыки Python на новый уровень.

Обзор функции Pythonsuper()

Если у вас есть опыт работы с объектно-ориентированными языками, возможно, вы уже знакомы с функциональностьюsuper().

Если нет, не бойтесь! Хотяofficial documentation является довольно техническим, на высоком уровнеsuper() дает вам доступ к методам в суперклассе из подкласса, который наследуется от него.

Толькоsuper() возвращает временный объект суперкласса, который затем позволяет вам вызывать методы этого суперкласса.

Почему вы хотите сделать что-нибудь из этого? В то время как возможности ограничены вашим воображением, распространенным вариантом использования является создание классов, расширяющих функциональность ранее созданных классов.

Вызов ранее созданных методов с помощьюsuper() избавляет вас от необходимости переписывать эти методы в вашем подклассе и позволяет заменять суперклассы с минимальными изменениями кода.

super() в одиночном наследовании

Если вы не знакомы с концепциями объектно-ориентированного программирования, терминinheritance может быть вам незнаком. Наследование - это концепция объектно-ориентированного программирования, в которой класс наследует (илиinherits) атрибуты и поведение от другого класса без необходимости их повторной реализации.

По крайней мере, для меня легче понять эти концепции, глядя на код, поэтому давайте напишем классы, описывающие некоторые формы:

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width

class Square:
    def __init__(self, length):
        self.length = length

    def area(self):
        return self.length * self.length

    def perimeter(self):
        return 4 * self.length

Здесь есть два похожих класса:Rectangle иSquare.

Вы можете использовать их, как показано ниже:

>>>

>>> square = Square(4)
>>> square.area()
16
>>> rectangle = Rectangle(2,4)
>>> rectangle.area()
8

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

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

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width

# Here we declare that the Square class inherits from the Rectangle class
class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

Здесь вы использовалиsuper() для вызова__init__() классаRectangle, что позволяет использовать его в классеSquare без повторения кода. Ниже основные функциональные возможности остаются после внесения изменений:

>>>

>>> square = Square(4)
>>> square.area()
16

В этом примереRectangle является суперклассом, аSquare - подклассом.

Поскольку методыSquare иRectangle.__init__() очень похожи, вы можете просто вызвать метод суперкласса.__init__() (Rectangle.__init__()) из методаSquare используяsuper(). Это устанавливает атрибуты.length и.width, даже если вам просто нужно было передать один параметрlength конструкторуSquare.

Когда вы запустите это, даже если ваш классSquare не реализует его явно, вызов.area() будет использовать метод.area() в суперклассе и печатать16. КлассSquareinherited.area() из классаRectangle.

Note: Чтобы узнать больше о наследовании и объектно-ориентированных концепциях в Python, обязательно ознакомьтесь сObject-Oriented Programming (OOP) in Python 3.

Что может сделать для васsuper()?

Итак, чтоsuper() может сделать для вас при одинарном наследовании?

Как и в других объектно-ориентированных языках, он позволяет вам вызывать методы суперкласса в вашем подклассе. Основным вариантом использования этого является расширение функциональности унаследованного метода.

В приведенном ниже примере вы создадите классCube, который наследуется отSquare и расширяет функциональные возможности.area() (унаследованный от классаRectangle черезSquare) для расчета площади поверхности и объема экземпляраCube:

class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

class Cube(Square):
    def surface_area(self):
        face_area = super().area()
        return face_area * 6

    def volume(self):
        face_area = super().area()
        return face_area * self.length

Теперь, когда вы построили классы, давайте посмотрим на площадь поверхности и объем куба с длиной стороны3:

>>>

>>> cube = Cube(3)
>>> cube.surface_area()
54
>>> cube.volume()
27

Caution: обратите внимание, что в нашем примере выше,super() сам по себе не будет вызывать методы за вас: вы должны вызвать метод самого прокси-объекта.

Здесь вы реализовали два метода для классаCube:.surface_area() и.volume(). Оба этих вычисления основаны на вычислении площади одной грани, поэтому вместо повторного вычисления площади вы используетеsuper() для расширения вычисления площади.

Также обратите внимание, что в определении классаCube нет.__init__(). ПосколькуCube наследуется отSquare, а.__init__() на самом деле ничего не делает дляCube иначе, чем дляSquare, вы можете пропустить его определение, и .__init__() суперкласса (Square) будет вызываться автоматически.

super() возвращает объект делегата родительскому классу, поэтому вы вызываете нужный метод непосредственно на нем:super().area().

Это не только избавляет нас от необходимости переписывать вычисления площади, но также позволяет нам изменять внутреннюю логику.area() в одном месте. Это особенно удобно, когда у вас есть несколько подклассов, наследующих один суперкласс.

Asuper() Глубокое погружение

Прежде чем перейти к множественному наследованию, давайте кратко рассмотрим механизмsuper().

Хотя в примерах выше (и ниже)super() вызывается без каких-либо параметров,super() также может принимать два параметра: первый - это подкласс, а второй параметр - объект, являющийся экземпляром этого подкласса.

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

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

    def perimeter(self):
        return 2 * self.length + 2 * self.width

class Square(Rectangle):
    def __init__(self, length):
        super(Square, self).__init__(length, length)

В Python 3 вызовsuper(Square, self) эквивалентен вызовуsuper() без параметров. Первый параметр относится к подклассуSquare, а второй параметр относится к объектуSquare, которым в данном случае являетсяself. Вы также можете вызыватьsuper() с другими классами:

class Cube(Square):
    def surface_area(self):
        face_area = super(Square, self).area()
        return face_area * 6

    def volume(self):
        face_area = super(Square, self).area()
        return face_area * self.length

В этом примере вы устанавливаетеSquare в качестве аргумента подкласса дляsuper() вместоCube. Это заставляетsuper() начать поиск подходящего метода (в данном случае.area()) на одном уровне вышеSquare в иерархии экземпляров, в данном случаеRectangle.

В этом конкретном примере поведение не меняется. Но представьте, чтоSquare также реализовал функцию.area(), которую вы хотели убедиться, чтоCube не используется. Вызовsuper() таким образом позволяет вам это сделать.

Caution: Хотя мы много возимся с параметрами дляsuper(), чтобы изучить, как это работает под капотом, я бы не стал делать это регулярно.

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

Как насчет второго параметра? Помните, что это объект, который является экземпляром класса, используемого в качестве первого параметра. Например,isinstance(Cube, Square) должен возвращатьTrue.

Включая экземпляр объекта,super() возвращаетbound method: метод, который привязан к объекту, который дает методу контекст объекта, такой как любые атрибуты экземпляра. Если этот параметр не включен, возвращаемый метод является просто функцией, не связанной с контекстом объекта.

Для получения дополнительной информации о связанных методах, несвязанных методах и функциях прочтите документацию Pythonon its descriptor system.

Note: Техническиsuper() не возвращает метод. Он возвращаетproxy object. Это объект, который делегирует вызовы правильным методам класса, не создавая для этого дополнительного объекта.

super() в множественном наследовании

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

Кратный обзор наследования

Есть еще один вариант использования, в которомsuper() действительно хорош, и он не так распространен, как сценарий с единичным наследованием. В дополнение к одиночному наследованию Python поддерживает множественное наследование, при котором подкласс может наследовать от нескольких суперклассов, которые не обязательно наследуются друг от друга (также известное какsibling classes).

Я очень наглядный человек, и я считаю, что диаграммы невероятно полезны для понимания подобных концепций. На рисунке ниже показан очень простой сценарий множественного наследования, где один класс наследует от двух не связанных (одноуровневых) суперклассов:

Чтобы лучше проиллюстрировать множественное наследование в действии, вот вам пример кода, показывающий, как вы можете построить правильную пирамиду (пирамиду с квадратным основанием) изTriangle иSquare:

class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return 0.5 * self.base * self.height

class RightPyramid(Triangle, Square):
    def __init__(self, base, slant_height):
        self.base = base
        self.slant_height = slant_height

    def area(self):
        base_area = super().area()
        perimeter = super().perimeter()
        return 0.5 * perimeter * self.slant_height + base_area

Note: Терминslant height может быть вам незнаком, особенно если вы давно не посещали уроки геометрии или работали над какими-либо пирамидами.

Высота наклона - это высота от центра основания объекта (например, пирамиды) до его поверхности до вершины этого объекта. Вы можете узнать больше о наклонных высотах вWolframMathWorld.

В этом примере объявляются классTriangle и классRightPyramid, который наследуется как отSquare, так и отTriangle.

Вы увидите другой метод.area(), который используетsuper() так же, как и в одиночном наследовании, с целью достижения методов.perimeter() и.area(), определенных полностью в Rectangle класс.

Note: Вы можете заметить, что приведенный выше код еще не использует какие-либо свойства, унаследованные от классаTriangle. Более поздние примеры будут полностью использовать наследование как отTriangle, так и отSquare.

Проблема, однако, в том, что оба суперкласса (Triangle иSquare) определяют.area(). Уделите секунду и подумайте, что может произойти, когда вы вызовете.area() наRightPyramid, а затем попробуйте вызвать его, как показано ниже:

>>>

>> pyramid = RightPyramid(2, 4)
>> pyramid.area()
Traceback (most recent call last):
  File "shapes.py", line 63, in 
    print(pyramid.area())
  File "shapes.py", line 47, in area
    base_area = super().area()
  File "shapes.py", line 38, in area
    return 0.5 * self.base * self.height
AttributeError: 'RightPyramid' object has no attribute 'height'

Вы догадались, что Python попытается вызватьTriangle.area()? Это из-за того, что называетсяmethod resolution order.

Note: Как мы заметили, что был вызванTriangle.area(), а не, как мы надеялись,Square.area()? Если вы посмотрите на последнюю строку трассировки (передAttributeError), вы увидите ссылку на конкретную строку кода:

return 0.5 * self.base * self.height

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

Порядок разрешения методов

Порядок разрешения методов (илиMRO) сообщает Python, как искать унаследованные методы. Это удобно, когда вы используетеsuper(), потому что MRO сообщает вам, где именно Python будет искать метод, который вы вызываете с помощьюsuper(), и в каком порядке.

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

>>>

>>> RightPyramid.__mro__
(, ,
 , ,
 )

Это говорит нам о том, что методы будут искать сначала вRightpyramid, затем вTriangle, затем вSquare, затем вRectangle, а затем, если ничего не будет найдено, вobject, из которого происходят все классы.

Проблема здесь в том, что интерпретатор ищет.area() вTriangle доSquare иRectangle, а при нахождении.area() вTriangle Python называет его вместо того, который вы хотите. ПосколькуTriangle.area() ожидает наличия атрибутов.height и.base, Python выдаетAttributeError.

К счастью, у вас есть некоторый контроль над тем, как строится MRO. Просто изменив подпись классаRightPyramid, вы можете выполнять поиск в нужном вам порядке, и методы будут разрешены правильно:

class RightPyramid(Square, Triangle):
    def __init__(self, base, slant_height):
        self.base = base
        self.slant_height = slant_height
        super().__init__(self.base)

    def area(self):
        base_area = super().area()
        perimeter = super().perimeter()
        return 0.5 * perimeter * self.slant_height + base_area

Обратите внимание, чтоRightPyramid частично инициализируется.__init__() из классаSquare. Это позволяет.area() использовать.length на объекте, как задумано.

Теперь вы можете построить пирамиду, осмотреть MRO и рассчитать площадь поверхности:

>>>

>>> pyramid = RightPyramid(2, 4)
>>> RightPyramid.__mro__
(, ,
, ,
)
>>> pyramid.area()
20.0

Вы видите, что MRO теперь соответствует вашим ожиданиям, и вы также можете осмотреть область пирамиды благодаря.area() и.perimeter().

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

Это вызывает проблемы с разрешением метода, поскольку будет вызван первый экземпляр.area(), обнаруженный в списке MRO.

Когда вы используетеsuper() с множественным наследованием, обязательно проектируйте свои классы наcooperate. Частично это гарантирует, что ваши методы будут уникальными, чтобы их можно было разрешить в MRO, убедившись, что сигнатуры методов уникальны - будь то с помощью имен методов или параметров методов.

В этом случае, чтобы избежать полного пересмотра кода, вы можете переименовать метод.area() классаTriangle в.tri_area(). Таким образом, методы области могут продолжать использовать свойства класса, а не принимать внешние параметры:

class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height
        super().__init__()

    def tri_area(self):
        return 0.5 * self.base * self.height

Давайте также воспользуемся этим в классеRightPyramid:

class RightPyramid(Square, Triangle):
    def __init__(self, base, slant_height):
        self.base = base
        self.slant_height = slant_height
        super().__init__(self.base)

    def area(self):
        base_area = super().area()
        perimeter = super().perimeter()
        return 0.5 * perimeter * self.slant_height + base_area

    def area_2(self):
        base_area = super().area()
        triangle_area = super().tri_area()
        return triangle_area * 4 + base_area

Следующая проблема заключается в том, что в коде нет делегированного объектаTriangle, как для объектаSquare, поэтому вызов.area_2() даст намAttributeError, поскольку.base и.height не имеют значений.

Вам нужно сделать две вещи, чтобы это исправить:

  1. Все методы, вызываемые с помощьюsuper(), должны иметь вызов версии этого метода суперкласса. Это означает, что вам нужно будет добавитьsuper().__init__() к методам.__init__() дляTriangle иRectangle.

  2. Измените дизайн всех вызовов.__init__(), чтобы они использовали словарь ключевых слов. Смотрите полный код ниже.

В этом коде есть ряд важных отличий:

  • kwargs is modified in some places (such as RightPyramid.__init__()): Это позволит пользователям этих объектов создавать их экземпляры только с аргументами, которые имеют смысл для этого конкретного объекта.

  • Setting up named arguments before **kwargs: Вы можете увидеть это вRightPyramid.__init__(). Это дает аккуратный эффект выталкивания этого ключа прямо из словаря**kwargs, так что к тому времени, когда он окажется в конце MRO в классеobject,**kwargs будет пусто.

Note: Следить за состояниемkwargs здесь может быть непросто, поэтому вот таблица вызовов.__init__() по порядку, показывающая класс, которому принадлежит этот вызов, и содержимоеkwargs во время этого звонка:

Учебный класс Именованные Аргументы kwargs

RightPyramid

base,slant_height

Square

length

base,height

Rectangle

length,width

base,height

Triangle

base,height

Теперь, когда вы используете эти обновленные классы, у вас есть это:

>>>

>>> pyramid = RightPyramid(base=2, slant_height=4)
>>> pyramid.area()
20.0
>>> pyramid.area_2()
20.0

Оно работает! Вы использовалиsuper() для успешной навигации по сложной иерархии классов, одновременно используя наследование и композицию для создания новых классов с минимальной повторной реализацией.

Несколько вариантов наследования

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

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

С помощью композиции вы можете добавить к своим классам очень специфические функции из специального простого класса, называемогоmixin.

Поскольку эта статья посвящена наследованию, я не буду вдаваться в подробности о композиции и о том, как использовать ее в Python, но вот небольшой пример использованияVolumeMixin для придания конкретной функциональности нашим 3D-объектам - в данном случае , расчет объема:

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def area(self):
        return self.length * self.width

class Square(Rectangle):
    def __init__(self, length):
        super().__init__(length, length)

class VolumeMixin:
    def volume(self):
        return self.area() * self.height

class Cube(VolumeMixin, Square):
    def __init__(self, length):
        super().__init__(length)
        self.height = length

    def face_area(self):
        return super().area()

    def surface_area(self):
        return super().area() * 6

В этом примере код был переработан и теперь включает миксинVolumeMixin. Затем миксин используетсяCube и даетCube возможность вычислить его объем, что показано ниже:

>>>

>>> cube = Cube(2)
>>> cube.surface_area()
24
>>> cube.volume()
8

Этот миксин можно использовать таким же образом в любом классе, для которого определена область и для которого формулаarea * height возвращает правильный объем.

Asuper() Резюме

В этом руководстве вы узнали, как зарядить ваши классыsuper(). Ваше путешествие началось с обзора одиночного наследования, а затем было показано, как легко вызывать методы суперкласса с помощьюsuper().

Затем вы узнали, как множественное наследование работает в Python, и методы комбинированияsuper() с множественным наследованием. Вы также узнали о том, как Python разрешает вызовы методов, используя порядок разрешения методов (MRO), а также о том, как проверять и изменять MRO, чтобы обеспечить вызов соответствующих методов в подходящее время.

Дополнительные сведения об объектно-ориентированном программировании на Python и использованииsuper() см. В следующих ресурсах: