В Python классы являются фундаментальным понятием. Это основа стандартной библиотеки, работы большинства популярных программ и самого языка. Если вы хотите стать больше, чем просто начинающим программистом, вы должны понимать суть и принцип работы с классами и объектами.
Что такое классы
Это базовый программный компонент ООП. В Python классы используются для реализации новых типов объектов и создаются с помощью специальной инструкции class. Внешне они напоминают стандартные встроенные виды данных, такие как числа или последовательности. Но у объектов класса есть существенное различие – поддержка наследования.
Объектно-ориентированное программирование в Python полностью базируется на иерархическом наследовании классов. Это универсальный способ адаптации и многократного использования кода. Но объектно-ориентированный подход не является обязательным. Python без проблем допускает исключительно процедурное и функциональное программирование.
Главная задача классов в Python – упаковка данных и исполняемого кода. Синтаксически они похожи на инструкции def. Подобно функциям, они создают свои пространства имен, которые можно неоднократно вызывать из любой части программы. Зачем же тогда они нужны? Классы – это более мощный и универсальный инструмент. Сильнее всего их потенциал раскрывается в момент создания новых объектов.
Важность классов и принцип наследования
У каждого нового объекта есть свое пространство имен, которое можно программировать, вводить переменные и создавать любые функции. А также есть атрибуты, унаследованные от класса: object.attribute. В этом заключается смысл ООП.
Благодаря наследованию, создается древо иерархии. На практике это выглядит следующим образом. Когда интерпретатор встречает выражение object.attribute, он начинает искать первое вхождение attribute в указанном class. Не обнаружив attribute, интерпретатор продолжает поиск во всех связанных классах, находящихся в дереве выше, по направлению слева направо.
В древо поиска входят:
- суперклассы, которые находятся на самом верху иерархии и реализуют общее поведение;
- подклассы – находятся ниже;
- экземпляры – элементы программы с унаследованным поведением.
- Instance2;
- Class1;
- Class2;
- Class3.
Имя .w ,будет найдено в суперклассе Class3. На терминологии ООП – это означает, что Instance 2 «наследует» атрибут .w от Class3.
Обратите внимание, что экземпляры на рисунке наследуют только четыре атрибута: .w, .x, .y и .z:
- Для экземпляров Instance1.x и Instance2.x атрибут .x будет найден в Class 1, где поиск остановится, потому что Class 1 находится в дереве ниже, чем Class 2.
- Для Instance1.y и Instance2.y атрибут .y будет найден в Class 1, где поиск остановится, потому что это единственное место, где он появляется.
- Для экземпляров Instance1.z и Instance2.z интерпретатор найдет .z в Class 2, потому что он располагается в дереве левее, чем Class3.
- Для Instance2.name атрибут .name будет найден в Instance2 без поиска по дереву.
Предпоследний пункт является самым важным. Он демонстрирует, как Class 1 переопределяет атрибут .x, замещая версию .x суперкласса Class 2.
Объекты, экземпляры и методы
ООП оперирует двумя главными понятиями: классы и объекты. Классы создают новые типы, а объекты классов в Python являются их экземплярами. Например, все целочисленные переменные относятся к встроенному типу данных int. На языке ООП они являются экземплярами класса int.
Классы создаются инструкциями, а объекты с помощью вызовов. Они могут хранить данные и обладать своим функционалом или методами классов. В Python терминология играет важную роль. С ее помощью программисты отличают независимые функции от тех, что принадлежат классам. Переменные, относящиеся к объектам, называют полями.
Различают два вида полей в ООП. Первый – это переменные, принадлежащие целому классу, второй – переменные отдельных экземпляров. Поля и методы вместе являются атрибутами класса. В Python они записываются в блоке кода после ключевого слова class.
Методы и значение self
Методы – это функции с дополнительным именем self. Оно добавляется к началу списка параметров. При желании переменную можно назвать другим именем, но такая инициатива среди программистов не приветствуется. Self – это стандартное, легко узнаваемое в коде имя. Тем более на работу с ним рассчитаны некоторые среды разработки.
Чтобы лучше понять значение self в ООП, представим, что у нас есть класс с именем ClassA и методом methodA:
- >>>class ClassA;
- def methodA (self, аргумент1, аргумент2).
Объект objectA является экземпляром ClassA и вызов метода выглядит следующим образом:
- >>>objectA.methodA(аргумент1, аргумент2).
Когда интерпретатор видит эту строчку, он ее автоматически преобразует следующим образом: ClassA.methodA(objectA, аргумент1, аргумент2). То есть экземпляр класса использует переменную self как ссылку на самого себя.
Как создавать переменные, методы и экземпляры классов
Предлагаем разобрать практический пример из интерактивной оболочки Python. Создание класса «ЭкспериментПервый» начинается с составной инструкции class:
- >>>class ЭкспериментПервый:
- def setinf(self, значение): #создаем метод первый с аргументами
- self.data = значение
- def display(self): #метод второй
- print(self.data) #напечатать данные экземпляра.
После обязательного отступа следует блок с вложенными инструкциями def, в которых двум объектам функций присваиваются имена setinf и display. С их помощью создаются атрибуты ЭкспериментПервый.setinf и ЭкспериментПервый.display. Фактически любое имя, которому присваивается значение на верхнем уровне во вложенном блоке, становится атрибутом.
Чтобы увидеть, как работают методы, необходимо создать два экземпляра:
- >>>x = ЭкспериментПервый () # Создаются два экземпляра;
- >>>y = ЭкспериментПервый () # Каждый является отдельным пространством имен.
Изначально экземпляры не хранят никакой информации и абсолютно пусты. Но они связанны со своим классом:
- >>>x.setinf(«Учим Питон») #Вызов метода, в котором self – это x.
- >>>y.setinf(3.14) #Эквивалентно: ЭкспериментПервый.setinf(y, 3.14)
Если через имя экземпляров x, y обратиться к атрибуту .setinf объекта класса ЭкспериментПервый, то в результате поиска по дереву наследования интерпретатор возвращает значение атрибута класса.
- >>>x.display() #У x и y свои значения self.data
- Учим Питон
- >>>y.display()
- 3.14.
Перегрузка операторов
В языке Python классы могут перегружать операторы выражений. Такая возможность делает экземпляры похожими на встроенные типы данных. Процесс заключается в реализации методов со специальными именами, начинающимися и заканчивающимися двойным подчеркиванием.
Рассмотрим в действии __init__ и __sub__. Первый метод называют конструктором класса. В Python __init__ выполняет перегрузку операции создания экземпляров. Второй метод __sub__ реализует операцию вычитания.
- >>>class Перегрузка: #создается новый класс
- def __init__(self, start):
- self.data = start
- def __sub__(self, other): # экземпляр минус other
- return Перегрузка(self.data - other) #Результатом будет новый экземпляр
- >>>A = Перегрузка(10) #__init__(A, 10)
- >>>B = A – 2 #__sub__(B, 2)
- >>>B.data #B – это новый экземпляр класса Перегрузка
- 8.
Подробнее о методе __init__
Метод __init__ используется чаще всего при работе с классами. Он незаменим для инициализации различных объектов. __init__ не нужно отдельно вызывать. При создании нового экземпляра метод автоматически получает аргументы, указанные в скобках.
С помощью методов перегрузки можно реализовать любые операции со встроенными типами данных. Большинство используются только при решении специальных задач, в которых необходимо, чтобы объекты имитировали поведение стандартных объектов.
Методы наследуются от суперклассов и не являются обязательными. На начальных этапах можно легко без них обойтись. Но для полного погружения в программирование и суть ООП нужен навык работы с операторами.
Метод __getitem__
Метод __getitem__ выполняет перегрузку доступа к элементу по индексу. Если он наследуется или присутствует в определении класса, то при каждой операции индексирования интерпретатор будет вызывать его автоматически. Например, когда экземпляр F появляется в выражении извлечения элемента по индексу, таком как F[i], интерпретатор Python вызывает метод __getitem__, передает объект F в первом аргументе и индекс, указанный в квадратных скобках, во втором.
Следующий класс «ПримерИндексации» возвращает квадрат значения индекса:
- >>>class ПримерИндексации:
- def __getitem__(self, index):
- return index ** 2
- >>>F = ПримерИндексации ()
- >>>F[2] #Выражение F[i] вызывает F.__getitem__(i)
- 4
- >>>for i in range(5):
- print(F[i], end=« ») # Вызывает __getitem__(F, i) в каждой итерации
- 0 1 4 9 16
С помощью этого же метода можно выполнить операцию извлечения среза, к которой часто прибегают во время работы с последовательностями. При обработке списков стандартный синтаксис операции выглядит следующим образом:
- >>>Список = [13, 6, «и», «с», 74,9]
- >>>Список[2:4]
- [«и», «с»]
- >>>Список[1:]
- [6, «и», «с», 74,9]
- >>>Список[:-1]
- [13, 6, «и», «с»]
- >>>Список[::2]
- [13, «и», 74,9]
Класс, реализующий метод __getitem__:
- >>>class Индексатор:
- мой_список = [13, 6, «и», «с», 74,9]
- def __getitem__(self, индекс): #Вызывается при индексировании или извлечении среза
- print(«getitem: », индекс)
- return self.мой_список[индекс] #Выполняет индексирование или извлекает срез
- >>>X = Индексатор()
- >>>X[0] #При индексировании __getitem__ получает целое число
- getitem: 0
- 13
- >>>X[2:4] # При извлечении среза __getitem__ получает объект среза
- getitem: slice(2, 4, None)
- [«и», «с»]
Обращения к атрибутам
Для получения ссылки на атрибут используется специальный метод __getattr__. Он вызывается с именем атрибута в виде строки в случаях обнаружения попытки получить ссылку на несуществующий или неопределенный атрибут. Когда интерпретатор может обнаружить искомый объект в дереве наследования, __getattr__.не вызывается.
Метод удобен для обобщенной обработки запросов к атрибутам:
- >>>class Gone:
- def __getattr__(self, atname):
- if atname == «age»:
- return 20
- else:
- raise AttributeError, atname
- >>>D = Gone()
- >>>D.age
- 20
- >>>D.name
- AttributeError: name
У класса Gone и его экземпляра D своих атрибутов нет. Поэтому при обращении к D.age автоматически вызывается метод __getattr__. Сам экземпляр передается как self, а имя неопределенного «age» в строке atname. Класс возвращает результат обращения к имени D.age, несмотря на то, что данного атрибута у него нет.
Если классом не предусматривается обработка атрибута, метод __getattr__ вызывает встроенное исключение, и тем самым передает интерпретатору информацию о том, что имя в действительности является неопределенным. В данном случае попытка обратиться к имени D.name приводит к появлению ошибки.
Аналогичным образом работает метод перегрузки операторов __setattr__, перехватывая каждую попытку присвоить значение атрибуту. Если этот метод прописан в теле класса, выражение «self.атрибут = значение» будет преобразовано в вызов метода self.__setattr_(«атрибут», значение).
Мы описали только несколько из существующих методов перегрузки. Весь перечень находится в стандартном руководстве языка и включает гораздо больше имен.
Дополнительные возможности
ООП иногда используют для сложных и нестандартных задач. Благодаря наследованию классов в Python, поведение встроенных типов данных и их возможности поддаются расширению и адаптации.
Если вас не устраивает тот факт, что индексация в последовательностях начинается с нуля, вы можете это исправить с помощью инструкции class. Для этого нужно создать подкласс типа list с новыми именами всех типов и реализовать необходимые изменения. Также в ООП на языке Python существуют декораторы функций, статические методы и множество других сложных и специальных приемов.