Множественное наследование - это возможность у класса потомка наследовать функционал не от одного, а от нескольких родителей. Благодаря этому мы можем создавать сложные структуры, сохраняя простой и легко-поддерживаемый код.
Во многих языках программирования нет множественного наследования, так что давайте разбираться, как это вообще работает.
Например, у нас есть класс автомобиля:
class Auto:
def ride(self):
print("Riding on ground")
Также у нас есть класс для лодки:
class Boat:
def swim(self):
print("Sailing in the ocean")
Теперь, если нам нужно запрограммировать автомобиль-амфибию, который будет плавать в воде и ездить по земле, мы вместо написания нового класса можем просто унаследовать от уже существующих, просто написав их через запятую:
class Auto:
def ride(self):
print("Riding on a ground")
class Boat:
def swim(self):
print("Sailing in the ocean")
class Amphibian(Auto, Boat):
pass
a = Amphibian()
a.ride()
a.swim()
Теперь наш класс имеет атрибуты и методы обоих родителей (их может быть сколько угодно).
Обратите внимание, что объект класса Amphibian будет одновременно объектом класса Auto и Boat, то есть:
a = Amphibian()
isinstance(a, Auto)
# True
isinstance(a, Boat)
# True
isinstance(a, Amphibian)
# True
Миксин, он же примесь, — это тип классов, которые нужны, чтобы добавлять к обычным классам какие-то методы или атрибуты, но эти классы не используются для создания объектов, только как примесь. (Нас ничего не останавливает создать объект этого класса, но задача в другом)
Представим, что мы программируем класс для автомобиля.
Мы хотим, чтобы у нас была возможность слушать музыку в машине.
Конечно, можно просто добавить метод play_music()
в класс Car
:
class Car:
def ride(self):
print("Riding a car")
def play_music(self, song):
print(f"Now playing: {song}.")
c = Car()
c.ride()
# Riding a car
c.play_music("Queen - Bohemian Rhapsody")
# Now playing: Queen - Bohemian Rhapsody
Но что, если у нас есть еще и телефон, радио или любой другой девайс, с которого мы будем слушать музыку. В таком случае лучше вынести функционал проигрывания музыки в отдельный класс-миксин:
class MusicPlayerMixin:
def play_music(self, song):
print(f"Now playing: {song}.")
Мы можем "домешивать" этот класс в любой, где нужна функция проигрывания музыки:
class Smartphone(MusicPlayerMixin):
pass
class Radio(MusicPlayerMixin):
pass
class Amphibian(Auto, Boat, MusicPlayerMixin):
pass
В рамках изучения Django мы будем довольно много использовать такие классы, рекомендую детально ознакомиться.
Итак, классы-наследники могут использовать родительские атрибуты и методы. Но что, если у нескольких родителей будут одинаковые атрибуты или методы? Какой метод в таком случае будет использовать наследник?
Рассмотрим классический пример:
class A:
def hi(self):
print("A")
class B(A):
def hi(self):
print("B")
class C(A):
def hi(self):
print("C")
class D(B, C):
pass
d = D()
d.hi()
Эта ситуация, так называемое ромбовидное наследование (diamond problem), решается в Python путем установления порядка разрешения методов.
В Python3 для определения порядка используется алгоритм поиска в ширину, то есть сначала интерпретатор будет искать
метод hi()
в классе B, если его там нет - в классе С, потом A. Важно в каком порядке написаны классы для наследования!
В Python второй версии используется алгоритм поиска в глубину, то есть в данном случае - сначала B, потом - А, потом С.
В Python3 можно посмотреть в каком порядке будут проинспектированы родительские классы при помощи метода класса mro()
:
Чтобы посмотреть, в каком порядке Python будет искать атрибуты или методы у родителей, у любого класса можно вызывать
метод mro()
:
>> D.mro()
[ <class '__main__.D'>, < class '__main__.B' >, < class '__main__.C' >, < class '__main__.A' >, < class 'object' >]
Обратите внимание, в конце всегда будет object
, если вы используете любой Python.
Потому что вообще все отнаследовано от него, как я и говорил на прошлом занятии. (Все это объект!)
Если по какой-то причине вас не устраивает существующий порядок, есть возможность вызвать метод ровно из того класса, откуда вам надо, но это считается плохой практикой и лучше так не делать, а полностью поменять структуру.
Если вам необходимо использовать метод конкретного родителя, например, hi()
класса С, нужно напрямую вызвать его по
имени класса, передав self
в качестве аргумента:
# НЕ НАДО ТАК ДЕЛАТЬ!!!
class D(B, C):
def call_hi(self):
C.hi(self)
d = D()
d.call_hi()
Большая статья про МРО и вообще множественное наследование тут
Магические методы, также известные как dunder
методы (от double underscore, тут игра слов, dunder - болван), являются
специальными методами, которые
начинаются и заканчиваются двойным подчеркиванием, например, __init__
. Эти методы позволяют нам настраивать поведение
объектов классов и переопределять встроенные функции и операторы.
Магические методы - это специальные методы, которые позволяют вам переопределить или настроить поведение объектов в Python. Они выполняют специфические задачи и автоматически вызываются при использовании различных операторов и функций.
Магические методы можно условно разделить на несколько категорий:
- Инициализация и удаление объектов:
__init__
,__del__
- Представление объектов:
__str__
,__repr__
- Перегрузка операторов:
__add__
,__sub__
,__mul__
и т.д. - Контейнеры и последовательности:
__len__
,__getitem__
,__setitem__
,__delitem__
,__contains__
- Итерации:
__iter__
,__next__
- Контекстные менеджеры:
__enter__
,__exit__
- Другие методы:
__call__
,__hash__
,__eq__
и многие другие
Метод __init__
вызывается, когда объект класса создается. Он инициализирует объект.
class Person:
def __init__(self, name: str, age: int):
self.name = name
self.age = age
alice = Person("Alice", 30)
bob = Person(name='Bob', age=35)
print(alice.name) # Alice
print(alice.age) # 30
print(bob.name) # Bob
print(bob.age) # 35
Инит есть практически у каждого класса, это очень часто используемый метод
Метод __del__
вызывается перед уничтожением объекта.
class Person:
def __init__(self, name):
self.name = name
def __del__(self):
print(f"Deleting {self.name}")
alice = Person("Alice")
del alice # Deleting Alice
Методы __str__
и __repr__
позволяют вам определить, как объект будет представлен в виде строки.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name}, {self.age} years old"
def __repr__(self):
return f"Person(name={self.name}, age={self.age})"
alice = Person("Alice", 30)
print(str(alice)) # Alice, 30 years old
print(repr(alice)) # Person(name=Alice, age=30)
Используйте __str__()
когда нужно строковое представление объекта для конечных пользователей, акцентируя внимание на
читаемости, а не на полноте. Используйте __repr__()
для создания строки, которая будет интересна разработчикам, стремясь
к точности и однозначности представления.
Что такое перегрузка? Это возможность переопределить действие которое уже существует.
Магические методы позволяют перегружать операторы. Например, метод __add__
перегружает оператор +.
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __str__(self):
return f"Vector({self.x}, {self.y})"
v1 = Vector(2, 3)
v2 = Vector(1, 1)
print(v1 + v2) # Vector(3, 4)
-
Арифметические операторы
__add__(self, other)
: для оператора+
__sub__(self, other)
: для оператора-
__mul__(self, other)
: для оператора*
__truediv__(self, other)
: для оператора/
__floordiv__(self, other)
: для оператора//
__mod__(self, other)
: для оператора%
__pow__(self, other)
: для оператора**
__radd__(self, other)
: для оператора+ (правосторонний)
__rsub__(self, other)
: для оператора- (правосторонний)
__rmul__(self, other)
: для оператора * (правосторонний)`__rtruediv__(self, other)
: для оператора/ (правосторонний)
__rfloordiv__(self, other)
: для оператора// (правосторонний)
__rmod__(self, other)
: для оператора% (правосторонний)
__rpow__(self, other)
: для оператора** (правосторонний)
_iadd__(self, other)
: для оператора+=
__isub__(self, other)
: для оператора-=
__imul__(self, other)
: для оператора*=
__itruediv__(self, other)
: для оператора/=
__ifloordiv__(self, other)
: для оператора//=
__imod__(self, other)
: для оператора%=
__ipow__(self, other)
: для оператора**=
class Number:
def __init__(self, value):
self.value = value
def __add__(self, other):
return Number(self.value + other.value)
def __sub__(self, other):
return Number(self.value - other.value)
def __mul__(self, other):
return Number(self.value * other.value)
def __truediv__(self, other):
return Number(self.value / other.value)
def __floordiv__(self, other):
return Number(self.value // other.value)
def __mod__(self, other):
return Number(self.value % other.value)
def __pow__(self, other):
return Number(self.value ** other.value)
def __str__(self):
return str(self.value)
n1 = Number(10)
n2 = Number(2)
print(n1 + n2) # 12
print(n1 - n2) # 8
print(n1 * n2) # 20
print(n1 / n2) # 5.0
print(n1 // n2) # 5
print(n1 % n2) # 0
print(n1 ** n2) # 100
-
Операторы сравнения
__eq__(self, other)
: для оператора==
__ne__(self, other)
: для оператора!=
__lt__(self, other)
: для оператора<
__le__(self, other)
: для оператора<=
__gt__(self, other)
: для оператора>
__ge__(self, other)
: для оператора>=
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __eq__(self, other):
return self.age == other.age
def __lt__(self, other):
return self.age < other.age
def __str__(self):
return f"{self.name}, {self.age}"
p1 = Person("Alice", 30)
p2 = Person("Bob", 25)
print(p1 == p2) # False
print(p1 > p2) # True
-
Логические операторы
__and__(self, other)
: для оператора&
__or__(self, other)
: для оператора|
__xor__(self, other)
: для оператора^
__invert__(self)
: для оператора~
__rand__(self, other)
: для оператора& (правосторонний)
__ror__(self, other)
: для оператора| (правосторонний)
__rxor__(self, other)
: для оператора^ (правосторонний)
__iand__(self, other)
: для оператора&=
__ior__(self, other)
: для оператора|=
__ixor__(self, other)
: для оператора^=
class Bitwise:
def __init__(self, value):
self.value = value
def __and__(self, other):
return Bitwise(self.value & other.value)
def __or__(self, other):
return Bitwise(self.value | other.value)
def __xor__(self, other):
return Bitwise(self.value ^ other.value)
def __invert__(self):
return Bitwise(~self.value)
def __str__(self):
return str(self.value)
b1 = Bitwise(6) # 110 in binary
b2 = Bitwise(3) # 011 in binary
print(b1 & b2) # 2 (010 in binary)
print(b1 | b2) # 7 (111 in binary)
print(b1 ^ b2) # 5 (101 in binary)
print(~b1) # -7 (two's complement)
-
Смешанные операторы
__neg__(self)
: для оператора унарного минуса-
__pos__(self)
: для оператора унарного плюса+
__abs__(self)
: для функцииabs()
__invert__(self)
: для оператора~
__complex__(self)
: для функцииcomplex()
__int__(self)
: для функцииint()
__float__(self)
: для функцииfloat()
__round__(self, n)
: для функцииround()
__index__(self)
: для функцийhex(), oct(), bin()
__trunc__(self)
: для функцииmath.trunc()
__floor__(self)
: для функцииmath.floor()
__ceil__(self)
: для функцииmath.ceil()
class Number:
def __init__(self, value):
self.value = value
def __neg__(self):
return Number(-self.value)
def __pos__(self):
return Number(+self.value)
def __abs__(self):
return Number(abs(self.value))
def __int__(self):
return int(self.value)
def __float__(self):
return float(self.value)
def __round__(self, n):
return round(self.value, n)
def __index__(self):
return self.value
def __str__(self):
return str(self.value)
n = Number(-5.5)
print(-n) # 5.5
print(+n) # -5.5
print(abs(n)) # 5.5
print(int(n)) # -5
print(float(n)) # -5.5
print(round(n, 1)) # -5.5
print(hex(n)) # -0x5
Методы, такие как __len__
, __getitem__
, __setitem__
, __delitem__
, и __contains__
, позволяют реализовать поведение контейнеров.
class CustomList:
def __init__(self):
self.items = []
def __len__(self):
return len(self.items)
def __getitem__(self, index):
return self.items[index]
def __setitem__(self, index, value):
self.items[index] = value
def __delitem__(self, index):
del self.items[index]
def __contains__(self, item):
return item in self.items
cl = CustomList()
cl.items.append(1)
cl.items.append(2)
print(len(cl)) # 2
print(cl[0]) # 1
cl[1] = 3
print(cl[1]) # 3
del cl[0]
print(len(cl)) # 1
print(3 in cl) # True
Для создания итераторов в Python используются методы __iter__
и __next__
.
О том как это работает, вас ждет целая лекция ближе к концу курса!
class Counter:
def __init__(self, max):
self.max = max
self.current = 0
def __iter__(self):
return self
def __next__(self):
if self.current >= self.max:
raise StopIteration
self.current += 1
return self.current
c = Counter(3)
for number in c:
print(number) # 1 2 3
Методы __enter__
и __exit__
позволяют использовать объекты в контексте with.
class ManagedFile:
def __init__(self, filename):
self.filename = filename
def __enter__(self):
self.file = open(self.filename, 'w')
return self.file
def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()
with ManagedFile('test.txt') as f:
f.write('Hello, world!')
Метод __call__
позволяет сделать объект вызываемым, как функцию.
class Greeter:
def __init__(self, name):
self.name = name
def __call__(self, greeting):
return f"{greeting}, {self.name}!"
g = Greeter("Alice")
print(g("Hello")) # Hello, Alice!
Это свойство мы будем использовать на занятии по декораторам!
__hash__
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __hash__(self):
return hash((self.name, self.age))
p1 = Person("Alice", 30)
p2 = Person("Alice", 30)
print(hash(p1) == hash(p2)) # True
Это далеко не полный список того что можно делать с классами, меджик методов гораздо больше. Все что вам надо знать, это что на любое действие уже существует заготовка. Нужно только найти что именно переписывать.
Программирование часто требует работы с различными типами ошибок и исключений. В Python существует множество встроенных исключений, таких как ValueError
, TypeError
и IndexError
, которые помогают обрабатывать различные виды ошибок. Однако иногда возникает необходимость создать собственные исключения для более точной и специфичной обработки ошибок в вашей программе. В этой статье мы рассмотрим, как создавать и использовать собственные исключения в Python.
Собственные исключения позволяют:
- Улучшить читабельность кода: Вы можете использовать описательные имена исключений, чтобы было ясно, какая ошибка произошла и почему.
- Обеспечить точность обработки ошибок: Вы можете точно указать, какие ошибки должны быть перехвачены и обработаны.
- Создавать многоуровневую иерархию ошибок: Позволяет создавать базовые и специализированные исключения для более гибкой обработки ошибок.
Для создания собственного исключения в Python, необходимо создать новый класс, который наследует от базового класса исключений, обычно это Exception
.
class MyCustomError(Exception):
"""Класс для пользовательского исключения."""
class InvalidInputError(Exception):
"""Исключение вызывается, когда ввод недействителен."""
def __init__(self, message, value):
self.message = message
self.value = value
super().__init__(self.message)
def __str__(self):
return f'{self.message}: {self.value}'
После создания собственного исключения, вы можете использовать его в своем коде, как и любое другое встроенное исключение.
def divide(a, b):
if b == 0:
raise InvalidInputError("Деление на ноль", b)
return a / b
try:
result = divide(10, 0)
except InvalidInputError as e:
print(e)
Создание иерархии исключений может быть полезным, если ваш код может генерировать различные виды ошибок, которые имеют общие черты.
class ApplicationError(Exception):
"""Базовый класс для всех исключений приложения."""
pass
class DatabaseError(ApplicationError):
"""Исключения, связанные с базой данных."""
pass
class FileNotFoundError(ApplicationError):
"""Исключения, связанные с отсутствием файла."""
pass
def connect_to_database():
raise DatabaseError("Не удалось подключиться к базе данных")
try:
connect_to_database()
except ApplicationError as e:
print(f"Произошла ошибка приложения: {e}")
except DatabaseError as e:
print(f"Ошибка базы данных: {e}")
Практика:
- К созданному на прошлом занятии классу студент, задаем ему имя, возраст и оценки, через
__init__
- Добавляем метод для добавления оценки
- Добавляем метод(ы) вычисления среднего балла
- Прописываем меджик метод (или методы) которые позволяют найти студента с наилучшим средним балом из списка
- Берем класс группы из прошлого занятия
- Добавляем возможность добавить студента к группе
- Добавляем возможность удалить студента из группы
- Добавляем возможность найти группу в которой учится студент с самым высоким средним баллом