Книга: Марк Лутц - Изучаем Python, 5-е изд., Т. 2
Назад: Предисловие
Дальше: Более реалистичный пример

Основы написания классов

 

Теперь, когда был представлен краткий обзор ООП, самое время посмотреть, каким образом все выливается в фактический код. В этой главе начинается наполнение недостающими синтаксическими деталями модели классов в Python.
Если в прошлом вы не занимались ООП, тогда классы поначалу могут показаться отчасти сложными. Чтобы облегчить освоение программирования классов, мы начнем подробное исследование ООП с того, что рассмотрим в настоящей главе несколько базовых классов в действии. Приведенные здесь детали будут расширены в последующих главах части, но в своей элементарной форме классы Python понимать легко.
На самом деле классы обладают только тремя отличительными особенностями. На базовом уровне они главным образом представляют собой пространства имен, что во многом подобно модулям, которые обсуждались в части V. Однако в отличие от модулей классы также поддерживают генерирование множества объектов, наследование пространств имен и перегрузку операций. Давайте начнем наш тур по оператору class с исследования каждой из этих отличительных особенностей по очереди.
Классы генерируют множество объектов экземпляров
Чтобы понять, как работает идея множества объектов, сначала необходимо осознать, что в модели ООП языка Python имеются два вида объектов: объекты классов и объекты экземпляров. Объекты классов обеспечивают стандартное поведение и служат фабриками для объектов экземпляров. Объекты экземпляров являются действительными объектами, обрабатываемыми вашей программой — каждый представляет собой самостоятельное пространство имен, но наследует (т.е. автоматически получает доступ) имена от класса, из которого он был создан. Объекты классов происходят из операторов, а экземпляры — из вызовов; при каждом обращении к классу вы получаете новый экземпляр этого класса.
Такая концепция генерации объектов сильно отличается от большинства других программных конструкций, рассмотренных до сих пор в книге. По существу классы являются фабриками для генерирования множества экземпляров. По контрасту с этим в отдельно взятой программе импортируется только одна копия каждого модуля. Фактически именно потому так работает функция reload, обновляя разделяемый объект единственного экземпляра на месте. Благодаря классам каждый экземпляр может иметь собственные независимые данные, поддерживая множество версий объекта, который моделируется классом.
В данной роли экземпляры классов похожи на поддерживаемое для каждого вызова состояние замыканий (фабричных функций), которые обсуждались в главе 17, но они представляют собой естественную часть модели классов, а состояние в классах реализовано в виде явных атрибутов вместо неявных ссылок на области видимости. Вдобавок это всего лишь часть того, что делают классы — они также поддерживают настройку через наследование, перегрузку операций и множество линий поведения через методы. Вообще говоря, классы являются более совершенным программным инструментом, хотя ООП и функциональное программирование не считаются взаимоисключающими парадигмами. Мы можем сочетать их, используя инструменты функционального программирования в методах, реализуя методы, которые сами представляют собой генераторы, создавая определяемые пользователем итераторы (как будет показано в главе 30) и т.д.
Ниже приведен краткий обзор основных возможностей ООП в Python с точки зрения двух типов объектов. Как вы увидите, классы Python в чем-то похожи на определения def и модули, но они могут сильно отличаться от того, к чему вы привыкли в других языках.
Объекты классов обеспечивают стандартное поведение
В результате выполнения оператора class мы получаем объект класса. Далее представлена сводка по основным характеристикам классов Python.
• Оператор class создает объект класса и присваивает его имени. В точности как оператор def определения функции оператор class является исполняемым. После достижения и запуска он генерирует новый объект класса и присваивает его имени, указанному в заголовке class. Также подобно def операторы class обычно выполняются при первом импортировании файлов, где они находятся.
• Присваивания внутри операторов class создают атрибуты классов. Как и в файлах модулей, присваивания на верхнем уровне внутри оператора class (не вложенные в def) генерируют атрибуты в объекте класса. Формально оператор class определяет локальную область видимости, которая превращается в пространство имен атрибутов для объекта класса подобно глобальной области видимости модуля. После выполнения оператора class атрибуты класса доступны посредством уточнения с помощью имени: объект.имя.
• Атрибуты класса снабжают объект состоянием и поведением. Атрибуты объекта класса хранят информацию о состоянии и описывают поведение, которое разделяется всеми экземплярами, создаваемыми из класса; операторы def определения функций, вложенные внутрь class, генерируют методы, которые обрабатывают экземпляры.
Объекты экземпляров являются конкретными элементами
При обращении к объекту класса мы получаем объект экземпляра. Ниже приведен краткий обзор ключевых моментов, касающихся экземпляров класса.
• Обращение к объекту класса как к функции создает новый объект экземпляра. При каждом обращении к классу он создает и возвращает новый объект экземпляра. Экземпляры представляют конкретные элементы в предметной области программы.
• Каждый объект экземпляра наследует атрибуты класса и получает собственное пространство имен. Объекты экземпляров, созданные из классов, являются новыми пространствами имен. Объекты экземпляров начинают свое существование пустыми, но наследуют атрибуты, имеющиеся в объектах классов, из которых они были сгенерированы.
• Присваивания атрибутам аргумента self в методах создают атрибуты для отдельного экземпляра. Внутри функций методов класса первый аргумент (по соглашению называемый self) ссылается на обрабатываемый объект экземпляра; присваивания атрибутам аргумента self создают либо изменяют данные в экземпляре, но не в классе.
Конечным результатом оказывается то, что классы определяют общие разделяемые данные и поведение плюс генерируют экземпляры. Экземпляры отражают конкретные сущности приложения и хранят собственные данные, которые могут варьироваться от объекта к объекту.
Первый пример
Давайте рассмотрим реальный пример, чтобы увидеть, как описанные выше идеи работают на практике. Первым делом определим класс по имени FirstClass, выполнив оператор class в интерактивной подсказке:
»> class FirstClass: # Определить объект класса
def setdata(self, value) : # Определить методы класса
self .data = value # self - это экземпляр def display(self):
print(self.data) # self.data: для каждого экземпляра
Здесь мы работаем в интерактивной подсказке, но по обыкновению такой оператор находится в файле модуля и выполняется при его импортировании. Подобно функциям, создаваемым с помощью операторов def, этот класс не появится до тех пор, пока Python не доберется и не выполнит показанный оператор.
Как и все составные операторы, оператор class начинается со строки заголовка с именем класса, после чего следует тело с одним или несколькими вложенными операторами, (обычно) набранными с отступом. В приведенном примере вложенными операторами являются def; они определяют функции, которые реализуют поведение класса, предназначенное для экспортирования.
В части IV было указано, что def на самом деле представляет собой присваивание. В примере операторы def присваивают объекты функций именам setdata и display в области видимости оператора class, а потому генерируют атрибуты, присоединяемые к классу — FirstClass . setdata и FirstClass . display. В действительности любое имя, присвоенное на верхнем уровне вложенного блока класса, становится атрибутом этого класса.
Функции внутри класса, как правило, называются методами. Они создаются посредством нормальных операторов def и поддерживают все, что вам уже известно о функциях (т.е. могут иметь стандартные значения аргументов, возвращать значения, выдавать элементы по запросу и т.п.). Но первый аргумент в функции метода при ее вызове автоматически получает подразумеваемый объект экземпляра — объект, на котором произведен вызов. Нам необходимо создать пару экземпляров, чтобы посмотреть, как все работает:
>>> х = FirstClass () # Создать два экземпляра
»> у = FirstClass () # Каждый представляет собой новое пространство имен
Обращаясь к классу таким способом (обратите внимание на круглые скобки), мы генерируем объекты экземпляров, представляющие собой просто пространства имен, которые имеют доступ к атрибутам своих классов. Собственно говоря, в этой точке мы имеем три объекта: два экземпляра и класс. В действительности мы располагаем тремя связанными пространствами имен, как иллюстрируется на рис. 27.1. В терминах ООП мы говорим, что экземпляр х “является” FirstClass, равно как и у — они оба наследуют имена, присоединенные к классу.
X || является
- data
FirstClass
-setdata
у У является
- display
-data
Рис. 27.1. Классы и экземпляры связывают объекты пространств имен в дерево классов, в котором ищутся атрибуты при поиске в иерархии наследования. Здесь атрибут data находится в экземплярах, но setda ta и display присутствуют в классах, расположенных выше них
Два экземпляра начинают свое существование как пустые, но имеют ссылки на класс, из которого они были сгенерированы. Если мы уточним экземпляр с помощью имени атрибута, который находится в объекте класса, тогда Python извлечет имя из класса посредством поиска в иерархии наследования (при условии, что он также не присутствует в экземпляре):
»> х. setdata ("King Arthur") # Вызвать методы: self - это х
»> у.setdata(3.14159) # Выполняется FirstClass.setdata (у, 3.14159)
Ни х, ни у не имеет собственного атрибута setdata, поэтому чтобы найти его, Python следует по ссылке из экземпляра в класс. Вот и все, что нужно для наследования в Python: оно происходит во время уточнения атрибутов и предусматривает лишь поиск имен в связанных объектах — в данном случае за счет следования по ссылкам “является” на рис. 27.1.
В функции setdata класса FirstClass передаваемое значение присваивается self .data. Внутри метода self (имя, по соглашению назначаемое крайнему слева аргументу) автоматически ссылается на обрабатываемый экземпляр (х или у), так что присваивания сохраняют значения в пространствах имен экземпляров, а не класса; подобным образом создавались имена data на рис. 27.1.
Поскольку классы способны генерировать множество экземпляров, методы должны с помощью аргумента self получать обрабатываемый экземпляр. Вызвав метод display класса для вывода self .data, мы заметим, что он отличается для каждого экземпляра; с другой стороны, само имя display одинаковое в х и у, т.к. оно поступает (наследуется) от класса:
>>> х. display () # self .data отличается в каждом экземпляре
King Arthur
>>> у.display() # Выполняется FirstClass.display(у)
3.14159
Обратите внимание, что в каждом экземпляре мы сохраняем в члене data объекты разных типов — строку и число с плавающей точкой. Как и со всем остальным в Python, для атрибутов экземпляров (иногда называемых членами) не предусмотрено каких-либо объявлений; подобно простым переменным они появляются при первом присваивании значений. На самом деле, если бы мы вызвали display на одном из наших экземпляров перед вызовом setdata, то инициировали бы ошибку неопределенного имени — атрибут по имени data не существует в памяти до тех пор, пока не будет присвоен внутри метода setdata.
В качестве еще одного способа оценить, насколько динамична эта модель, имейте в виду, что мы можем изменять атрибуты в самом классе, присваивая self в методах, или за пределами класса путем присваивания явному объекту экземпляра:
»> x.data = "New value" # Можно получать/устанавливать атрибуты
»> х.display() § И за пределами класса тоже
New value
Хотя и менее часто, мы могли бы генерировать совершенно новый атрибут в пространстве имен экземпляра, присваивая значение его имени за пределами функций методов класса:
>» х. another name = "spam" # Здесь можно также устанавливать новые атрибуты!
Такой оператор присоединит к объекту экземпляра х новый атрибут по имени another name, который может применяться или нет любым методом класса. Классы обычно создают все атрибуты экземпляра путем присваивания аргумента self, но они не обязаны поступать так — программы могут извлекать, изменять или создавать атрибуты для любых объектов, ссылками на которые они располагают.
Обычно не имеет смысла добавлять данные, которыми класс не в состоянии пользоваться, и это можно предотвратить с помощью добавочного кода “защиты”, основанного на перегрузке операции доступа к атрибутам, как будет обсуждаться позже в книге (в главах 30 и 39). Тем не менее, свободный доступ к атрибутам приводит к снижению сложности синтаксиса и есть ситуации, когда он даже полезен — например, при реализации разновидности записей данных, которые будут демонстрироваться позже в главе.
Классы настраиваются через наследование
Давайте перейдем ко второму крупному отличию классов. Помимо того, что классы служат фабриками для генерирования множества объектов экземпляров, они также предоставляют возможность вносить изменения за счет ввода новых компонентов (называемых подклассами) вместо изменения существующих компонентов на месте.
Как мы уже видели, объекты экземпляров, сгенерированные из класса, наследуют атрибуты этого класса. Python также позволяет классам быть унаследованными от других классов, открывая возможность создания иерархий классов, которые специализируют поведение. Переопределяя атрибуты в подклассах, которые находятся ниже в иерархии, мы переопределяем более общие определения таких атрибутов выше в дереве. По сути, чем ниже мы углубляемся в иерархию, тем более специфическим становится программное обеспечение. Здесь отсутствуют какие-либо параллели с модулями, чьи атрибуты находятся в единственном плоском пространстве имен, которое не поддается настройке.
Экземпляры в Python наследуют от классов, а классы — от суперклассов. Ниже описаны ключевые идеи, лежащие в основе механизма наследования атрибутов.
• Суперклассы перечисляются внутри круглых скобок в заголовке class. Чтобы заставить класс наследовать атрибуты от другого класса, просто укажите другой класс внутри круглых скобок в строке заголовка нового оператора class. Класс, выполняющий наследование, обычно называется подклассом, а класс, от которого производится наследование, является его суперклассом.
• Классы наследуют атрибуты от своих суперклассов. Точно так же, как экземпляры наследуют имена атрибутов, определенные в их классах, классы наследуют все имена атрибутов, которые определены в их суперклассах; при доступе к атрибутам Python находит их автоматически, если они не существуют в подклассах.
• Экземпляры наследуют атрибуты от всех доступных классов. Каждый экземпляр получает имена от класса, из которого он сгенерирован, а также от всех суперклассов этого класса. При поиске имени Python проверяет экземпляр, затем его класс и, наконец, все суперклассы.
• Каждая ссылка объект. атрибут инициирует новый независимый поиск. Python выполняет независимый поиск в дереве классов для каждого выражения с извлечением атрибута. Сюда входят ссылки на экземпляры и классы, сделанные за пределами операторов class (например, X.атрибут), а также ссылки на атрибуты экземпляра аргумента self в функциях методов класса. Каждое выражение self .атрибут в методе вызывает новый поиск для атрибута в self и выше.
• Изменения в логику вносятся за счет создания подклассов, а не модификации суперклассов. Переопределяя имена суперклассов в подклассах ниже в иерархии (дерева классов), подклассы замещают и тем самым настраивают унаследованное поведение.
Совокупный эффект — и основная цель всего поиска подобного рода — заключается в том, что классы лучше поддерживают разложение на составляющие и настройку кода, чем другие языковые инструменты, рассмотренные до сих пор. С одной стороны, они позволяют нам свести к минимуму избыточность кода (и в итоге сократить расходы на сопровождение), вынося операции в единственную разделяемую реализацию. С другой стороны, они дают нам возможность программировать путем настройки того, что уже существует, а не его изменения на месте или написания кода с нуля.
: Строго говоря, наследование Python оказывается чуть более развитым, чем | здесь описано, когда мы задействуем дескрипторы нового стиля и метак-
| лассы (сложные темы, исследуемые позже), но мы можем благополучно
^ ограничиваться экземплярами и их классами, как в этом месте книги, так и в большинстве прикладного кода Python. Формально наследование определяется в главе 40.
Второй пример
Чтобы подкрепить иллюстрацией роль наследования, текущий пример будет построен на основе предыдущего. Первым делом мы определим новый класс SecondClass, который наследует все имена FirstClass и предоставляет одно собственное имя:
>>> class SecondClass(FirstClass): # Наследует setdata
def display(self): # Изменяет display
print (1 Current value = "%sn ' % self, data)
Класс SecondClass определяет метод display для вывода в другом формате. За счет определения атрибута с таким же именем, как у атрибута в FirstClass, класс SecondClass фактически замещает атрибут display в своем суперклассе.
Вспомните, что поиск в иерархии наследования направлен вверх от экземпляров к подклассам и далее к суперклассам, останавливаясь на первом найденном вхождении имени атрибута. В данном случае, поскольку имя display в SecondClass будет обнаружено перед таким же именем в FirstClass, мы говорим, что SecondClass переопределяет display из FirstClass. Действие по замещению атрибутов путем их переопределения ниже в дереве иногда называют перегрузкой.
Конечный результат здесь в том, что SecondClass специализирует FirstClass за счет изменения поведения метода display. С другой стороны, класс SecondClass (и любые созданные из него экземпляры) по-прежнему наследует метод setdata от FirstClass буквально. Давайте в целях демонстрации создадим экземпляр:
>>> z = SecondClass ()
>>> z.setdata(42) # Находит setdata в FirstClass
»> z.display() # Находит переопределенный метод в SecondClass
Current value = '’42"
Как и ранее, мы создаем объект экземпляра SecondClass посредством обращения к нему. Вызов setdata приводит к выполнению версии из FirstClass, но атрибут display на этот раз поступает из SecondClass и выводит специальное сообщение. На рис. 27.2 показаны задействованные пространства имен.
SecondClass
\ Z.data | I Z.display j I Z.setdata j
*•••»•*#•••»••*«•
Puc. 21.2. Специализация: переопределение унаследованных имен за счет их повторного определения в расширениях ниже в дереве классов. Здесь SecondClass переопределяет и тем самым настраивает метод display для своих экземпляров
Касательно ООП важно отметить один важный момент: специализация, введенная в SecondClass, является полностью внешней по отношению к FirstClass. Таким образом, она не затрагивает существующие или будущие объекты FirstClass, подобные х из предыдущего примера:
>» х.display() #х - по-прежнему экземпляр FirstClass (выводит старое сообщение)
New value
Вместо изменения класса FirstClass мы настроили его. Естественно, это искусственный пример, но в качестве правила запомните, что поскольку наследование дает возможность вносить изменения такого рода во внешние компоненты (т.е. в подклассы), классы часто поддерживают расширение и многократное использование лучше, чем функции или модули.
Классы являются атрибутами в модулях
Прежде чем двигаться дальше, следует отметить, что с именем класса не связано ничего магического. Оно представляет собой всего лишь переменную, которой при выполнении оператора class присваивается объект, и на объект можно ссылаться с помощью любого нормального выражения. Например, если бы вместо набора в интерактивной подсказке класс FirstClass был помещен в файл модуля, тогда мы могли бы импортировать его и применять его имя обычным образом в строке заголовка class:
from modulename import FirstClass # Копировать имя в текущую область видимости class SecondClass(FirstClass) : # Использовать имя класса напрямую def display(self): ...
Или вот эквивалент:
import modulename # Доступ к целому модулю
class SecondClass(modulename.FirstClass): # Уточнение для ссылки def display(self): ...
Как и все остальное, имена классов всегда существуют внутри модуля, так что они должны следовать всем правилам, которые мы обсуждали в части V. Например, в единственном файле модуля может находиться несколько классов — подобно другим операторам в модуле операторы class выполняются во время импортирования для определения имен, которые становятся индивидуальными атрибутами модуля. В более общем случае каждый модуль может произвольно смешивать любое количество переменных, функций и классов, причем все имена в модуле ведут себя одинаково. В целях демонстрации ниже приведено содержимое food.py:
# food.py
var =1 # food.var
def func(): . . . # food.func
class spam: ... # food, spam
class ham: ... # food.ham
class eggs: ... # food.eggs
Сказанное остается справедливым, даже если модуль и класс имеют совпадающие имена. Например, при наличии файла person.py со следующим содержимым:
class person: ... для извлечения класса необходимо обычным образом указать модуль:
import person # Импортировать модуль
х = person.person () # Класс внутри модуля
Несмотря на то что такой путь может выглядеть избыточным, он обязателен: per son. per son ссылается на класс person внутри модуля person. Указание только person приводит к получению модуля, но не класса, если только не используется оператор from:
from person import person # Получить класс из модуля х = person () # Использовать имя класса
Как в случае любой другой переменной, мы не можем увидеть класс в файле без предварительного импортирования и его извлечения из включающего файла. Если это кажется непонятным, тогда не применяйте одинаковые имена для модуля и класса внутри него. На самом деле по соглашению, принятому в Python, имена классов должны начинаться с буквы верхнего регистра, чтобы сделать их более различимыми:
import person # Нижний регистр для имен модулей
х = person. Person () # Верхний регистр для имен классов
Кроме того, имейте в виду, что хотя и классы, и модули являются пространствами имен для присоединения атрибутов, они соответствуют очень разным структурам исходного кода: модуль отражает целый файл, а класс является оператором внутри файла. Мы обсудим такие отличия позже в данной части книги.
Классы могут перехватывать операции Python
Давайте перейдем к рассмотрению третьего и последнего отличия между классами и модулями: перегрузке операций. Используя простые термины, перегрузка операций позволяет объектам, сознанным из классов, перехватывать и реагировать на операции, которые работают со встроенными типами: сложение, нарезание, вывод, уточнение и т.д. По большей части это просто механизм автоматической диспетчеризации — выражения и другие встроенные операции передают управление реализациям в классах. Здесь тоже нет ничего схожего с модулями: модули могут реализовывать вызовы функций, но не поведение выражений.
Несмотря на возможность реализации всего поведения класса в виде функций методов, перегрузка операций позволяет объектам более тесно интегрироваться с объектной моделью Python. Кроме того, поскольку перегрузка операций заставляет наши объекты действовать подобно встроенным объектам, это способствует созданию более согласованных и легких в изучении объектных интерфейсов, а также делает возможной обработку объектов, основанных на классах, с помощью кода, который написан в расчете на интерфейс встроенного типа. Ниже приведено краткое изложение главных идей, лежащих в основе перегрузки операций.
• Методы, имена которых содержат удвоенные символы подчеркивания (_X_),
являются специальными привязками. В классах Python мы реализуем перегрузку операций за счет предоставления особым образом именованных методов для перехвата операций. В языке Python определено фиксированное и неизменяемое отображение каждой операции на метод со специальным именем.
• Такие методы вызываются автоматически, когда экземпляры встречаются во встроенных операциях. Скажем, если объект экземпляра наследует метод _add_, то этот метод вызывается всякий раз, когда объект появляется в выражении с операцией +. Возвращаемое значение метода становится результатом соответствующего выражения.
• Классы могут переопределять большинство встроенных операций с типами. Существуют десятки специальных имен методов для перегрузки операций, которые можно перехватывать и реализовывать почти каждую операцию, действующую на встроенных типах. Сюда входят не только операции выражений, но также базовые операции наподобие вывода и создания объектов.
• Для методов перегрузки операций не предусмотрены стандартные реализации и ни один из них не является обязательным. Если класс не определяет или не наследует какой-то метод перегрузки операции, то это просто означает, что соответствующая операция не поддерживается для экземпляров класса. Например, если метод_add_отсутствует, тогда выражения + будут приводить к исключениям.
• Классы нового стиля имеют ряд стандартных реализаций, но не для распространенных операций. В Python З.Х и в так называемых классах “нового стиля” из Python 2.Х, которые мы определим позже, корневой класс по имени object
предоставляет стандартные реализации для нескольких методов_X_, но их
немного, и они не относятся к числу наиболее часто применяемых операций.
• Операции позволяют интегрировать классы в объектную модель Python. За счет перегрузки операций для типов определяемые пользователем объекты, которые мы реализуем посредством классов, могут действовать в точности как встроенные типы и потому обеспечивать согласованность, а также совместимость с ожидаемыми интерфейсами.
Перегрузка операций является необязательной возможностью; она используется разработчиками инструментов для других программистов на Python, а не разработчиками прикладных приложений. Откровенно говоря, вероятно вы не должны применять перегрузку операций лишь потому, что это выглядит умным или “крутым”. Если класс не нуждается в имитации интерфейсов встроенных типов, то обычно необходимо придерживаться более просто именованных методов. Скажем, зачем приложению, работающему с базой данных сотрудников, поддерживать выражения вроде * и +? Именованные методы, подобные giveRaise и promote, как правило, будут иметь гораздо больший смысл.
Таким образом, мы не будем вдаваться в детали каждого метода перегрузки операции, доступного в Python. Однако имеется один метод перегрузки операции, который
вы наверняка встретите почти в любом реалистичном классе Python: метод_init_,
известный как метод конструктора и используемый для инициализации состояния
объектов. Методу_init_должно уделяться особое внимание, поскольку наряду с
аргументом self он оказывается ключевым условием для чтения и понимания большинства объектно-ориентированного кода на Python.
Третий пример
Рассмотрим еще один пример. На этот раз мы определим подкласс класса SecondClass из предыдущего раздела и реализуем три особым образом именованных атрибута, которые будут автоматически вызываться Python:
• _init_выполняется, когда создается новый объект экземпляра: self является новым объектом ThirdClass;
• _add_выполняется, когда экземпляр ThirdClass присутствует в выражении +;
• _str_выполняется, когда объект выводится (формально при его преобразовании в отображаемую строку встроенной функцией str или ее внутренним эквивалентом Python).
В новом подклассе также определен нормально именованный метод mul, который изменяет объект экземпляра на месте. Вот код нового подкласса:
»> class ThirdClass(SecondClass): # Унаследован от SecondClass
def_init_(self, value): # Вызывается для ThirdClass (value)
self .data = value
def_add (self, other) : # Вызывается для self + other
return ThirdClass(self.data + other)
def_str_(self) : # Вызывается для print (self) , str ()
return 1[ThirdClass: %s] ' % self.data def mul(self/ other) : Изменение на месте: именованный метод self .data *= other
>>> a = ThirdClass ('abc') >>> a.display()
Current value = "abc"
>>> print(a)
[ThirdClass: abc]
# Вызывается_init_
# Вызывается унаследованный метод
#_str_: возвращает отображаемую строку
»> b = а + 'xyz'
>>> b.display()
Current value = "abcxyz" #_add_; создает новый экземпляр
# b имеет все методы класса ThirdClass
#_str_; возвращает отображаемую строку
»> print (b)
[ThirdClass: abcxyz]
>» a.mul (3)
# mul: изменяет экземпляр на месте
>>> print (а)
[ThirdClass: abcabcabc]
Класс ThirdClass “является” SecondClass, поэтому его экземпляры наследуют настроенный метод display от класса SecondClass из предыдущего раздела. Тем не менее, на этот раз при создании экземпляра ThirdClass передается аргумент (1 abc1).
Аргумент передается аргументу value конструктора_init_и присваивается здесь
атрибуту self .data. Совокупный эффект в том, что ThirdClass организован так, чтобы устанавливать атрибут data автоматически во время конструирования, не требуя последующего вызова setdata.
Кроме того, объекты ThirdClass теперь могут появляться в выражениях + и вызовах print. В случае выражения + интерпретатор Python передает объект экземпляра
слева аргументу self и значение справа аргументу other в методе_add_(рис. 27.3);
любое возвращаемое значение_add_становится результатом выражения + (вскоре
мы более подробно обсудим его результат).
а 4- 3
_add_(setf, other)
Рис. 27.3. При перегрузке операции выражений и другие встроенные операции, выполняемые над классом, отображаются на методы с особыми именами в классе. Такие специальные методы необязательны и могут быть унаследованы обычным образом. В данном случае выражение + запускает метод_add_
Для вызова print интерпретатор Python передает выводимый объект аргументу self в методе_str_; любая возвращаемая этим методом строка становится отображаемой строкой для объекта. Благодаря методу_str_(или его более подходящему
двойнику_г ер г_, который мы будем использовать в следующей главе) мы можем
применять для вывода объектов данного класса нормальный вызов print вместо обращения к специальному методу display.
Особым образом именованные методы, такие как_init_,_add_и_str_,
наследуются подклассами и экземплярами в точности подобно любым другим именам, присваиваемым в операторе class. Если они не реализованы в классе, тогда Python ищет такие имена во всех суперклассах класса, как обычно. Имена методов для перегрузки операций также не являются встроенными или зарезервированными словами; они представляют собой всего лишь атрибуты, которые Python ищет, когда объекты появляются в разнообразных контекстах. Обычно Python вызывает их автоматически, но иногда они могут вызываться также и в вашем коде. Например, как мы увидим в следующей главе, метод_init_часто вызывается вручную для запуска шагов инициализации в суперклассе.
Возвращать результаты или нет
Некоторые методы для перегрузки операций, скажем,_str_, требуют результатов, но другие обладают большей гибкостью. Например, обратите внимание на то, как
метод_add_создает и возвращает новый объект экземпляра класса ThirdClass,
вызывая ThirdClass с результирующим значением, что в свою очередь запускает _init_для инициализации результатом. Это общее соглашение, которое объясняет, почему переменная b в листинге имеет метод display; она тоже является объектом ThirdClass, т.к. именно его возвращает операция + для объектом данного класса. По существу тип становится более распространенным.
И напротив, метод mul изменяет текущий объект экземпляра на месте, заново присваивая атрибут self. Чтобы сделать последнее, мы могли бы перегрузить операцию выражения *, но тогда результат слишком бы отличался от поведения * для встроенных типов, таких как числа и строки, где операция * всегда создает новые объекты. Общая практика требует, чтобы перегруженные операции работали таким же образом, как их встроенные реализации. Однако поскольку перегрузка операций в действительности представляет собой просто механизм диспетчеризации между выражениями и методами, в объектах собственных классов вы можете интерпретировать операции любым желаемым способом.
Для чего используется перегрузка операций?
Как проектировщик класса, вы сами решаете, применять перегрузку операций или нет. Выбор зависит просто от того, насколько вы хотите, чтобы ваш объект был похож по виду и поведению на встроенные типы. Как упоминалось ранее, если опустить метод перегрузки операции и не наследовать его от суперкласса, тогда соответствующая операция для экземпляров поддерживаться не будет; при попытке ее использовать возникнет исключение (или в некоторых случаях вроде вывода будет применяться стандартная реализация).
По правде говоря, многие методы для перегрузки операций, как правило, применяются при реализации объектов, имеющих математическую природу; скажем, класс вектора или матрицы может перегружать операцию сложения, но класс сотрудника — вряд ли. Для более простых классов вы можете вообще не использовать перегрузку и при реализации поведения объектов взамен полагаться на явные вызовы методов.
С другой стороны, вы можете принять решение применять перегрузку операция, если определяемые пользователем объекты необходимо передавать функции, которая написана так, что ожидает операций, доступных для встроенного типа вроде списка или словаря. Реализация в классе того же самого набора операций будет гарантировать, что ваши объекты поддерживают такой же ожидаемый объектный интерфейс, а потому совместимы с функцией. Хотя мы не станем раскрывать в книге каждый метод для перегрузки операций, в главе 30 будет дан обзор дополнительных распространенных методик перегрузки операций.
Одним из методов перегрузки, который мы будем часто здесь использовать, является метод конструктора_init_, применяемый для инициализации вновь созданных
объектов экземпляров и присутствующий почти в каждом реалистичном классе. Из-за того, что конструктор позволяет классам немедленно заполнять атрибуты в своих новых экземплярах, он удобен практически во всех видах классов, которые вам доведется реализовывать. На самом деле, хотя атрибуты экземпляра в Python не объявляются, обычно легко выяснить, какие атрибуты будет иметь экземпляр, проинспектировав метод_init_его класса.
Разумеется, нет ничего плохого в том, чтобы проводить эксперименты с интересными языковыми инструментами, но они не всегда переносятся в производственный код. С течением времени и накоплением опыта вы начнете считать такие программные структуры и указания естественными и чуть ли не автоматическими.
Простейший в мире класс Python
В этой главе мы начали подробное исследование синтаксиса оператора class, но я хотел бы напомнить еще раз, что базовая модель наследования, которую производят классы, очень проста — в действительности она включает в себя всего лишь поиск атрибутов в деревьях связанных объектов. Фактически мы можем создать класс, не содержащий вообще ничего. Следующий оператор создает класс без присоединенных атрибутов, т.е. объект пустого пространства имен:
»> class rec: pass # Объект пустого пространства имен
Оператор заполнителя pass (обсуждаемый в главе 13) здесь нужен потому, что в классе нет ни одного метода. После создания класса путем запуска показанного выше оператора в интерактивной подсказке мы можем заняться присоединением атрибутов к классу, присваивая его именам значения полностью за пределами исходного оператора class:
»> rec.name = 'Bob' # Просто объект с атрибутами
»> rec.age = 40
Создав с помощью присваивания атрибуты, мы можем извлекать их с использованием обычного синтаксиса. В случае применения подобным образом класс напоминает “структуру” в С или “запись” в Pascal. По существу это объект с присоединенными к нему именами полей (как мы увидим далее, похожий трюк с ключами словаря требует набора дополнительных символов):
»> print (rec.name) # Подобен структуре С или записи
Bob
Обратите внимание, что прием работает, даже когда еще нет ни одного жземтшра класса; классы сами по себе являются объектами и без экземпляров. В действительности они представляют собой всего лишь автономные пространства имен; до тех пор, пока у нас есть ссылка на класс, мы в любой момент можем устанавливать либо изменять его атрибуты. Тем не менее, взгляните, что происходит, когда мы создаем два экземпляра:
»> х = гес() # Экземпляры наследуют имена класса
»> у = гес()
Экземпляры начинают свое существование как объекты совершенно пустых пространств имен. Однако поскольку экземпляры запоминают класс, из которого были созданы, они будут извлекать атрибуты, присоединенные нами к классу, через наследование:
>>> x.name, у.name # пате хранится только в классе
('Bob', ’Bob')
На самом деле эти экземпляры сами не имеют атрибутов; они просто извлекают атрибут name из объекта класса, где он хранится. Если же мы присваиваем атрибуту экземпляра, тогда создается (или изменяется) атрибут в одном объекте, но не в другом — критически важно то, что ссылки на атрибуты инициируют поиск в иерархии наследования, а присваивания атрибутов влияют только на объекты, в которых присваивания выполнялись. Здесь это означает, что х получает собственный атрибут name, но у по-прежнему наследует атрибут name, присоединенный к классу выше в дереве:
>>> x.name = 'Sue' # Но присваивание изменяет только х
»> rec.name, x.name, у.name
(’Bob', 'Sue', ’Bob')
На самом деле, как будет более детально исследовано в главе 29, атрибуты объекта пространства имен обычно реализуются как словари, а деревья наследования классов представляют собой (говоря в общем) всего лишь словари, содержащие связи с другими словарями. Если вы знаете, куда смотреть, то сможете увидеть это явно.
Например, атрибут_diet_является словарем пространств имен для большинства объектов, основанных на классах. Ряд классов могут дополнительно (или взамен)
определять атрибуты в_slots_— расширенное и редко используемое средство,
которое упоминается в главе 28, но будет более детально рассматриваться в главах 31
и 32. Обычно_diet_буквально представляет собой пространство имен атрибутов
экземпляра.
В целях иллюстрации ниже приведено взаимодействие в Python 3.7; порядок следования имен и набор внутренних имен_X_могут варьироваться от выпуска к выпуску, к тому же мы отфильтровали встроенные имена с помощью генераторного выражения, как поступали ранее, но все присвоенные нами имена присутствуют:
>>> list(rec. diet_. keys())
['_module_’_diet_1_weakref_'_doc ', 'name', 'age']
»> list(name for name in rec._diet_if not name. s tarts with ('_'))
['age', 'name']
>>> list(x._diet_.keys())
['name’]
>>> list(y._diet_.keys()) # list() не требуется в Python 2.X
[]
Здесь словарь пространств имен класса содержит присвоенные ранее атрибуты name и аде, экземпляр х имеет собственный атрибут name, а экземпляр у все еще пуст. Из-за такой модели атрибут часто может извлекаться либо посредством индексирования словаря, либо с помощью записи атрибута, но только если он присутствует в обрабатываемом объекте. Запись атрибута инициирует поиск в иерархии наследования, но индексирование ищет только в одиночном объекте (как будет показано позже, оба подхода исполняют допустимые роли):
»> x.name, х._diet_['name'] # Представленные здесь атрибуты являются
# ключами словаря
('Sue', 'Sue')
>>> x.age # Но извлечение атрибута проверяет также классы
40
>» х._diet_['age'] # Индексирование словаря не производит поиск
# в иерархии наследования
KeyError: 'аде'
Ошибка ключа: 'аде1
Для упрощения поиска в иерархии наследования при извлечении атрибутов каждый экземпляр имеет связь со своим классом, которую создает Python — она называется _class_и ее можно просмотреть:
>>> х._class__# Связь экземпляра с классом
eclass '_main_.recf>
Классы также располагают атрибутом_bases_, который является кортежем
ссылок на их объекты суперклассов — в данном примере только подразумеваемый корневой класс object в Python З.Х, исследуемый позже (в Python 2.Х взамен получается пустой кортеж):
>» rec._bases__# Связь с суперклассами, () в Python 2.Х
(<class 1 object'>,)
Эти два атрибута показывают, каким образом деревья классов буквально представлены в памяти. Внутренние детали подобного рода знать необязательно (деревья классов вытекают из выполняемого кода), но они часто помогают прояснить модель.
Главное, на что стоит обратить внимание — модель классов Python чрезвычайно динамична. Классы и экземпляры представляют собой всего лишь объекты пространств имен с атрибутами, создаваемыми на лету через присваивания. Такие присваивания, как правило, происходят внутри записываемых вами операторов class, но могут встречаться везде, где имеется ссылка на один из объектов в дереве.
Даже методы, которые обычно создаются с помощью операторов def, вложенных в class, могут быть созданы совершенно независимо от любого объекта класса. Например, следующий код определяет простую функцию, принимающую один аргумент, за пределами любого класса:
»> def uppemame (obj) :
return obj .name.upper () # По-прежнему необходим аргумент self (obj)
Здесь пока еще ничего не связано с классом; uppername является простой функцией и может вызываться в данной точке при условии передачи ей объекта obj с атрибутом паше, значение которого имеет метод upper. Экземпляры нашего класса соответствуют ожидаемому интерфейсу и запускают преобразование строк в верхний регистр:
>>> upper name (х) # Вызов как простой функции
'SUE'
Тем не менее, если мы присвоим эту простую функцию атрибуту нашего класса, она становится методом, допускающим вызов через любой экземпляр, а также через имя самого класса при условии передачи экземпляра вручную — методика, которую мы задействуем в следующей главе:
>>> rec.method = uppername
»> x.method()
’SUE'
# Теперь это метод класса!
# Запустить метод для обработки х
# То же самое, но передать у для self
»> у.method()
'ВОВ'
»> rec.method(x)
'SUE'
# Можно вызывать через экземпляр или класс
Обычно классы заполняются посредством операторов class, а атрибуты экземпляров создаются с помощью присваиваний атрибутам self в функциях методов. Однако мы еще раз отметим, что поступать так необязательно; ООП в Python главным образом касается поиска атрибутов в связанных объектах пространств имен.
Снова о записях: классы или словари
Хотя простые классы в предыдущем разделе были предназначены для иллюстрации основ модели классов, те методики, которые в них задействованы, могут также применяться в реальной работе. Скажем, в главах 8 и 9 демонстрировалось использование словарей, кортежей и списков для хранения в программах свойств сущностей, в общем случае называемых записями. Оказывается, что классы способны быть более эффективными в такой роли — они упаковывают информацию подобно словарям, но могут также умещать в себе логику обработки в форме методов. Для справочных целей ниже приведен пример записей на основе кортежа и словаря, которые применялись ранее в книге (здесь используется один из многочисленных приемов реализации словарей):
>>> rec = ('Bob', 40.5, ['dev1, ’mgr']) # Запись на основе кортежа »> print (rec [0])
Bob
>>> rec ■ {}
>>> rec['name’] = 'Bob' # Запись на основе словаря
»> rec[' age' ] = 40.5 # Или {...}, diet (n=v) и т.д.
»> rec [' jobs' ] = [' dev' , ' mgr' ]
>>>
>>> print(rec['name'])
Bob
Код эмулирует инструменты, похожие на записи в других языках. Тем не менее, как только что выяснилось, существует также множество способов делать то же самое с применением классов. Пожалуй, простейший из них предусматривает замену ключей атрибутами:
>>> class rec: pass
>>> rec.name = 'Bob' # Запись на основе класса
>» rec.age = 40.5
>>> rec.jobs * ['dev', 'mgr']
»>
>>> print (rec. name)
Bob
Показанный выше код существенно меньше, чем эквивалент в виде словаря. В нем с помощью оператора class создается объект пустого пространства имен. С течением времени полученный пустой класс заполняется путем присваивания атрибутов класса, как и ранее.
Прием работает, но для каждой отличающейся записи будет требоваться новый оператор class. Вероятно, более естественно взамен генерировать экземпляры пустого класса для представления каждой отличающейся записи:
>» class rec: pass
>>> persl = rec() # Записи на основе экземпляров
»> persl.name = 'Bob'
»> persl. jobs = [' dev' , 'mgr' ]
>>> persl.age = 40.5
»>
>» pers2 = rec() .
>>> pers2.name = 'Sue1
>>> pers2.jobs = ['dev', 'cto']
>>>
»> persl.name, pers2.name
('Bob', ’Sue')
Здесь мы создали две записи из одного и того же класса. Как и классы, экземпляры начинают свое существование пустыми. Затем мы заполняем записи, делая присваивания их атрибутам. Однако на этот раз существуют два отдельных объекта, а потому два разных атрибута паше. Фактически экземпляры того же самого класса даже не обязаны иметь одинаковые наборы имен атрибутов; в приведенном примере один из них располагает уникальным именем аде. Экземпляры в действительности являются отличающимися пространствами имен, так что каждый имеет отдельный словарь атрибутов. Хотя обычно экземпляры согласованно заполняются методами класса, они намного гибче, чем можно было бы ожидать.
Наконец, мы можем вместо этого создать более развитый класс с целью реализации записи и ее обработки — то, что словари, ориентированные на данные, не поддерживают напрямую:
>>> class Person:
def_init_(self, name, jobs, age=None) : # Класс = данные + логика
self .name = name self, jobs = jobs self .age = age def info(self):
return (self.name, self.jobs)
>>> reel = Person ('Bob' , ['dev' , ’mgr'] ,40.5) # Вызовы конструктора »> rec2 = Person (' Sue' , [' dev' , ' cto' ])
»>
>>> reel, jobs, rec2.info() # Атрибуты + методы
([’dev’, ’mgr’], ('Sue', ['dev', 'cto']))
Такая схема также создает множество экземпляров, но теперь класс не пустой: мы добавили логику (методы) для инициализации экземпляров при их создании и сбора атрибутов в кортеж по запросу. Конструктор придает экземплярам некоторую согласованность, всегда устанавливая атрибуты паше, job и аде, хотя последний атрибут может не указываться. Вместе методы класса и атрибуты экземпляра образуют пакет, объединяющий данные и логику.
Мы могли бы дальше расширять этот код, добавляя логику для расчета заработных плат, разбора имен и т.п. В конце концов, мы можем поместить класс в более крупную иерархию, чтобы наследовать и настраивать существующий набор методов через автоматический поиск атрибутов классов, или даже сохранять экземпляры класса в файле с помощью модуля pickle, обеспечивая их постоянство. На самом деле мы так и поступим — в следующей главе рассмотренная аналогия между классами и записями будет расширена за счет реализации более реалистичного рабочего примера, который продемонстрирует основы классов в действии.
Чтобы отдать должное другим инструментам в показанной выше форме два вызова конструктора больше напоминают словари, созданные все за раз, но все-таки классы характеризуются меньшим беспорядком и предоставляют дополнительные методы обработки. В действительности вызовы конструктора класса больше похожи на именованные кортежи из главы 9 — это имеет смысл с учетом того, что именованные кортежи являются классами с добавочной логикой для отображения атрибутов на смещения кортежей:
»> rec = diet(name='Bob1, age=40.5, jobs-['dev', 'mgr']) # Словари
»> rec = {'name’: 'Bob', 'age’: 40.5, ’jobs': ['dev1, 'mgr’])
>>> rec = Rec ('Bob', 40.5, ['dev' , 'mgr']) # Именованные кортежи
В заключение отметим, что хотя типы вроде словарей и кортежей обладают гибкостью, классы позволяют нам добавлять к объектам поведение способами, которые не поддерживаются встроенными типами и простыми функциями напрямую. Несмотря на то что мы можем хранить функции в словарях, их использование для обработки подразумеваемых экземпляров оказывается далеко не таким естественным и структурированным, как это сделано в классах. Сказанное прояснится в следующей главе.
Резюме
В главе были представлены основы написания классов на Python. Вы изучили синтаксис оператора class и узнали, как его применять для построения дерева наследования классов. Вы также выяснили, как Python автоматически заполняет первый аргумент в функциях методов, как атрибуты присоединяются к объектам в дереве классов с помощью простого присваивания и как особым образом именованные методы для перегрузки операций перехватывают и реализовывают встроенные операции, работающие с нашими экземплярами (например, выражения и вывод).
Теперь, когда вы узнали все об особенностях создания классов в Python, в следующей главе мы займемся более крупным и реалистичным примером, в котором увязывается вместе большинство того, что было изучено в ООП до сих пор, и представим ряд новых тем. Затем мы продолжим исследование создания классов, сделав второй проход по модели, чтобы восполнить детали, которые ради простоты здесь были опущены. Но прежде чем двигаться дальше, закрепите пройденный материал главы, ответив на контрольные вопросы.
Проверьте свои знания: контрольные вопросы
1. Как классы связаны с модулями?
2. Каким образом создаются экземпляры и классы?
3. Где и как создаются атрибуты класса?
4. Где и как создаются атрибуты экземпляра?
5. Что self означает в классе Python?
6. Каким образом реализовывать перегрузку операций в классе Python?
7. Когда может понадобиться поддержка перегрузки операций в классах?
8. Какой метод перегрузки операции используется наиболее часто?
9. Какие две концепции обязательно знать для понимания объектно-ориентированного кода на Python?
Проверьте свои знания: ответы
1. Классы всегда вкладываются внутрь модуля; они являются атрибутами объекта модуля. Классы и модули являются пространствами имен, но классы соответствуют операторам (не целым файлам) и поддерживают такие понятия ООП, как множество экземпляров, наследование и перегрузку операций (все перечисленное модули не поддерживают). До известной степени модуль подобен классу с единственным экземпляром без наследования, который соответствует полному файлу кода.
2. Классы создаются путем выполнения операторов class; экземпляры создаются за счет обращения к классу, как если бы он был функцией.
3. Атрибуты класса создаются путем выполнения присваивания атрибутам объекта класса. Они обычно генерируются присваиваниями верхнего уровня, вложенными внутрь оператора class — каждое имя, присвоенное в блоке оператора class, становится атрибутом объекта класса (формально локальная область видимости оператора class превращается в пространство имен для атрибутов объекта класса, что во многом похоже на модуль). Тем не менее, атрибуты класса можно также создавать путем их присваивания везде, где имеется ссылка на объект класса — даже за пределами оператора class.
4. Атрибуты экземпляра создаются посредством присваивания значений атрибутам объекта экземпляра. Они обычно создаются в функциях методов класса, реализованных внутри оператора class, с помощью присваивания значений атрибутам аргумента self (который всегда является подразумеваемым экземпляром). Однако их тоже можно создавать присваиванием везде, где присутствует ссылка на экземпляр, даже за пределами оператора class. Обычно все атрибуты экземпляра инициализируются в методе конструктора_init_; таким
образом, более поздние вызовы методов могут предполагать, что атрибуты уже существуют.
5. self — это имя, обычно назначаемое первому (крайнему слева) аргументу в функции метода класса; Python автоматически заполняет его объектом экземпляра, который представляет собой подразумеваемый объект вызова метода. Данный аргумент не обязан называться self (хотя соглашение очень строгое); важна его позиция. (Бывшие программисты на C++ или Java могут предпочесть назначать ему имя this, поскольку в языках C++ и Java такое имя отражает ту же самую идею; тем не менее, в Python этот аргумент должен всегда быть явным.)
6. Перегрузка операций реализуется в классе Python посредством особым образом именованных методов; имена начинаются и заканчиваются двумя символами подчеркивания, чтобы сделать их уникальными. Имена не являются встроенными или зарезервированными; Python всего лишь автоматически выполняет их, когда экземпляр встречается в соответствующей операции. Сам Python определяет отображения операций на специальные имена методов.
7. Перегрузка операций полезна для реализации объектов, которые имеют сходство со встроенными типами (например, последовательностей или числовых объектов, таких как матрицы), и для имитации интерфейса встроенного типа, ожидаемого порцией кода. Имитация интерфейсов встроенных типов дает возможность передавать экземпляры классов, которые также содержат информацию состояния (т.е. атрибуты, запоминающие данные между вызовами операции). Однако вы не должны применять перегрузку операций, когда будет достаточно простого именованного метода.
8. Наиболее часто используется метод конструктора_init_; почти каждый
класс применяет этот метод для установки начальных значений атрибутов экземпляра и выполнения других задач начального запуска.
9. Двумя краеугольными камнями объектно-ориентированного кода Python являются специальный аргумент self в функциях методов и метод конструктора _init_; зная их, вы должны быть в состоянии читать большинство объектноориентированного кода на Python — помимо них это практически пакеты функций. Конечно, поиск в иерархии наследования тоже имеет значение, но self
представляет автоматический объектный аргумент, а метод_init_широко
рас пространен.
ГЛАВА 28
Назад: Предисловие
Дальше: Более реалистичный пример