Поведение наследования

PythonPythonBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

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

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/ControlFlowGroup(["Control Flow"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/ControlFlowGroup -.-> python/conditional_statements("Conditional Statements") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") python/ObjectOrientedProgrammingGroup -.-> python/encapsulation("Encapsulation") subgraph Lab Skills python/conditional_statements -.-> lab-132500{{"Поведение наследования"}} python/classes_objects -.-> lab-132500{{"Поведение наследования"}} python/inheritance -.-> lab-132500{{"Поведение наследования"}} python/encapsulation -.-> lab-132500{{"Поведение наследования"}} end

Понимание одиночного и множественного наследования

В этом шаге мы узнаем о двух основных типах наследования в Python: одиночном наследовании и множественном наследовании. Наследование является фундаментальным концептом в объектно - ориентированном программировании, который позволяет классу наследовать атрибуты и методы от других классов. Мы также рассмотрим, как Python определяет, какой метод вызывать, когда есть несколько кандидатов, процесс, известный как разрешение методов (method resolution).

Одиночное наследование

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

Сначала откройте новый терминал в WebIDE. После открытия терминала запустите интерпретатор Python, введя следующую команду и нажав Enter:

python3

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

class A:
    def spam(self):
        print('A.spam')

class B(A):
    def spam(self):
        print('B.spam')
        super().spam()

class C(B):
    def spam(self):
        print('C.spam')
        super().spam()

В этом коде класс B наследует от класса A, а класс C наследует от класса B. Функция super() используется для вызова метода родительского класса.

После определения этих классов мы можем узнать порядок, в котором Python ищет методы. Этот порядок называется Порядком разрешения методов (Method Resolution Order, MRO). Чтобы увидеть MRO класса C, введите следующий код:

C.__mro__

Вы должны увидеть вывод, похожий на следующий:

(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

Этот вывод показывает, что Python сначала ищет метод в классе C, затем в классе B, затем в классе A и, наконец, в базовом классе object.

Теперь создадим экземпляр класса C и вызовем его метод spam(). Введите следующий код:

c = C()
c.spam()

Вы должны увидеть следующий вывод:

C.spam
B.spam
A.spam

Этот вывод демонстрирует, как работает super() в цепочке одиночного наследования. Когда C.spam() вызывает super().spam(), он вызывает B.spam(). Затем, когда B.spam() вызывает super().spam(), он вызывает A.spam().

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

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

Введите следующий код в интерпретаторе Python:

class Base:
    def spam(self):
        print('Base.spam')

class X(Base):
    def spam(self):
        print('X.spam')
        super().spam()

class Y(Base):
    def spam(self):
        print('Y.spam')
        super().spam()

class Z(Base):
    def spam(self):
        print('Z.spam')
        super().spam()

Теперь мы создадим класс M, который наследует от нескольких родительских классов X, Y и Z. Введите следующий код:

class M(X, Y, Z):
    pass

M.__mro__

Вы должны увидеть следующий вывод:

(<class '__main__.M'>, <class '__main__.X'>, <class '__main__.Y'>, <class '__main__.Z'>, <class '__main__.Base'>, <class 'object'>)

Этот вывод показывает Порядок разрешения методов для класса M. Python будет искать методы в этом порядке.

Создадим экземпляр класса M и вызовем его метод spam():

m = M()
m.spam()

Вы должны увидеть следующий вывод:

X.spam
Y.spam
Z.spam
Base.spam

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

Создадим еще один класс N с родительскими классами в другом порядке:

class N(Z, Y, X):
    pass

N.__mro__

Вы должны увидеть следующий вывод:

(<class '__main__.N'>, <class '__main__.Z'>, <class '__main__.Y'>, <class '__main__.X'>, <class '__main__.Base'>, <class 'object'>)

Теперь создадим экземпляр класса N и вызовем его метод spam():

n = N()
n.spam()

Вы должны увидеть следующий вывод:

Z.spam
Y.spam
X.spam
Base.spam

Это демонстрирует важный концепт: в множественном наследовании Python порядок родительских классов в определении класса определяет Порядок разрешения методов. Функция super() следует этому порядку, независимо из какого класса она вызывается.

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

exit()

Создание системы валидации с использованием наследования

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

Создание базового класса валидатора

Сначала нам нужно создать базовый класс для наших валидаторов. Для этого создадим новый файл в WebIDE. Вот как вы можете сделать это: кликните на "File" > "New File", или вы можете использовать сочетание клавиш. После открытия нового файла назовите его validate.py.

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

## validate.py
class Validator:
    @classmethod
    def check(cls, value):
        return value

В этом коде мы определили класс Validator с методом check. Метод check принимает значение в качестве аргумента и просто возвращает его без изменений. Декоратор @classmethod используется, чтобы сделать этот метод методом класса. Это означает, что мы можем вызывать этот метод на самом классе, не создавая экземпляр класса.

Добавление валидаторов типов

Далее мы добавим несколько валидаторов, которые проверяют тип значения. Эти валидаторы будут наследовать от класса Validator, который мы только что создали. Вернитесь к файлу validate.py и добавьте следующий код:

class Typed(Validator):
    expected_type = object
    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        return super().check(value)

class Integer(Typed):
    expected_type = int

class Float(Typed):
    expected_type = float

class String(Typed):
    expected_type = str

Класс Typed является подклассом Validator. Он имеет атрибут expected_type, который изначально установлен на object. Метод check в классе Typed проверяет, является ли данное значение ожидаемого типа. Если это не так, он вызывает исключение TypeError. Если тип правильный, он вызывает метод check родительского класса с помощью super().check(value).

Классы Integer, Float и String наследуются от Typed и указывают точный тип, который они должны проверять. Например, класс Integer проверяет, является ли значение целым числом.

Тестирование валидаторов типов

Теперь, когда мы создали наши валидаторы типов, давайте протестируем их. Откройте новый терминал и запустите интерпретатор Python, выполнив следующую команду:

python3

После запуска интерпретатора Python мы можем импортировать и протестировать наши валидаторы. Вот некоторый код для их тестирования:

from validate import Integer, String

Integer.check(10)  ## Should return 10

try:
    Integer.check('10')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

String.check('10')  ## Should return '10'

При выполнении этого кода вы должны увидеть что-то вроде этого:

10
Error: Expected <class 'int'>
'10'

Мы также можем использовать эти валидаторы в функции. Давайте попробуем это:

def add(x, y):
    Integer.check(x)
    Integer.check(y)
    return x + y

add(2, 2)  ## Should return 4

try:
    add('2', '3')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

При выполнении этого кода вы должны увидеть:

4
Error: Expected <class 'int'>

Добавление валидаторов значений

До сих пор мы создали валидаторы, которые проверяют тип значения. Теперь добавим несколько валидаторов, которые проверяют само значение, а не его тип. Вернитесь к файлу validate.py и добавьте следующий код:

class Positive(Validator):
    @classmethod
    def check(cls, value):
        if value < 0:
            raise ValueError('Expected >= 0')
        return super().check(value)

class NonEmpty(Validator):
    @classmethod
    def check(cls, value):
        if len(value) == 0:
            raise ValueError('Must be non-empty')
        return super().check(value)

Валидатор Positive проверяет, является ли значение неотрицательным. Если значение меньше 0, он вызывает исключение ValueError. Валидатор NonEmpty проверяет, имеет ли значение ненулевую длину. Если длина равна 0, он вызывает исключение ValueError.

Компоновка валидаторов с использованием множественного наследования

Теперь мы объединим наши валидаторы с использованием множественного наследования. Множественное наследование позволяет классу наследовать от более чем одного родительского класса. Вернитесь к файлу validate.py и добавьте следующий код:

class PositiveInteger(Integer, Positive):
    pass

class PositiveFloat(Float, Positive):
    pass

class NonEmptyString(String, NonEmpty):
    pass

Эти новые классы объединяют проверку типа и проверку значения. Например, класс PositiveInteger проверяет, что значение является целым числом и неотрицательным. Здесь порядок наследования имеет значение. Валидаторы проверяются в порядке, указанном в определении класса.

Тестирование составных валидаторов

Давайте протестируем наши составные валидаторы. В интерпретаторе Python выполните следующий код:

from validate import PositiveInteger, PositiveFloat, NonEmptyString

PositiveInteger.check(10)  ## Should return 10

try:
    PositiveInteger.check('10')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

try:
    PositiveInteger.check(-10)  ## Should raise ValueError
except ValueError as e:
    print(f"Error: {e}")

NonEmptyString.check('hello')  ## Should return 'hello'

try:
    NonEmptyString.check('')  ## Should raise ValueError
except ValueError as e:
    print(f"Error: {e}")

При выполнении этого кода вы должны увидеть:

10
Error: Expected <class 'int'>
Error: Expected >= 0
'hello'
Error: Must be non-empty

Это показывает, как мы можем комбинировать валидаторы, чтобы создать более сложные правила валидации.

После завершения тестирования вы можете выйти из интерпретатора Python, выполнив следующую команду:

exit()
✨ Проверить решение и практиковаться

Применение валидаторов к классу Stock

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

Создание класса Stock

Сначала нам нужно создать новый файл. В WebIDE создайте новый файл с именем stock.py. Этот файл будет содержать код для нашего класса Stock. Теперь добавьте следующий код в файл stock.py:

## stock.py
from validate import PositiveInteger, PositiveFloat

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        self._shares = PositiveInteger.check(value)

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        self._price = PositiveFloat.check(value)

    def cost(self):
        return self.shares * self.price

Разберем, что делает этот код:

  1. Мы начинаем с импорта валидаторов PositiveInteger и PositiveFloat из модуля validate. Эти валидаторы помогут нам убедиться, что количество акций является положительным целым числом, а цена - положительным числом с плавающей точкой.
  2. Затем мы определяем класс Stock. Внутри класса у нас есть метод __init__. Этот метод вызывается, когда мы создаем новый объект Stock. Он принимает три параметра: name, shares и price и присваивает их атрибутам объекта.
  3. Мы используем свойства (properties) и сеттеры (setters) для валидации значений shares и price. Свойство - это способ контролировать доступ к атрибуту, а сеттер - это метод, который вызывается, когда мы пытаемся установить значение этого атрибута. Когда мы устанавливаем атрибут shares, вызывается метод PositiveInteger.check, чтобы убедиться, что значение является положительным целым числом. Аналогично, когда мы устанавливаем атрибут price, вызывается метод PositiveFloat.check, чтобы убедиться, что значение является положительным числом с плавающей точкой.
  4. Наконец, у нас есть метод cost. Этот метод вычисляет общую стоимость акций, умножая количество акций на цену.

Тестирование класса Stock

Теперь, когда мы создали наш класс Stock, нам нужно протестировать его, чтобы убедиться, что валидаторы работают правильно. Откройте новый терминал и запустите интерпретатор Python. Вы можете сделать это, выполнив следующую команду:

python3

После запуска интерпретатора Python мы можем импортировать и протестировать наш класс Stock. Введите следующий код в интерпретатор Python:

from stock import Stock

## Create a valid stock
s = Stock('GOOG', 100, 490.10)
print(f"Name: {s.name}, Shares: {s.shares}, Price: {s.price}")
print(f"Cost: {s.cost()}")

## Try setting an invalid shares value
try:
    s.shares = -10
except ValueError as e:
    print(f"Error setting shares: {e}")

## Try setting an invalid price value
try:
    s.price = "not a price"
except TypeError as e:
    print(f"Error setting price: {e}")

При выполнении этого кода вы должны увидеть вывод, похожий на следующий:

Name: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0
Error setting shares: Expected >= 0
Error setting price: Expected <class 'float'>

Этот вывод показывает, что наши валидаторы работают как ожидалось. Класс Stock не позволяет нам устанавливать недопустимые значения для shares и price. Когда мы пытаемся установить недопустимое значение, возникает ошибка, и мы можем поймать и вывести эту ошибку.

Понимание, как наследование помогает

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

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

  1. Импортировать валидатор NonEmptyString из модуля validate. Этот валидатор поможет нам проверить, что название акции не является пустой строкой.
  2. Добавить сеттер свойства для атрибута name в классе Stock. Этот сеттер будет использовать метод NonEmptyString.check() для валидации названия акции.

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

После завершения тестирования вы можете выйти из интерпретатора Python, выполнив следующую команду:

exit()
✨ Проверить решение и практиковаться

Резюме

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

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