Заряжай свои классы с помощью 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
. КлассSquare
inherited.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
не имеют значений.
Вам нужно сделать две вещи, чтобы это исправить:
-
Все методы, вызываемые с помощью
super()
, должны иметь вызов версии этого метода суперкласса. Это означает, что вам нужно будет добавитьsuper().__init__()
к методам.__init__()
дляTriangle
иRectangle
. -
Измените дизайн всех вызовов
.__init__()
, чтобы они использовали словарь ключевых слов. Смотрите полный код ниже.
В этом коде есть ряд важных отличий:
-
kwargs
is modified in some places (such asRightPyramid.__init__()
): Это позволит пользователям этих объектов создавать их экземпляры только с аргументами, которые имеют смысл для этого конкретного объекта. -
Setting up named arguments before
**kwargs
: Вы можете увидеть это вRightPyramid.__init__()
. Это дает аккуратный эффект выталкивания этого ключа прямо из словаря**kwargs
, так что к тому времени, когда он окажется в конце MRO в классеobject
,**kwargs
будет пусто.
Note: Следить за состояниемkwargs
здесь может быть непросто, поэтому вот таблица вызовов.__init__()
по порядку, показывающая класс, которому принадлежит этот вызов, и содержимоеkwargs
во время этого звонка:
Учебный класс | Именованные Аргументы | kwargs |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
Теперь, когда вы используете эти обновленные классы, у вас есть это:
>>>
>>> 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()
см. В следующих ресурсах: