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

Проектирование с использованием классов

 

До сих пор в текущей части книги внимание было сосредоточено на применении инструмента ООП на языке Python — класса. Но ООП также связано с вопросами проектирования, т.е. с тем, каким образом использовать классы для моделирования полезных объектов. В этой главе мы затронем несколько основных идей ООП и представим ряд дополнительных примеров, более реалистичных, чем многие показанные ранее.
Попутно мы реализуем некоторые распространенные паттерны проектирования в ООП на Python, такие как наследование, композиция, делегирование и фабрики. Мы также исследуем концепции классов, ориентированные на проектирование, вроде псевдозакрытых атрибутов, множественного наследования и связанных методов.
Одно заблаговременное примечание: некоторые упоминаемые здесь термины, относящиеся к проектированию, требуют большего объема пояснений, чем я смог предложить в книге. Если данный материал вызвал у вас интерес, тогда я рекомендую в качестве следующего шага почитать какую-нибудь книгу по объектно-ориентированному проектированию или паттернам проектирования. Как вы вскоре убедитесь, хорошая новость заключается в том, что Python делает многие традиционные паттерны проектирования тривиальными.
Python и объектно-ориентированное программирование
Давайте начнем с обзора — реализация ООП в языке Python может быть сведена к трем идеям.
Наследование
Наследование основано на поиске атрибутов в Python (в выражениях X.name). Полиморфизм
В выражении X.method смысл method зависит от типа (класса) подчиненного объекта X.
Инкапсуляция
Методы и операции реализуют поведение, хотя сокрытие данных по умолчанию является соглашением.
К этому времени вы должны иметь хорошее представление о том, что такое наследование в Python. Ранее мы также несколько раз обсуждали полиморфизм в Python; он вытекает из отсутствия объявлений типов. Поскольку атрибуты всегда распознаются во время выполнения, объекты, которые реализуют те же самые интерфейсы, автоматически становятся взаимозаменяемыми; клиенты не обязаны знать о том, какие виды объектов реализуют методы, вызываемые клиентами.
Инкапсуляция означает пакетирование в Python, т.е. сокрытие деталей реализации за интерфейсом объекта. Она вовсе не означает принудительную защиту, несмотря на возможность ее реализации с помощью кода, как мы увидим в главе 39. Однако инкапсуляция доступна и полезна в Python: она позволяет изменять реализацию интерфейса объекта, не влияя на пользователей этого объекта.
Полиморфизм означает интерфейсы, а не сигнатуры вызовов
Некоторые языки ООП определяют полиморфизм как подразумевающий перегрузку функций на основе сигнатур типов их аргументов — количестве и/или типах переданных аргументов. Ввиду отсутствия объявлений типов в Python такая концепция фактически неприменима; как будет показано, полиморфизм в Python базируется на интерфейсах объектов, а не на типах.
Если вы тоскуете по тем дням, когда программировали на C++, то можете попробовать перегрузить методы посредством списков их аргументов, примерно так:
class С:
def meth(self, х):
def meth(self, x, y, z) :
Код запустится, но из-за того, что def просто присваивает объект имени в области видимости класса, останется лишь одно определение функции метода — последнее. Иными словами, ситуация такая же, как если бы вы ввели X = 1 и затем X = 2; имя X получило бы значение 2. Следовательно, может существовать только одно определение имени метода.
В случае реальной необходимости вы всегда можете написать код выбора на основе типов, используя идеи проверки типов, с которыми мы встречались в главах 4 и 9 первого тома, либо инструменты списков аргументов из главы 18 первого тома:
class С:
def meth(self, *args):
if len(args) ==1: # Ветвление no количеству аргументов
elif type(arg[0]) == int: # Ветвление no типам аргументов
# (или isinstance ())
Тем не менее, обычно вы не должны поступать так — это не стиль Python. Как было описано в главе 16 первого тома, вы обязаны писать код в расчете только на интерфейс объекта, но не на специфический тип данных. В итоге код будет полезен для более широкой категории типов и приложений, как сейчас, так и в будущем:
class С:
def meth(self, х):
х.operation() # Предположим, что х делает что-то правильное
Также в целом считается лучше применять отличающиеся имена методов для отдельных операций, а не полагаться на сигнатуры вызовов (не имеет значения, на каком языке вы пишете код).
Хотя объектная модель Python прямолинейна, основное мастерство в ООП проявляется в способе объединения классов для достижения целей программы. В следующем разделе начинается обзор особенностей использования классов более крупными программами для извлечения из них выгоды.
Объектно-ориентированное программирование и наследование: отношения "является"
Мы уже довольно глубоко исследовали механику наследования, но сейчас я хотел бы показать вам пример того, как оно может применяться для моделирования взаимосвязей из реального мира. С точки зрения программиста наследование вводится уточнениями атрибутов, которые инициируют поиск имен в экземплярах, в их классах и затем в любых суперклассах. С точки зрения проектировщика наследование является способом указания членства в наборе: класс определяет набор свойств, которые могут наследоваться и настраиваться более специфическими наборами (т.е. подклассами).
В целях иллюстрации давайте создадим робота по приготовлению пиццы, о котором шла речь в начале данной части книги. Предположим, мы решили проверить альтернативные пути карьерного продвижения и открыть пиццерию (совсем неплохо складывается карьера). Одним из первых дел, которые понадобится выполнить, будет наем персонала для обслуживания клиентов, готовки еды и т.д. Будучи в душе инженерами, мы решили построить робота для приготовления пиццы, но по причине своей политической и кибернетической корректности мы также решили сделать нашего робота полноправным сотрудником с заработной платой.
Наша команда из пиццерии может быть определена с помощью четырех классов, находящихся в файле примера employees .ру для Python З.Х и 2.Х, содержимое которого приведено ниже. Наиболее универсальный класс, Employee, предоставляет общее поведение, такое как повышение зарплат (giveRaise) и вывод (_герг_). Есть
два вида сотрудников и потому два подкласса Employee — Chef (шеф-повар) и Server (официант). В обоих подклассах переопределяется метод work, чтобы выводить более специфические сообщения. Наконец, наш робот по приготовлению пиццы моделируется даже более специфическим классом — PizzaRobot представляет собой разновидность класса Chef, который является видом Employee. В терминах ООП мы называем такие отношения связями “является”: робот является шеф-поваром, который является сотрудником. Вот что находится в файле employees .ру:
# Файл employees .ру (Python 2.Х + З.Х)
from _future_ import print_function
class Employee:
def _init_(self, name, salary=0):
self.name = name self.salary = salary def giveRaise(self, percent):
self.salary = self.salary + (self.salary * percent) def work(self):
print(self.name, "does stuff") # делает что-то
def _repr_(self) :
return "<Employee: name=%s, salary=%s>" % (self.name, self.salary)
class Chef(Employee):
def _init_(self, name):
Employee._init_(self, name, 50000)
def work(self):
print(self.name, "makes food") # готовит еду
class Server(Employee):
def _init_(self, name):
Employee._init_(self, name, 40000)
def work(self):
print(self.name, "interfaces with customer") # взаимодействует
# с клиентом
class PizzaRobot(Chef):
def _init_(self, name):
Chef._init_(self, name)
def work(self):
print(self.name, "makes pizza") # готовит пиццу
if _name_ == "_main_":
# Создать робота по имени bob
# Выполняется унаследованный метод_герг_
# Выполняется действие, специфичное для типа
# Повысить зарплату роботу bob на 20%
bob = PizzaRobot('bob') print(bob) bob.work() bob.giveRaise(0.20) print(bob); print()
for klass in Employee, Chef, Server, PizzaRobot:
obj = klass(klass._name_)
obj.work()
Когда мы запускаем код самотестирования, включенный в этот модуль, создается робот для приготовления пиццы по имени bob, который наследует имена от трех классов: PizzaRobot, Chef и Employee. Например, при выводе bob выполняется метод Employee._repr_, а при повышении заработной платы bob вызывается метод
Employee. giveRaise, потому что его находит процедура поиска в иерархии наследования:
с:\code> python employees.ру
<Employee: name=bob, salary=50000>
bob makes pizza
<Employee: name=bob, salary=60000.0>
Employee does stuff
Chef makes food
Server interfaces with customer
PizzaRobot makes pizza
В иерархии классов подобного рода вы всегда можете создавать экземпляры любых классов, а не только тех, которые расположены в нижней части дерева. Скажем, внутри цикла for в коде самотестирования данного модуля создаются экземпляры всех четырех классов; каждый реагирует по-разному при вызове метода work, т.к. реализации этого метода во всех классах отличаются. Например, робот bob получает метод work от самого специфичного (т.е. находящегося ниже всех) класса PizzaRobot.
Разумеется, классы лишь эмулируют объекты реального мира. В настоящий момент метод work выводит сообщение, но позже его можно было бы расширить для выполнения реальной работы (если вы воспринимаете данный раздел слишком буквально, тогда взгляните на интерфейсы Python к устройствам, таким как последовательные порты, платы микроконтроллеров Arduino и одноплатные микрокомпьютеры Raspberry Pi).
Объектно-ориентированное программирование и композиция: отношения "имеет"
Понятие композиции было введено в главах 26 и 28. С точки зрения программиста композиция затрагивает внедрение других объектов в объект контейнера и их активизацию для реализации методов контейнера. С точки зрения проектировщика композиция является еще одним способом представления отношений в предметной области. Но вместо членства в наборе композиция имеет дело с компонентами — частями целого.
Композиция также отражает взаимосвязи между частями, называемые отношениями “имеет”. В некоторых книгах по объектно-ориентированному проектированию на композицию ссылаются как на агрегирование или проводят различие между этими двумя терминами, используя агрегирование для описания более слабой зависимости между контейнером и его содержимым. Здесь под “композицией” понимается просто совокупность внедренных объектов. Составной класс обычно предоставляет собственный интерфейс и реализует его, направляя выполнение действий внедренным объектам.
После реализации классов сотрудников давайте поместим их в пиццерию и предоставим работу. Наша пиццерия является составным объектом: в ней есть духовой шкаф, а также сотрудники вроде официантов и шеф-поваров. Когда клиент входит и размещает заказ, компоненты пиццерии приступают к действиям — официант принимает заказ, шеф-повар готовит пиццу и т.д. Следующий пример (файл pizzashop.ру) выполняется одинаково в Python З.Х и 2.Х и эмулирует все объекты и отношения в описанном сценарии:
# Файл pizzashop .ру (Python 2.Х + З.Х)
from _future_ import print_function
from employees import PizzaRobot, Server
class Customer:
def _init_(self, name):
self.name = name def order(self, server):
print(self.name, "orders from", server) # заказы от
def pay(self, server):
print(self.name, "pays for item to", server) # плата за единицу class Oven:
def bake(self):
print("oven bakes") class PizzaShop:
# духовой шкаф выпекает
# Внедрить другие объекты
# Робот по имени bob
def _init_(self) :
self.server = Server('Pat') self.chef = PizzaRobot('Bob') self.oven = Oven() def order(self, name):
# Активизировать другие объекты
# Заказы клиента, принятые официантом
customer = Customer(name) customer.order(self.server) self.chef.work() self.oven.bake() customer.pay(self.server)
if _name_ == "_main_":
# Создать составной объект
# Эмулировать заказ клиента Homer
# Эмулировать заказ клиента Shaggy
scene = PizzaShop () scene.order('Homer') print(’...') scene.order(1 Shaggy')
Класс PizzaShop является контейнером и контроллером; его конструктор создает и внедряет экземпляры классов сотрудников, код которых написан в предыдущем разделе, а также экземпляр определенного в текущем разделе класса Oven. Когда в коде самотестирования модуля вызывается метод order класса PizzaShop, внедренным объектам предлагается выполнить их действия по очереди. Обратите внимание, что мы создаем новый объект Customer для каждого заказа и передаем внедренный объект Server методам Customer; клиенты приходят и уходят, но официант представляет собой часть составного объекта пиццерии. Также обратите внимание, что сотрудники по-прежнему участвуют в отношении наследования; композиция и наследование — работающие совместно инструменты.
После запуска модуля пиццерия обслужит два заказа — от клиента Homer и от клиента Shaggy:
c:\code> python pizzashop.ру
Homer orders from <Employee: name=Pat, salary=40000>
Bob makes pizza
oven bakes
Homer pays for item to <Employee: name=Pat, salary=40000>
Shaggy orders from <Employee: name=Pat, salary=40000>
Bob makes pizza
oven bakes
Shaggy pays for item to <Employee: name=Pat, salary=40000>
Опять-таки это по большей части игрушечная эмуляция, но объекты и взаимодействия отражают работу составных объектов. В качестве эмпирического правила запомните: классы могут представлять почти любые объекты и отношения, которые удастся выразить с помощью предложения; просто замените имена существительные классами (скажем, Oven), а глаголы методами (например, bake), и вы получите первое приближение к проектному решению.
Снова об обработчиках потоков данных
В качестве примера композиции, который может оказаться чуть более осязаемым, чем роботы по приготовлению пиццы, вспомним обобщенную функцию обработчика потоков данных, частично реализованную во введении в ООП в главе 26:
def processor(reader, converter, writer): while True:
data = reader.read() if not data: break data = converter(data) writer.write(data)
Вместо применения простой функции мы можем написать код класса, который для выполнения своей работы использует композицию, чтобы обеспечить большую структурированность и поддержку наследования. В файле streams .ру со следующим содержимым для Python З.Х/2.Х демонстрируется возможный способ реализации класса (в нем также видоизменяется одно имя метода, потому что мы действительно запустим этот код):
class Processor:
def _init_(self, reader, writer):
self.reader = reader self.writer = writer
def process(self): while True:
data = self.reader.readline() if not data: break data = self.converter(data) self.writer.write(data)
def converter(self, data): assert False, 'converter must be defined' # Или сгенерировать исключение
Класс Processor определяет метод converter и ожидает его заполнения подклассами; он является примером модели абстрактных суперклассов, обрисованной в главе 29 (оператор assert более подробно рассматривается в части VII и просто генерирует исключение, если результатом проверки оказывается False). Реализованные подобным образом объекты reader и writer внедряются внутрь экземпляра класса (композиция), и мы поставляем логику преобразования в подклассе, а не передаем ее функции converter (наследование). Вот как выглядит содержимое файла converters .ру:
from streams import Processor
class Uppercase(Processor) : def converter(self, data): return data.upper ()
if _name_ == '_main_' :
import sys
obj = Uppercase(open('trispam.txt'), sys.stdout) obj.process()
Здесь класс Uppercase наследует логику цикла обработки потоков данных (и все остальное, что может быть реализовано в суперклассах). Ему необходимо определить только то, что в нем уникально — логику преобразования данных. При запуске файла создается и выполняется экземпляр, который читает из файла trispam, txt и записывает в поток данных stdout эквивалент в верхнем регистре содержимого:
c:\code> type trispam.txt
spam
Spam
SPAM!
c:\code> python converters.py
SPAM
SPAM
SPAM!
Чтобы обрабатывать различные виды потоков, при создании экземпляра передавайте разные типы объектов. Ниже вместо потока данных применяется выходной файл:
C:\code> python >>> import converters
»> prog = converters. Uppercase (open (' trispam. txt') , open (' trispamup. txt', ' w')) >>> prog.process()
C:\code> type trispamup.txt
SPAM
SPAM
SPAM!
Но, как предполагалось ранее, мы также могли бы передавать произвольные объекты, реализованные в виде классов, которые определяют обязательные интерфейсы с методами ввода и вывода. Далее приведен простой пример передачи класса средства записи, который помещает текст внутрь HTML-дескрипторов:
С: \code> python
»> from converters import Uppercase
»>
»> class HTMLize:
def write(self, line):
print('<PRE>%s</PRE>' % line.rstrip())
>>> Uppercase(open('trispam.txt1), HTMLize()).process()
<PRE>SPAM</PRE>
<PRE>SPAM</PRE>
<PRE>SPAM!</PRE>
Если вы проследите поток управления рассмотренного примера, то заметите, что мы получаем преобразование в верхний регистр (путем наследования) и форматирование HTML (за счет композиции), хотя основной логике обработки в исходном суперклассе Processor ничего не известно ни об одном, ни о другом шаге. Код обработки интересуется только тем, чтобы средства записи имели метод write и определяли метод по имени convert; его не заботит, что делают упомянутые методы, когда вызываются. Такой полиморфизм и инкапсуляция логики лежат в основе мощи классов в Python.
В том виде, как есть, суперкласс Processor предлагает только цикл просмотра файлов. В более реалистичном проекте мы могли бы расширить его с целью поддержки дополнительных программных инструментов для подклассов и в процессе работы превратить Processor в полнофункциональный прикладной фреймворк. Написание такого инструмента один раз в суперклассе позволяет его многократно использовать во всех разрабатываемых программах. Даже в этом простом примере из-за того, что благодаря классам было настолько много пакетировано и унаследовано, нам пришлось написать код лишь для шага форматирования HTML, а остальное досталось бесплатно.
За еще одним примером композиции в работе обратитесь к упражнению 9 в конце главы 32 и его решению в приложении Г; он похож на пример с пиццерией. В этой книге мы сосредоточили внимание на наследовании, потому что оно является главным инструментом, который сам язык предлагает для ООП. Но на практике композиция может применяться наравне с наследованием в качестве способа структурирования классов, особенно в более крупных системах. Мы видели, что наследование и композиция часто являются дополняющими друг друга (а временами и взаимоисключающими) методиками. Однако поскольку композиция относится к вопросам проектирования, выходящим за рамки языка Python и самой книги, за дополнительными сведениями по данной теме я отсылаю вас к другим ресурсам.
Что потребует внимания: классы и постоянство
В текущей части книги я несколько раз упоминал о поддержке постоянства объектов с помощью модулей pickle и shelve в Python, т.к. она особенно хорошо работает с экземплярами классов. На самом деле указанные инструменты оказываются достаточно убедительными для того, чтобы вообще использовать классы — организуя обработку посредством pickle и shelve экземпляра класса, мы получаем хранилище, которое содержит объединенные вместе данные и логику.
Скажем, помимо того, что разработанные в главе классы пиццерии позволяют эмулировать взаимодействия реального мира, они могли бы также служить основой
базы данных с постоянным хранением. С применением модулей pickle или shelve из Python экземпляры классов можно сохранять на диске за один шаг. Мы использовали shelve для хранения экземпляров классов в учебном руководстве по ООП в главе 28, но интерфейс pickle также удивительно легко применять с объектами:
import pickle object = SomeClassO
file = open (filename, ’wb') # Создать внешний файл pickle.dump(object, file) # Сохранить объект в файле
import pickle
file = open(filename, 'rb')
object = pickle.load(file) # Извлечь объект в более позднее время Модуль pickle преобразует объекты из памяти в сериализированные потоки байтов (строки в Python), которые можно хранить в файлах, отправлять по сети и т.д.; распаковка с помощью pickle преобразует потоки байтов в идентичные объекты в памяти. Модуль shelve похож, но сохраняет объекты, автоматически обработанные pickle, в базе данных с доступом по ключу, которая экспортирует словарный интерфейс: import shelve object = SomeClassO dbase = shelve.open(filename)
dbase ['key'] = object # Сохранить под ключом
import shelve
dbase = shelve.open(filename)
object = dbaset’key1] # Извлечь объект в более позднее время
В нашем примере с пиццерией использование классов для моделирования сотрудников означает возможность создания простой базы данных за счет выполнения небольшой дополнительной работы. Сохранение таких объектов экземпляров в файле с помощью pickle делает их постоянными между запусками программы на Python:
>>> from pizzashop import PizzaShop >>> shop = PizzaShop()
»> shop.server, shop.chef
(<Employee: name=Pat, salary=40000>, <Employee: name=Bob, salary=50000>) >>> import pickle
>>> pickle.dump(shop, open(1shopfile.pkl', 'wb'))
Здесь в файле сохраняется весь составной объект shop целиком. Чтобы позже поместить его в другой сеанс или программу, также достаточно одного шага. В действительности объекты, восстановленные подобным образом, предохраняют свое состояние и поведение:
>>> import pickle
>>> obj = pickle, load (open ('shopf ile. pk.1', 'rb'))
»> obj . server, obj . chef
(<Employee: name=Pat, salary=40000>, <Employee: name=Bob, salary=50000>) >» obj . order (' LSP')
LSP orders from <Employee: name=Pat, salary-40000>
Bob makes pizza oven bakes
LSP pays for item to <Employee: name=Pat, salary=40000>
Это просто запускает эмуляцию в существующем виде, но мы могли бы расширить модель пиццерии, чтобы отслеживать запасы, выручку и т.п. — ее сохранение в файле после внесения изменений предохранит обновленное состояние. Дополнительные сведения о модулях pickle и shelve ищите в руководстве по стандартной библиотеке, а также в главах 9 (первого тома), 28 и 37.
Объектно-ориентированное программирование и делегирование: промежуточные объекты-оболочки
Наряду с наследованием и композицией программисты, занимающиеся ООП, часто говорят о делегировании, что обычно подразумевает объекты контроллеров с внедренными другими объектами, которым они передают запросы операций. Контроллеры могут заниматься административными действиями, такими как ведение журналов либо проверка достоверности доступа, добавляя дополнительные шаги к компонентам интерфейса или отслеживая активные экземпляры.
В известном смысле делегирование является особой формой композиции с единственным внедренным объектом, управляемым классом оболочки (иногда называемого промежуточным классом), который предохраняет большую часть или весь интерфейс внедренного объекта. Понятие промежуточных классов временами применяется и к другим механизмам, таким как вызовы функций; при делегировании нас интересуют промежуточные классы для всех линий поведения объекта, включая вызовы методов и прочие операции.
Такая концепция была введена через пример в главе 28, и в Python она часто реализуется с помощью метода_getattr_, рассмотренного в главе 30. Поскольку этот
метод перегрузки операции перехватывает доступ к несуществующим атрибутам, класс оболочки может использовать_getattr_дня маршрутизации произвольного доступа к внутреннему объекту. Из-за того, что метод_getattr_дает возможность маршрутизировать запросы атрибутов обобщенным образом, класс оболочки предохраняет интерфейс внутреннего объекта и сам может добавлять дополнительные операции.
В качестве обзора взгляните на содержимое файла trace .ру (одинаково выполняющегося в Python 2.Х и З.Х):
class Wrapper:
def _init_(self, object) :
self.wrapped = object # Сохранить объект
def _getattr_(self, attrname):
print('Trace: ' + attrname) # Трассировать извлечение
return getattr(self.wrapped, attrname) # Делегировать извлечение
Вспомните из главы 30, что метод_getattr_получает имя атрибута в виде строки. В коде атрибут извлекается из внутреннего объекта по строковому имени посредством встроенной функции getattr — вызов getattr (X, N) похож на X. N, но N является выражением, вычисляемым в строку во время выполнения, а не переменной. На самом
деле вызов getattr (X,N) подобен выражению X._diet_[N], но первый вариант
также производит поиск в иерархии наследования, как и X. N, а второй — нет (за информацией об атрибуте_diet_обращайтесь в главу 22 первого тома и в главу 29).
Вы можете применять подход с классом оболочки, продемонстрированный в модуле, для управления доступом к любому объекту с атрибутами — спискам, словарям и даже классам и экземплярам. Здесь класс Wrapper просто выводит трассировочное сообщение при каждом доступе к атрибуту и делегирует запрос атрибута внутреннему объекту wrapped:
»> from trace import Wrapper
>>> x = Wrapper ([1, 2, 3]) # Создать оболочку для списка
>» х.append(4) # Делегировать списковому методу
Trace: append
>>> x.wrapped # Вывести внутренний объект
[1, 2, 3, 4]
# Создать оболочку для словаря
# Делегировать словарному методу
>» х = Wrapper ({'а': 1, * b ’ : 2})
»> list (х.keys ())
Trace: keys ['а', 'b']
Совокупный эффект заключается в дополнении целого интерфейса объекта wrapped добавочным кодом в классе Wrapper. Мы можем использовать его для регистрации вызовов методов в журналах, направления вызовов методов дополнительной или специальной логике, адаптации класса к новому интерфейсу и т.д.
В следующей главе мы снова вернемся к понятиям внутренних объектов и делегирования операций как одному из способов расширения встроенных типов. Если вы заинтересовались паттерном проектирования с делегированием, тогда также обратитесь к обсуждению в главах 32 и 39 декораторов функций — тесно связанной концепции, предназначенной для дополнения вызова специфической функции или метода, а не целого интерфейса объекта, и декораторов классов, которые служат способом автоматического добавления таких основанных на делегировании оболочек ко всем экземплярам класса.
Примечание, касающееся нестыковки версий. Как было показано в примере из главы 28, делегирование интерфейсов объектов общими классами посредников значительно изменяется в Python З.Х, когда внутренние объекты реализуют методы перегрузки операций. Формально это является отличительной особенностью классов нового стиля и может присутствовать также в коде Python 2.Х, если такая возможность включена; согласно следующей главе в Python З.Х она обязательна и потому часто считается изменением Python З.Х.
В стандартных классах Python 2.Х методы перегрузки операций, запускаемые встроенными операциями, маршрутизируются через обобщенные методы перехвата атрибутов вроде_getattr_. Например, вывод внутреннего объекта напрямую приводит к вызову упомянутого метода для метода _герг_или_str_, который затем передает вызов внутреннему
объекту. Такая схема действует для_iter_,_add_и других методов
операций, рассмотренных в предыдущей главе.
В Python З.Х подобное больше не происходит: вывод не запускает метод _getattr_(или родственный ему_getattribute_, который мы исследуем в следующей главе) и взамен применяется стандартное отображение. В Python З.Х классы нового стиля ищут методы, неявно вызываемые встроенными операциями, в классах и полностью пропускают нормальный поиск в экземплярах. Явные операции извлечения атрибутов имен направляются методу_getattr_одинаково в Python 2.Х и З.Х, но поиск методов встроенных операций отличается аспектами, которые могут повлиять на ряд инструментов, основанных на делегировании.
В следующей главе мы рассмотрим указанную проблему как изменение классов нового стиля, а также увидим ее вживую в главах 38 и 39 в контексте управляемых атрибутов и декораторов. Пока просто примите к сведению, что для кодовых схем делегирования может потребоваться переопределение методов перегрузки операций в классах оболочек (вручную, с помощью инструментов или посредством суперклассов), если они используются внутренними объектами, и вы хотите, чтобы их перехватывали классы нового стиля.
Псевдозакрытые атрибуты классов
Помимо более масштабных целей проектные решения классов должны также принимать меры относительно использования имен. Скажем, в учебном примере из главы 28 мы отмечали, что методы, определенные внутри класса универсального инструмента, могут быть модифицированы подклассами, если к ним открыт доступ, и упомянули
о компромиссах такой политики — наряду с поддержкой настройки и прямых вызовов методов они также открыты для случайных замещений.
В части V мы выяснили, что каждое имя, которому присваивается значение на верхнем уровне файла модуля, экспортируется. По умолчанию то же самое справедливо для классов — сокрытие данных является соглашением, а клиенты могут извлекать либо изменять атрибуты в любом классе или экземпляре, на который они имеют ссылку. На самом деле в терминах C++ все атрибуты “открыты” и “виртуальны”; все они везде доступны и ищутся динамически во время выполнения.
Тем не менее, на сегодняшний день в Python поддерживается понятие “корректировки” (т.е. расширения) имен для локализации некоторых имен в классах. Скорректированные имена иногда неправильно называют “закрытыми атрибутами”, но в действительности они представляют собой всего лишь способ локализации имени в классе, который его создал — корректировка имен не предотвращает доступ к ним из кода за пределами класса. Данное средство предназначено главным образом для того, чтобы избежать конфликтов между пространствами имен в экземплярах, а не для ограничения доступа к именам в целом; следовательно, скорректированные имена лучше называть “псевдозакрытыми”, чем “закрытыми”.
Псевдозакрытые имена являются продвинутой и совершенно необязательной возможностью; вы вряд ли сочтете их крайне полезными, пока не начнете создавать универсальные инструменты или более крупные иерархии классов для применения в проектах, где принимает участие много программистов. На самом деле они не всегда используются, даже когда вероятно должны — чаще всего программисты на Python записывают внутренние имена с одиночным символом подчеркивания (наподобие X). Это просто неформальное соглашение, которое уведомляет о том, что имя, как правило, не должно изменяться (для самого Python оно ничего не значит).
Однако поскольку вы можете видеть такой прием в коде других людей, то обязаны знать о нем, даже если сами его не применяете. И как только вы изучите его преимущества и контексты использования, то можете счесть данное средство более полезным в собственном коде, чем осознают некоторые программисты.
Обзор корректировки имен
Вот как работает корректировка имен: любые имена внутри оператора class, которые начинаются с двух символов подчеркивания, но не заканчиваются ими, автоматически расширяются, чтобы содержать впереди имя включающего класса. Например, имя вроде_X внутри класса Spam автоматически изменяется на Spam_X: исходное имя снабжается префиксом в виде одиночного символа подчеркивания и именем включающего класса. Из-за того, что модифицированное имя содержит имя включающего класса, оно обычно уникально и не конфликтует с похожими именами, созданными другими классами в иерархии.
Корректировка имен происходит только для имен, появляющихся внутри кода оператора class, и затем только для имен, которые начинаются с двух символов подчеркивания. Тем не менее, она работает для каждого имени, предваренного двумя символами подчеркивания — атрибутов классов (в том числе имен методов) и атрибутов экземпляров, присвоенных в self. Скажем, в классе Spam метод по имени_meth
после корректировки превращается в Spam_meth, а ссылка на атрибут экземпляра
self._X видоизменяется на self .Spam_X.
Несмотря на корректировку, до тех пор, пока в классе везде, где есть ссылка на имя, применяется версия с двумя символами подчеркивания, все ссылки по-прежнему будут работать. Однако поскольку добавлять атрибуты к экземпляру могут сразу несколько классов, корректировка имен помогает избежать конфликтов — но чтобы увидеть, почему, нам необходимо перейти к конкретному примеру.
Для чего используются псевдозакрытые атрибуты?
Одна из основных проблем, которые призваны смягчить псевдозакрытые атрибуты, связана со способом хранения атрибутов экземпляров. В Python все атрибуты экземпляров оказываются в единственном объекте экземпляра в нижней части дерева классов и разделяются всеми функциями методов уровня класса, которым передается экземпляр. Такая модель отличается от модели, принятой в C++, где каждый класс получает собственное пространство для определяемых им данных-членов.
Внутри метода класса в Python всякий раз, когда метод присваивает значение какому-то атрибуту self (например, self. атрибут = значение), он изменяет или создает атрибут в экземпляре (вспомните, что поиск в иерархии наследования происходит только при ссылке, не в случае присваивания). Поскольку это справедливо, даже если множество классов в иерархии присваивают значение тому же самому атрибуту, возможны конфликты.
Скажем, пусть при реализации класса программист предполагает, что класс владеет атрибутом по имени X в экземпляре. В методах данного класса указанное имя устанавливается и позже извлекается:
class Cl:
def methl(self) : self.X = 88 # Я предполагаю, что атрибут X - мой def meth2(self): print(self.X)
Далее предположим, что еще один программист, работающий изолированно, делает то же самое допущение в другом классе:
class С2:
def metha(self) : self.X =99 # Я тоже def methb(self): print(self.X)
Сами по себе оба класса функционируют. Проблема возникнет, если два класса когда-нибудь смешаются в одном дереве классов:
class СЗ(Cl, С2): ...
I = С3() # В I есть только один атрибут X!
Теперь значение, которое каждый класс получает для выражения self.X, будет зависеть о того, какой класс выполнял присваивание последним. Из-за того, что все присваивания self. X относятся к тому же самому одиночному экземпляру, существует только один атрибут X — I. X — вне зависимости от того, сколько классов применяют такое имя атрибута.
Это не проблема, если она ожидается, и так в действительности взаимодействуют классы — экземпляр является разделяемой памятью. Тем не менее, чтобы гарантировать принадлежность атрибута классу, который его использует, необходимо снабдить имя префиксом в виде двух символов подчеркивания везде, где оно применяется в классе, как показано в следующем файле pseudoprivate .ру, работающем в Python 2.Х/З.Х:
class Cl:
def methl(self) : self._X =88 # Теперь я имею свой атрибут X
def meth2(self) : print (self._X) # Становится _С1_X в I
class С2:
def metha(self): self. X = 99
def methb(self): print(self._X)
class СЗ(Cl, C2): pass I = C3()
# Я тоже
# Становится С2 X в I
§ В I есть два имени X
I.methlO; I.methaO
print(I._diet_)
I.meth2(); I.methbO
При наличии таких префиксов перед добавлением к экземпляру атрибуты X будут расширены для включения имен своих классов. Если вы выполните вызов dir на I либо проинспектируете его словарь пространства имен после присваивания значений атрибутам, то увидите расширенные имена, _С 1_X и _С2_X, а не X. Поскольку расширение делает имена более уникальными внутри экземпляра, разработчики классов могут вполне безопасно предполагать, что они по-настоящему владеют любыми именами, которые снабжают префиксами в форме двух символов подчеркивания:
% python pseudoprivate.ру
{ 1_С2_X': 99, '_С1_X': 88}
88
99
Показанный трюк позволяет избежать потенциальных конфликтов имен в экземпляре, но учтите, что он не означает подлинную защиту. Зная имя включающего класса, вы rio-прежнему можете получать доступ к любому из двух атрибутов везде, где имеется ссылка на экземпляр, с использованием полностью развернутого имени (например, I ._С1_X = 77). Кроме того, имена все еще могут конфликтовать, когда неосведомленные программисты явно применяют расширенную схему именования (маловероятно, но возможно). С другой стороны, описанное средство делает менее вероятными случайные конфликты с именами какого-то класса.
Псевдозакрытые атрибуты также полезны в более крупных фреймворках либо инструментах, помогая избежать введения новых имен методов, которые могут неумышленно скрыть определения где-то в другом месте дерева классов, и снизить вероятность замещения внутренних методов именами, определяемыми ниже в дереве. Если метод предназначен для использования только внутри класса, который может смешиваться с другими классами, тогда префикс в виде двух символов подчеркивания фактически гарантирует, что снабженный им метод не будет служить препятствием другим именам в дереве, особенно в сценариях с множественным наследованием:
class Super:
def method (self) : . . . # Действительный прикладной метод
class Tool:
def _method (self) : . . . # Превращается в _Tool_method
def other(self): self._method () # Использовать внутренний метод
class Subl(Tool, Super): ...
def actions(self): self.method() # Выполняется Super.method, как и ожидалось
class Sub2(Tool) :
def _init_(self) : self.method =99 # He нарушает работу Tool._method
Мы кратко упоминали множественное наследование в главе 26, и будем исследовать его более глубоко позже в текущей главе. Вспомните, что поиск в суперклассах производится в соответствии с их порядком слева направо в строках заголовка class. Здесь это означает, что класс Subl отдает предпочтение атрибутам Tool перед атрибутами Super. Хотя в приведенном примере мы могли бы заставить Python выбирать методы прикладного класса первыми, изменив порядок указания суперклассов в заголовке класса Subl, псевдозакрытые атрибуты решают проблему в целом. Псевдозакрытые имена также препятствуют случайному переопределению подклассами имен внутренних методов, как происходит в Sub2.
Опять-таки я должен отметить, что данная возможность имеет тенденцию применяться главным образом в более крупных проектах, где участвует много программистов, да и то лишь для избранных имен. Не поддавайтесь искушению излишне загромождать свой код; используйте эту возможность только для имен, которые действительно нуждаются в управлении со стороны единственного класса. Несмотря на полезность в ряде универсальных инструментов, основанных на классах, для более простых программ она, вероятно, будет излишеством.
За дополнительными примерами применения возможности именования_X обращайтесь к подмешиваемым классам в файле lister .ру, представленном далее в главе, а также к обсуждению декораторов класса Private в главе 39.
Если вас заботит защита вообще, тогда можете еще раз просмотреть эмуляцию
закрытых атрибутов экземпляров в разделе “Доступ к атрибутам: _getattr_ и
_setattr_” главы 30 и более полный декоратор класса Private, который будет
построен с помощью делегирования в главе 39. Хотя в классах Python и возможна эмуляция подлинного управления доступом, на практике так поступают редко даже в крупных системах.
Методы являются объектами: связанные или несвязанные методы
Методы в целом и связанные методы в частности упрощают реализацию многих проектных целей на Python. Мы встречали связанные методы в главе 30 при изучении метода_call_. Как выяснится в приводимой здесь полной истории, они оказываются более общими и гибкими, чем можно было ожидать.
В главе 19 вы узнали, что функции можно обрабатывать как нормальные объекты. Методы тоже являются разновидностью объектов и могут использоваться таким же способом, как другие объекты — их разрешено присваивать именам, передавать в функции, сохранять в структурах данных и т.д. — и подобно простым функциям квалифицировать как объекты “первого класса”. Однако поскольку доступ к методам класса возможен из экземпляра или класса, на самом деле в Python они поступают в двух вариантах.
Объекты несвязанных методов (класса): без self
Доступ к функциональному атрибуту класса путем уточнения с помощью класса возвращает объект несвязанного метода. Чтобы вызвать метод, потребуется в первом аргументе явно указать объект экземпляра. В Python З.Х несвязанный метод представляет собой то же самое, что и простая функция, и может вызываться через имя класса; в Python 2.Х он является отдельным типом и не может быть вызван без указания экземпляра.
Объекты связанных методов (экземпляра): пары self + функция
Доступ к функциональному атрибуту класса путем уточнения с помощью экземпляра возвращает объект связанного метода. В объект связанного метода Python автоматически помещает экземпляр и функцию, так что для вызова метода передавать экземпляр не нужно.
Оба вида методов являются полноценными объектами; подобно строкам и числам их можно как угодно передавать в пределах программы. При запуске они оба также требуют указания экземпляра в своем первом аргументе (т.е. значения self). Вот почему мы должны были явно передавать экземпляр, когда вызывали методы суперкласса из методов подкласса в предшествующих примерах (включая файл employees.ру в этой главе); формально такие вызовы попутно производят объекты несвязанных методов.
При вызове объекта связанного метода Python предоставляет экземпляр автоматически — экземпляр, применяемый для создания объекта связанного метода. Таким образом, объекты связанных методов обычно взаимозаменяемы с объектами простых функций, что делает их особенно удобными для интерфейсов, изначально ориентированных на функции (реалистичный сценарий использования в графических пользовательских интерфейсах описан во врезке “Что потребует внимания: обратные вызовы связанных методов” далее в главе).
В целях иллюстрации предположим, что мы определили следующий класс:
class Spam:
def doit(self, message): print(message)
Теперь в нормальной операции мы создаем экземпляр и вызываем его метод, выводящий переданный аргумент:
objectl = Spam()
objectl.doit('hello world')
Тем не менее, в действительности прямо перед круглыми скобками вызова метода генерируется объект связанного метода. Фактически мы можем извлечь связанный метод, не вызывая его. Подобно всем выражениям результатом выражения объект. имя будет объект. Ниже такое выражение возвращает объект связанного метода с упакованным экземпляром (objectl) и функцией метода (Spam.doit). Мы можем присвоить эту пару связанного метода другому имени и затем вызвать его, как если бы оно было простой функцией:
objectl = Spam()
х = objectl.doit # Объект связанного метода: экземпляр + функция
х('hello world') # Тот же эффект, что и objectl.doit )
С другой стороны, если для того, чтобы добраться до doit, мы указываем класс, тогда получаем обратно объект несвязанного метода, который является просто ссылкой на объект функции. Для вызова метода такого типа мы обязаны передать экземпляр как крайний слева аргумент — иначе он отсутствует в выражении, а метод его ожидает:
objectl = Spam()
t = Spam.doit # Объект несвязанного метода (функция в Python З.Х: см. далее)
t (objectl, 'howdy') # Передача экземпляра (если метод его ожидает в Python З.Х)
Более того, то же самое правило применяется внутри метода класса, если мы ссылаемся на атрибуты self, которые относятся к функциям в классе. Выражение self .метол представляет собой объект связанного метода, потому что self — объект экземпляра:
class Eggs:
def ml(self, n): print(n) def m2(self):
x = self .ml # Еще один объект связанного метода х(42) # Выглядит подобно простой функции
Eggs().m2() # Выводит 42
В большинстве случаев вы вызываете методы немедленно после их извлечения посредством уточнения атрибутов, поэтому не всегда замечаете попутно сгенерированные объекты методов. Но когда вы начнете писать код, который вызывает объекты обобщенным образом, вам придется позаботиться о специальной трактовке несвязанных методов — они обычно требуют передачи явного объекта экземпляра.
S Дополнительное исключение из данного правила ищите в обсуждении статических методов и методов класса в следующей главе и при кратком упоминании в следующем разделе. Подобно связанным методам статические ■ ^ ^ методы могут маскироваться под базовые функции, т.к. при вызове они не ожидают передачи экземпляров. Говоря формально, Python поддерживает три вида методов уровня класса (методы экземпляров, статические методы и методы класса), a Python З.Х также позволяет присутствовать в классах простым функциям. Методы метаклассов, рассматриваемые в главе 40, тоже являются отдельными, но по существу они представляют собой методы класса с меньшей областью видимости.
Несвязанные методы являются функциями в Python З.Х
В Python З.Х из языка было исключено понятие несвязанных методов. То, что здесь мы описывали как несвязанный метод, в Python З.Х трактуется как простая функция. В большинстве случаев вашему коду это безразлично; оба способа предусматривают передачу экземпляра в первом аргументе при вызове метода через экземпляр.
Однако это может повлиять на программы, которые выполняют явную проверку типов — если вы выводите тип метода уровня класса без экземпляра, то получите “unbound method” (“несвязанный метод”) в Python 2.Х и “function” (“функция”) в Python З.Х.
Кроме того, в Python З.Х допускается вызывать метод без экземпляра при условии, что метод его не ожидает, и метод вызывается только через класс и никогда через экземпляр. То есть Python З.Х будет передавать экземпляр методам только для вызовов через экземпляр. При вызове через класс передавать экземпляр вручную понадобится только в случае, если метод его ожидает:
C:\code> с:\python37\python
>>> class Selfless:
def_init_(self, data) :
self.data - data def selfless(argl, arg2): # Простая функция в Python З.Х
return argl + arg2 def normal(self, argl, arg2): # При вызове ожидается экземпляр return self .data + argl + arg2
»> X * Selfless (2)
>>> X. normal (3, 4) # Экземпляр передается self автоматически: 2+(3+4)
9
>>> Selfless .normal (X, 3, 4) # Метод ожидает self: передать вручную
9
>>> Selfless. self less (3, 4) # Без передачи экземпляра : в Python З.Х
# работает, в Python 2.Х - нет!
1
Последний тест в Python 2.Х потерпит неудачу, потому что несвязанные методы по умолчанию требуют передачи экземпляра; в Python З.Х он работает из-за того, что такие методы трактуются как простые функции, не нуждающиеся в экземпляре. Несмотря на то что в Python З.Х перестают отлавливаться некоторые потенциальные ошибки (что, если программист забыл передать экземпляр?), появляется возможность использовать методы класса как простые функции до тех пор, пока им не передается аргумент экземпляра self и они не рассчитывают на него.
Тем не менее, следующие два вызова по-прежнему терпят неудачу в Python З.Х и 2.Х. Первый (вызов через экземпляр) автоматически передает экземпляр методу, который его не ожидает, в то время как второй (вызов через класс) не передает экземпляр методу, который его ожидает (сообщения об ошибках получены в версии Python 3.7):
»> X. selfless (3, 4)
TypeError: selfless () takes 2 positional arguments but 3 were given
Ошибка типа: self less () принимает 2 позиционных аргумента, но было предоставлено 3
>» Self less, normal (3, 4)
TypeError: normal() missing 1 required positional argument: 'arg2'
Ошибка типа: в normal () отсутствует 1 обязательный позиционный аргумент: arg2
Из-за такого изменения встроенная функция staticmethod и декоратор, описанный в следующей главе, не нужны в Python З.Х для методов без аргумента self, которые вызываются только через имя класса и никогда через экземпляр — такие методы запускаются как простые функции, не получая аргумент экземпляра. В Python 2.Х подобные вызовы приводят к ошибкам, если только экземпляр не передан вручную или метод не помечен как статический (статические методы более подробно обсуждаются в следующей главе).
Об отличиях в поведении Python З.Х знать важно, но в любом случае связанные методы обычно важнее с практической точки зрения. Поскольку они объединяют вместе экземпляр и функцию в единый объект, их обобщенно можно трактовать как вызываемые объекты. В следующем разделе все сказанное демонстрируется в коде.
Связанные методы и другие вызываемые объекты
Как упоминалось ранее, связанные методы могут обрабатываться как обобщенные объекты подобно простым функциям — их можно произвольно передавать в рамках программы. Кроме того, поскольку связанные методы объединяют функцию и экземпляр в единый пакет, они могут трактоваться как любой другой вызываемый объект и не требовать специального синтаксиса при вызове. Например, в следующем коде четыре объекта связанных методов сохраняются в списке и позже вызываются с помощью нормальных выражений вызовов:
>» class Number:
def_init_(self, base) :
self .base = base def double (self) :
return self.base * 2 def triple(self):
return self.base * 3
# Объекты экземпляров класса
# Состояние + методы
# Нормальные прямые вызовы
>>> x = Number (2)
>>> у = Number (3)
>>> z = Number (4)
>>> x.double()
4
»> acts = [x.double, y.double, y.triple, z.double] # Список связанных методов »> for act in acts: # Вызовы откладываются
print (act ()) # Вызов, как если бы это были функции
4
6
9
8
Подобно простым функциям объекты связанных методов имеют собственную информацию для интроспекции, включая атрибуты, которые предоставляют доступ к образующим пару объекту экземпляра и функции метода. Вызов связанного метода просто направляется этой паре:
>>> bound = х.double
>>> bound._self_, bound._func_
(< main_.Number object at Ox...etc...>, <function Number.double at Ox...etc...>)
>>> bound._self_.base
2
>>> bound() # Вызывается bound._func_(bound._self_, . . .)
4
Другие вызываемые объекты
На самом деле связанные методы являются всего лишь одним из нескольких типов вызываемых объектов в Python. Как демонстрируется ниже, простые функции, определенные посредством def или lambda, экземпляры, которые наследуют_call_, и
связанные методы экземпляров могут обрабатываться и вызываться одинаково:
>>> def square (arg) :
return arg **2 # Простые функции (def или lambda)
»> class Sum:
def_init_(self, val) : # Вызываемые экземпляры
self.val = val def_call_(self, arg) :
return self.val + arg
>>> class Product:
def_init_(self, val) : # Связанные методы
self.val = val def method (self, arg) : return self.val * arg
>» sobject = Sum(2)
>>> pobject = Product(3)
>>> actions = [square, sobject, pobject.method] # Функция, экземпляр, метод
»> for act in actions: # Все три вызываются одинаково
print(act(5)) # Вызов любого вызываемого
# объекта с одним аргументом
25
7
15
>>> actions[-1](5) # Индексирование, включение, отображение
15
»> [act(5) for act in actions]
[25, 7, 15]
>» list (map (lambda act: act(5) , actions))
[25, 7, 15]
Говоря формально, классы тоже относятся к категории вызываемых объектов, но мы обычно вызываем их для создания экземпляров, а не для выполнения фактической работы — одиночное действие лучше реализовывать как простую функцию, чем класс с конструктором, но показанный далее класс служит иллюстрацией своей вызываемой природы:
>» class Negate:
# Классы также являются вызываемыми объектами
# Но вызываются для создания объектов,
# не для выполнения работы
# Формат вывода экземпляров
def_init_(self, val) :
self .val = -val
def_repr_(self) :
return str(self.val)
>» actions = [square, sobject, pobject.method, Negate] # Вызвать также и класс >» for act in actions: print(act(5))
25
7
15
-5
>>> [act(5) for act in actions] # Выполняется_repr_, а не_str_!
[25, 7, 15, -5]
>» table = {act(5) : act for act in actions) # Включение словаря из Python З.Х/2.1 >» for (key, value) in table. items () :
print(’{0:2} => {1}'.format(key, value)) # str.format из Python 2.6+/3.X
25 => <function square at 0x0000000002987400>
15 => <bound method Product.method of <_main_.Product object at ...etc...»
-5 => <class ’_main_.Negate'>
7 => <_main_.Sum object at 0x000000000298BE48>
Как видите, связанные методы и в целом модель вызываемых объектов Python входят в состав многочисленных способов, заложенных в проектное решение Python, которые делают его невероятно гибким языком.
Теперь вы должны понимать объектную модель методов. Другие примеры связанных методов в работе приведены ниже во врезке “Что потребует внимания: обратные вызовы связанных методов”, а также в разделе “Выражения вызовов:_call_” предыдущей главы.
Что потребует внимания: обратные вызовы связанных методов
Поскольку связанные методы автоматически образуют пару из экземпляра и функции метода класса, их можно применять везде, где ожидается простая функция. Одним из наиболее распространенных мест, где вы встретите воплощение этой идеи, является код регистрации методов как обработчиков событий в интерфейсе tkinter (Tkinter в Python 2.Х), с которым мы уже сталкивались ранее. Вот простой случай:
def handler () :
...использовать для хранения состояния глобальные переменные или области видимости замыканий...
widget = Button(text='spam', command=handler)
Чтобы зарегистрировать обработчик для событий щелчков на кнопке, мы обычно передаем в ключевом аргументе command вызываемый объект, не принимающий аргументов. Здесь подходят имена функций (и lambda), а потому методы уровня класса, хотя они должны быть связанными методами, если при вызове ожидают указания экземпляра:
class MyGui:
def handler(self):
...использовать для хранения состояния self.attr... def makewidgets(self):
b = Button(text='spam', command=self.handler)
Обработчиком событий является self .handler — объект связанного метода, который запоминает self и MyGui. handler. Поскольку self будет ссылаться на исходный экземпляр, когда handler позже вызывается при поступлении событий, метод будет иметь доступ к атрибутам экземпляра, которые способны предохранять состояние между событиями, а также к методам уровня класса. В случае простых функций состояние взамен должно сохраняться в глобальных переменных или областях видимости объемлющих функций.
Еще один способ обеспечения совместимости классов с API-интерфейсами, основанными на функциях, был описан при обсуждении метода перегрузки операций _call_в главе 30, а другой инструмент, часто используемый при обратных вызовах, рассматривался при исследовании lambda в главе 19 первого тома. Как там отмечалось, обычно вам не нужно помещать связанный метод внутрь lambda; связанный метод в предыдущем примере уже откладывает вызов (обратите внимание на отсутствие круглых скобок, инициирующих вызов), поэтому добавлять lambda было бы бессмысленно!
Классы являются объектами: обобщенные фабрики объектов
Временами проектные решения на основе классов требуют создания объектов в ответ на условия, которые невозможно предсказать на стадии написания кода программы. Паттерн проектирования “Фабрика” делает возможным такой отложенный подход. Во многом благодаря гибкости Python фабрики могут принимать многочисленные формы, ряд которых вообще не кажутся особыми.
Из-за того, что классы также являются объектами “первого класса”, их легко передавать в рамках программы, сохранять в структурах данных и т.д. Вы также можете передавать классы функциями, которые генерируют произвольные виды объектов; в кругах объектно-ориентированного проектирования такие функции иногда называются фабриками. Фабрики могут оказаться крупным делом в строго типизированных языках вроде C++, но довольно просты в реализации на Python.
Скажем, синтаксис вызова, представленный в главе 18 первого тома, позволяет вызывать любой класс с любым количеством позиционных или ключевых аргументов конструктора за один шаг для создания экземпляра любого вида :
def factory(aClass, *pargs, **kargs) : # Кортеж или словарь с переменным
# количеством аргументов return aClass(*pargs, **kargs) # Вызывает aClass (или apply в Python 2.X)
class Spam:
def doit(self, message): print(message)
class Person:
def _init_(self, name, job=None):
self, name = name self. job = job
objectl = factory(Spam) # Создать объект Spam
object2 = factory(Person, "Arthur", "King") # Создать объект Person
object3 = factory (Person, name= ' Brian') # Тоже самое, с ключевым ар гу-
# ментом и стандартным значением
В коде мы определяем функцию генератора объектов по имени factory. Она ожидает передачи объекта класса (подойдет любой класс) наряду с одним и более аргументов для конструктора класса. Для вызова функции и возвращения экземпляра применяется специальный синтаксис с переменным количеством аргументов.
В оставшемся коде примера просто определяются два класса и генерируются экземпляры обоих классов за счет их передачи функции factory. И это единственная фабричная функция, которую вам когда-либо придется писать на Python; она работает для любого класса и любых аргументов конструкторов. Ниже функция factory демонстрируется в действии (файл factory.ру):
>>> objectl.doit(99)
99
»> object2.name, object2.job
('Arthur', 'King')
»> object3.name, object3.job
('Brian', None)
К настоящему времени вы должны знать, что абсолютно все в Python является объектом “первого класса” — в том числе классы, которые обычно представляют собой лишь входные данные для компилятора в языках, подобных C++. Передавать их таким способом вполне естественно. Однако как упоминалось в начале текущей части книги, задачи ООП в Python решаются только с помощью объектов, полученных из классов.
Для чего используются фабрики?
Так в чем же польза от функции factory (помимо повода проиллюстрировать в книге объекты классов)? К сожалению, трудно показать приложения паттерна проектирования “Фабрика” без приведения кода большего объема, чем для этого есть место. Тем не менее, в целом такая фабрика способна сделать возможной изоляцию кода от деталей динамически конфигурируемого создания объектов.
Например, вспомните пример функции processor, абстрактно представленной в главе 26, и затем снова в качестве примера композиции ранее в этой главе. Она принимает объекты reader и writer для обработки произвольных потоков данных. Первоначальной версии функции processor вручную передавались экземпляры специализированных классов вроде FileWriter и SocketReader с целью настройки обрабатываемых потоков данных; позже мы передавали жестко закодированные объекты файла, потока и форматера. В более динамическом сценарии для настройки потоков данных могли бы применяться внешние механизмы, такие как конфигурационные файлы или графические пользовательские интерфейсы.
В динамическом мире подобного рода может отсутствовать возможность жесткого кодирования в сценариях процедуры для создания объектов интерфейса к потокам данных и взамен их придется создавать во время выполнения в соответствии с содержимым конфигурационного файла.
Такой файл может просто задавать строковое имя класса потока данных, подлежащего импортированию из модуля, плюс необязательный аргумент для вызова конструктора. Здесь могут пригодиться функции или код в стиле фабрик, потому что он позволяет извлекать и передавать классы, код которых не реализован заблаговременно в программе. На самом деле классы могут даже вообще не существовать в момент, когда пишется код:
classname = ...извлечь из конфигурационного файла и произвести разбор. . . classarg = ...извлечь из конфигурационного файла и произвести разбор... import streamtypes # Настраиваемый код
aclass = getattr(streamtypes, classname) # Извлечь из модуля reader = factory(aclass, classarg) # Или aclass(classarg)
processor(reader, ...)
Здесь снова используется встроенная функция getattr для извлечения атрибута модуля по строковому имени (она похожа на выражение объект. атрибут, но атрибут является строкой). Поскольку в приведенном фрагменте кода предполагается наличие у конструктора единственного аргумента, строго говоря, он не нуждается в функции factory — мы могли бы создавать экземпляр с помощью aclass (classarg). Однако фабричная функция может оказаться более полезной при наличии неизвестных списков аргументов, а общий паттерн проектирования “Фабрика” способен повысить гибкость кода.
Множественное наследование: "подмешиваемые" классы
Наш последний паттерн проектирования является одним из самых полезных и послужит предметом для построения более реалистичного примера, чтобы завершить настоящую главу и подготовить почву для дальнейших исследований. В качестве бонуса код, который мы здесь напишем, может стать удобным инструментом.
Многие проектные решения, основанные на классах, требуют объединения разрозненных наборов методов. Как уже было показано, в строке заголовка оператора class в круглых скобках можно указывать более одного суперкласса. Поступая так, мы задействуем множественное наследование — класс и его экземпляры наследуют имена из всех перечисленных суперклассов.
Для нахождения атрибута процедура поиска в иерархии наследования Python обходит все суперклассы, указанные в заголовке оператора class, слева направо до тех пор, пока не обнаружит соответствие. Формально из-за того, что любой из суперклассов может иметь собственные суперклассы, такой поиск может быть чуть сложнее для более крупных деревьев классов.
• В классических классах (стандарт вплоть до Python 3.0) поиск атрибутов во всех случаях продолжается сначала в глубину на всем пути к вершине дерева наследования и затем слева направо. Такой порядок обычно называется DFLR (Depth-First, Left-to-Right — сначала в глубину, слева направо).
• В классах нового стиля (необязательные в Python 2.Х и стандарт в Python З.Х) поиск атрибутов обычно происходит, как было ранее, но в ромбовидных схемах продолжается по уровням дерева до перехода вверх, т.е. больше в манере сначала в ширину. Такой порядок обычно называется MRO нового стиля (Method Resolution Order — порядок распознавания методов), хотя он применяется для всех атрибутов, а не только для методов.
Второе из описанных правил поиска исчерпывающе объясняется при обсуждении классов нового стиля в следующей главе. Несмотря на то что без кода следующей главы ромбовидные схемы понять трудно (и создавать их самостоятельно доведется редко), они образуются, когда множество классов в дереве используют общий суперкласс; порядок поиска нового стиля спроектирован так, чтобы посещать разделяемый суперкласс только один раз и после всех его подклассов. Тем не менее, в любой из двух моделей, когда класс имеет множество суперклассов, поиск в них производится слева направо согласно порядку указания суперклассов в строках заголовка оператора class.
В целом множественное наследование хорошо подходит для моделирования объектов, которые принадлежат более чем одному набору. Скажем, человек может быть инженером, писателем, музыкантом и т.д., а потому наследовать свойства от всех наборов подобного рода. Благодаря множественному наследованию объекты получают объединение всех линий поведения из всех своих суперклассов. Вскоре мы увидим, что множественное наследование также позволяет классам функционировать в качестве универсальных пакетов смешиваемых атрибутов.
Хотя множественное наследование — полезный паттерн проектирования, его главный недостаток в том, что оно может привести к конфликту, когда то же самое имя метода (или другого атрибута) определено сразу в нескольких суперклассах.
Возникший конфликт разрешается либо автоматически за счет порядка поиска в иерархии наследования, либо вручную в коде.
• Стандартный способ. По умолчанию процедура поиска в иерархии наследования выбирает первое найденное вхождение атрибута, когда на атрибут производится ссылка обычным образом, например, self .method (). В таком режиме Python выбирает самый нижний и крайний слева атрибут в классических классах и при неромбовидных схемах во всех классах; в классах нового стиля при ромбовидных схемах может быть выбран вариант справа или выше.
• Явный способ. В некоторых моделях на основе классов иногда необходимо выбирать атрибут явно, ссылаясь на него через имя класса, скажем, superclass .method (self). Ваш код разрешает конфликт и переопределяет стандартный способ поиска — чтобы выбрать вариант справа или выше принятого по умолчанию при поиске в иерархии наследования.
Проблема возникает, только когда то же самое имя появляется во множестве суперклассов, и вы не хотите использовать первое унаследованное. Поскольку в типичном коде на Python она не настолько распространена, как может показаться, мы отложим исследование деталей до следующей главы, где рассматриваются классы нового стиля вместе с их инструментами MRO и super, а также возвратимся к ней при анализе затруднений в конце главы. Однако сначала мы продемонстрируем практический сценарий применения для инструментов, основанных на множественном наследовании.
Реализация подмешиваемых классов отображения
Возможно, множественное наследование чаще всего используется для “подмешивания” универсальных методов из суперклассов. Такие суперклассы обычно называются подмешиваемыми классами — они предоставляют методы, которые добавляются к прикладным классам через наследование. В некотором смысле подмешиваемые классы похожи на модули: они предлагают пакеты методов для применения в своих клиентских подклассах. Тем не менее, в отличие от простых функций методы в подмешиваемых классах также могут принимать участие в иерархиях наследования и иметь доступ к экземпляру self для использования информации о состоянии и других методов в своих деревьях.
Например, как мы видели, стандартный способ, которым Python выводит объект экземпляра класса, не особенно практичен:
>>> class Spam:
def_init_(self) : # Метод_repr_ или_str_ отсутствует
self.datal = "food"
>>> X = Spam()
>>> print(X) # Стандартный способ: имя класса + адрес (идентификатор)
<_main_.Spam object at 0x00000000029CA908> # To же самое в Python 2.X,
но всегда instance
В учебном примере из главы 28 и при обсуждении перегрузки операций в главе
30 было показано, что вы сами можете предоставлять метод_str_или_герг_
для реализации специального строкового представления. Но вместо написания кода одного из них в каждом классе, который желательно выводить, почему бы не реализовать данный метод один раз в классе универсального инструмента и наследовать его во всех своих классах?
Именно для этого предназначены подмешиваемые классы. Однократное определение метода отображения в подмешиваемом суперклассе позволяет его многократно применять везде, где требуется специальный формат вывода — даже в классах, которые уже могут иметь другой суперкласс. Мы уже видели инструменты, выполняющие связанную работу.
• Класс AttrDisplay из главы 28 форматировал атрибуты экземпляров в обобщенном методе_герг_, но не поднимался по дереву классов и задействовал
только режим одиночного наследования.
• Модуль classtree .ру из главы 29 определял функции для подъема по деревьям классов и их схематического изображения, но он попутно не отображал атрибуты объектов и не был спроектирован как наследуемый класс.
Здесь мы собираемся возвратиться к методикам указанных примеров и расширить их, чтобы реализовать набор из трех подмешиваемых классов, которые будут служить обобщенными инструментами отображения для списков атрибутов экземпляров, унаследованных атрибутов и атрибутов всех объектов в дереве классов. Мы будем также использовать созданные инструменты в режиме множественного наследования и введем в действие кодовые методики, которые сделают классы лучше подходящими для применения в качестве обобщенных инструментов.
В отличие от главы 28 мы построим реализацию на основе метода_str_, а не
герг_. Отчасти это проблема стиля, а роль инструментов ограничивается print
и str, но разрабатываемые отображения будут достаточно обогащенными, чтобы считаться более дружественными к пользователю, чем представление как в коде. Такая политика также оставляет клиентским классам возможность реализации альтернативного низкоуровневого отображения посредством_герг_при эхо-выводе в интерактивной подсказке и для вложенных появлений. Использование_герг_по-прежнему допускает альтернативную версию_str_, но природа отображений, которые мы
реализуем более строго, предполагает применение_str_. Отличия между_str_
и_герг_обсуждались в главе 30.
Вывод списка атрибутов экземпляра с помощью_diet_
Давайте начнем с простого случая — вывод списка атрибутов, присоединенных к экземпляру. В показанном далее коде, находящемся в файле listinstance.ру, определяется подмешиваемый класс по имени Listinstance, который перегружает метод
_str_для всех классов, содержащих его в строках заголовков своих операторов
class. Из-за реализации в виде класса Listinstance является обобщенным инструментом, чью логику форматирования можно использовать для экземпляров любого клиентского подкласса:
#!python
• Файл listinstance.ру (Python 2.Х + З.Х)
class Listinstance: it »» »»
Подмешиваемый класс, который предоставляет форматированную функцию print() или str () для экземпляров через наследование реализованного
в нем метода _str_; отображает только атрибуты экземпляра; self
является экземпляром самого нижнего класса;
имена _X предотвращают конфликты с атрибутами клиента
if п и
def _attrnames(self):
result = 1 ' for attr in sorted (self._diet_) :
result += '\t%s=%s\nf % (attr, self._diet_[attr])
return result
def _str_(self) :
return '<Instance of %s, address %s:\n%s>'
% (
# Имя класса
# Адрес
# Список имя=значение
self._class_._name_,
id(self),
self._attrnames() )
if _name_ == '_main_' :
import testmixin testmixin.tester(Listinstance)
Весь приводимый в разделе код выполняется в Python 2.Х и З.Х. Есть одно замечание: данный код демонстрирует классическую схему включения, и вы могли бы уменьшить его объем за счет более лаконичной реализации метода_attrnames посредством генераторного выражения, запускаемого строковым методом join, но результат оказался бы менее ясным — такие выражения обычно должны подталкивать к поиску более простых альтернатив:
def _attrnames(self):
return '1.join('\t%s=%s\n' % (attr, self._diet_ [attr])
for attr in sorted(self._diet_))
Для извлечения имени класса и атрибутов экземпляра в Listinstance применяются ранее исследованные приемы.
• Каждый экземпляр имеет встроенный атрибут_class_, ссылающийся на
класс, из которого он был создан, а каждый класс содержит атрибут_name_,
ссылающийся на имя в заголовке, так что выражение self._class_._name_
извлекает имя класса экземпляра.
• Класс Listinstance выполняет большую часть своей работы, просто просматривая словарь атрибутов экземпляра (вспомните, что он экспортируется в
_diet_) для формирования строки с именами и значениями всех атрибутов
экземпляра. Ключи словаря сортируются, чтобы обойти любые отличия в упорядочивании между выпусками Python.
В этих отношениях класс Listinstance похож на реализацию отображения атрибутов в главе 28; фактически его можно считать лишь вариацией на данную тему. Однако наш класс использует две дополнительные методики.
• Он отображает адрес экземпляра в памяти, вызывая встроенную функцию id, которая возвращает адрес любого объекта (по определению уникальный идентификатор объекта, который будет полезен при дальнейшей модификации кода).
• Он применяет схему псевдозакрытого именования для своего рабочего метода:
_attrnames. Как объяснялось ранее в главе, любое имя подобного рода Python
автоматически локализует во включающем его классе, дополняя имя атрибута именем класса (в данном случае оно превращается в Listinstance_attrnames).
Это остается справедливым для атрибутов класса (вроде методов) и атрибутов экземпляра, присоединенных к self. Как отмечалось в пробной версии в главе 28, такое поведение полезно в универсальных инструментах, поскольку оно гарантирует, что их имена не будут конфликтовать с любыми именами, используемыми в клиентских подклассах.
Из-за того, что Listinstance определяет метод перегрузки операции_str_, создаваемые из этого класса экземпляры автоматически отображают свои атрибуты при выводе, давая чуть больше информации, чем простой адрес. Ниже показана работа класса в режиме одиночного наследования, когда он подмешивается к классу из предыдущего раздела (код выполняется в Python З.Х и 2.Х одинаково, хотя герг из Python
2.Х по умолчанию отображает метку instance, а не object):
>>> from listinstance import Listinstance
»> class Spam (Listinstance) : # Наследует метод_str_
def_init_(self) :
self.datal * 'food'
>>> x = Spam()
»> print (x) # print () и str () запускают_str_
cinstance of Spam, address 43034496: datal=food
>
Вы можете также извлекать и сохранять выходной список в виде строки, не выводя его посредством str, а эхо-вывод интерактивной подсказки по-прежнему применяет стандартный формат, потому что возможность реализации метода_герг_мы
оставили клиентам как вариант:
>>> display = str(x) # Вывести строку для интерпретации управляющих символов »> display
'cinstance of Spam, address 43034496:\n\tdatal=food\n>'
>>> x # Метод_repr_ по-прежнему использует стандартный формат
<_main_.Spam object at 0x000000000290A780>
Класс Listinstance пригоден для любого создаваемого класса — даже классов, которые уже имеют один и более суперклассов. Именно здесь пригодится множественное наследование, за счет добавления Listinstance к перечню суперклассов в строках заголовка оператора class (т.е. подмешивая класс Listinstance) вы получаете его метод _str_“бесплатно” наряду с тем, что наследуете из существующих суперклассов.
В файле testmixinO .ру приведен пробный тестовый сценарий:
# Файл testmixinO.ру
from listinstance import Listinstance # Получить класс инструмента для вывода списка атрибутов
class Super:
def _init_(self) : # Метод_init_ суперкласса
self.datal = 'spam' # Создать атрибуты экземпляра
def ham(self) : pass
class Sub (Super, Listinstance) : # Подмешивание ham и_str_
def _init_(self) : # Классы, выводящие списки атрибутовf
# имеют доступ к self
Super._init_(self)
self.data2 = 'eggs' § Дополнительные атрибуты экземпляра
self.data3 = 42
def spam (self) : # Определить здесь еще один метод
pass
if _name_ == '_main_' :
X = Sub()
print (X) # Выполняется подмешанный метод_str_
Класс Sub наследует имена из Super и Listinstance; он содержит собственные имена и имена из обоих суперклассов. Когда вы создаете экземпляр Sub и выводите его, то автоматически получаете специальное представление, подмешанное из Listinstance (в данном случае вывод сценария одинаков в Python З.Х и 2.Х, исключая адреса объектов, которые вполне естественно могут варьироваться от процесса к процессу):
c:\code> python testmixinO.ру
<Instance of Sub, address 44304144: datal=spam data2=eggs data3=42
>
Тестовый сценарий testmixinO работает, но имя тестируемого класса в нем жестко закодировано, что затрудняет экспериментирование с альтернативными версиями — чем мы вскоре займемся. Для обеспечения более высокой гибкости мы можем позаимствовать код из примера с перезагрузкой модулей в главе 25 первого тома и передавать объект, подлежащий тестированию, как иллюстрируется в показанном ниже усовершенствованном тестовом сценарии testmixin, который фактически используется в коде самотестирования всех модулей с классами вывода списков. В данном контексте передаваемый инструменту тестирования объект является подмешиваемым классом, а не функцией, но принцип похож: в Python абсолютно все квалифицируется как объект “первого класса”:
#!python
# Файл testmixin.ру (Python 2.Х + З.Х)
Н IV IV
Обобщенный инструмент тестирования подмешиваемых классов вывода списков: он похож на средство транзитивной перезагрузки модулей из главы 25 первого тома, но ему передается объект класса (не функции), а в testByNames добавлена загрузка модуля и класса по строковым именам в соответствии с паттерном проектирования 'Фабрика'.
VI V! IV
import importlib
def tester(listerclass, sept=False) : class Super:
def _init_(self) : # Метод_init_ суперкласса
self.datal = 'spam' # Создать атрибуты экземпляра
def ham(self): pass
class Sub (Super, listerclass) : # Подмешивание ham и_str_
def _init_(self) : # Классы, выводящие списки атрибутов,
# имеют доступ к self
Super._init_(self)
self.data2 = 'eggs' # Дополнительные атрибуты экземпляра
self.data3 = 42
def spam(self): # Определить здесь еще один метод
pass
instance = Sub () # Возвратить экземпляр с помощью_str_
# класса, выводящего список
print (instance) # Выполняется подмешанный метод_str_
# (или через str(x))
if sept: print('-' * 80)
def testByNames(modname, classname, sept=False):
modobject = importlib.import_module(modname) # Импортировать no
# строковым именам listerclass = getattr(modobject, classname) # Извлечь атрибуты no
# строковым именам
tester(listerclass, sept)
if _name_ == '_main_' :
testByNames('listinstance', 'Listinstance', True) § Протестировать все
# три класса testByNames('listinherited', 'Listlnherited', True) testByNames('listtree', 'ListTree', False)
Одновременно в сценарий также добавлена возможность указывать тестируемый модуль и класс по строковым именам, которая задействована в его коде самотестирования — приложение механики описанного ранее паттерна проектирования “Фабрика”. Ниже новый сценарий демонстрируется в работе, запускаемый модулем с классом вывода списка, который импортирует его для тестирования собственного класса (снова с одинаковыми результатами в Python 2.Х и З.Х). Можно также запустить сам тестовый сценарий, но в этом режиме тестируются два варианта класса вывода списка, которые мы пока еще не видели (и не реализовывали!):
c:\code> python listinstance.ру
cinstance of Sub, address 43256968: datal=spam data2=eggs data3=42
>
c:\code> python testmixin.py
cinstance of Sub, address 43977584: datal=spam data2=eggs data3=42
>
. . . и результаты тестов двух других классов вывода списков, которые еще предстоит создать...
Реализованный до сих пор класс Listinstance работает в любом классе, куда он подмешивается, потому что self ссылается на экземпляр подкласса, в который помещается Listinstance, каким бы он ни был. Опять-таки в известной мере подмешиваемые классы представляют собой классовый эквивалент модулей — пакеты методов, полезных в разнообразных клиентах. Скажем, ниже показано, как Listinstance работает в режиме одиночного наследования с экземплярами другого класса, загруженного с помощью import, и отображает атрибуты, значения которым присваиваются за пределами класса:
>>> import listinstance
>>> class С(listinstance.ListInstance): pass »> x = C()
»> x.a, x.b, x.c = 1,2,3 >>> print(x)
cinstance of C, address 43230824: a=l b=2 c=3
>
Помимо обеспечиваемой подмешиваемыми классами полезности они оптимизируют сопровождение кода, как и все классы. Например, если позже вы решите расширить метод_str_класса Listinstance путем добавления вывода всех атрибутов
класса, унаследованных экземпляром, то безопасно можете сделать это; поскольку метод _str_наследуемый, его изменение автоматически обновляет отображение каждого подкласса, который импортирует и подмешивает класс Listinstance. И так как теперь официально “позже” уже наступило, давайте перейдем к следующему разделу и выясним, как может выглядеть такое расширение.
Вывод списка унаследованных атрибутов с помощью dir
В том виде, как есть, наш подмешиваемый класс Listerlnstance отображает только атрибуты экземпляра (т.е. имена, присоединенные к самому объекту экземпляра). Тем не менее, класс легко расширить для отображения всех атрибутов, доступных из экземпляра — собственных и унаследованных из его классов. Трюк предусматривает
применение встроенной функции dir вместо просмотра словаря_diet_; словарь
хранит только атрибуты экземпляра, но функция также собирает все унаследованные атрибуты в Python 2.2 и последующих версиях.
Описанная схема реализована в приведенной далее модификации; она помещена в собственный модуль для облегчения тестирования, но если бы существующие клиенты взамен использовали данную версию, тогда они получили бы новое отображение автоматически (и вспомните из главы 25 первого тома, что конструкция as оператора import позволяет назначать новой версии ранее применяемое имя):
#!python
# Файл listinherited.ру (Python 2.Х + З.Х)
class Listinherited:
»» Н II
Применяет dir() для сбора атрибутов экземпляра и имен, унаследованных из его классов/
в Python З.Х отображается больше имен, чем в Python 2.Х из-за наличия подразумеваемого суперкласса object в модели классов нового стиля;
getattr() извлекает унаследованные имена не в self._diet_;
используйте _str_, а не _герг_, иначе произойдет зацикливание при
вызове связанных методов!
и ft I»
def _attrnames(self):
result = ' ’
for attr in dir (self) : # di r () экземпляра
if attr[:2] == '_' and attr [-2:] == '_’: # Пропуск внутренних имен
result += r\t%s\n' % attr else:
result += l\t%s=%s\nr % (attr, getattr(self, attr)) return result
def _str_(self) :
return 'cinstance of %s, address %s:\n%s>' % (
self._class_._name_, # Имя класса
id(self) , # Адрес
self._attrnames()) # Список имя=значение
if _name_ == '_main_' :
import testmixin
testmixin.tester(Listinherited)
Обратите внимание, что в коде пропускаются значения имен_X_; большинство из них являются внутренними именами, о которых мы обычно не заботимся в обобщенных списках подобного рода. В данной версии также должна использоваться встроенная функция getattr для извлечения атрибутов по строковым именам вместо индексации словаря атрибутов экземпляра — getattr задействует протокол поиска в иерархии наследования, а ряд имен, помещаемых здесь в список, не хранятся в самом экземпляре.
Чтобы протестировать новую версию, запустите ее файл напрямую — он передает тестовой функции из файла testmixin.ру определяемый в нем класс для применения в качестве подмешиваемого в подклассе. Однако выходные данные тестовой функции и класса, выводящего список атрибутов, варьируются от выпуска к выпуску, т.к. результаты dir отличаются. В Python 2.Х мы получаем следующий вывод; в имени метода класса для вывода списка атрибутов легко заметить корректировку имен в действии (мне пришлось усечь некоторые полные значения, чтобы они уместились на печатной странице):
c:\code> c:\python27\python listinherited.ру
cinstance of Sub, address 35161352:
_ListInherited_attrnames=<bound method Sub._attrnames of <test...
не показано...>>
_doc_
_init_
_module_
_str_
datal=spam
data2=eggs
data3=42
ham=<bound method Sub.ham of <testmixin.Sub instance at 0x00000... не показано. . . »
spam=<bound method Sub.spam of ctestmixin.Sub instance at 0x00000... не показано. . . »
>
В Python З.Х отображается больше атрибутов, потому что все классы относятся к “новому стилю” и наследуют имена из подразумеваемого суперкласса object; более подробно об этом пойдет речь в главе 32. Поскольку из стандартного суперкласса наследуется настолько много имен, часть имен здесь не показаны — в Python 3.7 их в сумме 32. Запустите файл самостоятельно, чтобы получить полный список:
c:\code> c:\python37\python listinherited.py
<Instance of Sub, address 48032432:
_ListInherited_attrnames=<bound method Listinherited._attrnames of
<testmixin. . .не показано. . .»
_class_
_delattr_
_diet_
_dir_
_doc_
_e4_
. . . остальные из 32 имен не показаны. . .
_repr_
_setattr_
_sizeof_
_str_
subclasshook
_weakref_
datal=spam
data2=eggs
data3=42
ham=<bound method tester.<locals>.Super.ham of <testmixin. tester.<locals>.Sub object at 0x02DCEAB0>>
spam=<bound method tester.<locals>.Sub.spam of <testmixin. tester.<locals>.Sub object at 0x02DCEAB0>>
>
Как одно возможное усовершенствование, направленное на решение проблемы с ростом количества унаследованных имен и длинных значений, в следующей альтернативной версии_attrnames из файла listinherited2 .ру в пакете примеров для
книги имена с двумя символами подчеркивания группируются отдельно, а переносы строк для длинных значений атрибутов сводятся к минимуму. Обратите внимание на отмену % с помощью % %, так что остается только один символ для финальной операции форматирования:
def_attrnames(self, indent=' '*4):
result = 1 Unders%s\n%s%%s\nOthers%s\n' % ('-'*77, indent, '-'*77) unders = []
for attr in dir(self): # dir() экземпляра
if attr[:2] == '_' and attr[-2:] == 1_# Пропуск внутренних имен
unders.append(attr) else:
display = str(getattr(self, attr) )[:82-(len(indent) + len(attr))] result += ' %s%s=%s\n' % (indent, attr, display) return result % ', '. join(unders)
Благодаря такому изменению тестовый вывод класса становится чуть сложнее, но также более компактным и полезным:
c:\code> c:\python27\python listinherited2.ру
cinstance of Sub, address 36299208:
Unders--------------------------------------------------------------------
_doc_, _init_, _module_, _str_
Others--------------------------------------------------------------------
_ListInherited_attrnames=<bound method Sub._attrnames of <testmixin.
Sub insta
datal=spam
data2=eggs
data3=42
ham=<bound method Sub.ham of <testmixin.Sub instance at 0х000000000229Е1С8»
spam=<bound method Sub.spam of <testmixin.Sub instance at 0x00000000022 9Е1С8»
>
c:\code> c:\python37\python listinherited2.py
<Instance of Sub, address 48159472:
Unders--------------------------------------------------------------------
_sizeof_, _str_, _subclasshook_, _weakref_
Others--------------------------------------------------------------------
_ListInherited_attrnames=<bound method Listinherited._attrnames of
<testmixin
datal=spam
data2=eggs
data3=42
ham=
<bound method tester.<locals>.Super.ham of ctestmixin.tester.<locals>.Sub о spam=
<bound method tester.<locals>.Sub.spam of ctestmixin.tester.<locals>.Sub о
>
Формат отображения — открытая для решения задача (например, стандартный модуль Python для “симпатичного вывода” pprint тоже способен предложить варианты), а потому дальнейшее совершенствование оставлено в качестве упражнения. Так или иначе, но класс, выводящий атрибуты в дереве классов, может оказаться более полезным.
Зацикливание в_герг_. Одно предостережение — теперь, когда мы отображаем также и унаследованные методы, для перегрузки операции вывода вместо_герг_должен использоваться метод_str_. В случае применения _герг_код попадет в бесконечный цикл рекурсии — отображение
значения метода запускает_герг_класса этого метода, чтобы отобразить сам класс. То есть, если_герг_класса, выводящего список атрибутов, попытается отобразить метод, то класс отображаемого метода снова запустит_герг_класса, выводящего список атрибутов. Проблема тонкая, но реальная! Чтобы удостовериться в ее наличии, измените_str_
на_герг_. Если вы обязаны использовать_герг_в контексте подобного рода, тогда избежать циклов можно за счет применения isinstance для сравнения типа значений атрибутов с types .MethodType из стандартной библиотеки и пропуска таких атрибутов.
Вывод списка атрибутов для объектов в деревьях классов
Давайте займемся последним расширением. В текущем виде класс, выводящий список атрибутов, включает в него унаследованные имена, но не указывает, из каких классов они были получены. Тем не менее, как было показано в примере classtree.ру ближе к концу главы 29, реализовать подъем по деревьям наследования классов в коде довольно легко. Приведенный далее подмешиваемый класс (файл listtree.ру) задействует ту же самую методику для отображения атрибутов, сгруппированных по классам, где они находятся — он схематически выводит полное физическое дерево классов, в ходе дела отображая атрибуты, которые присоединены к каждому объекту. Читателю все еще придется делать предположения относительно наследования атрибутов, но результат предоставляет гораздо больше деталей, чем простой плоский список:
#!python
# Файл listtree.ру (Python 2.Х + З.Х)
class ListTree: ff ft >1
Подмешиваемый класс, который возвращает в _str_ результат обхода целого
дерева классов и атрибуты всех его объектов, начиная с self и выше; запускается print () и str() и возвращает сформированную строку;
использует схему именования атрибутов _X, чтобы избежать конфликтов
имен в клиентах; явно рекурсивно обращается к суперклассам, для ясности применяет str.format().
It fT Tf def_attrnames(self, obj, indent):
spaces = ' ' * (indent + 1) result = ' '
for attr in sorted (obj._diet_) :
if attr.startswith('_') and attr.endswith('_'):
result += spaces + '{0}\n'.format(attr) else:
result += spaces + '{0}={1}\n'.format(attr, getattr(obj, attr)) return result
def_listclass(self, aClass, indent):
dots = ' . ' * indent
if aClass in self._visited:
return '\n{0}<Class {1}:, address {2}: (see above)>\n’.format( dots,
aClass._name_,
id(aClass))
else:
self._visited[aClass] = True
here = self._attrnames(aClass, indent)
above = ' '
for super in aClass._bases_:
above += self._listclass(super, indent+4)
return '\n{0}<Class {1}, address {2}:\n{3}{4}{5}>\n' . format ( dots,
aClass._name_,
id(aClass), here, above, dots)
def _str_(self) :
self._visited = {}
here = self._attrnames (self, 0)
import testmixin testmixin.tester(ListTree)
Класс ListTree достигает своей цели путем обхода дерева наследования — он начинает с_class_экземпляра, затем рекурсивно проходит по всем его суперклассам, перечисленным в_bases_класса, и попутно просматривает атрибут_diet_
каждого объекта. При раскрутке рекурсии он добавляет каждую порцию дерева к результирующей строке.
Понимание рекурсивных программ подобного рода может требовать определенного времени, но с учетом произвольной формы и глубины деревьев классов в действительности у нас нет выбора (кроме реализации явных эквивалентов со стеком вроде представленных в главах 19 и 25 первого тома, которые оказываются не намного проще и ради экономии места здесь не приводятся). Однако для максимальной понятности код класса List Tree написан так, чтобы сделать его работу как можно более ясной.
Скажем, вы могли бы заменить оператор цикла из метода_listclass , показанный ниже в первом фрагменте кода, неявно запускаемым генераторным выражением, которое показано во втором фрагменте. Но второй фрагмент кода выглядит излишне запутанным в этом контексте (рекурсивные вызовы, внедренные в генераторное выражение) и не обеспечивает явного преимущества в плане производительности, особенно принимая во внимание ограниченные рамки данной программы (ни одна из альтернатив не создает временный список, хотя первая могла бы создавать больше временных результатов в зависимости от внутренней реализации строк, конкатенации и join — то, что потребует измерения времени посредством инструментов из главы 21 первого тома):
above = ' '
for super in aClass._bases_:
above += self._listclass(super, indent+4)
...или...
above = ' ' . join(
self._listclass(super, indent+4) for super in aClass._bases_)
Вы могли бы также реализовать конструкцию else в_listclass так, как показано ниже и делалось в предыдущем издании книги. Такая альтернативная версия помещает все в список аргументов format, полагается на тот факт, что вызов join запускает генераторное выражение и его рекурсивные вызовы до того, как операция форматирования начнет формировать результирующий текст, и выглядит более сложной для понимания:
self._visited[aClass] = True
genabove = (self._listclass(c, indent+4) for с in aClass._bases_)
return '\n{0}<Class {1}, address {2}:\n{3}{4}{5}>\n'.format( dots,
aClass._name_,
id(aClass),
self._attrnames(aClass, indent), # Запускается перед
# форматированием!
''.join(genabove), dots)
Как всегда, явная реализация лучше неявной, и сам ваш код может быть не менее важным фактором, чем используемые в нем инструменты.
Также обратите внимание на применение в этой версии строкового метода format из Python З.Х и Python 2.6/2.7 вместо выражений форматирования % в попытке сделать подстановки яснее; при настолько большом количестве подстановок явные номера аргументов способны облегчить восприятие кода. Короче говоря, в данной версии мы заменили первую строку второй:
return 'cinstance of %s, address %s:\n%s%s>' %(...) # Выражение
return 'cinstance of {0}, address {1}:\n{2}{3}>'.format (...) # Метод
Запуск класса, выводящего дерево
Для тестирования необходимо запустить файл модуля с этим классом, как делалось ранее; он передает сценарию testmixin.ру класс ListTree, чтобы подмешать его в подкласс в тестовой функции. Вот вывод, полученный в Python 2.Х:
c:\code> c:\python27\python listtree.ру
cinstance of Sub, address 36690632:
_ListTree_visited={}
datal=spam
data2=eggs
data3=42
....<Class Sub, address 36652616:
_doc_
_init_
_module_
spam=<unbound method Sub.spam>
........<Class Super, address 36652712:
_doc_
_init_
_module_
ham=<unbound method Super.ham>
........>
........<Class ListTree, address 30795816:
_ListTree_attrnames=<unbound method ListTree._attrnames>
_ListTree_listclass=<unbound method ListTree._listclass>
_doc_
_module_
_str_
........>
. . . . >
>
Обратите внимание в выводе на то, что теперь в Python 2.Х методы являются несвязанными, т.к. мы напрямую извлекаем их из классов. В версии из предыдущего раздела они отображались как связанные методы, потому что Listinherited взамен извлекал
их из экземпляров с помощью getattr (первая версия индексировала словарь_diet_
экземпляра и вообще не отображала методы, унаследованные из классов). Кроме того,
таблица_visited из класса, выводящего дерево, в словаре атрибутов экземпляра
имеет скорректированное имя; если только мы не крайне невезучи, то оно не будет конфликтовать с другими данными. Имена некоторых методов класса, выводящего дерево, также скорректированы, чтобы сделать их псевдозакрытыми.
Как показано ниже, в Python З.Х мы снова получаем добавочные атрибуты, которые могут варьироваться внутри линейки Python З.Х, и дополнительные суперклассы — в следующей главе вы узнаете, что в Python З.Х все классы верхнего уровня автоматически наследуются от встроенного класса object; в классах Python 2.Х можно делать то же самое вручную, если для них выбрано поведение классов нового стиля. Также обратите внимание, что атрибуты, которые в Python 2.Х были несвязанными методами, в Python З.Х представляют собой простые функции, как объяснялось ранее в главе (здесь снова ради экономии пространства удалены многие встроенные атрибуты класса object; запустите listtree.ру самостоятельно, чтобы получить их полный список):
c:\code> c:\python37\python listtree.py
<Instance of Sub, address 48294960:
_ListTree_visited={}
datal=spam
data2=eggs
data3=42
....<Class Sub, address 48361520:
_doc_
init
_module_
spam=<function tester.<locals>.Sub.spam at 0x02ElBC48>
........<Class Super, address 48319768:
_diet_
_doc_
_init_
_module_
_weakref_
ham=<function tester.<locals>.Super.ham at 0x02ElBBB8>
............<Class object, address 1465979880:
_class_
_delattr_
_dir_
_doc_
_eq_
. . . остальные атрибуты не показаны: всего их 23. . .
_герг_
_setattr_
_sizeof_
_str_
_subclasshook_
............>
........>
........<Class ListTree, address 14115808:
_ListTree_attrnames=<function ListTree._attrnames at 0x02ElB9C0>
_ListTree_listclass=<function ListTree._listclass at 0x02ElBA08>
_diet_
_doc_
_module_
_str_
_weakref_
............<Class object:, address 1465979880: (see above)>
........>
. . . . >
>
В этой версии устранена возможность двукратного вывода одного и того же объекта класса за счет ведения таблицы посещенных классов (вот почему включается id объекта — чтобы служить ключом для ранее отображенного элемента в дереве). Подобно инструменту транзитивной перегрузки модулей из главы 25 первого тома словарь помогает избежать повторений в выводе, потому что объекты классов являются хешируемыми и могут быть ключами словаря; множество обеспечило бы аналогичную функциональность.
Формально циклы в деревьях наследования классов обычно невозможны. Класс должен быть уже определен, чтобы его можно было указывать в качестве суперкласса, и Python сгенерирует исключение, если вы попытаетесь позже создать цикл за счет изменения_bases_, но механизм учета посещенных классов не допускает повторного вывода класса:
>>> class С: pass >» class В (С) : pass
>>> С._bases_= (В,) # Черная магия!
TypeError: а _bases_ item causes an inheritance cycle
Ошибка типа: элемент_bases_ вызывает цикл наследования
Вариант использования: отображение значений имен с символами подчеркивания
Текущая версия также избегает отображения крупных внутренних объектов, снова пропуская имена_X_. Если вы поместите в комментарии код, который трактует такие имена особым образом:
result += spaces + '{0}={1}\n'.format(attr, getattr(obj, attr))
то их значения станут нормально отображаться. Ниже приведен вывод в Python 2.Х после внесения этого временного изменения, содержащий значения всех атрибутов в дереве классов:
c:\code> c:\python27\python listtree.ру
cinstance of Sub, address 35750408:
_ListTree_visited={}
datal=spam
data2=eggs
data3=42
....<Class Sub, address 36353608:
_module_=testmixin
ham=<unbound method Super.ham>
........>
........<Class ListTree, address 31254568:
_ListTree_attrnames=<unbound method ListTree._attrnames>
_ListTree_listclass=<unbound method ListTree._listclass>
_doc_=
Подмешиваемый класс, который возвращает в _str_ результат обхода целого
дерева классов и атрибуты всех его объектов, начиная с self и выше; запускается print() и str() и возвращает сформированную строку; использует
схему именования атрибутов _X, чтобы избежать конфликтов имен в клиентах;
явно рекурсивно обращается к суперклассам, для ясности применяет str.format().
_module_=_main_
_str_=<unbound method ListTree._str >
........>
. . . . >
>
Вывод теста в Python З.Х оказывается гораздо более длинным и в целом может оправдать отделение имен с символами подчеркивания, которое делалось ранее:
c:\code> c:\python37\python listtree.ру
<Instance of Sub, address 47901712:
_ListTree_visited={}
datal=spam
data2=eggs
data3=42
....<Class Sub, address 47968304:
_doc_=None
_init_=<function tester.<locals>.Sub._init_ at 0x02DBBC00>
_module_=testmixin
spam=<function tester.<locals>.Sub.spam at 0x02DBBC48>
........<Class Super, address 47922456:
_diet_= { '_module_': 'testmixin', '_init_': <function
tester.<locals>.Super._init_at 0x02DBBB70>, 'ham': <function
tester.<locals>.Super.ham at 0x02DBBBB8>, '_diet_': <attribute '_diet_'
of 'Super' objects>, 1_weakref_': <attribute '_weakref_' of 'Super'
objects>, '_doc_': None}
_doc_=None
_init_=<function tester.<locals>.Super._init_ at 0x02DBBB70>
_module =testmixin
_weakref_=<attribute '_weakref_' of 'Super' objects>
ham=<function tester.<locals>.Super.ham at 0x02DBBBB8>
............<Class object, address 1465979880:
_class_=<class 'type’>
_delattr_=<slot wrapper '_delattr_' of 'object' objects>
_dir_=<method '_dir_' of 'object' objects>
_doc_=The most base type
_eq_=<slot wrapper '_eq_' of 'object' objects>
_format_=<method '_format_' of 'object' objects>
_ge_=<slot wrapper '_ge_' of 'object' objects>
_getattribute_=
<slot wrapper '_getattribute_' of 'object' objects>
_gt_=<slot wrapper '_gt_' of 'object' objects>
_hash_=<slot wrapper '_hash_' of 'object' objects>
_init_=<slot wrapper '_init_' of 'object' objects>
_init_subclass_=
<built-in method _init_subclass_ of type object at 0x57 6113E8>
_le_=<slot wrapper '_le_' of 'object' objects>
_It_=<slot wrapper '_It_' of 'object' objects>
_ne_=<slot wrapper '_ne_' of 'object' objects>
_new_=<built-in method _new_ of type object at 0x57 6113E8>
_reduce_=<method '_reduce_' of 'object' objects>
_reduce_ex_=<method '_reduce_ex_' of 'object' objects>
_repr_=<slot wrapper '_repr_' of 'object' objects>
_setattr_=<slot wrapper '_setattr_' of 'object' objects>
_sizeof_=<method '_sizeof_' of 'object' objects>
_str_=<slot wrapper '_str_' of 'object' objects>
_subclasshook_=
<built-in method _subclasshook_ of type object at 0x576113E8>
............>
........>
........<Class ListTree, address 46293984:
_ListTree_attrnames=<function ListTree._attrnames at 0x02DBB9C0>
_ListTree_listclass=<function ListTree._listclass at 0x02DBBA08>
_diet_={'_module_': '_main_'_doc_': "\n Подмешиваемый
класс, который возвращает в _str_ результат обхода целого дерева классов
\п и атрибуты всех его объектов, начиная с selfn выше; запускается print() и str () \п и возвращает сформированную строку; использует схему именования
атрибутов X, \п чтобы избежать конфликтов имен в клиентах; явно рекурсивно обращается к суперклассам, \п для ясности применяет str.format().\n ",
'_ListTree_attrnames': <function ListTree._attrnames at 0x02DBB9C0>,
'_ListTree__listclass' : <function ListTree._listclass at 0x02DBBA08>,
'_str_' : «function ListTree._str_ at 0x02DBBA50>,
Подмешиваемый класс, который возвращает в _str_ результат обхода
целого дерева классов и атрибуты всех его объектов, начиная с self и выше; запускается print() и str() и возвращает сформированную строку; использует
схему именования атрибутов _X, чтобы избежать конфликтов имен в клиентах;
явно рекурсивно обращается к суперклассам, для ясности применяет str.format().
_mo du1е_=_ma in_
_str_=<function ListTree._str_ at 0x02DBBA50>
_weakref_=<attribute '_weakref_' of 'ListTree' objects>
............«Class object:, address 1465979880: (see above)>
. >
Вариант использования: запуск для более крупных модулей
Ради интереса удалите комментарии в строках обработки имен с символами подчеркивания и попробуйте подмешать данный класс во что-то более массивное, скажем, в класс Button из модуля Python с комплектом инструментов для построения графических пользовательских интерфейсов tkinter. В общем случае вам понадобится указать имя класса ListTree первым (крайним слева) в заголовке class, чтобы выбирался его метод_str_; класс Button тоже имеет такой метод, но при множественном наследовании первым всегда выполняется поиск в крайнем слева суперклассе.
Вывод получается довольно большим (более 18 тысяч символов и 320 строк в Python З.Х — и почти 35 тысяч символов и 336 строк, если вы забыли удалить комментарий с кода обнаружения символов подчеркивания!), поэтому запустите код, чтобы
увидеть полный список. Обратите внимание, что атрибут словаря_visited нашего
класса, выводящего дерево, безвредно смешивается с атрибутами, которые создал сам модуль tkinter. Если вы работаете с Python 2.Х, то также не забудьте применять имя модуля Tkinter вместо tkinter:
>>> from listtree import ListTree
>» from tkinter import Button # Оба класса имеют метод_str_
»> class MyButton(ListTree, Button): pass # ListTree первый: используется
# его метод_str_
>>> В = MyButton (text=' spam')
»> open (' save tree. txt' , 'w') .write (str (В)) # Сохранить файл для просмотра
# в будущем
18497
>» len (open (' save tree, txt') .readlines ()) # Строк в файле
320
»> print (В) # Вывод всего дерева
«Instance of MyButton, address 47980912:
_ListTree_visited={}
_name=!mybutton
_tclCommands=[]
_w=.!mybutton children={} master=.
...очень многое не показано. . .
>
>>> S = str (В) # Или вывод только первой части
»> print (S [: 1000])
«Instance of MyButton, address 47980912:
_ListTree_visited={}
_name=!mybutton _tclCommands=[ ]
_w=.'mybutton children={} master=.
tk=«_tkinter.tkapp object at 0x02DFAF08> widgetName=button
....«Class MyButton, address 48188696:
_doc_
_module_
........«Class ListTree, address 46687200:
_ListTree_attrnames=<function ListTree._attrnames at 0x02DFBA50>
_ListTree__listclass=«function ListTree._listclass at 0x02DFBA98>
_diet_
_doc_
_module_
_str_
_weakref_
............«Class object, address 1465979880:
_class_
_delattr_
_dir_
_doc_
_ecL_
_format_
_ge_
_getattribute_
_gt_
_hash_
_init_
_init_subclass_
_ie_
_It_
_ne_
_new_
_reduce_
Поэкспериментируйте с кодом самостоятельно. Суть здесь в том, что ООП направлено на многократное использование кода, а подмешиваемые классы являются ярким примером. Как и почти все остальное в программировании, множественное наследование при надлежащем применении может быть полезным механизмом. Тем не менее, на практике оно представляет собой развитое средство и может стать сложным в случае небрежного или чрезмерного использования. Мы возвратимся к данной теме, когда будем рассматривать затруднения в конце следующей главы.
Собирающий модуль
Наконец, чтобы еще больше облегчить импортирование наших инструментов, мы можем предоставить собирающий модуль, который объединяет их в единое пространство имен — импортирование только одного этого модуля открывает доступ сразу ко всем трем подмешиваемым классам:
# Файл lister.ру
# Для удобства собирает в одном модуле все три класса, выводящие атрибуты
from listinstance import Listinstance from listinherited import Listinherited from listtree import ListTree
Lister = ListTree # Выбрать стандартный класс, выводящий атрибуты
Импортеры могут работать с индивидуальными именами классов или назначать им псевдоним в виде общего имени, применяемом в подклассах, которое можно модифицировать в операторе import:
>>> import lister
>» lister .Listinstance # Использовать специфический класс, выводящий атрибуты cclass ’listinstance.Listinstance'>
>» lister.Lister # Использовать стандартный класс Lister
<class 'listtree.ListTree'>
>>> from lister import Lister # Использовать стандартный класс Lister
»> Lister
<class 1listtree.ListTree'>
>» from lister import Listinstance as Lister # Использовать псевдоним Lister »> Lister
cclass 'listinstance.Listinstance'>
Python часто делает гибкие API-интерфейсы инструментов почти автоматическими.
Возможности для совершенствования: MRO, слоты, графические пользовательские интерфейсы
Подобно большинству программ есть очень многое, что мы могли бы еще сделать здесь. Далее приведены советы по расширению, которые вы можете счесть полезными. Некоторые из них выливаются в интересные проекты, а два служат плавным переходом к материалу следующей главы, но из-за ограниченного пространства они оставлены в качестве упражнений для самостоятельного выполнения.
Общие идеи: графические пользовательские интерфейсы, внутренние имена
Группирование имен с двумя символами подчеркивания, как делалось ранее, может способствовать сокращению размера древовидного отображения, хотя
некоторые имена вроде_init_определяются пользователем и заслуживают
специального обращения. Схематическое изображение дерева в графическом пользовательском интерфейсе тоже может считаться естественным следующим шагом — комплект инструментов tkinter, задействованный в примерах из предыдущего раздела, поставляется вместе с Python и предлагает базовую, но легкую поддержку, а есть альтернативы с более широкими возможностями, хотя они сложнее. Дополнительные указания по этому поводу ищите в разделе “Указания на будущее'’ главы 28.
Физические деревья или наследование: использование MRO (предварительный обзор)
В следующей главе мы также обсудим модель классов нового стиля, которая модифицирует порядок поиска для одного особого случая множественного наследования (ромбы). Там мы также исследуем атрибут объектов классов нового
стиля class._mro_— кортеж, который дает применяемый при наследовании
порядок поиска в дереве классов, известный как MRO нового стиля.
В текущем виде наш класс ListTree схематически отображает физическую форму дерева наследования и ожидает, что пользователь сделает вывод о том, откуда унаследован тот или иной атрибут. В этом заключалась его цель, но универсальное средство просмотра объектов могло бы также использовать кортеж MRO, чтобы автоматически ассоциировать атрибут с классом, из которого он унаследован. За счет обследования MRO нового стиля (или упорядочение DFLR классических классов) для каждого унаследованного атрибута в результате вызова dir мы можем эмулировать поиск в иерархии наследования Python и сопоставлять атрибуты с их исходными объектами в отображенном физическом дереве классов.
На самом деле мы будем писать код, который очень близок к этой идее, в модуле mapattrs из следующей главы и многократно применять его тестовые классы для демонстрации идеи, так что дождитесь эпилога данной истории. Он мог бы использоваться взамен или в дополнение к отображению физических местоположений атрибутов в_attrnames; обе формы снабжали бы программистов полезными сведениями. Такой подход позволяет также учитывать слоты, которые рассматриваются в следующем примечании.
Виртуальные данные: слоты, свойства и многое другое (предварительный обзор)
Поскольку классы Listinstance и ListTree просматривают словари пространств имен_diet_экземпляров, они представлены здесь для иллюстрации ряда тонких проблем проектирования. В классах Python некоторые имена, ассоциированные с данными экземпляра, могут не храниться в самом экземпляре. Сюда входят свойства нового стиля, слоты и дескрипторы, представленные в следующей главе, а также атрибуты, динамически вычисляемые во всех классах с
помощью инструментов вроде_getattr_. Имена упомянутых “виртуальных”
атрибутов не хранятся в словаре пространства имен экземпляра, поэтому ни одно из них не будет отображаться как часть собственных данных экземпляра.
Из всего перечисленного слоты кажутся наиболее тесно связанными с экземпляром; они хранят данные в экземплярах, хотя их имена не появляются в словарях пространств имен экземпляров. Свойства и дескрипторы тоже ассоциированы с экземплярами, но не занимают место в экземпляре, их вычислительная природа гораздо более явная и они могут показаться ближе к методам уровня класса, чем данные экземпляров.
Как мы увидим в следующей главе, слоты функционируют подобно атрибутам экземпляров, но создаются и управляются автоматически создаваемыми элементами в классах. Они являются относительно редко применяемым вариантом классов нового стиля, где атрибуты экземпляров объявляются в атрибуте
класса_slots_и физически не хранятся в словаре_diet_экземпляра;
на самом деле слоты могут вообще подавлять_diet_. По указанной причине инструменты, которые отображают экземпляры путем просмотра только их пространств имен, не будут напрямую связывать экземпляр с атрибутами, хра-
нящимися в слотах. В существующем виде класс ListTree отображает слоты как атрибуты класса, когда бы они ни появлялись (хотя не относит их к экземпляру), а класс Listinstance вообще их не отображает.
Хотя это обретет больше смысла после изучения самого средства в следующей главе, оно оказывает воздействие на код здесь и на похожие инструменты.
Скажем, если в textmixin.py мы присвоим_slots_=[ 'datal' ] в Super
и_slots_= [ 'data3 1 ] в Sub, то два класса, выводящие дерево, отобразят в
экземпляре только атрибут data2. Класс ListTree также отобразит datal и data3, но как атрибуты объектов классов Super и Sub и со специальным форматом для их значений (формально они являются дескрипторами уровня класса — еще одним инструментом нового стиля, представляемым в главе 32).
В следующей главе объясняется, что для отображения атрибутов из слотов как имен экземпляров инструменты обычно должны использовать dir, чтобы получить список всех атрибутов — физически присутствующих и унаследованных — и затем применять либо getattr для извлечения их значений из экземпляра, либо_diet_в обходах дерева для извлечения значений из их источника наследования и принимать отображение реализаций некоторых из них в классах. Поскольку dir включает имена унаследованных “виртуальных” атрибутов (в том числе слоты и свойства), они попадут в набор экземпляра. Как также обнаружится, MRO может содействовать здесь для сопоставления атрибутов dir с их источниками или для ограничения отображения экземпляров именами, записанными в определяемых пользователем классах, за счет отсеивания имен, которые унаследованы из встроенного класса object.
Класс Listinherited невосприимчив к большинству этого, т.к. он уже отображает полный результирующий набор dir, который содержит имена_diet_
и имена_slots_всех классов, хотя в существующем виде его использование
минимально. Вариант ListTree, употребляющий методику с dir наряду с последовательностью MRO для сопоставления атрибутов с классами, будет применяться также к слотам, потому что основанные на слотах имена появляются в результатах_diet_класса по отдельности как инструменты управления слотами, но не в_diet_экземпляра.
Альтернативно в качестве политики мы могли бы просто позволить коду обрабатывать основанные на слотах атрибуты так, как он делает в текущий момент, вместо того, чтобы усложнять его для учета редко используемого и продвинутого средства, которое в наши дни даже считается сомнительной практикой. Слоты и нормальные атрибуты экземпляров являются разными видами имен. В действительности отображение имен слотов как атрибутов классов, а не экземпляров, формально будет более точным — в следующей главе мы увидим, что хотя их реализация находится в классах, занимаемое ими пространство располагается в экземплярах.
В конечном итоге попытка собрать все “виртуальные” атрибуты, ассоциированные с классом, может оказаться чем-то вроде несбыточной мечты. Обрисованные здесь методики способны решить задачу со слотами и свойствами, но некоторые атрибуты являются полностью динамическими, вообще не имея какой-либо физической основы: атрибуты, вычисляемые при извлечении обобщенным методом наподобие_getattr_, не относятся к данным в классическом смысле. Инструменты, которые пытаются отображать данные в на-
столько динамическом языке, как Python, должны сопровождаться предупреждением о том, что отдельные данные в лучшем случае нематериальны!
Мы также предложим небольшое расширение кода, представленного в настоящем разделе, в упражнениях в конце этой части книги, чтобы выводить имена суперклассов в круглых скобках в начале отображения экземпляров. Для лучшего понимания предшествующих двух пунктов нам необходимо завершить текущую главу и перейти к следующей и последней в данной части книги.
Другие темы, связанные с проектированием
В этой главе мы исследовали наследование, композицию, делегирование, множественное наследование, связанные методы и фабрики — все распространенные паттерны проектирования, применяемые для объединения классов в программах на Python. Но, по правде говоря, мы лишь слегка коснулись поверхности предметной области, связанной с паттернами проектирования. В других местах книги вы обнаружите обзор остальных тем, относящихся к проектированию, таких как:
• абстрактные суперклассы (глава 29);
• декораторы (главы 32 и 39);
• подклассы типов (глава 32);
• статические методы и методы классов (глава 32);
• управляемые атрибуты (главы 32 и 38);
• метаклассы (главы 32 и 40).
Тем не менее, за дополнительными сведениями о паттернах проектирования я рекомендую обратиться к другим источникам, посвященным ООП в целом. Хотя паттерны важны и зачастую более естественны в ООП на Python, чем на других языках, они не являются специфическими для самого языка Python, а представляют собой предмет, который лучше всего усваивается с опытом.
Резюме
В главе рассматривались избранные способы использования и комбинирования классов с целью оптимизации возможности их многократного применения и приобретения других преимуществ — то, что обычно считается задачами проектирования, которые часто независимы от какого-либо конкретного языка программирования (хотя Python способен облегчить их реализацию). Вы изучили делегирование (помещение объектов внутрь промежуточных классов), композицию (управление внедренными объектами) и наследование (получение линий поведения от других классов), а также ряд более экзотических концепций, таких как псевдозакрытые атрибуты, множественное наследование, связанные методы и фабрики.
В следующей главе мы завершаем исследование классов и ООП обзором более сложных тем, связанных с классами. Определенные материалы могут быть интереснее разработчикам инструментов, нежели прикладным программистам, но они заслуживают ознакомления большинством из тех, кто занимается ООП на Python — если уж не для своего кода, то для кода, написанного другими, в котором необходимо разобраться. Но сначала закрепите пройденный материал главы, ответив на контрольные вопросы.
Проверьте свои знания: контрольные вопросы
1. Что такое множественное наследование?
2. Что такое делегирование?
3. Что такое композиция?
4. Что такое связанные методы?
5. Для чего используются псевдозакрытые атрибуты?
Проверьте свои знания: ответы
1. Множественное наследование происходит, когда класс наследуется от нескольких суперклассов; оно полезно для смешивания множества пакетов кода, основанного на классах. Общий порядок поиска атрибутов определяется порядком слева направо, в котором суперклассы указаны в строках заголовка оператора
class.
2. Делегирование подразумевает помещение объекта внутрь промежуточного класса, который добавляет дополнительное поведение и передает остальные операции внутреннему объекту. Промежуточный класс предохраняет интерфейс внутреннего объекта.
3. Композиция представляет собой методику, посредством которой класс контроллера внедряет в себя и управляет несколькими объектами, а также обеспечивает все собственные интерфейсы; она является способом построения более крупных структур с помощью классов.
4. Связанные методы объединяют экземпляр и функцию метода; их можно вызывать, не передавая явно объект экземпляра, поскольку исходный экземпляр по-прежнему доступен.
5. Псевдозакрытые атрибуты (чьи имена начинаются с двух символом подчеркивания, но не заканчиваются ими:_X) используются для локализации имен
во включающем классе. К ним относятся атрибуты класса вроде методов, определенных внутри класса, и атрибуты экземпляра self, которым присваиваются значения в методах класса. Такие имена расширяются для включения имени класса, что делает их в целом уникальными.
ГЛАВА 32
Назад: Перегрузка операций
Дальше: Расширенные возможности классов