Главная страница
Навигация по странице:

  • Множественное наследование

  • Классы – это объекты: фабрики универсальных объектов

  • Математический анализ. 3е издание


    Скачать 4.86 Mb.
    Название3е издание
    АнкорМатематический анализ
    Дата04.02.2022
    Размер4.86 Mb.
    Формат файлаpdf
    Имя файлаpython_01.pdf
    ТипДокументы
    #351981
    страница79 из 98
    1   ...   75   76   77   78   79   80   81   82   ...   98
    648
    Глава 25. Шаблоны проектирования с классами сможете многократно использовать его во всех своих программах. Да
    же в этом простом примере благодаря тому, что с помощью классов можно упаковать и унаследовать так много, все, что нам пришлось сделать – это реализовать этап преобразования в формат HTML, а все остальное у нас уже и так имелось.
    Еще один пример композиции в действии приводится в упражнении 9
    в конце главы 26, а его решение – в приложении B. Он напоминает пример с пиццерией. В этой книге мы сосредоточились на наследова
    нии, потому что это основной инструмент, который обеспечивает объ
    ектноориентированные возможности в языке Python. Однако на практике прием композиции используется ничуть не реже, чем насле
    дование, в качестве способа организации классов, особенно в крупных системах. Как мы видели, наследование и композиция – часто взаимо
    дополняющие (а иногда и альтернативные) приемы.
    Композиция – это проблема проектирования, которая далеко выходит за рамки языка Python и этой книги, поэтому полный охват этой темы я оставляю за другими источниками информации.
    ООП и делегирование
    В ООП часто используется термин делегирование, под которым обычно подразумевается наличие объектаконтроллера, куда встраиваются другие объекты, получающие запросы на выполнение операций. Кон
    троллеры могут решать административные задачи, такие как слеже
    ние за попытками доступа и т. д. В языке Python делегирование часто реализуется с помощью метода __getattr__, потому что он перехваты
    вает попытки доступа к несуществующим атрибутам. Класс+обертка
    (иногда называется проксиклассом) может использовать метод
    __getattr__
    для перенаправления обращений к обернутому объекту.
    Классобертка имеет интерфейс обернутого объекта и может добавлять дополнительные операции.
    В качестве примера рассмотрим файл trace.py:
    class wrapper:
    def __init__(self, object):
    self.wrapped = object # Сохранить объект
    def __getattr__(self, attrname):
    print 'Trace:', attrname # Отметить факт извлечения
    return getattr(self.wrapped, attrname) # Делегировать извлечение
    В главе 24 говорилось, что метод __getattr__ получает имя атрибута в виде строки. В этом примере для извлечения из обернутого объекта атрибута, имя которого представлено в виде строки, используется встроенная функция getattr; вызов getattr(X, N) аналогичен выраже
    нию X.N за исключением того, что N – это выражение, которое во время выполнения представлено строкой, а не именем переменной. Фактиче
    ски вызов getattr(X, N) по его действию можно сравнить с выражени

    Множественное наследование
    649
    ем X.__dict__[N], только в первом случае дополнительно выполняется поиск в дереве наследования, как в выражении X.N, а во втором – нет
    (подробнее об атрибуте __dict__ рассказывается в разделе «Словари пространств имен» в главе 24).
    Такой прием, реализованный в этом классеобертке, можно использо
    вать для управления доступом к любому объекту с атрибутами – спи
    скам, словарям и даже к классам и экземплярам. Ниже приводится класс wrapper, который просто выводит сообщение при каждом обраще
    нии к атрибуту и делегирует этот запрос обернутому объекту wrapped:
    >>> from trace import wrapper
    >>> x = wrapper([1,2,3]) # Обернуть список
    >>> x.append(4) # Делегировать операцию методу списка
    Trace: append
    >>> x.wrapped # Вывести обернутый объект
    [1, 2, 3, 4]
    >>> x = wrapper({"a": 1, "b": 2}) # Обернуть словарь
    >>> x.keys() # Делегировать операцию методу словаря
    Trace: keys
    ['a', 'b']
    В результате интерфейс обернутого объекта расширяется за счет мето
    дов классаобертки. Этот подход может использоваться для регистра
    ции вызовов методов, перенаправления вызовов методов дополнитель
    ному или адаптированному программному коду и т. д.
    В главе 26 мы еще вернемся к обернутым объектам и делегированию операций как к одному из способов расширения встроенных типов. Ес
    ли шаблон проектирования с делегированием заинтересовал вас, тогда смотрите обсуждение декораторов функций в главе 26 – это очень близкая концепция, призванная обеспечить расширение отдельных функций и методов, а не всего интерфейса объекта.
    Множественное наследование
    В строке заголовка инструкции class в круглых скобках может быть перечислено более одного суперкласса. В этом случае используется то,
    что называется множественным наследованием, класс и его экземп
    ляры наследуют имена всех перечисленных суперклассов.
    При поиске атрибутов интерпретатор Python выполняет обход супер
    классов, указанных в строке заголовка класса, слева направо, пока не будет найдено первое совпадение. С технической точки зрения поиск сначала продолжается по направлению снизу вверх всеми возможными путями, вплоть до вершины дерева наследования, а затем слева направо,
    потому что любой суперкласс может иметь собственные суперклассы.
    Вообще множественное наследование хорошо использовать для модели
    рования объектов, принадлежащих более чем одной группе. Например,

    650
    Глава 25. Шаблоны проектирования с классами человек может быть инженером, писателем, музыкантом и т. д., и на
    следовать свойства всех этих групп.
    Пожалуй, самый распространенный случай, где используется множе
    ственное наследование, – это «смешивание» методов общего назначе
    ния из нескольких суперклассов. Обычно такие суперклассы называ
    ются классами+смесями – они предоставляют методы, которые добав
    ляются в прикладные классы наследованием. Например, способ выво
    да экземпляра класса, используемый в Python по умолчанию, не отличается информативностью:
    >>> class Spam:
    ... def __init__(self): # Нет метода __repr__
    ... self.data1 = "food"
    >>> X = Spam()
    >>> print X # По умолчанию: класс, адрес
    <__main__.Spam instance at 0x00864818>
    Как было отмечено в предыдущем разделе, где рассматривалась пере
    грузка оператора, существует возможность с помощью метода __repr__
    реализовать свою собственную операцию вывода. Но вместо того чтобы воспроизводить метод __repr__ в каждом классе, который предполагает
    ся выводить на экран, почему бы не написать его всего один раз в классе инструментов общего назначения и не наследовать его во всех ваших классах?
    Для этого и используются классысмеси. В следующем файле my+
    tools.py
    определяется класссмесь с именем Lister, переопределяющий метод __repr__ в каждом классе, который наследует его. Он просто ска
    нирует словарь атрибутов экземпляра (экспортируется в виде атрибу
    та __dict__) и собирает строку из имен и значений всех атрибутов эк
    земпляра. Так как классы – это объекты, логика форматирования класса Lister может использоваться и для экземпляров любых под
    классов – это универсальный инструмент.
    1
    Класс Lister использует два специальных приема, чтобы получить из экземпляра имя класса и адрес. Каждый экземпляр имеет встроенный атрибут __class__, указывающий на класс, из которого он был создан,
    и каждый класс имеет атрибут __name__, указывающий на имя в заго
    ловке, поэтому выражение self.__class__.__name__ возвращает имя класса экземпляра. Получить адрес экземпляра в памяти можно с по
    мощью встроенной функции id, способной возвращать адрес любого объекта (по определению, уникальный идентификатор объекта):
    1
    Альтернативный способ реализации похожего поведения можно найти в примере модуля person.py в конце главы 24. В нем также просматривают
    ся словари пространств имен, но там не предполагается пропуск имен, на
    чинающихся и заканчивающихся двумя символами подчеркивания.

    Множественное наследование
    651
    ############################################################################
    # Lister можно подмешивать в любые классы, чтобы обеспечить форматированный
    # вывод экземпляров через наследование метода __repr__, реализованного здесь;
    # self – это экземпляр самого нижнего класса.
    ############################################################################
    class Lister:
    def __repr__(self):
    return ("" %
    (self.__class__.__name__, # Имя класса
    id(self), # Адрес
    self.attrnames()) ) # Список имя=значение
    def attrnames(self):
    result = ''
    for attr in self.__dict__.keys(): # Словарь имен экземпляра
    if attr[:2] == '__':
    result = result + "\tname %s=\n" % attr else:
    result = result+"\tname %s=%s\n" % (attr, self.__dict__ [attr])
    return result
    Экземпляры, унаследовавшие этот класс, при выводе будут автомати
    чески отображать свои атрибуты, давая о себе больше информации,
    чем просто свой адрес:
    >>> from mytools import Lister
    >>> class Spam(Lister):
    ... def __init__(self):
    ... self.data1 = 'food'
    >>> x = Spam()
    >>> x
    name data1=food
    >
    Класс Lister будет полезен для наследования любыми вашими класса
    ми, у которых уже есть суперкласс. Это тот случай, где удобно исполь
    зовать множественное наследование: добавляя класс Lister в список су
    перклассов в строке заголовка класса (вмешивая его), вы получаете в свое распоряжение его метод __repr__, что не мешает наследовать дру
    гой существующий суперкласс. Это демонстрирует файл testmixin.py:
    from mytools import Lister # Получить класс с инструментами
    class Super:
    def __init__(self): # Метод суперкласса __init__
    self.data1 = "spam"
    class Sub(Super, Lister): # Наследовать __repr__ из классасмеси
    def __init__(self): # Lister имеет доступ к self
    Super.__init__(self)
    self.data2 = "eggs" # Дополнительные атрибуты экземпляра
    self.data3 = 42

    652
    Глава 25. Шаблоны проектирования с классами if __name__ == "__main__":
    X = Sub()
    print X # __repr__ из классасмеси
    Здесь класс Sub наследует имена из двух классов, Super и Lister – этот объект состоит из своих собственных имен и из имен обоих суперклас
    сов. Если создать и вывести экземпляр класса Sub, автоматически будет получено адаптированное представление, подмешанное из класса Lister:
    C:\lp3e> python testmixin.py
    name data3=42
    name data2=eggs name data1=spam
    >
    Метод класса Lister будет работать в любом классе, унаследовавшем его, потому что аргумент self ссылается на экземпляр подкласса, ко
    торый наследует Lister, каким бы этот подкласс ни был. Если позднее вы решите усовершенствовать класс Lister, чтобы метод __repr__ выво
    дил также все атрибуты класса, которые были унаследованы экземп
    ляром, вы без опаски сможете сделать это – так как это наследуемый метод, изменение в реализации Lister.__repr__ автоматически начнет действовать и во всех подклассах, которые импортируют его.
    1
    В некотором смысле, классысмеси – это классы, эквивалентные моду
    лям, потому что они упаковывают методы, которые будут полезны са
    мым разным клиентам. Ниже демонстрируется работа класса Lister в режиме единственного наследования с экземплярами различных классов:
    >>> from mytools import Lister
    >>> class x(Lister):
    ... pass
    >>> t = x()
    1
    Если вам любопытно, как это произойдет, вернитесь назад к разделу «Слова
    ри пространств имен» в главе 24. Там мы видели, что у каждого класса име
    ется свой атрибут __bases__, который является кортежем объектов супер
    классов. Универсальный класс просмотра иерархии может выполнять обход дерева наследования от экземпляра класса __class__ к его классу и затем с по
    мощью атрибута __bases__ ко всем его суперклассам рекурсивно, как это де
    лается в модуле classtree.py, показанном ранее. В версии Python 2.2 и более поздних реализовать это можно еще проще, поскольку теперь встроенная функция dir автоматически включает имена унаследованных атрибутов. Ес
    ли не требуется отображать структуру дерева наследования, то можно просто сканировать список, получаемый от функции dir, вместо просмотра списка ключей словаря и использовать функцию getattr для получения значения атрибута по его имени, заданному в виде строки, вместо обхода словаря. Мы еще вернемся к этой идее в одном из заключительных примеров к этой части.

    Классы – это объекты: фабрики универсальных объектов
    653
    >>> t.a = 1; t.b = 2; t.c = 3
    >>> t
    name b=2
    name a=1
    name c=3
    >
    ООП тесно связано с повторным использованием программного кода,
    и классысмеси в этом отношении представляют собой мощный инст
    румент. Как почти все в программировании, множественное наследо
    вание может быть благом при грамотном применении; но при неакку
    ратном и чрезмерном употреблении эта возможность может осложнить вам жизнь. Мы вернемся к этому вопросу как к одной из типичных проблем в конце следующей главы. В этой главе мы также познако
    мимся с возможностью (в классах нового стиля) изменять порядок по
    иска для одного специального случая множественного наследования.
    Классы – это объекты:
    фабрики универсальных объектов
    Классы – это объекты, поэтому их легко можно передавать между ком
    понентами программы, сохранять в структурах данных и т. д. Можно также передавать классы функциям, которые создают объекты произ
    вольных типов, – в кругах, связанных с ООП, такие функции иногда на
    зывают фабриками. В языках со строгой типизацией, таких как C++,
    реализация таких функций – достаточно сложная задача, но в языке
    Python она становится почти тривиальной. Функция apply и новейшая синтаксическая конструкция, с которыми мы познакомились в гла
    ве 17, могут вызывать любые классы с любым числом аргументов кон
    структоров за один присест, генерируя экземпляр любого типа:
    1
    def factory(aClass, *args): # Кортеж переменного числа аргументов
    return apply(aClass, args) # Вызывайте aClass, или: aClass(*args)
    class Spam:
    def doit(self, message):
    print message class Person:
    def __init__(self, name, job):
    self.name = name self.job = job
    1
    Фактически функция apply может вызывать любой вызываемый объект,
    включая функции, классы и методы. Функция factory в этом примере так
    же может вызывать любые вызываемые объекты, а не только классы (не
    смотря на имя аргумента). Кроме того, обратите внимание: в последних версиях Python синтаксис вызова aClass(*args) вообще предпочтительнее,
    чем вызов встроенной функции apply(aClass, args).

    654
    Глава 25. Шаблоны проектирования с классами object1 = factory(Spam) # Создать объект Spam
    object2 = factory(Person, "Guido", "guru") # Создать объект Person
    В этом фрагменте определена функциягенератор объектов с именем factory
    . Она ожидает получить объект класса (любого) вместе с одним или более аргументов конструктора класса. Она создает экземпляр с помощью функции apply и возвращает его.
    Остальная часть примера просто определяет два класса и генерирует экземпляры этих классов, передавая функции factory. И это единст
    венная фабричная функция, которую вам придется написать на языке
    Python, – она работает с любыми классами и с любыми аргументами конструктора.
    Следует заметить, что здесь возможно одно небольшое улучшение, ко
    торое заключается в обеспечении поддержки именованных аргумен
    тов конструктора; фабричная функция может собрать их в аргумент
    **args и передать функции apply в виде третьего аргумента:
    def factory(aClass, *args, **kwargs): # +kwargs
    return apply(aClass, args, kwargs) # Вызвать aClass
    К настоящему времени вы должны знать, что в языке Python все явля
    ется «объектом», включая и сами классы, которые в других языках,
    таких как С++, являются лишь объявлениями для компилятора. Од
    нако, как упоминалось в начале шестой части книги, в языке Python только объекты, созданные из классов, являются субъектами ООП.
    Зачем нужны фабрики?
    Итак, чем же хороша функция factory (помимо иллюстрации того, что классы являются объектами)? К сожалению, довольно сложно проде
    монстрировать применение этого шаблона проектирования, потому что для этого необходимо привести фрагмент программного кода боль
    ший, чем позволяет пространство книги. Тем не менее, такая фабрика могла бы помочь изолировать программный код от динамически на
    страиваемой конструкции объекта.
    Вспомним пример функции processor, представленный в главе 22, и за
    тем пример применения принципа композиции в этой главе. В обоих случаях принимаются объекты, выполняющие чтение и запись обра
    батываемого потока данных.
    В оригинальной версии этого примера мы вручную передавали экзем
    пляры специализированных классов, таких как FileWriter и Socket
    Reader
    , для адаптации под обрабатываемые потоки данных – позднее мы передавали жестко заданные объекты файла, потока и преобразо
    вания. В других случаях внешние источники данных могут опреде
    ляться настройками в конфигурационных файлах или в элементах управления графического интерфейса.
    В таком динамическом мире не представляется возможным жестко за
    давать в сценарии объекты, реализующие интерфейс к потоку дан

    Методы – это объекты: связанные и несвязанные методы
    655
    ных, но вполне возможно создавать их во время выполнения, в соот
    ветствии с содержимым конфигурационных файлов.
    Например, в файле с настройками может определяться имя класса по
    тока, который должен быть импортирован из модуля, и дополнитель
    ные аргументы конструктора. В этой ситуации могла бы пригодиться фабричная функция или эквивалентный ей фрагмент программного кода, потому что они могли бы позволить нам получить и передать классы, не определяя их заранее в программе. В действительности воз
    можно представить себе, что требуемые классы даже не существовали в тот момент, когда мы писали свой программный код:
    classname = ...определяется конфигурационным файлом...
    classarg = ...определяется конфигурационным файлом...
    import streamtypes # Специализированный программный код
    aclass = getattr(streamtypes, classname) # Извлечь из модуля
    reader = factory(aclass, classarg) # Получить экземпляр aclass(classarg)
    processor(reader, ...)
    Здесь встроенная функция getattr снова используется для извлечения атрибута модуля, имя которого задано в виде строки (это все равно,
    что записать выражение obj.attr, где attr – это строка). Так как этот фрагмент предполагает наличие у конструктора единственного аргу
    мента, то, строго говоря, здесь не требуется ни функция factory, ни функция apply – мы могли бы просто создать экземпляр класса обра
    щением aclass(classarg). Эти функции более полезны в случаях, когда количество аргументов неизвестно заранее, то есть когда универсаль
    ная фабричная функция способна повысить гибкость реализации. За дополнительной информацией по этой теме обращайтесь к книгам, ко
    торые посвящены вопросам ООП и шаблонам проектирования.
    1   ...   75   76   77   78   79   80   81   82   ...   98


    написать администратору сайта