Понимание наследования классов в Python 3

Вступление

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

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

Что такое наследование?

Inheritance - это когда класс использует код, созданный внутри другого класса. Если мы думаем о наследовании с точки зрения биологии, мы можем думать о ребенке, унаследовавшем определенные черты от своего родителя. То есть ребенок может наследовать рост или цвет глаз родителя. Дети также могут иметь одинаковую фамилию со своими родителями.

Классы, называемыеchild classes илиsubclasses, наследуют методы и переменные отparent classes илиbase classes.

Мы можем представить себе родительский класс с именемParent, у которого естьclass variables дляlast_name,height иeye_color, которые наследует дочерний классChild изParent.

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

Родительские классы

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

Допустим, у нас есть общий родительский классBank_account, у которого есть дочерние классыPersonal_account иBusiness_account. Многие методы между личными и корпоративными счетами будут похожи, например методы снятия и внесения денег, поэтому они могут принадлежать к родительскому классуBank_account. ПодклассBusiness_account будет иметь специфические для него методы, включая, возможно, способ сбора бизнес-записей и форм, а также переменнуюemployee_identification_number.

Точно так же классAnimal может иметь методыeating() иsleeping(), а подклассSnake может включать свои собственные специфические методыhissing() иslithering().

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

Мы создадим новый файл с именемfish.py и начнем с__init__() constructor method, который мы заполним переменными классаfirst_name иlast_name для каждого объектаFish. или подкласс.

fish.py

class Fish:
    def __init__(self, first_name, last_name="Fish"):
        self.first_name = first_name
        self.last_name = last_name

Мы инициализировали нашу переменнуюlast_name строкой"Fish", потому что мы знаем, что у большинства рыбок будет эта фамилия.

Давайте также добавим некоторые другие методы:

fish.py

class Fish:
    def __init__(self, first_name, last_name="Fish"):
        self.first_name = first_name
        self.last_name = last_name

    def swim(self):
        print("The fish is swimming.")

    def swim_backwards(self):
        print("The fish can swim backwards.")

Мы добавили методыswim() иswim_backwards() в классFish, чтобы каждый подкласс также мог использовать эти методы.

Поскольку большинство рыб, которые мы будем создавать, считаютсяbony fish (так как у них скелет сделан из кости), а неcartilaginous fish (поскольку у них скелет сделан из хряща) , мы можем добавить еще несколько атрибутов к методу__init__():

fish.py

class Fish:
    def __init__(self, first_name, last_name="Fish",
                 skeleton="bone", eyelids=False):
        self.first_name = first_name
        self.last_name = last_name
        self.skeleton = skeleton
        self.eyelids = eyelids

    def swim(self):
        print("The fish is swimming.")

    def swim_backwards(self):
        print("The fish can swim backwards.")

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

Детские классы

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

Например, дочерний классGoldfish, который является подклассом классаFish, сможет использовать методswim(), объявленный вFish, без необходимости его объявления.

Мы можем думать о каждом дочернем классе как о классе родительского класса. То есть, если у нас есть дочерний класс с именемRhombus и родительский класс с именемParallelogram, мы можем сказать, что aRhombusis aParallelogram, как и Goldfishis aFish.

Первая строка дочернего класса выглядит немного иначе, чем не дочерние классы, так как вы должны передать родительский класс в дочерний класс в качестве параметра:

class Trout(Fish):

КлассTrout является дочерним по отношению к классуFish. Мы знаем это благодаря включению в скобки словаFish.

С дочерними классами мы можем добавить дополнительные методы, переопределить существующие родительские методы или просто принять родительские методы по умолчанию с ключевым словомpass, что мы и сделаем в этом случае:

fish.py

...
class Trout(Fish):
    pass

Теперь мы можем создать объектTrout без необходимости определять какие-либо дополнительные методы.

fish.py

...
class Trout(Fish):
    pass

terry = Trout("Terry")
print(terry.first_name + " " + terry.last_name)
print(terry.skeleton)
print(terry.eyelids)
terry.swim()
terry.swim_backwards()

Мы создали объектTroutterry, который использует каждый из методов классаFish, хотя мы не определили эти методы в дочернем классеTrout. Нам нужно было только передать значение"Terry" переменнойfirst_name, потому что все другие переменные были инициализированы.

Когда мы запустим программу, мы получим следующий вывод:

OutputTerry Fish
bone
False
The fish is swimming.
The fish can swim backwards.

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

fish.py

...
class Clownfish(Fish):

    def live_with_anemone(self):
        print("The clownfish is coexisting with sea anemone.")

Затем давайте создадим объектClownfish, чтобы увидеть, как это работает:

fish.py

...
casey = Clownfish("Casey")
print(casey.first_name + " " + casey.last_name)
casey.swim()
casey.live_with_anemone()

Когда мы запустим программу, мы получим следующий вывод:

OutputCasey Fish
The fish is swimming.
The clownfish is coexisting with sea anemone.

Выходные данные показывают, что объектClownfishcasey может использовать методыFish__init__() иswim(), а также метод своего дочернего классаlive_with_anemone()с.

Если мы попытаемся использовать методlive_with_anemone() в объектеTrout, мы получим ошибку:

Outputterry.live_with_anemone()
AttributeError: 'Trout' object has no attribute 'live_with_anemone'

Это связано с тем, что методlive_with_anemone() принадлежит только дочернему классуClownfish, а не родительскому классуFish.

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

Переопределение родительских методов

До сих пор мы рассматривали дочерний классTrout, который использовал ключевое словоpass для наследования всего поведения родительского классаFish, и еще один дочерний классClownfish, который унаследовал все поведения родительского класса, а также создал свой собственный уникальный метод, специфичный для дочернего класса. Однако иногда нам захочется использовать некоторые из поведений родительского класса, но не все. Когда мы меняем методы родительского класса, мыoverrideих.

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

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

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

В свете этого мы переопределим метод конструктора__init__() и методswim_backwards(). Нам не нужно изменять методswim(), поскольку акулы - это рыба, которая умеет плавать. Давайте посмотрим на этот дочерний класс:

fish.py

...
class Shark(Fish):
    def __init__(self, first_name, last_name="Shark",
                 skeleton="cartilage", eyelids=True):
        self.first_name = first_name
        self.last_name = last_name
        self.skeleton = skeleton
        self.eyelids = eyelids

    def swim_backwards(self):
        print("The shark cannot swim backwards, but can sink backwards.")

Мы переопределили инициализированные параметры в методе__init__(), так что переменнаяlast_name теперь установлена ​​равной строке"Shark", переменнаяskeleton теперь установлена ​​равной строка"cartilage", а для переменнойeyelids теперь установлено логическое значениеTrue. Каждый экземпляр класса также может переопределять эти параметры.

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

Теперь мы можем создать экземпляр дочернего классаShark, который по-прежнему будет использовать методswim() родительского классаFish:

fish.py

...
sammy = Shark("Sammy")
print(sammy.first_name + " " + sammy.last_name)
sammy.swim()
sammy.swim_backwards()
print(sammy.eyelids)
print(sammy.skeleton)

Когда мы запустим этот код, мы получим следующий вывод:

OutputSammy Shark
The fish is swimming.
The shark cannot swim backwards, but can sink backwards.
True
cartilage

Дочерний классShark успешно переопределил методы__init__() иswim_backwards() родительского классаFish, а также унаследовал методswim() родительского класса.

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

Функцияsuper()

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

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

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

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

Чтобы увидеть, как это работает, давайте изменим наш дочерний классTrout. Поскольку форель обычно является пресноводной рыбой, давайте добавим переменнуюwater к методу__init__() и установим ее равной строке"freshwater", но затем сохраним остальные переменные и параметры родительского класса:

fish.py

...
class Trout(Fish):
    def __init__(self, water = "freshwater"):
        self.water = water
        super().__init__(self)
...

Мы переопределили метод__init__() в дочернем классеTrout, предоставив другую реализацию__init__(), которая уже определена его родительским классомFish. В методе__init__() нашего классаTrout мы явно вызвали метод__init__() классаFish.

Поскольку мы переопределили метод, нам больше не нужно передаватьfirst_name в качестве параметра вTrout, и если бы мы действительно передали параметр, мы бы вместо этого сбросилиfreshwater. Поэтому мы инициализируемfirst_name, вызывая переменную в нашем экземпляре объекта.

Теперь мы можем вызывать инициализированные переменные родительского класса, а также использовать уникальную дочернюю переменную. Давайте использовать это в экземпляреTrout:

fish.py

...
terry = Trout()

# Initialize first name
terry.first_name = "Terry"

# Use parent __init__() through super()
print(terry.first_name + " " + terry.last_name)
print(terry.eyelids)

# Use child __init__() override
print(terry.water)

# Use parent swim() method
terry.swim()
OutputTerry Fish
False
freshwater
The fish is swimming.

Выходные данные показывают, что объектterry дочернего классаTrout может использовать как специфичную для ребенка переменную__init__()water, так и возможность вызоваFish родительские переменные__init__() дляfirst_name,last_name иeyelids.

Встроенная функция Pythonsuper() позволяет нам использовать методы родительского класса даже при переопределении определенных аспектов этих методов в наших дочерних классах.

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

Multiple inheritance - это когда класс может наследовать атрибуты и методы более чем одного родительского класса. Это может позволить программам уменьшить избыточность, но может также внести определенную сложность, а также двусмысленность, так что это должно быть сделано с учетом общего дизайна программы.

Чтобы показать, как работает множественное наследование, давайте создадим дочерний классCoral_reef, а не унаследованный от классаCoral и классаSea_anemone. Мы можем создать метод в каждом из них, а затем использовать ключевое словоpass в дочернем классеCoral_reef:

coral_reef.py

class Coral:

    def community(self):
        print("Coral lives in a community.")


class Anemone:

    def protect_clownfish(self):
        print("The anemone is protecting the clownfish.")


class CoralReef(Coral, Anemone):
    pass

КлассCoral имеет метод под названиемcommunity(), который печатает одну строку, а классAnemone имеет метод под названиемprotect_clownfish(), который печатает другую строку. Затем мы вызываем оба класса в наследованиеtuple. Это означает, чтоCoral наследуется от двух родительских классов.

Давайте теперь создадим экземпляр объектаCoral:

coral_reef.py

...
great_barrier = CoralReef()
great_barrier.community()
great_barrier.protect_clownfish()

Объектgreat_barrier установлен как объектCoralReef и может использовать методы обоих родительских классов. Когда мы запустим программу, мы увидим следующий вывод:

OutputCoral lives in a community.
The anemone is protecting the clownfish.

Вывод показывает, что методы из обоих родительских классов эффективно использовались в дочернем классе.

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

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

Заключение

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

Наследование в объектно-ориентированном кодировании может позволить придерживаться СУХОГО (не повторяйся) принципа разработки программного обеспечения, что позволяет делать больше с меньшими затратами кода и повторений. Наследование также заставляет программистов задуматься о том, как они проектируют создаваемые ими программы, чтобы обеспечить эффективность и ясность кода.