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

  • Метод Перегружает Вызывается

  • p in X

  • X = Squares(1, 5) >>> X[1]

  • X = Squares(1, 5) >>> [n for n in X]

  • Несколько итераторов в одном объекте

  • S = ace >>> for x in S: ... for y in S: ... print x + y

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


    Скачать 4.86 Mb.
    Название3е издание
    АнкорМатематический анализ
    Дата04.02.2022
    Размер4.86 Mb.
    Формат файлаpdf
    Имя файлаpython_01.pdf
    ТипДокументы
    #351981
    страница74 из 98
    1   ...   70   71   72   73   74   75   76   77   ...   98
    606
    Глава 24. Подробнее о программировании классов
    1. При вызове x.delegate интерпретатор отыскивает метод delegate в классе Super, начиная поиск от экземпляра класса Provider и дви
    гаясь вверх по дереву наследования. Экземпляр x передается мето
    ду в виде аргумента self, как обычно.
    2. Внутри метода Super.delegate выражение self.action приводит к за
    пуску нового, независимого поиска в дереве наследования, начиная от экземпляра self и дальше вверх по дереву. Поскольку аргумент self ссылается на экземпляр класса Provider, метод action будет найден в подклассе Provider.
    Такой способ «восполнения пробелов» в реализации – обычное дело для платформ ООП. По крайней мере, в терминах метода delegate такие суперклассы, как в этом примере, иногда называют абстрактными су+
    перклассами –
    классы, которые предполагают, что часть их функцио
    нальности будет реализована их подклассами. Если ожидаемый метод не определен в подклассе, интерпретатор возбудит исключение с сооб
    щением о неопределенном имени, когда поиск в дереве наследования завершится неудачей. Разработчики классов иногда делают такие тре
    бования к подклассам более очевидными с помощью инструкций assert или возбуждая встроенное исключение NotImplementedError:
    class Super:
    def method(self):
    print 'in Super.method'
    def delegate(self):
    self.action()
    def action(self):
    assert 0, 'action must be defined!'
    Мы познакомимся с инструкцией assert в главе 27, а пока лишь заме
    чу, что если выражение возвращает ложь, она возбуждает исключение с сообщением об ошибке. В данном случае выражение всегда возвра
    щает ложь (0), чтобы вызвать появление об ошибке, если метод не бу
    дет переопределен и поиск по дереву наследования остановится на этой версии. В некоторых классах, напротив, в таких методахзаглуш
    ках исключение NotImplementedError возбуждается напрямую. Инст
    рукцию возбуждения исключений raise мы рассмотрим в главе 27.
    Более реалистичный пример использования концепций, представлен
    ных в этом разделе, вы найдете в упражнении 8 в конце главы 26 и в ре
    шении этого упражнения в разделе «Часть VI, Классы и ООП» (прило
    жение B). Такое частичное наследование является традиционным спо
    собом введения в ООП, но оно постепенно исчезает из арсенала многих разработчиков.
    Перегрузка операторов
    Перегрузку операторов мы коротко рассмотрели в предыдущей главе,
    а здесь мы обсудим все более детально и рассмотрим несколько наиболее

    Перегрузка операторов
    607
    часто используемых методов перегрузки. Ниже приводится краткий обзор ключевых идей, лежащих в основе механизма перегрузки:

    Перегрузка операторов в языке Python позволяет классам участво
    вать в обычных операциях.

    Классы в языке Python могут перегружать все операторы выраже
    ний.

    Классы могут также перегружать такие операции, как вывод, вы
    зов функций, обращение к атрибутам и т. д.

    Перегрузка делает экземпляры классов более похожими на встро
    енные типы.

    Перегрузка заключается в реализации в классах методов со специ
    альными именами.
    Рассмотрим простой пример перегрузки в действии. Если в классе при
    сутствуют определенные методы со специальными именами, интерпре
    татор автоматически будет вызывать их при появлении экземпляров класса в выражениях, выполняющих перегруженные операции. На
    пример, класс Number в следующем файле number.py реализует метод пе
    регрузки операции создания экземпляра (__init__), а также метод реа
    лизации операции вычитания (__sub__). Специальные методы, такие как эти, позволяют перехватывать и выполнять встроенные операции:
    class Number:
    def __init__(self, start): # Вызов Number(start)
    self.data = start def __sub__(self, other): # Выражение: экземпляр – другой
    return Number(self.data  other) # Результат – новый экземпляр
    >>> from number import Number # Извлечь класс из модуля
    >>> X = Number(5) # Number.__init__(X, 5)
    >>> Y = X 2 # Number.__sub__(X, 2)
    >>> Y.data # Y – новый экземпляр класса Number
    3
    Как уже обсуждалось ранее, конструктор __init__, присутствующий в этом примере, – это наиболее часто используемый метод перегрузки оператора в языке Python, потому что он присутствует в большинстве классов. В этом разделе мы изучим некоторые другие инструменты,
    связанные с перегрузкой, и рассмотрим наиболее типичные примеры их использования.
    Общие методы перегрузки операторов
    Почти все, что можно делать с объектами встроенных типов, такими как целые числа и списки, можно реализовать и в классах – с помощью специальных методов перегрузки операторов. В табл. 24.1 перечисле
    ны наиболее часто используемые, но на самом деле их намного больше.
    На деле многие методы перегрузки существуют в нескольких версиях
    (например, __add__, __radd__ и __iadd__ для сложения). Исчерпывающий

    608
    Глава 24. Подробнее о программировании классов список имен специальных методов вы найдете в других книгах, посвя
    щенных языку Python, и в справочных руководствах.
    Таблица 24.1. Общие методы перегрузки операторов
    Все методы перегрузки имеют имена, начинающиеся и заканчиваю
    щиеся двумя символами подчеркивания, что отличает их от других имен, которые вы обычно определяете в своих классах. Отображение операторов выражений или операций на методы со специальными именами предопределяется языком Python (и описывается в стандарт
    ном руководстве по языку). Например, по определению языка опера
    тор + всегда отображается на имя __add__ независимо от того, что в дей
    ствительности делает метод __add__.
    Все методы перегрузки операторов являются необязательными – если какойто метод не реализован, это лишь означает, что соответствующая ему операция не поддерживается классом (а при попытке применить такую операцию возбуждается исключение). Большинство методов пе
    регрузки операторов используются только при решении специальных
    Метод
    Перегружает
    Вызывается
    __init__
    Конструктор
    При создании объекта: X = Class()
    __del__
    Деструктор
    При уничтожении объекта
    __add__
    Оператор +
    X + Y
    , X += Y
    __or__
    Оператор | (побитовое ИЛИ) X | Y, X |= Y
    __repr__
    ,
    __str__
    Вывод, преобразование print X
    , repr(X), str(X)
    __call__
    Вызовы функций
    X()
    __getattr__
    Обращение к атрибуту
    X.undefined
    __setattr__
    Присваивание атрибутам
    X.any = value
    __getitem__
    Доступ к элементу по ин
    дексу
    X[key]
    , циклы for и другие конст
    рукции итерации, при отсутствии метода __iter__
    __setitem__
    Присваивание элементу по индексу
    X[key] = value
    __len__
    Длина len(X)
    , проверка истинности
    __cmp__
    Сравнение
    X == Y
    , X < Y
    __lt__
    Специальное сравнение
    X < Y
    (в ином случае __cmp__)
    __eq__
    Специальное сравнение
    X == Y
    (в ином случае __cmp__)
    __radd__
    Правосторонний оператор + Не_экземпляр + X
    __iadd__
    Добавление (увеличение)
    X += Y
    (в ином случае __add__)
    __iter__
    Итерационный контекст
    Циклы for, оператор in, генерато
    ры списков, map и другие

    Перегрузка операторов
    609
    задач, когда необходимо, чтобы объекты имитировали поведение встро
    енных типов, однако конструктор __init__ присутствует в большинст
    ве классов. Мы уже познакомились с конструктором __init__, который вызывается на этапе инициализации, и с несколькими другими, пере
    численными в табл. 24.1. Теперь мы исследуем примеры использова
    ния некоторых других методов из таблицы.
    _
    _getitem_
    _ реализует доступ к элементам по индексу
    Метод __getitem__ реализует операции доступа к элементам по индек
    су. Когда экземпляр X появляется в выражении извлечения элемента по индексу, таком как X[i], интерпретатор Python вызывает метод
    __getitem__
    , наследуемый этим экземпляром (если он имеется), переда
    вая методу объект X в первом аргументе и индекс в квадратных скоб
    ках во втором аргументе. Например, следующий класс возвращает квадрат значения индекса:
    >>> class indexer:
    ... def __getitem__(self, index):
    ... return index ** 2
    >>> X = indexer()
    >>> X[2] # Выражение X[i] вызывает __getitem__(X, i).4
    >>> for i in range(5):
    ... print X[i],
    0 1 4 9 16
    _
    _getitem_
    _ и _
    _iter_
    _ реализуют итерации
    Здесь описывается прием, который не всегда очевиден для начинаю
    щих программистов, но на практике может оказаться необычайно по
    лезным. Инструкция for многократно применяет операцию индекси
    рования к последовательности, используя индексы от нуля и выше,
    пока не будет получено исключение выхода за границы. Благодаря этому метод __getitem__ представляет собой один из способов перегруз
    ки итераций в языке Python – если этот метод реализован, инструкции циклов for будут вызывать его на каждом шаге цикла, с постоянно увеличивающимся значением смещения. Это один из случаев, когда
    «купив один предмет, другой получаешь в подарок», – любой встроен
    ный или определяемый пользователем объект, к которому применима операция индексирования, также может участвовать в итерациях:
    >>> class stepper:
    ... def __getitem__(self, i):
    ... return self.data[i]
    >>> X = stepper() # X – это экземпляр класса stepper
    >>> X.data = "Spam"
    >>>

    610
    Глава 24. Подробнее о программировании классов
    >>> X[1] # Индексирование, вызывается __getitem__
    'p'
    >>> for item in X: # циклы for вызывают __getitem__
    ... print item, # for индексирует элементы 0..N
    S p a m
    Фактически это случай, когда «купив один предмет, в подарок полу
    чаешь целую связку». Любой класс, поддерживающий циклы for, ав
    томатически поддерживает все итерационные контексты, имеющиеся в языке Python, многие из которых мы видели в более ранних главах
    (другие итерационные контексты описываются в главе 13). Напри
    мер, оператор проверки на принадлежность in, генераторы списков,
    встроенная функция map, присваивание списков и кортежей и конст
    рукторы типов также автоматически вызывают метод __getitem__, ес
    ли он определен:
    >>> 'p' in X # Во всех этих случаях вызывается __getitem__
    1
    >>> [c for c in X] # Генератор списков
    ['S', 'p', 'a', 'm']
    >>> map(None, X) # Функция map
    ['S', 'p', 'a', 'm']
    >>> (a, b, c, d) = X # Присваивание последовательностей
    >>> a, c, d
    ('S', 'a', 'm')
    >>> list(X), tuple(X), ''.join(X)
    (['S', 'p', 'a', 'm'], ('S', 'p', 'a', 'm'), 'Spam')
    >>> X
    <__main__.stepper instance at 0x00A8D5D0>
    На практике этот прием может использоваться для создания объектов,
    которые реализуют интерфейс последовательностей, и для добавления логики к операциям над встроенными типами – мы рассмотрим эту идею, когда будем расширять встроенные типы в главе 26.
    Итераторы, определяемые пользователями
    В настоящее время все итерационные контексты в языке Python пыта
    ются сначала использовать метод __iter__ и только потом – метод
    __getitem__
    . То есть при выполнении обхода элементов объекта пред
    почтение отдается итерационному протоколу, с которым мы познако
    мились в главе 13, – если итерационный протокол не поддерживается объектом, вместо него используется операция индексирования.
    С технической точки зрения итерационные контексты вызывают встроенную функцию iter, чтобы определить наличие метода __iter__,
    который должен возвращать объект итератора. Если он предоставля

    Перегрузка операторов
    611
    ется, то интерпретатор Python будет вызывать метод next объекта ите
    ратора для получения элементов до тех пор, пока не будет возбуждено исключение StopIteration. Если метод __iter__ отсутствует, интерпре
    татор переходит на использование схемы с использованием метода
    __getitem__
    и начинает извлекать элементы по индексам, пока не будет возбуждено исключение IndexError.
    В новой схеме классы реализуют итераторы простой реализацией ите
    рационного протокола, представленного в главах 13 и 17 (за дополни
    тельной информацией об итераторах возвращайтесь к этим главам).
    Например, в следующем файле iters.py определяется класс итератора,
    который возвращает квадраты чисел:
    class Squares:
    def __init__(self, start, stop): # Сохранить состояние при создании
    self.value = start  1
    self.stop = stop def __iter__(self): # Возвращает итератор в iter()
    return self def next(self): # Возвращает квадрат в каждой итерации
    if self.value == self.stop:
    raise StopIteration self.value += 1
    return self.value ** 2
    % python
    >>> from iters import Squares
    >>> for i in Squares(1, 5): # for вызывает iter(),
    который вызывает __iter__()
    ... print i, # на каждой итерации вызывается next()
    1 4 9 16 25
    Здесь объект итератора – это просто экземпляр self, поэтому метод next является частью этого класса. В более сложных ситуациях объект итератора может быть определен как отдельный класс и объект со сво
    ей собственной информацией о состоянии, с целью поддержки не
    скольких активных итераций на одних и тех же данных (совсем скоро мы рассмотрим это на примере). Об окончании итераций интерпрета
    тору сообщается с помощью инструкции raise (подробнее о возбужде
    нии исключений рассказывается в следующей части книги).
    Эквивалентная реализация с использованием __getitem__ может ока
    заться менее естественной, потому что цикл for явно выполняет пере
    бор всех смещений от нуля и выше; смещения, передаваемые методу,
    могут оказаться связаны с диапазоном воспроизводимых значений лишь косвенно (диапазон 0..N может потребоваться отображать на диапазон start..stop). Поскольку объекты, возвращаемые методом
    __iter__
    , явно манипулируют информацией о своем состоянии и сохра
    няют ее между вызовами метода next, такая реализация может быть более универсальной, чем использование метода __getitem__.

    612
    Глава 24. Подробнее о программировании классов
    C другой стороны, итераторы, реализованные на основе метода
    __iter__
    , иногда могут оказаться более сложными и менее удобными,
    чем метод __getitem__. Но они действительно предназначены для ите
    раций, а не для случайной индексации – фактически, они вообще не перегружают операцию индексирования:
    >>> X = Squares(1, 5)
    >>> X[1]
    AttributeError: Squares instance has no attribute '__getitem__'
    (AttributeError: экземпляр Squares не имеет атрибута '__getitem__')
    Схема на основе метода __iter__ реализована также во всех остальных итерационных контекстах, к которыс применим метод __getitem__
    (проверка на вхождение, конструкторы, присваивание последователь
    ностей и т. д.). Однако, в отличие от __getitem__, схема на основе мето
    да __iter__ предназначена для выполнения обхода элементов один раз,
    а не несколько. Например, элементы класса Squares можно обойти все
    го один раз – для каждой последующей итерации необходимо будет создавать новый объект итератора:
    >>> X = Squares(1, 5)
    >>> [n for n in X] # Получить все элементы
    [1, 4, 9, 16, 25]
    >>> [n for n in X] # Теперь объект пуст
    []
    >>> [n for n in Squares(1, 5)] # Создать новый объект итератора
    [1, 4, 9, 16, 25]
    >>> list(Squares(1, 3))
    [1, 4, 9]
    Примечательно, что этот пример можно было бы реализовать проще,
    применив функциигенераторы (которые имеют отношение к итерато
    рам и были представлены в главе 17):
    >>> from __future__ import generators # Требуется только в Python 2.2
    >>>
    >>> def gsquares(start, stop):
    ... for i in range(start, stop+1):
    ... yield i ** 2
    >>> for i in gsquares(1, 5):
    ... print i,
    1 4 9 16 25
    В отличие от класса, функция автоматически сохраняет информацию о своем состоянии между итерациями. Конечно, для реализации тако
    го искусственного примера можно было бы вообще не использовать ни один из этих приемов, а просто использовать цикл for, функцию map или генератор списков, чтобы создать сразу весь список. Нередко са
    мый лучший и самый быстрый способ в языке Python оказывается еще и самым простым:

    Перегрузка операторов
    613
    >>> [x ** 2 for x in range(1, 6)]
    [1, 4, 9, 16, 25]
    Однако реализация на базе классов может оказаться лучше при моде
    лировании более сложных итераций, особенно когда возможность со
    хранения информации о состоянии и наследование могут принести су
    щественную выгоду. Один из таких случаев исследуется в следующем разделе.
    Несколько итераторов в одном объекте
    Ранее я упоминал, что объект итератора может быть определен как от
    дельный класс, со своей собственной информацией о состоянии, что обеспечивает поддержку протекания нескольких итерационных про
    цессов с одним и тем же набором данных. Посмотрим, что происходит при выполнении обхода элементов встроенных типов, таких как строка:
    >>> S = 'ace'
    >>> for x in S:
    ... for y in S:
    ... print x + y,
    aa ac ae ca cc ce ea ec ee
    Здесь внешний цикл получает итератор строки вызовом функции iter,
    и каждый вложенный цикл делает то же самое, чтобы получить незави
    симый итератор. Так как каждый итератор хранит свою собственную информацию о состоянии, каждый цикл управляет своим собственным положением в строке, независимо от любых других активных циклов.
    Чтобы добиться того же эффекта в итераторах, определяемых пользо
    вателем, метод __iter__ должен не просто возвращать аргумент self,
    а создавать новый объект итератора со своей информацией о состоянии.
    Например, в следующем примере определяется класс итератора, кото
    рый пропускает каждый второй элемент. Поскольку объект итератора создается заново для каждой итерации, он обеспечивает поддержку нескольких активных циклов одновременно:
    class SkipIterator:
    def __init__(self, wrapped):
    self.wrapped = wrapped # Информация о состоянии
    self.offset = 0
    def next(self):
    if self.offset >= len(self.wrapped): # Завершить итерации
    raise StopIteration else:
    item = self.wrapped[self.offset] # иначе перешагнуть и вернуть
    self.offset += 2
    return item class SkipObject:
    def __init__(self, wrapped): # Сохранить элемент,
    self.wrapped = wrapped # который будет использоваться

    1   ...   70   71   72   73   74   75   76   77   ...   98


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