Обращение к элементу по индексу и итерации

Доступ к элементам по индексу и извлечение срезов: __getitem__ и __setitem__

Класс возвращает квадрат значения индекса:

>>> class Indexer:
...     def __getitem__(self, index):
...         return index ** 2
...
>>> X = Indexer()
>>> X[2]                     # Выражение X[i] вызывает X.__getitem__(i)
4
>>> for i in range(5):
...     print(X[i], end=' ') # Вызывает __getitem__(X, i) в каждой итерации
...
0 1 4 9 16

Объект slice

При указании среза передается объект slice.

L = [5, 6, 7, 8, 9]

Срез Объект slice Результат
L[2:4] L[slice(2, 4)] [7, 8]
L[1:] L[slice(1, None)] [6, 7, 8, 9]
L[:-1] L[slice(None, -1)] [5, 6, 7, 8]
L[::2] L[slice(None, None, 2)] [5, 7, 9]
>>> class Indexer(object):
        data = [5, 6, 7, 8, 9]
        def __setitem__(self, index, value):    # Реализует присваивание
...                                             # по индексу или по срезу
            self.data[index] = value            # Приcваивание по индексу или по срезу
...     def __getitem__(self, index):           # Вызывается при индексировании или
...         print('getitem:', index)            # извлечении среза
...         return self.data[index]             # Выполняет индексирование
...                                             # или извлекает срез
>>> X = Indexer()
>>> X[0]                # При индексировании __getitem__ получает целое число
getitem: 0              
5
>>> X[1]
getitem: 1
6
>>> X[-1]
getitem: -1
9
>>> X[2:4]              # При извлечении среза __getitem__ получает объект среза
getitem: slice(2, 4, None)
[7, 8]
>>> X[1:]
getitem: slice(1, None, None)
[6, 7, 8, 9]
>>> X[:-1]
getitem: slice(None, -1, None)
[5, 6, 7, 8]
>>> X[::2]
getitem: slice(None, None, 2)
[5, 7, 9]

До Python 3.0 были отдельные методы \_getslice__, __setslice__. Сейчас вместо них используют __getitem__, __setitem__ (в них добавлена обработка объектов-срезов)_

Преобразование в целое число __index__

Иногда объект могут использовать как индекс. Для работы такого синтаксиса переопределите __index__

>>> class C:
...     def __index__(self):
...         return 255
...
>>> X = C()
>>> hex(X)          # Целочисленное значение
'0xff'
>>> bin(X)
'0b11111111'
>>> oct(X)
'0o377'
>>> ('C' * 256)[255]
'C'
>>> ('C' * 256)[X]  # X используется как индекс (не X[i])
'C'
>>> ('C' * 256)[X:] # X используется как индекс (не X[i:])
'C'

В Python 2.6 были отдельные методы __hex__ и __oct__.

Итерация по индексам __getitem__

Операция for использует операцию индексирования к последовательности, где индексы от 0 и выше, пока не выйдет за границу последовательности (исключение, for его сам обработает). То есть __getitem__ - один из способов реализовать перебор в for.

Что реализовано с использованием __getitem__:

  • for
  • in
  • map (функция)
  • генераторы списков (кортежей)
  • присваивание списов (кортежей)
>>> class stepper:
...     def __getitem__(self, i):
...         return self.data[i]
...
>>> X = stepper()       # X - это экземпляр класса stepper
>>> X.data = 'Spam'
>>>
>>> X[1]                # Индексирование, вызывается __getitem__
'p'
>>> for item in X:      # Циклы for вызывают __getitem__
...     print(item, end=' ')    # Инструкция for индексирует элементы 0..N
...
S p a m
>>> 'p' in X            # Во всех этих случаях вызывается __getitem__
True
>>> [c for c in X]      # Генератор списков
['S', 'p', 'a', 'm']
>>> list(map(str.upper, X)) # Функция map (в версии 3.0
['S', 'P', 'A', 'M']    # требуется использовать функцию list)
>>> (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>

Итераторы __iter__ и __next__

На самом деле, сначала пытается вызваться __iter__, а если его нет, то метод __getitem__

Как перебирается последовательность:

  • вызывается iter(), которая вызывает __iter__() - возвращает объект итератора.
    • вызывается метод next(it) (который вызывает it.__next__()), пока не получим исключение StopIteration.
  • иначе вызывается getitem(), пока не получим исключение IndexError.

Можно определить свои итераторы

Можно определить итерацию по циклу (уже есть в itertools.cycle

from itertools import cycle

li = [0, 1, 2, 3]

running = True
licycle = cycle(li)
while running:
    elem = next(licycle)

Можно возвращать квадраты индексов:

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: # Также вызывается функцией next
            raise StopIteration
        self.value += 1
        return self.value ** 2

использование:

>>> from iters import Squares
>>> for i in Squares(1, 5):         # for вызывает iter(), который вызывает __iter__()
...     print(i, end=' ')           # на каждой итерации вызывается __next__()
...
1 4 9 16 25
>>> X = Squares(1, 5)               # Выполнение итераций вручную: эти действия выполняет
                                    # инструкция цикла
>>> I = iter(X)                     # iter вызовет __iter__
>>> next(I)                         # next вызовет __next__
1
>>> next(I)
4
...часть строк опущена...
>>> next(I)
25
>>> next(I)                         # Исключение можно перехватить с помощью инструкции try
StopIteration

Заметим, что реализация такого поведения через __getitem__ будет сложнее (индексы start..stop придется отображать на 0..stop-strart)

__iter__ предназначена для обхода один раз. __getitem__ - для множественного обращения к элементу.

Квадраты проще реализовать через написание генератора, а не через определение нового класса с итератором.

>>> def gsquares(start, stop):
... for i in range(start, stop+1):
... yield i ** 2
...
>>> for i in gsquares(1, 5): # или: (x ** 2 for x in range(1, 5))
... print(i, end=' ')
...
1 4 9 16 25
>>> [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, end=’ ‘)
...
aa ac ae ca cc ce ea ec ee
  • как внешний, так и внутренний цикл получает свой итератор, вызывая iter()
  • каждый итератор хранит свою информацию о положении в строке (не зависит от других циклов).

  • Однократные проходы:

    • функции-генераторы и выражения-генераторы;
    • map, zip
  • Многократные (независимые) проходы:
    • range
    • list, tuple и тп

Для независимых итераторов __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
    def __iter__(self):
        return SkipIterator(self.wrapped)   # Каждый раз новый итератор

if __name__ == '__main__':
    alpha = 'abcdef'
    skipper = SkipObject(alpha)             # Создать объект-контейнер
    I = iter(skipper)                       # Создать итератор для него
    print(next(I), next(I), next(I))        # Обойти элементы 0, 2, 4
    for x in skipper:                       # for вызывает __iter__ автоматически
        for y in skipper:                   # Вложенные циклы for также вызывают __iter__
            print(x + y, end=' ')           # Каждый итератор помнит свое состояние, смещение

Напечатает:

a c e
aa ac ae ca cc ce ea ec ee

Сравним с кодом, который использует уже существующие инструменты:

>>> S = 'abcdef'
>>> S = S[::2]                              # Новый объект
>>> for x in S:
...     for y in S:
...         print(x + y, end=' ')
...
aa ac ae ca cc ce ea ec ee
  • Код уже написан и отлажен (плюс).
  • Создаются новые объекты (срезы), а не честная итерация в том же объекте (минус).

Где нужно писать такие множественные итераторы? Итерации по выборке из БД.

Проверка на вхождение: __contains__, __iter__, __getitem__

Проверку на вхождение можно организовать через __iter__ или __getitem__, но лучше реализовать специальный метод __contains__.

Посмотрим, что когда вызывается (для этого сделаем класс на с хорошо итерируемыми данными, например, list):

class Iters:
    def __init__(self, value):
        self.data = value
    def __getitem__(self, i):           # Крайний случай для итераций
        print('get[%s]:' % i, end='')   # А также для индексирования и срезов
        return self.data[i]
    def __iter__(self):                 # Предпочтительный для итераций
        print('iter=> ', end='')        # Возможен только 1 активный итератор
        self.ix = 0
        return self
    def __next__(self):
        print('next:', end='')
        if self.ix == len(self.data): 
            raise StopIteration
        item = self.data[self.ix]
        self.ix += 1
        return item
    def __contains__(self, x):          # Предпочтительный для оператора 'in'
        print('contains: ', end='')
        return x in self.data

X = Iters([1, 2, 3, 4, 5])      # Создать экземпляр
print(3 in X)                   # Проверка на вхождение
for i in X:                     # Циклы
    print(i, end=' | ')
print()
print([i ** 2 for i in X])      # Другие итерационные контексты
print( list(map(bin, X)) )
I = iter(X)                     # Обход вручную (именно так действуют
while True:                     # другие итерационные контексты)
    try:
        print(next(I), end=' @ ')
    except StopIteration:
        break

Напечатает:

contains: True
iter=> next:1 | next:2 | next:3 | next:4 | next:5 | next:
iter=> next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=> next:next:next:next:next:next:[‘0b1’, ‘0b10’, ‘0b11’, ‘0b100’, ‘0b101’]
iter=> next:1 @ next:2 @ next:3 @ next:4 @ next:5 @ next:

Теперь закоментируем метод __contains__ и запустим код еще раз:

iter=> next:next:next:True
iter=> next:1 | next:2 | next:3 | next:4 | next:5 | next:
iter=> next:next:next:next:next:next:[1, 4, 9, 16, 25]
iter=> next:next:next:next:next:next:[‘0b1’, ‘0b10’, ‘0b11’, ‘0b100’, ‘0b101’]
iter=> next:1 @ next:2 @ next:3 @ next:4 @ next:5 @ next:

Видим iter=> next:next:next:True, что проверка делается через __iter__.

Если закоментируем и __contains__, и __iter__, то будет вызываться __getitem__:

get[0]:get[1]:get[2]:True
get[0]:1 | get[1]:2 | get[2]:3 | get[3]:4 | get[4]:5 | get[5]:
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:[1, 4, 9, 16, 25]
get[0]:get[1]:get[2]:get[3]:get[4]:get[5]:['0b1', '0b10', '0b11', '0b100','0b101']
get[0]:1 @ get[1]:2 @ get[2]:3 @ get[3]:4 @ get[4]:5 @ get[5]:

results matching ""

    No results matching ""