Введение
В этом практическом занятии (лабораторной работе) вы узнаете о поведении наследования в Python. В частности, вы сосредоточитесь на том, как работает функция super() и как реализуется кооперативное наследование. Наследование является фундаментальным концептом в объектно - ориентированном программировании, которое позволяет классам наследовать атрибуты и методы от родительских классов для повторного использования кода и создания иерархических структур классов.
В этом практическом опыте вы поймете различные типы наследования в Python, включая одиночное и множественное наследование. Вы также научитесь использовать функцию super() для навигации по иерархиям наследования, реализовать практический пример кооперативного множественного наследования и применить эти концепции для создания системы валидации. Основной файл, созданный в рамках этого практического занятия, это validate.py.
Понимание одиночного и множественного наследования
В этом шаге мы узнаем о двух основных типах наследования в 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
Разберем, что делает этот код:
- Мы начинаем с импорта валидаторов
PositiveIntegerиPositiveFloatиз модуляvalidate. Эти валидаторы помогут нам убедиться, что количество акций является положительным целым числом, а цена - положительным числом с плавающей точкой. - Затем мы определяем класс
Stock. Внутри класса у нас есть метод__init__. Этот метод вызывается, когда мы создаем новый объектStock. Он принимает три параметра:name,sharesиpriceи присваивает их атрибутам объекта. - Мы используем свойства (properties) и сеттеры (setters) для валидации значений
sharesиprice. Свойство - это способ контролировать доступ к атрибуту, а сеттер - это метод, который вызывается, когда мы пытаемся установить значение этого атрибута. Когда мы устанавливаем атрибутshares, вызывается методPositiveInteger.check, чтобы убедиться, что значение является положительным целым числом. Аналогично, когда мы устанавливаем атрибутprice, вызывается методPositiveFloat.check, чтобы убедиться, что значение является положительным числом с плавающей точкой. - Наконец, у нас есть метод
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() для вызова методов из нескольких родительских классов.
Например, если мы хотим убедиться, что название акции не является пустой строкой, мы можем выполнить следующие шаги:
- Импортировать валидатор
NonEmptyStringиз модуляvalidate. Этот валидатор поможет нам проверить, что название акции не является пустой строкой. - Добавить сеттер свойства для атрибута
nameв классеStock. Этот сеттер будет использовать методNonEmptyString.check()для валидации названия акции.
Это показывает, как наследование, особенно множественное наследование с использованием функции super(), позволяет нам создавать гибкие компоненты, которые можно переиспользовать в разных комбинациях.
После завершения тестирования вы можете выйти из интерпретатора Python, выполнив следующую команду:
exit()
Резюме
В этом практическом занятии вы узнали о поведении наследования в Python и освоили несколько ключевых концепций. Вы изучили разницу между одиночным и множественным наследованием, поняли, как функция super() обходит порядок разрешения методов (Method Resolution Order, MRO), научились реализовывать кооперативное множественное наследование и применили наследование для создания практической системы валидации.
Вы также создали гибкий фреймворк валидации с использованием наследования и применили его к реальному примеру с классом Stock, что показывает, как наследование может создавать переиспользуемые и композируемые компоненты. Основные выводы включают в себя то, как работает функция super() в одиночном и множественном наследовании, способность множественного наследования комбинировать функциональность и использование сеттеров свойств с валидаторами. Эти концепции являются фундаментальными для объектно-ориентированного программирования на Python и широко используются в реальных приложениях.