Знание принципов объектно-ориентированного программирования позволяет сделать код более гибким и читаемым, а также избегать повтора. С помощью классов можно создавать даже собственные типы данных. Давайте начнем с основного синтаксиса.
Самый простой класс можно создать с помощью ключевого слова class
:
class MyClass:
pass
Ключевое слово pass
означает, что класс будет пустым, его также можно применять и в функциях.
Как видите, мы создали самый простой класс.
Метод - это обычная функция, которая находится внутри класса. У метода всегда первым аргументом является self
- ==экземпляр класса==. Экземпляр класса - это конкретное воплощение класса, которое имеет свои уникальные значения для атрибутов данных и методов.
Существуют особые ==магические методы== (их еще называют dunder methods, сокр. от double underscores), которые позволяют влиять на особый функционал класса. Все они имеют двойные нижние подчеркивания по бокам.
Одним из таких является метод __init__
. Он влияет на то, что будет происходить при создании экземпляра класса. Давайте создадим его внутри нашего класса.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
Давайте теперь создадим экземпляр класса Person
:
walter = Person("Walter", 52)
Новый экземпляр класса Person
был успешно создан.
Магические методы служат для того, чтобы настроить работу со встроенными средствами языка, такими как, например, операторы, реализация протоколов и т.д.
Давайте создадим обычный метод:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def hello(self):
print("Hello")
Теперь мы берем наш экземпляр walter
и вызываем метод hello
:
walter.hello()
Все работает, сообщение Hello
напечаталось
Атрибуты - это переменные, которые присваиваются экземплярам класса. Мы можем получать доступ к атрибутам вот так:
walter.age #52
walter.name #Walter
Также можно создавать атрибуты вот так:
class Person:
type = "human"
Теперь у нас есть атрибут type
. Его можно было объявить и в методе __init__
:
def __init__(self, name, age):
self.name = name
self.age = age
self.type = "human"
Еще можно добавлять атрибуты прямо экземплярам класса:
walter.surename = "White"
С двумя подчеркиваниями могут быть еще и атрибуты, например,
self.__class__.__name__
является атрибутом, который хранит имя класса.
Атрибутам присваивается новое значение с помощью оператора присваивания =
:
walter.age = 50
Теперь атрибуту age
присвоено значение 50
. Таким же образом можно менять и все остальные атрибуты.
Атрибуты удаляются с помощью ключевого слова del
:
del walter.age
Если попытаться обратиться к этому атрибуту, то будет получена ошибка:
Traceback (most recent call last):
File "D:\...\main.py", line 9, in <module>
print(walter.age)
^^^^^^^^^^
AttributeError: 'Person' object has no attribute 'age'
Сообщение говорит, что такого атрибута нет, а значит удаление прошло удачно.
Обращение к атрибутам всегда происходит через self
, Например, чтобы обратиться к атрибуту age
внутри какого-либо метода в классе, нужно написать self.age
. Давайте создадим метод say_hello
и заодно научимся вызывать методы.
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def say_hello(self):
print(f"Hello, i'm {self.name} and i'm {self.age} yo")
walter = Person("Walter", 52)
walter.say_hello()
Программа работает как ожидалось:
Hello, i'm Walter and i'm 52 yo
Наследование позволяет создать гибкую структуру классов, при которой один является надстройкой над другим. Класс, от которого наследуется другой класс, называется родительским или суперклассом. А класс, который наследуется, называется дочерним или подклассом. Я создам пару классов, чтобы показать как это работает:
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def say_hello(self):
print(f"Hello, i'm {self.name} and i'm {self.age} yo")
class ChildOfPerson(Person):
def __init__(self, name, age, toys):
super().__init__(name, age)
self.toys = toys
Теперь создадим экземпляр класса ChildofPerson
:
james = ChildOfPerson('James', 8, ["lego", "ps5"])
И проверим все ли на месте и ко всему ли мы имеем доступ:
print(james.name)
print(james.age)
print(james.toys)
james.say_hello()
И имеем такой вывод:
James
8
['lego', 'ps5']
Hello, i'm James and i'm 8 yo
Все работает, но как? Давайте вернемся к коду выше. Метод __init__()
класса ChildOfPerson
устроен так:
def __init__(self, name, age, toys):
Передаем все те же аргументы, что и в __init__
класса Person
, но дополнительно добавляем аргумент toys
.
super().__init__(name, age)
Здесь мы вызываем метод __init__
родительского класса и передаем в него аргументы. Мы должны обращаться к родительскому классу как super()
.
self.toys = toys
Тут мы просто прописываем еще один атрибут для класса.
Таким образом мы расширили родительский класс, модифицировав метод __init__
добавлением нового атрибута. Так можно поступать с любыми методами.
Множественное наследование - подход, при котором у класса есть несколько родительских классов. Например, базовый пример выглядит вот так:
class Biological:
def live(self):
print('Breathe, eat, sleep')
class Social:
def communicate(self):
print('I can speak')
class Human(Biological, Social):
def __init__(self, name) -> None:
self.name = name
bob = Human('Bob')
bob.live()
bob.communicate()
Как и ожидалось, вывод такой:
Breathe, eat, sleep
I can speak
__mro__
Существует такой атрибут - __mro__
. Он определяет порядок разрешения методов. Например, для класса Human
этот атрибут будет таким:
print(Human.__mro__)
(<class '__main__.Human'>, <class '__main__.Biological'>, <class '__main__.Social'>, <class 'object'>)
Как мы видим, все понятно, ничего сложного. Если мы обращаемся к какому-то методу класса Human
, то он сначала ищется в самом классе, а затем в Biological
а затем в Social
. Этот пример довольно простой и поэтому не должно возникать никаких сложностей. Но бывает так, что возникает конфликт имен, об этом ниже.
Эта проблема возникает, когда два или более родительских класса имеют общий базовый класс, и класс-наследник наследует от этих родительских классов. Получается конструкция в виде ромба. В результате может возникнуть неопределенность относительно того, какой метод или атрибут должен быть использован.
class Root:
def ping(self):
print(f'{self}.ping() in Root')
def pong(self):
print(f'{self}.pong() in Root')
def __repr__(self):
cls_name = type(self).__name__
return f'<instance of {cls_name}>'
class A(Root):
def ping(self):
print(f'{self}.ping() in A')
super().ping()
def pong(self):
print(f'{self}.pong() in A')
super().pong()
class B(Root):
def ping(self):
print(f'{self}.ping() in B')
super().ping()
def pong(self):
print(f'{self}.pong() in B')
class Leaf(A, B):
def ping(self):
print(f'{self}.ping() in Leaf')
super().ping()
leaf1 = Leaf()
leaf1.ping()
print('*' * 40)
leaf1.pong()
Магический метод
__repr__
нужен для строкового представления, когда мы передаем экземпляр класса вprint()
. Есть другой метод -__str__
, но там есть некоторые отличия.
При запуске этого кода получается такой вывод.
<instance of Leaf>.ping() in Leaf
<instance of Leaf>.ping() in A
<instance of Leaf>.ping() in B
<instance of Leaf>.ping() in Root
****************************************
<instance of Leaf>.pong() in A
<instance of Leaf>.pong() in B
В этом примере класс Leaf
наследуется от классов A
и B
, которые оба наследуются от класса Root
. Когда мы вызываем leaf1.ping()
или leaf1.pong()
, возникает вопрос: какой метод будет вызван — метод из A
или метод из B
.
Python решает проблему ромбовидного наследования с помощью Метода разрешения порядка (MRO), который я вам показывал ранее. MRO определяет порядок, в котором Python ищет методы и атрибуты в иерархии классов. В Python используется алгоритм C3 Linearization, который обеспечивает последовательный и предсказуемый порядок поиска.
ООП - обширная тема, которая требует длительного изучения. На этом сайте будут выпускаться еще статьи, которые позволят разобраться в этом. Сохраните этот сайт в закладки и делитесь им с друзьями.
Для написания сложных программ необходимо использовать сторонние библиотеки. Они позволяют использовать готовый функционал. Также можно использовать модули для компоновки вашего приложения.
В Python 3.10 был добавлен новый функционал - сопоставление с шаблонами с помощью ключевых слов match и case. В статье также рассматривается производительность match/case по сравнению с if/else.
В этой статье описано, что означают различные классы исключений. Также рассмотрено создание собственных классов исключений.
Обработка исключений — это важная часть программирования на языке Python, которая позволяет программе продолжать выполнение даже в случае возникновения ошибок. В этой статье мы рассмотрим основные принципы и методы обработки исключений, а также узнаем, почему они так важны для создания надежных и стабильных программ.