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

Детали реализации классов

 

Если вы еще не полностью уловили суть ООП в Python, то не переживайте; после первого тура мы собираемся погрузиться чуть глубже и заняться исследованием представленных ранее концепций. В этой и следующей главах мы еще раз взглянем на механизм классов. Здесь мы займемся изучением классов, методов и наследования, формализуя и расширяя ряд идей реализации, которые были введены в главе 27. Поскольку класс является нашим последним инструментом пространств имен, мы также подведем итоги по концепциям пространств имен и областей видимости Python.
В следующей главе продолжается углубленный проход по механизму классов за счет раскрытия одного специфического аспекта: перегрузки операций. Помимо представления дополнительных деталей в текущей и следующей главах также предоставляется возможность исследования ряда более крупных классов, чем те, которые были исследованы до сих пор.
Замечание по содержанию: если вы читаете последовательно, тогда некоторые материалы главы окажутся обзором и сводкой по темам, представленным в учебном примере из предыдущей главы, которые здесь пересматриваются с точки зрения языка, с меньшими и более автономными примерами для читателей, не знакомых с ООП. У вас может возникнуть искушение пропустить часть данной главы, но обязательно просмотрите обзор пространств имен, т.к. в нем объясняются некоторые тонкости в модели классов Python.
Оператор class
Хотя оператор class в Python на первый взгляд может выглядеть похожим на инструменты в других языках ООП, при ближайшем рассмотрении выясняется, что он совершенно отличается о того, к чему привыкли программисты. Например, как и в C++, оператор class представляет собой главный инструмент ООП в Python, но в отличие от C++ оператор class языка Python class не является объявлением. Подобно def оператор class создает объекты и неявно присваивает их — при выполнении он генерирует объект класса и сохраняет ссылку на него в имени, используемом в заголовке. Также подобно def оператор class относится к настоящему исполняемому коду — ваш класс не существует до тех пор, пока Python не достигнет и не выполнит оператор class, который его определяет. Обычно это происходит во время импортирования модуля, содержащего оператор class, но не ранее.
Общая форма
Оператор class является составным, с телом, состоящим из операторов, которые обычно размещаются с отступом под заголовком. В заголовке внутри круглых скобок после имени класса приводится список суперклассов, разделенных запятыми. Указание более одного суперкласса приводит к множественному наследованию, которое мы формально обсудим в главе 31. Ниже показана общая форма оператора:
class имя{суперкласс,...): # Присваивание имени
атрибут = значение # Разделяемые данные класса
def метод(self,...): # Методы
self.атрибут = значение # Данные экземпляра
Любые присваивания внутри оператора class генерируют атрибуты класса, а особым образом именованные методы перегружают операции; скажем, функция по имени _init_вызывается на стадии конструирования объекта экземпляра, если она
определена.
Пример
Как мы уже видели, классы главным образом представляют собой всего лишь пространства имен — т.е. инструменты для определения имен (атрибутов), которые экспортируют данные и логику клиентам. Оператор class фактически определяет пространство имен. Как и в файле модуля, вложенные в тело class операторы создают атрибуты класса. Когда Python выполняет оператор class (не вызов класса), он запускает все операторы в теле от начала до конца. Присваивания, происходящие во время такого процесса, создают имена в локальной области видимости класса, которые становятся атрибутами в ассоциированном объекте класса. По этой причине классы напоминают и модули, и функции:
• подобно функциям операторы class являются локальными областями видимости, где находятся имена, созданные вложенными присваиваниями;
• подобно именам в модуле имена, присвоенные в операторе class, становятся атрибутами в объекте класса.
Главное отличие классов связано с тем, что их пространства имен формируют также основу наследования в Python; указанные атрибуты, которые не обнаруживаются в объекте класса или экземпляра, извлекаются из других классов.
Поскольку class представляет собой составной оператор, в его тело могут вкладываться операторы любого вида, т.е. print, присваивания, if, def и т.д. Все операторы внутри class выполняются при выполнении самого оператора class (не тогда, когда позже происходит вызов класса для создания экземпляра). Обычно операторы присваивания внутри оператора class создают атрибуты данных, а вложенные операторы def — атрибуты методов. Однако в общем случае любой вид присваивания значения имени на верхнем уровне оператора class создает атрибут с таким именем в результирующем объекте класса.
Например, присваивания значений простым объектам, отличным от функций, производят атрибуты данных, разделяемые всеми экземплярами:
>>> class SharedData:
spam =42 # Генерирует атрибут данных класса
»> х = SharedData () # Создание двух экземпляров
>>> у = SharedData ()
>>> x.spam, у.spam # Они наследуют и разделяют spam
# (известный как SharedData.spam)
(42, 42)
Поскольку имени spam присваивается значение на верхнем уровне оператора class, оно присоединяется к классу, а потому будет разделяться всеми экземплярами. Мы можем изменять его через имя класса и ссылаться посредством либо экземпляров, либо самого класса:
>>> SharedData. spam = 99
>» x.spam, у.spam, SharedData.spam
(99, 99, 99)
Такие атрибуты классов могут применяться для управления информацией, которая охватывает все экземпляры — скажем, счетчиком количества сгенерированных экземпляров (мы разовьем эту идею с помощью примера в главе 32). А пока посмотрим, что происходит в случае присваивания значения имени spam через экземпляр вместо класса:
>>> x.spam = 88
>>> x.spam, у.spam, SharedData.spam
(88, 99, 99)
Присваивания значений атрибутам экземпляра создают либо изменяют имена в экземпляре, а не в разделяемом классе. В более общем смысле поиск в иерархии наследования инициируется только при ссылке на атрибуты, не в случае присваивания: присваивание значения атрибуту объекта всегда изменяет данный объект, но никакой другой. Например, у. spam ищется в классе через наследование, но присваивание х. spam присоединяет имя к самому объекту х.
Ниже приведен более полный пример такого поведения, который сохраняет то же самое имя в двух местах. Пусть мы запускаем следующий класс:
class MixedNames: # Определить класс
data = ’spam* # Присвоить значение атрибуту класса
def _init_(self, value): # Присвоить значение имени метода
self .data = value # Присвоить значение атрибуту экземпляра
def display(self):
print(self.data, MixedNames.data) # Атрибут экземпляра, атрибут класса
Класс MixedNames содержит два оператора def, которые привязывают атрибуты класса к функциям методов.
Он также содержит оператор присваивания =; поскольку этот оператор присваивает значение имени data внутри class, оно существует в локальной области видимости класса и становится атрибутом объекта класса. Как и все атрибуты класса, data наследуется и разделяется всеми экземплярами класса, которые не имеют собственных атрибутов data.
Когда мы создаем экземпляры класса MixedNames, имя data присоединяется к ним путем присваивания self .data в методе конструктора:
>>> х = MixedNames(1) # Создать два объекта экземпляра
»> у = MixedNames (2) # Каждый имеет собственный атрибут data
»> х.display () ; y.displayO # Атрибуты self. data отличаются,
# MixedNames. data - тот же самый
1 spam
2 spam
Совокупный результат в том, что атрибут data присутствует в двух местах: в объектах экземпляров (создан присваиванием self. data в_init_) и в классе, от которого они наследуют имена (создан присваиванием data в class). Метод display класса выводит обе версии, сначала версию, уточненную посредством экземпляра self, и затем версию класса.
За счет применения таких методик для сохранения атрибутов в разных объектах мы определяем их область видимости. Когда имена присоединены к классам, они разделяются; в экземплярах имена фиксируют данные для каждого экземпляра, не разделяя поведение или данные. Несмотря на то что поиск в иерархии наследования находит для нас имена, мы всегда можем извлечь атрибут откуда угодно из дерева, получая доступ к желаемому объекту напрямую.
В предыдущем примере указание x.data или self .data возвратит имя экземпляра, которое в нормальной ситуации скрывает такое же имя в классе; тем не менее, MixedNames .data явно захватывает версию имени класса. В следующем разделе описана одна из наиболее распространенных ролей для таких кодовых схем, а также объясняется, как мы ее развернули в предыдущей главе.
Методы
Поскольку вы уже осведомлены о функциях, вам также известно о методах в классах. Методы являются просто объектами функций, которые создаются операторами def, вложенными в тело оператора class. С абстрактной точки зрения методы предоставляют поведение для наследования объектами экземпляров. С точки зрения программирования методы работают в точности таким же образом, как и простые функции, но с одним критически важным исключением: первый аргумент метода всегда получает объект экземпляра, который представляет собой подразумеваемый объект вызова метода. Другими словами, Python автоматически отображает вызовы методов экземпляра на функции методов класса, как показано далее. Вызовы методов, выполненные через экземпляр:
экземпляр. метод (аргументы. . .) автоматически транслируются в вызовы функций методов класса в следующей форме:
класс.метод{экземпляр, аргументы...)
где Python определяет класс, находя имя метода с использованием процедуры поиска в иерархии наследования. На самом деле в Python допустимы обе формы вызова.
Помимо нормального наследования имен атрибутов методов специальный первый аргумент является единственной реальной магией, стоящей за вызовами методов. В методе класса первый аргумент по соглашению называется self (формально важна только его позиция, но не имя). Аргумент self снабжает метод привязкой к экземпляру, на котором производился вызов — из-за того, что классы генерируют много объектов экземпляров, им необходимо применять этот аргумент для управления данными, которые варьируются от экземпляра к экземпляру.
Программисты на C++ могут счесть аргумент self в Python похожим на указатель this в C++. Однако в Python аргумент self всегда будет явным в коде: методы всегда обязаны проходить через self, чтобы извлечь или изменить атрибуты экземпляра, обрабатываемого текущим вызовом метода. Явная природа self умышленна — присутствие этого имени делает очевидным то, что вы используете в своем сценарии имена атрибутов экземпляра, а не имена в локальной или глобальной области видимости.
Пример метода
Чтобы пролить свет на упомянутые концепции, давайте перейдем к рассмотрению примера. Пусть мы определили такой класс:
class NextClass:
def printer(self, text): self.message = text print(self.message)
# Определить класс
# Определить метод
# Изменить экземпляр
# Получить доступ к экземпляру
Имя printer ссылается на объект функции; поскольку оно присваивается в области видимости оператора class, то становится атрибутом объекта класса и наследуется каждым экземпляром, созданным из класса. Обычно из-за того, что методы вроде printer предназначены для обработки экземпляров, мы вызываем их через экземпляры:
>>> х = NextClass() # Создать экземпляр
»> х. pr in ter (' instance call') # Вызвать его метод instance call
>>> х.message # Экземпляр изменился
'instance call'
Когда мы вызываем метод, подобным образом уточняя экземпляр, метод printer сначала ищется в иерархии наследования и затем его аргументу self автоматически присваивается объект экземпляра (х); аргумент text получает строку, переданную при вызове (’ instance call'). Обратите внимание, что поскольку Python автоматически передает первый аргумент self, то нам фактически понадобится передать один аргумент. Внутри printer имя self применяется для доступа или установки данных экземпляра, т.к. оно ссылается на экземпляр, который в текущий момент обрабатывается.
Тем не менее, как будет показано, методы могут вызываться одним из двух способов — через экземпляр или через сам класс. Например, мы также можем вызывать printer, указывая имя класса, при условии явной передачи экземпляра в аргументе
self:
>>> NextClass.printer(х, 'class call') # Прямой вызов через класс
class call
>>> х.message
'class call'
# Экземпляр снова изменяется
Вызовы, маршрутизируемые через экземпляр и класс, дают в точности тот же самый эффект до тех пор, пока мы передаем в форме вызова с классом один и тот же экземпляр. На самом деле вы по умолчанию получите сообщение об ошибке, если попытаетесь вызывать метод без экземпляра:
»> NextClass.printer('bad call1)
TypeError: unbound method printer () must be called with NextClass instance. . .
Ошибка типа: несвязанный метод printer () должен вызываться с экземпляром NextClass. . .
Вызов конструкторов суперклассов
Методы обычно вызываются через экземпляры. Однако вызовы методов через класс обнаруживаются в разнообразных специальных ролях. Один распространенный
сценарий касается метода конструктора. Подобно всем атрибутам метод_init_
ищется в иерархии наследования. Другими словами, на стадии конструирования
Python ищет и вызывает только один метод_init_. Если конструкторам подклассов
нужна гарантия того, что также выполняется логика стадии конструирования суперкласса, тогда в общем случае они обязаны явно вызывать метод_init_суперкласса
через класс:
class Super:
def _init_(self, x):
...стандартный код...
class Sub(Super):
def _init_(self, x, y) :
Super._init_(self, x) # Выполнить метод_init_ суперкласса
. . . специальный код. . . # Выполнить специальные действия инициализации
I = Sub (1, 2)
Это один из нескольких контекстов, в которых ваш код, вероятно, вызовет метод перегрузки операции напрямую. Естественно, вы должны вызывать конструктор суперкласса подобным образом, только если действительно хотите, чтобы он выполнился — без такого вызова подкласс заместит его полностью. Более реалистичная иллюстрация данной методики приводилась в форме примера класса Manager в предыдущей главе.
Другие возможности вызова методов
Описанная схема вызова методов через класс представляет собой общую основу расширения — вместо полной замены — поведения унаследованных методов. Она требует передачи явного экземпляра, т.к. по умолчанию это делают все методы. Формально причина в том, что в отсутствие любого специального кода методы являются методами экземпляров.
В главе 32 вы также ознакомитесь с более новым вариантом, появившимся в версии Python 2.2 — статическими методами, которые делают возможным написание методов, не ожидающих получения в своих первых аргументах объектов экземпляров. Такие методы способны действовать подобно простым функциям, не связанным с экземплярами, с именами, считающимися локальными по отношению к классам, внутри которых они реализованы, и могут применяться для управления данными классов. Связанная концепция, рассматриваемая в той же главе 32, метод класса, предусматривает передачу класса взамен экземпляра во время вызова метода; она может использоваться для управления данными каждого класса и ее наличие подразумевается в метаклассах.
Тем не менее, указанные расширения являются более сложными и обычно необязательными. В нормальной ситуации методу всегда должен передаваться экземпляр — то ли автоматически при его вызове через экземпляр, то ли вручную, когда метод вызывается через класс.
Как было указано во врезке “Как насчет super?" в главе 28, язык Python также располагает встроенной функцией super, которая позволяет вызывать методы суперкласса более обобщенно, но из-за ее недостатков и сложностей мы отложим представление функции super до главы 32. Дополнительные сведения ищите в упомянутой врезке; с этим вызовом связаны известные компромиссы в случае базового применения, а также экзотический расширенный сценарий использования, который для наибольшей эффективности требует универсального развертывания. По причине проблем такого рода предпочтение в книге отдается политике обращения к суперклассам по явному имени, в не посредством встроенной функции super; если вы — новичок в Python, тогда я рекомендую пока применять такой же подход, особенно во время первоначального изучения ООП. Освойте сейчас простой способ, а позже вы сможете сравнить его с другими.
Наследование
Разумеется, весь смысл пространства имен, созданного оператором class, заключается в поддержке наследования имен. В этом разделе мы расширим ряд механизмов и ролей наследования атрибутов в Python.
Как было показано, наследование в Python происходит, когда объект уточняется, и оно инициирует поиск в дереве определения атрибутов — в одном и большем числе пространств имен. Каждый раз, когда вы используете выражение вида объект. атрибут, где объект представляет собой объект экземпляра или класса, Python осуществляет поиск в дереве пространств имен снизу вверх, начиная с объекта, с целью нахождения первого атрибута, который удастся обнаружить. Сюда входят ссылки на атрибуты self в ваших методах. Поскольку определения, расположенные ниже в дереве, переопределяют те, что находятся выше, наследование формирует основу специализации.
Построение дерева атрибутов
На рис. 29.1 иллюстрируется способ построения деревьев атрибутов и наполнение их именами. В целом:
• атрибуты экземпляров генерируются операторами присваивания значений атрибутам self в методах;
• атрибуты классов создаются операторами (присваивания) в операторах class;
• ссылки на суперклассы образуются на основе списков классов внутри круглых скобок в заголовке оператора class.
Объекты
class S1:
>
class S2:
class X(S1,S2): defattr(self,...): self.attr = V
object = X()
объект.атрибут?
J
Puc. 29.1. Код программы создает дерево объектов в памяти, где будет происходить поиск со стороны наследования атрибутов. Обращение к классу создает новый экземпляру который запоминает свой класс, выполнение оператора class создает новый класс, а суперклассы перечисляются внутри круглых скобок в заголовке оператора class. Каждая ссылка на атрибут запускает новую процедуру восходящего поиска в дереве - даже ссылки на атрибуты
self внутри методов класса
Конечным результатом оказывается дерево пространств имен атрибутов, ведущее от экземпляра к классу, из которого он был создан, и ко всем суперклассам, перечисленным в заголовке оператора class. Всякий раз, когда вы применяете уточнение для извлечения имени атрибута из объекта экземпляра, в этом дереве инициируется восходящий поиск в направлении от экземпляров к суперклассам.
Во-вторых, как тоже отмечалось в главе 27, полная история о наследовании становится еще более запутанной, когда к смеси добавляются сложные темы наподобие метаклассов и дескрипторов — и по этой причине мы отложим формальное определение вплоть до главы 40. Однако при обычном использовании наследование представляет собой просто способ переопределения и, следовательно, настройки поведения, реализованного в классах.
Специализации унаследованных методов
Только что описанная модель наследования с поиском в дереве является великолепным способом специализации систем. Из-за того, что процесс наследования ищет сначала имена в подклассах до проверки суперклассов, подклассы способны замещать стандартное поведение за счет переопределения атрибутов своих суперклассов. Фактически вы можете строить целые системы в виде иерархий классов и затем их расширять путем добавления новых внешних подклассов, не изменяя существующую логику на месте.
Идея переопределения унаследованных имен положена в основу многообразия методик специализации. Например, подклассы могли бы полностью замещать унаследованные атрибуты, предоставлять атрибуты, которые ожидает обнаружить суперкласс, и расширять методы суперклассов, вызывая их в переопределяемых методах. Мы уже демонстрировали несколько таких схем в действии, а ниже приведен самодостаточный пример расширения:
»> class Super:
def method(self):
print('in Super.method')
>>> class Sub(Super):
def method(self): # Переопределить метод
print(1 starting Sub.method') # Добавить здесь нужные действия Super .method (self) # Запустить стандартное действие
print('ending Sub.method')
Вся суть здесь кроется в прямом вызове метода суперкласса. Класс Sub замещает функцию method суперкласса Super собственной специализированной версией, но внутри самого замещения Sub обращается к версии, экспортируемой Super, чтобы обеспечить выполнение стандартного поведения. Другими словами, Sub.method просто расширяет поведение Super .method, а не полностью замещает его:
»> х = Super () # Создать экземпляр Super
»> х. method () # Выполняется Super .method
in Super.method
>>> x = Sub() # Создать экземпляр Sub
»> x.method() # Выполняется Sub.method,
# вызывается Super.method
starting Sub.method in Super.method ending Sub.method
Такая схема реализации расширения часто используется в конструкторах; пример ищите в разделе “Методы” ранее в главе.
Методики связывания классов
Расширение является всего лишь одним способом связывания с суперклассом. В файле specialize .ру, обсуждаемом в настоящем разделе, определено множество классов, которые иллюстрируют разнообразные методики.
• Super. Определяет функцию method и метод delegate, который ожидает наличие в подклассе метода action.
• Inheritor. Не предоставляет никаких новых имен, поэтому получает все, что определено в Super.
• Replacer. Переопределяет method из Super посредством собственной версии.
• Extender. Настраивает method из Super, переопределяя и обеспечивая выполнение стандартного поведения.
• Provider. Реализует метод action, ожидаемый методом delegate из Super.
Исследуйте каждый из предложенных классов, чтобы получить представление о различных способах настройки их общего суперкласса. Вот содержимое файла:
class Super:
def method(self):
print('in Super.method') def delegate(self): self.action()
# Стандартное поведение
# Ожидается определение метода
# Буквальное наследование метода
# Полное замещение метода
# в Replacer .method
# Расширение поведения метода
# начало Extender.method
# конец Extender.method
# Заполнение обязательного метода
# в Provider.method
class Inheritor(Super): pass
class Replacer(Super) : def method(self):
print('in Replacer.method')
class Extender(Super): def method(self):
print(1 starting Extender.method')
Super.method(self) print('ending Extender.method')
class Provider(Super): def action(self):
print('in Provider.action1)
if _name_ == '_main_' :
for klass in (Inheritor, Replacer, Extender):
print (' \n' + klass ._name_ + '...')
klass().method() print('\nProvider. . . ' ) x = Provider() x.delegate()
Здесь полезно указать на несколько моментов. Прежде всего, обратите внимание на то, как код самотестирования в конце файла specialize .ру создает внутри цикла for экземпляры трех разных классов. Поскольку классы являются объектами, вы можете сохранять их в кортеже и создавать экземпляры обобщенным образом без дополнительного синтаксиса (позже идея будет раскрыта более подробно). Подобно модулям классы также имеют специальный атрибут_name_; он заранее устанавливается в строку, содержащую имя из заголовка класса. Вот что происходит после запуска файла:
% python specialize.ру
Inheritor... in Super.method
Replacer... in Replacer.method
Extender...
starting Extender.method in Super.method ending Extender.method
Provider... in Provider.action
Абстрактные суперклассы
В рамках классов из предыдущего примера Provider может быть самым важным для понимания. Когда мы вызываем метод delegate через экземпляр Provider, происходят два независимых поиска со стороны процедуры наследования.
1. В начальном вызове х. delegate интерпретатор Python находит метод delegate в Super, выполняя поиск в экземпляре Provider и выше. Экземпляр х передается в аргументе self метода как обычно.
2. Внутри метода Super. delegate метод self .action инициирует новый независимый поиск в self и выше. Из-за того, что self ссылается на экземпляр Provider, метод action обнаруживается в подклассе Provider.
Такая кодовая структура вида “заполнение пропусков” типична для объектно-ориентированных фреймворков. В более реалистичном контексте заполняемый подобным образом метод мог бы обрабатывать какое-то событие в графическом пользовательском интерфейсе, снабжать данными для визуализации как часть веб-страницы, анализировать текст дескриптора в XML-файле и т.д. — ваш подкласс предоставляет специфические действия, но оставшуюся часть всей работы выполняет фреймворк.
По крайней мере, в свете метода delegate суперкласс в этом примере является тем, что иногда называют абстрактным суперклассом — классом, который ожидает от своих подклассов предоставления частей своего поведения. Если ожидаемый метод в подклассе не определен, тогда после неудавшегося поиска при наследовании Python генерирует исключение неопределенного имени.
Временами создатели классов делают такие требования к подклассам более очевидными с помощью операторов assert или путем генерации встроенного исключения NotlmplementedError посредством операторов raise. В следующей части книги мы подробнее исследуем операторы, способные генерировать исключения; для краткого предварительного ознакомления ниже показана схема assert в действии:
class Super:
def delegate(self): self.action() def action(self):
assert False, 'action must be defined! ' # Если вызвана эта версия
>» X = Super ()
>» X. delegate ()
AssertionError: action must be defined!
AssertionError: метод action должен быть определен!
Мы рассмотрим оператор assert в главах 33 и 34; говоря кратко, если результатом вычисления его первого выражения будет False, тогда он генерирует исключение с указанным сообщением об ошибке. Здесь выражение всегда имеет результат False, чтобы инициировать выдачу сообщения об ошибке, когда метод не переопределен, и процесс наследования находит данную версию. В качестве альтернативы некоторые классы в таких методах-заглушках просто генерируют исключение NotlmplementedError, сигнализируя об ошибке:
class Super:
def delegate(self): self.action() def action(self):
raise NotlmplementedError('action must be defined!')
>» X = Super ()
>>> X.delegate()
NotlmplementedError: action must be defined!
NotlmplementedError: метод action должен быть определен!
Для экземпляров подклассов мы по-прежнему получаем исключение до тех пор, пока подкласс не предоставит ожидаемый метод для замещения стандартной версии в суперклассе:
>» class Sub (Super) : pass
»> X = Sub ()
»> X.delegate ()
NotlmplementedError: action must be defined!
>>> class Sub(Super):
def action(self): print('spam1)
»> X = Sub ()
>>> X.delegate()
spam
Упражнение 8 (систематизация животных в зоопарке) в конце главы 32 посвящено более реалистичному примеру реализации концепций, обсуждаемых в настоящем разделе (решение упражнения приведено в приложении Г). Такая систематизация представляет собой традиционный способ введения в ООП, хотя она лежит несколько в стороне от служебных обязанностей большинства разработчиков (приношу извинения тем читателям, кто по стечению обстоятельств работает в зоопарке!).
Абстрактные суперклассы в Python З.Х и Python 2.6+: обзор
С выходом версий Python 2.6 и 3.0 абстрактные суперклассы из предыдущего раздела (они же “абстрактные базовые классы”), которые требуют заполнения методов подклассами, также могут быть реализованы посредством специального синтаксиса классов. Способ написания кода слегка варьируется в зависимости от версии. В Python
З.Х мы применяем ключевой аргумент в заголовке оператора class вместе со специальным декораторным синтаксисом @, которые будут более подробно описаны далее в книге:
from abc import ABCMeta, abstractmethod
class Super(metaclass=ABCMeta):
@abstractmethod def method(self, . . .) : pass
Но в Python 2.6 и 2.7 взамен мы используем атрибут класса:
class Super:
_metaclass_ = ABCMeta
@abstractmethod def method(self, . . .) : pass
В обоих случаях результат тот же — мы не можем создать экземпляр, пока не определим метод ниже в дереве классов. Скажем, для примера из предыдущего раздела в Python З.Х предусмотрен специальный эквивалентный синтаксис:
>>> from abc import ABCMeta, abstractmethod
»>
>>> class Super (metaclass=ABCMeta) : def delegate(self): self.action()
@abstractmethod def action(self) : pass
»> X = Super ()
TypeError: Can't instantiate abstract class Super with abstract methods action
TypeError: невозможно создать экземпляр абстрактного класса Super с абстрактными методами action
»> class Sub (Super) : pass
»> X = Sub()
TypeError: Can't instantiate abstract class Sub with abstract methods action
TypeError: невозможно создать экземпляр абстрактного класса Sub с абстрактными методами action
»> class Sub (Super) :
def action(self): print('spam')
»> X = Sub ()
>>> X.delegate()
spam
При такой реализации создавать экземпляры класса с абстрактным методом нельзя (т.е. мы не можем создать экземпляр, обратившись к нему), пока в подклассах не будут определены все его абстрактные методы. Хотя такой подход требует написания большего объема кода и добавочных знаний, его потенциальное преимущество в том, что сообщения об ошибках, связанных с недостающими методами, выдаются при попытке создания экземпляра класса, а не позже, когда мы пытаемся вызвать отсутствующий метод. Это средство можно применять и для определения ожидаемого интерфейса, автоматически проверяемого в клиентских классах.
К сожалению, продемонстрированная схема также опирается на два расширенных языковых инструмента, с которыми мы пока еще не сталкивались — декораторы функций, представляемые в главе 32 и подробно рассматриваемые в главе 39, плюс объявления метаклассов, которые упоминаются в главе 32 и раскрываются в главе 40 — поэтому здесь мы не будем касаться остальных аспектов данного средства. Дополнительные сведения ищите в стандартных руководствах по Python, а также в готовых абстрактных суперклассах, предлагаемых Python.
Пространства имен: заключение
Итак, после того, как мы исследовали объекты классов и экземпляров, история пространств имен Python завершена. В справочных целях ниже приведена краткая сводка всех правил, применяемых для распознавания имен. Первое, что следует запомнить — уточненные и неуточненные имена трактуются по-разному, а некоторые области видимости предназначены для инициализации пространств имен объектов:
• неуточненные имена (например, X) имеют дело с областями видимости;
• уточненные имена (скажем, object.X) используют пространства имен объектов;
• определенные области видимости инициализируют пространства имен объектов (для модулей и классов).
Иногда эти концепции взаимодействуют — например, в object.X имя object ищется по областям видимости, после чего в результирующих объектах производится поиск имени X. Поскольку области видимости и пространства имен жизненно важны для понимания кода на Python, давайте подведем более детальные итоги по правилам.
Простые имена: глобальные, если не выполнено их присваивание
Как уже известно, неуточненные простые имена следуют правилу лексических областей видимости LEGB, кратко описанному при исследовании функций в главе 17 первого тома.
Присваивание (X = значение)
По умолчанию делает имя локальным: создает либо изменяет имя X в текущей локальной области видимости, если только оно не объявлено как global (или nonlocal в Python З.Х).
Ссылка (X)
Ищет имя X в текущей локальной области видимости, затем в любых объемлющих функциях, далее в текущей глобальной области видимости, затем во встроенной области видимости согласно правилу LEGB. Поиск во включающих классах не выполняется: взамен имена классов извлекаются как атрибуты объектов.
Кроме того, как упоминалось в главе 17 первого тома, некоторые конструкции в особых сценариях дополнительно локализуют имена (например, переменные в ряде включений и конструкции операторов try), но подавляющее большинство имен следуют правилу LEGB.
Имена атрибутов: пространства имен объектов
Мы также видели, что уточненные имена атрибутов ссылаются на атрибуты специфических объектов и подчиняются правилам для модулей и классов. В случае объектов классов и экземпляров правила ссылки дополняются с целью включения процедуры поиска при наследовании.
Присваивание (obj ect.X = зна чение)
Создает или модифицирует имя атрибута X в пространстве имен уточняемого объекта object и больше нигде. Подъем по дереву наследования происходит только при ссылке на атрибут, но не в случае присваивания значения атрибуту.
Ссылка (object. X)
Для объектов, основанных на классах, ищет имя атрибута X в object и затем во всех доступных классах выше, применяя процедуру поиска при наследовании. Для объектов, не основанных на классах, таких как модули, извлекает X из object напрямую.
Как упоминалось ранее, предшествующие правила отражают нормальный и типичный сценарий. Правила распознавания имен атрибутов могут варьироваться в классах, которые используют более развитые инструменты, особенно в классах нового стиля — вариант в Python 2.Х и стандарт в Python З.Х, которые мы будем исследовать в главе 32. Скажем, ссылочное наследование может обладать более широкими возможностями, чем здесь предполагается, когда развернуты метаклассы, а классы, которые задействуют инструменты управления атрибутами, такие как свойства, дескрипторы
и_setattr_, могут перехватывать и направлять присваивания значений атрибутам
произвольным образом.
На самом деле определенная процедура наследования запускается и при присваивании для нахождения дескрипторов с методом_set_в классах нового стиля; такие
инструменты переопределяют нормальные правила для ссылки и присваивания. В главе 38 мы подробно обсудим инструменты управления атрибутами, а в главе 40 формализуем наследование и то, как оно применяет дескрипторы. Пока что большинство читателей должны сосредоточить внимание на приведенных здесь нормальных правилах, которые охватывают большую часть прикладного кода на Python.
"Дзен" пространств имен: присваивания классифицируют имена
Из-за отличающихся процедур поиска для уточненных и неуточненных имен и множества уровней просмотра в обеих процедурах временами трудно сказать, где в итоге будет найдено то или иное имя. В Python критически важно место, где имени присваивается значение — место полностью определяет область видимости или объект, в котором имя будет находиться. В файле manynames .ру, содержимое которого показано ниже, демонстрируется перевод этого принципа в код, а также подытоживаются идеи пространств имен, встречающиеся повсеместно в книге (без учета не особо ясных областей видимости особых сценариев вроде включений):
# Файл manynames .ру
# Глобальное имя/атрибут модуля (X или manynames. X)
# Доступ к глобальному имени X (11)
# Локальная переменная в функции (X, скрывает X в модуле)
# Атрибут класса (С.Х)
X = 11
def f () : print(X)
def g():
X = 22 print(X)
class C:
X = 33
def m(self) :
X = 44 # Локальная переменная в методе (X)
self.Х = 55 # Атрибут экземпляра (экземпляр. X)
В файле одно и то же имя X присваивается пять раз — иллюстративная, хотя не совсем лучшая практика! Тем не менее, поскольку это имя присваивается в пяти разных местах, все пять X в программе являются совершенно разными переменными. Просматривая сверху вниз, вот что генерируют присваивания X: атрибут модуля (11), локальную переменную в функции (22), атрибут класса (33), локальную переменную в методе (44) и атрибут экземпляра (55). Хотя все пять имеют имя X, тот факт, что всем им присваивались значения в разных местах исходного кода, или то, что им были присвоены разные объекты, делает их всех уникальными переменными.
Вы должны посвятить время внимательному изучению примера, т.к. в нем собраны идеи, которые исследовались в нескольких последних частях книги. Осмыслив его должным образом, вы обретете знания пространств имен Python. Или можете запустить код и посмотреть, что произойдет. Вот остаток файла исходного кода, где создается экземпляр, а также и выводятся все переменные X, которые можно извлекать:
_name_ == '_main_'
i .
print(X)
#
11: имя из модуля (оно же manynames. X
#
за пределами файла)
f 0
#
11: глобальное имя
g()
#
22: локальное имя
print(X)
#
11: имя из модуля не изменилось
obj = C()
#
Создать экземпляр
print(obj.X)
#
33: имя из класса, унаследованное экземпляром
obj . m ()
#
Присоединения имени атрибута X к экземпляру
print(obj.X)
#
55: имя из экземпляра
print(С.X)
#
33: имя из класса (оно же obj.X, если X
#
в экземпляре отсутствует)
#print(C.m.X)
#
НЕУДАЧА: видимо только в методе
#print(g.X)
#
НЕУДАЧА: видимо только в функции
Вывод, полученный в результате запуска файла, указан в комментариях внутри кода; отследите их, чтобы посмотреть, как каждый раз осуществляется доступ к переменной по имени X. В частности обратите внимание, что мы можем указывать класс для извлечения его атрибута (С.Х), но извлечь локальные переменные внутри функций или методов извне их операторов def не удастся. Локальные переменные видимы только другому коду внутри def, и в действительности они существуют в памяти только в период, пока выполняется вызов функции или метода.
Некоторые имена, определенные в этом файле, видимы также за пределами файла другим модулям, но не забывайте, что мы обязаны всегда импортировать файл, прежде чем сможем иметь доступ к его именам — в конце концов, как раз изоляция имен является сущностью модулей:
# Файл otherfile.ру
import manynames
X - 66
print(X)
# 66:
здесь глобальное имя
print(manynames.X)
# 11:
после импортирования глобальные имена
становятся атрибутами
manynames.f()
# 11:
имя X из модуля manynames, не то, которое
находится здесь!
manynames.g()
# 22:
локальное имя из функции в другом файле
print(manynames.C.X)
# 33;
атрибут класса в другом модуле
I = manynames.С()
print(I.X)
# 33:
здесь все еще имя из класса
I.m()
print(I.X)
# 55:
а теперь имя из экземпляра!
Обратите внимание, что вызов manynames . f () выводит X из manynames, не имя X, присвоенное в данном файле — области видимости всегда определяются местоположением присваиваний в исходном коде (т.е. лексически) и никогда не зависят от того, что и куда импортируется. Кроме того, имейте в виду, что собственное имя X экземпляра не создается до тех пор, пока мы не вызовем I. m () — подобно всем переменным атрибуты появляются, когда им присваиваются значения, не ранее. Обычно мы создаем атрибуты экземпляров путем присваивания им значений в методах конструкторов _init_классов, но это не единственный вариант.
Наконец, как известно из главы 17 первого тома, функция в состоянии изменять имена за пределами самой себя с помощью операторов global и (в Python З.Х) nonlocal — указанные операторы предоставляют доступ по записи, но также модифицируют правила привязки присваивания к пространству имен:
X = 11 # Глобальное имя в модуле
def gl() :
print (X) # Ссылка на глобальное имя в модуле (11)
def g2 () : global X
X = 22 # Изменение глобального имени в модуле
def hi () :
X = 33 # Локальное имя в функции
def nested():
print (X) # Ссылка на локальное имя в объемлющей области видимости (33)
def h2 () :
X = 33 # Локальное имя в функции
def nested():
nonlocal X # Оператор Python З.Х
X = 44 # Изменение локального имени в объемлющей области видимости
Разумеется, в общем случае вы не должны использовать одно и то же имя для всех переменных в сценарии. Однако, как было продемонстрировано в примере, даже если вы поступите подобным образом, то пространства имен Python будут работать так, чтобы предотвратить непредумышленные конфликты между именами, применяемыми в одном контексте, и именами, задействованными в другом.
Вложенные классы: снова о правиле областей видимости LEGB
В предыдущем примере резюмировалось влияние на области видимости вложенных функций, которые обсуждалось в главе 17 первого тома. Оказывается, что классы тоже можно вкладывать — полезная кодовая схема в определенных видах программ, с последствиями в отношении областей видимости, которые естественным образом вытекают из имеющихся у вас знаний, но на первый взгляд могут показаться неочевидными. В этом разделе концепция иллюстрируется на примере.
Несмотря на то что классы обычно определяются на верхнем уровне модуля, иногда они являются вложенными в генерирующие их функции — вариация на тему “фабричных функций” (замыканий) из главы 17 первого тома с похожими ролями сохранения состояния. Там мы отмечали, что операторы class вводят новые локальные области видимости во многом подобно операторам def функций, которые следуют тому же самому правилу LEGB, как и определения функций.
Правило LEGB применяется к верхнему уровню самого класса и к верхнему уровню вложенных в него функций методов. Оба формируют уровень L этого правила — они представляют собой нормальные локальные области видимости с доступом к их именам, именам в любых объемлющих функциях, глобальным именам во включающем модуле и встроенной области видимости. Как и модули, после выполнения оператора class локальная область видимости класса превращается в пространство имен атрибутов.
Хотя классы имеют доступ к областям видимости объемлющих функций, они не действуют в качестве объемлющих областей видимости для кода, вложенного внутрь
класса: Python ищет имена, на которые произведена ссылка, в объемлющих функциях, но никогда не выполняет их поиск в объемлющих классах. То есть класс является локальной областью видимости и имеет доступ к объемлющим локальным областям видимости, но он не служит объемлющей локальной областью видимости для дальнейшего вложенного кода. Из-за того, что процесс поиска имен, используемых в функциях методов, пропускает включающий класс, атрибуты класса должны извлекаться как атрибуты объекта с применением наследования.
Например, в следующей функции nester все ссылки на X направляются в глобальную область видимости кроме последней, которая подбирает переопределение из локальной области видимости (данный раздел кода находится в файле classscope.ру, а вывод каждого примера описан в последних двух комментариях):
X = 1
def nester () : print(X) class С: print(X)
def methodl(self) print(X) def method2(self) X = 3 print(X)
I = C()
I.methodl()
I.method2()
print(X) nester () print ('-'*40)
# Глобальное имя: 1
# Глобальное имя: 1
# Глобальное имя: 1
# Скрывает глобальное имя
# Локальное имя: 3
Глобальное имя: 1 Остаток: 1, 1, 1, 3
Тем не менее, взгляните, что происходит, когда мы заново присваиваем значение тому же самому имени на уровнях вложенных функций. Переопределение X создает локальные имена, которые скрывают имена из объемлющих областей видимости, в точности как для простых вложенных функций; уровень включающего класса вовсе не изменяет это правило и на самом деле не имеет к нему отношения:
х = 1
def nester ():
X = 2 print(X) class С: print(X)
def methodl(self) print(X) def method2(self) X = 3 print(X)
I = C()
I.methodl()
I.method2()
print(X) nester() print(1 -'*40)
# Скрывает глобальное имя
# Локальное имя: 2
# В объемлющем def (nester) : 2
# В объемлющем def (nester) : 2
# Скрывает имя из объемлющего def (nester)
# Локальное имя: 3
# Глобальное имя: 1
# Остаток: 2, 2, 2, 3
А вот что случается, когда мы заново присваиваем значение тому же самому имени несколько раз по пути: присваивания в локальных областях видимости функций и классов скрывают глобальные имена или совпадающие локальные имена из объемлющих функций независимо от
X = 1
вложенности:
def nester () :
X = 2 print(X) class С:
# Скрывает глобальное имя
# Локальное имя: 2
# Локальное имя из класса скрывает имя из nester:
# Локальное имя: 3
# В объемлющем def (не 3 в классе!) : 2
# Унаследованное локальное имя класса: 3
# Скрывает имя из объемлющей области видимости
# Локальное имя: 4
# Скрывает имя из класса
# Находится в экземпляре: 5
X = 3
C.X или I.X
print(X)
def methodl(self) print(X) print(self.X) def method2(self)
X = 4
(nester, не класса) print(X) self.X = 5 print(self.X)
I = C()
I.methodl()
I.method2()
# Глобальное имя: 1
# Остаток: 2, 3, 2, 3, 4, 5
print(X)
nester ()
print('-'*40)
Самое главное, правила поиска для простых имен вроде X никогда не ищут во включающих операторах class — только в операторах def, модулях и встроенной области видимости (правило называется LEGB, а не CLEGB!). Скажем, в methodl имя X находится в def за пределами включающего класса, который имеет то же самое имя в своей локальной области видимости. Чтобы получить имена, присвоенные в классе (например, методы), мы обязаны извлекать их как атрибуты объекта класса или экземпляра через self .X в данном случае.
Хотите верьте, хотите нет, но позже в книге мы увидим сценарии применения для такой кодовой схемы с вложенными классами, особенно в ряде декораторов в главе 39. В этой роли объемлющая функция обычно служит фабрикой классов и предоставляет сохраненное состояние для последующего использования во включающем классе или в его методах.
Словари пространств имен: обзор
В главе 23 первого тома мы выяснили, что пространства имен модулей имеют конкретную реализацию в виде словарей, доступную через встроенный атрибут_diet_.
В главах 27 и 28 мы узнали, что то же самое остается справедливым для объектов классов и экземпляров — уточнение атрибутов внутренне обычно представляет собой операцию индексирования в словаре. На самом деле объекты классов и экземпляров в Python по большей части являются всего лишь словарями со связями между ними. Доступ к таким словарям, равно как к их связям, обеспечивается для участия в расширенных ролях (скажем, для создания инструментов).
Мы применяли ряд инструментов подобного рода в предыдущей главе, но чтобы подвести итог и помочь вам лучше понять, каким образом внутренне работают атрибуты, давайте запустим интерактивный сеанс, который отслеживает рост словарей пространств имен, когда задействованы классы. Теперь, обладая дополнительными знаниями о методах и суперклассах, мы можем предоставить здесь улучшенный обзор. Первым делом определим суперкласс и подкласс с методами, которые будут сохранять данные в своих экземплярах:
>>> class Super:
def hello(self):
self.datal = 'spam'
>>> class Sub(Super): def hola(self):
self.data2 = 'eggs'
Когда мы создаем экземпляр подкласса, он начинает свое существование с пустого словаря пространства имен, но имеет связи с классом для обеспечения работы поиска при наследовании. В действительности дерево наследования явно доступно в специальных атрибутах, которые можно исследовать. Экземпляры располагают атрибутом
_class_, связывающим их с классом, а классы имеют атрибут_bases_, который
представляет собой кортеж, содержащий связи с находящимися выше в дереве суперклассами (приведенный далее код запускался в Python 3.7; ваши форматы имен, внутренние атрибуты и порядок ключей могут отличаться):
»> X = Sub()
>>> X. diet__# Словарь пространства имен экземпляра
{)
»> X._class__# Класс экземпляра
<class '_main_.Sub’>
>>> Sub._bases__# Суперкласс класса
(<class ’_main_.Super’>,)
>» Super._bases__# Пустой кортеж () в Python 2.X
(<class 'object'>,)
Когда классы выполняют присваивание атрибутам self, они заполняют объекты экземпляров — т.е. атрибуты оказываются в словарях пространств имен экземпляров, а не классов. Пространство имен объекта экземпляра записывает данные, которые могут варьироваться от экземпляра к экземпляру, причем self является привязкой к этому пространству имен:
»> у = Sub ()
»> X.hello()
>>> X._diet_
{'datal1: 'spam'}
»> X.hola ()
>>> X. diet_
{'datal': 'spam', 'data2': 'eggs'}
»> list (Sub, diet_.keys())
['_module_', 'hola', '_doc_']
»> list (Super, diet_.keys())
['_module_'hello1, '_diet_', 1_weakref_', '_doc_']
»> Y._diet_
{}
Обратите внимание на добавочные имена с символами подчеркивания в словарях классов; Python устанавливает такие имена автоматически, и мы можем отфильтровать их с помощью генераторных выражений, которые были показаны в главах 27 и 28, но здесь не повторяются. Большинство таких имен в обычных программах не используется, но есть инструменты, применяющие некоторые из них (например, в _doc_хранятся строки документации, обсуждаемые в главе 15 первого тома).
Также обратите внимание, что Y, второй экземпляр, созданный в начале сеанса, в конце по-прежнему имеет пустой словарь пространства имен, хотя словарь экземпляра X заполнялся присваиваниями в методах. Опять-таки каждый экземпляр располагает независимым словарем пространства имен, пустым в самом начале и способным хранить атрибуты, которые полностью отличаются от атрибутов, записанных в словарях пространств имен других экземпляров того же самого класса.
Поскольку атрибуты в действительности представляют собой ключи словаря внутри Python, на самом деле существуют два способа извлечения и присваивания им значений — уточнение либо индексирование по ключу:
»> X.datal, X._diet_['datal']
('spam', 'spam')
»> X.data3 * 'toast'
>>> X._diet_
{'datal': 'spam', 'data2': 'eggs', 'data3': 'toast'}
»> X._diet_['data3' ] = 'ham'
»> X.data3
' ham'
Однако такая эквивалентность применима только к атрибутам, фактически присоединенным к экземпляру. Так как уточнение при извлечении атрибутов тоже выполняет поиск в дереве наследования, оно может получать доступ к унаследованным атрибутам, что невозможно сделать через индексирование в словаре пространства имен. Скажем, к унаследованному атрибуту X.hello нельзя обратиться посредством X._diet_[ ’hello’ ].
Поэкспериментируйте с этими специальными атрибутами самостоятельно, чтобы получить лучшее представление о том, как пространства имен действительно работают с атрибутами. Кроме того, попробуйте прогнать имеющиеся объекты через функцию dir, с которой мы встречались в предшествующих двух главах — вызов dir (X) подобен X._diet_. keys (), но помимо сортировки своего списка функция dir также включает ряд унаследованных и встроенных атрибутов. Даже если вам никогда не придется использовать все это при разработке собственных программ, знание того, что они являются всего лишь нормальными словарями, может содействовать пониманию пространств имен в целом.
В главе 32 мы также узнаем о слотах — усовершенствованной возможности классов нового стиля хранить атрибуты в экземплярах, но не в их словарях пространств имен. Их заманчиво трактовать как атрибуты классов, и они на самом деле появляются в пространствах имен, где управляют значениями для каждого экземпляра. Тем не менее, как мы увидим, слоты могут полностью воспрепятствовать созданию_diet_в экземпляре — потенциальная ситуация, возникновение которой обобщенные инструменты должны иногда учитывать за счет применения нейтральных к хранению средств, таких как dir и getattr.
Связи между пространствами имен: инструмент подъема по дереву
В предыдущем разделе демонстрировались специальные атрибуты экземпляров и
классов_class_и_bases_без реального объяснения, почему они могут вообще
интересовать. Формулируя кратко, упомянутые атрибуты позволяют инспектировать иерархии наследования внутри написанного вами кода. Например, их можно использовать для отображения дерева классов, как показано ниже в коде на Python З.Х и 2.Х:
#!python
» и ??
classtree.ру: подъем по деревьям наследования с применением связей между
пространствами имен и отображением находящихся выше суперклассов с отступом
согласно высоте
VV II и
def classtree(els, indent):
# Вывести здесь имя класса
# Вызвать рекурсивно для всех
# суперклассов
# Может посетить суперкласс
# более одного раза
# Показать экземпляр
# Подняться к его классу
def selftest ():
class A: pass class В(A): pass class С(A): pass class D (В, C): pass class E: pass class F(D,E): pass instancetree(B()) instancetree(F())
if _name_ == '_main_selftest ()
Функция classtree в этом сценарии рекурсивна — она выводит имя класса с использованием _name_и затем поднимается к его суперклассам, вызывая саму себя.
Такая реализация позволяет функции обходить деревья классов произвольной формы; рекурсия поднимается доверху и останавливается на корневых суперклассах,
которые имеют пустые атрибуты_bases_. Когда применяется рекурсия, каждый
активный уровень функции получает собственную копию локальной области видимости; здесь это означает, что аргументы els и indent будут разными на каждом уровне classtree.
Большую часть файла занимает код самотестирования. При автономном запуске в Python 2.Х он строит пустое дерево классов, создает два его экземпляра и выводит структуры их деревьев классов:
C:\code> c:\python27\python classtree.ру
Tree of <_main_.В instance at 0x00000000022C3A88>
. . .B
......A
Tree of <_main_.F instance at 0x00000000022C3A88>
. . .F
......D
......Е
В случае запуска в Python З.Х дерево включает подразумеваемый суперкласс object, который автоматически добавляется над автономными корневыми (т.е. самыми верхними) классами, потому что в Python З.Х все классы являются классами “нового стиля” — более подробно о таком изменении речь пойдет в главе 32:
C:\code> c:\python37\python classtree.ру
Tree of <_main_.selftest.<locals>.В object at 0x00000000012BC4A8>
. . .B ......A
.........object
Tree of <_main_.selftest.<locals>.F object at Ox00000000012BC4A8>
. . .F
......D
.........В
............A
...............object
.........С
............A
...............object
......E
.........object
Отступы, отмечаемые точками, используются для обозначения высоты дерева классов. Конечно, мы могли бы улучшить формат вывода и возможно даже изобразить его в графическом пользовательском интерфейсе. Однако и в существующем виде мы в состоянии импортировать эти функции везде, где желательно быстро отобразить дерево классов:
С:\с ode > с:\руthon3 7\руthon »> class Emp: pass
»> class Person (Emp) : pass
»> bob = Person ()
>>> import classtree »> classtree. instancetree (bob)
Tree of <_main_.Person object at 0x0000000002E8DC88>
...Person
......Emp
.........object
Независимо от того, придется ли вам когда-нибудь создавать или применять такие инструменты, в примере был продемонстрирован один из многих способов, которыми можно задействовать специальные атрибуты, открывающие доступ к внутренностям интерпретатора. Вы увидите еще один способ при реализации универсальных инструментов отображения классов lister.py в разделе “Множественное наследование: ‘подмешиваемые’ классы” главы 31 — там мы расширим эту методику, чтобы также отображать атрибуты каждого объекта в дереве классов и функционировать в качестве общего суперкласса.
В последней части книги мы снова возвратимся к таким инструментам в контексте построения инструментов Python в целом для создания инструментов, которые обеспечивают защиту атрибутов, проверку достоверности аргументов и т.д. Доступ к внутренностям интерпретатора дает возможность реализовывать мощные инструменты разработки, хотя требуется далеко не каждому программисту на Python.
Снова о строках документации
В примере из предыдущего раздела присутствует строка документации для его модуля, но не забывайте, что строки документации можно использовать также для компонентов классов. Строки документации, подробно рассмотренные в главе 15 первого тома, представляют собой строковые литералы, которые отображаются в верхней части разнообразных структур и автоматически сохраняются Python в атрибутах
_doc_соответствующих объектов. Они работают для файлов модулей, операторов
def определения функций, а также классов и методов.
Теперь, лучше зная классы и методы, в файле docstr.py, содержимое которого показано ниже, предлагается короткий, но исчерпывающий пример, демонстрирующий места, где допускается появление строк документации в коде. Все они могут быть блоками в утроенных кавычках или более простыми однострочными литералами вроде приведенных далее:
Основное преимущество строк документации заключается в том, что они не исчезают во время выполнения. Соответственно, если для объекта была предусмотрена строка документации, тогда вы можете дополнить объект атрибутом_doc_и извлечь его документацию (символы разрыва строки должным образом интерпретируются при выводе):
>>> import docstr >>> docstr. doc_
>>> x = docstr. spam ()
>» x.method()
I am: spam._doc_ or docstr. spam._doc_ or self._doc_
I am: spam.method._doc_ or self.method._doc_
В главе 15 первого тома обсуждался инструмент PyDoc, которому известно, как форматировать все эти строки в отчетах и на веб-страницах. Ниже представлен результат выполнения функции help для docstr в Python 3.7:
>>> help(docstr)
Help on module docstr:
NAME
docstr - I am: docstr._doc_
CLASSES
builtins.object spam
class spam(builtins.object)
I I am: spam._doc_ or docstr. spam._doc_ or self._doc_
I
I Methods defined here:
I
I method(self)
I I am: spam.method._doc_ or self.method._doc_
I Data descriptors defined here:
I
I _diet_
I dictionary for instance variables (if defined)
I
I _weakref_
I list of weak references to the object (if defined)
FUNCTIONS
func(args)
I am: docstr.func._doc_
FILE
c:\code\docstr.py
Строки документации доступны во время выполнения, но синтаксически они менее гибкие, чем комментарии #, которые могут появляться в программе где угодно. Обе формы являются полезными инструментами, и наличие любой документации по программе — хороший факт (разумеется, при условии, что она точна!). Как утверждалось ранее, эмпирическое правило “рекомендуемой методики” в Python предполагает применение строк документации для документирования функциональности (что объекты делают) и комментариев # для документирования на микро-уровнях (как работают загадочные порции кода).
Классы или модули
Наконец, давайте завершим главу кратким сравнением того, что было темами последних двух частей: модулей и классов. Поскольку и модули, и классы являются пространствами имен, различие между ними может сбивать с толку.
• Модули:
• реализуют пакеты данных/логики;
• создаются с помощью файлов с кодом на Python или расширений на других языках;
• используются путем импортирования;
• формируют верхний уровень структуры программы на Python.
• Классы:
• реализуют новые полнофункциональные объекты;
• создаются посредством операторов class;
• используются путем обращения к ним;
• всегда находятся внутри модуля.
Классы также поддерживают дополнительные возможности, отсутствующие у модулей, такие как перегрузка операций, создание множества экземпляров и наследование. Хотя классы и модули представляют собой пространства имен, вы должны уже понимать, что они являются очень разными вещами. Нам необходимо двигаться вперед, чтобы увидеть, насколько разными могут быть сами классы.
Резюме
В главе мы провели второй, углубленный экскурс в механизмы ООП на языке Python. Вы больше узнали о классах, методах и наследовании. Вдобавок мы завершили историю о пространствах имен и областях видимости в Python, расширив ее применительно к классам. Попутно мы взглянули на несколько более развитых концепций вроде абстрактных суперклассов, атрибутов данных классов, словарей и связей пространств имен, а также ручных вызовов методов и конструкторов суперкласса.
После выяснения механики создания классов на языке Python в главе 30 мы перейдем к специфическому аспекту этой механики: перегрузке операций. Затем мы исследуем распространенные паттерны проектирования и рассмотрим ряд способов, которыми обычно классы используются и объединяются для оптимизации многократного применения кода. Но сначала закрепите пройденный материал главы, ответив на контрольные вопросы.
Проверьте свои знания: контрольные вопросы
1. Что такое абстрактный суперкласс?
2. Что происходит, когда простое присваивание появляется на верхнем уровне оператора class?
3. Почему в классе может возникнуть потребность в ручном вызове метода _init_суперкласса?
4. Как можно дополнить унаследованный метод, не замещая его полностью?
5. Чем локальная область видимости класса отличается от локальной области видимости функции?
6. Какой город был столицей Ассирии?
Проверьте свои знания: ответы
1. Абстрактный суперкласс — это класс, который вызывает метод, но не наследует и не определяет его; он ожидает заполнения метода подклассом. Абстрактные суперклассы часто используются в качестве способа обобщения классов, когда поведение не может быть спрогнозировано до написания кода более специфического подкласса. Объектно-ориентированные фреймворки также применяют их как способ направления на определяемые клиентом настраиваемые операции.
2. Когда простой оператор присваивания (X = Y) появляется на верхнем уровне оператора class, он присоединяет к классу атрибут данных (Класс.X). Подобно всем атрибутам класса он будет разделяться всеми экземплярами; тем не менее, атрибуты данных не являются вызываемыми функциями методов.
3. В классе'должен вручную вызываться метод_init_суперкласса, если в нем
определяется собственный конструктор_init_и нужно, чтобы код конструктора суперкласса по-прежнему выполнялся. Сам интерпретатор Python автоматически запускает только один конструктор — самый нижний в дереве. Конструктор суперкласса обычно вызывается через имя класса с передачей экземпляра self вручную: Суперкласс._init_(self, . . .).
4. Чтобы вместо замещения дополнить унаследованный метод, его понадобится повторно определить в подклассе, но внутри этой новой версии вручную вызвать версию метода из суперкласса с передачей ей экземпляра self: Суперкласс.метод(self, ...).
5. Класс представляет собой локальную область видимости и имеет доступ к объемлющим локальным областям видимости, но он не служит в качестве локальной области видимости для добавочного вложенного кода. Подобно модулям после выполнения оператора class локальная область видимости класса превращается в пространство имен атрибутов.
6. Ашшур (или Калат-Шергат), Калах (или Нимруд), короткое время Дур-Шаррукин (или Хорсабад) и в заключение Ниневия.
ГЛАВА 30
Назад: Более реалистичный пример
Дальше: Перегрузка операций