Какая разница между __iter__ и __getitem__?

Это происходит в Python 2.7.6 и 3.3.3 для меня. Когда я определяю класс, подобный этому

class foo:
    def __getitem__(self, *args):
        print(*args)

И затем попробуйте выполнить итерацию (и то, что, как я думал, вызовет iter) в экземпляре,

bar = foo()
for i in bar:
    print(i)

он просто подсчитывает один для аргументов и печатает None навсегда. Является ли это преднамеренным в плане дизайна языка?

Пример вывода

0
None
1
None
2
None
3
None
4
None
5
None
6
None
7
None
8
None
9
None
10
None

Ответ 1

Да, это намеченный дизайн. Он документально подтвержден, проверен и основан на типах последовательностей, таких как str.

Версия __getitem__ является наследием до того, как у Python были современные итераторы. Идея заключалась в том, что любая последовательность (то есть индексируемая и имеющая длину) будет автоматически итерабельной, используя серию s [0], s [1], s [2],... до тех пор, пока не будет вызвана IndexError или StopIteration.

В Python 2.7, например, строки являются итерабельными из-за метода __getitem__ (тип str не имеет метода __iter__).

Напротив, протокол итератора позволяет любому классу быть итерабельным без необходимости индексирования (например, dicts и sets).

Вот как сделать итерируемый класс, используя устаревший стиль для последовательностей:

>>> class A:
        def __getitem__(self, index):
            if index >= 10:
                raise IndexError
            return index * 111

>>> list(A())
[0, 111, 222, 333, 444, 555, 666, 777, 888, 999]

Вот как сделать итерацию с помощью подхода __iter__:

>>> class B:
        def __iter__(self):
            yield 10
            yield 20
            yield 30


>>> list(B())
[10, 20, 30]

Для тех, кто интересуется деталями, соответствующий код находится в Object/iterobject.c:

static PyObject *
iter_iternext(PyObject *iterator)
{
    seqiterobject *it;
    PyObject *seq;
    PyObject *result;

    assert(PySeqIter_Check(iterator));
    it = (seqiterobject *)iterator;
    seq = it->it_seq;
    if (seq == NULL)
        return NULL;

    result = PySequence_GetItem(seq, it->it_index);
    if (result != NULL) {
        it->it_index++;
        return result;
    }
    if (PyErr_ExceptionMatches(PyExc_IndexError) ||
        PyErr_ExceptionMatches(PyExc_StopIteration))
    {
        PyErr_Clear();
        Py_DECREF(seq);
        it->it_seq = NULL;
    }
    return NULL;
}

и в Object/abstract.c:

int
PySequence_Check(PyObject *s)
{
    if (s == NULL)
        return 0;
    if (PyInstance_Check(s))
        return PyObject_HasAttrString(s, "__getitem__");
    if (PyDict_Check(s))
        return 0;
    return  s->ob_type->tp_as_sequence &&
        s->ob_type->tp_as_sequence->sq_item != NULL;
}

Ответ 2

__iter__ является предпочтительным способом повторения итерации объекта. Если он не определен, интерпретатор попытается смоделировать его поведение с помощью __getitem__. Взгляните здесь

Ответ 3

Чтобы получить ожидаемый результат, вам нужно иметь элемент данных с ограниченной длиной и возвращать каждый в последовательности:

class foo:
    def __init__(self):
        self.data=[10,11,12]

    def __getitem__(self, arg):
        print('__getitem__ called with arg {}'.format(arg))
        return self.data[arg]

bar = foo()
for i in bar:
    print('__getitem__ returned {}'.format(i)) 

Печать

__getitem__ called with arg 0
__getitem__ returned 10
__getitem__ called with arg 1
__getitem__ returned 11
__getitem__ called with arg 2
__getitem__ returned 12
__getitem__ called with arg 3

Или вы можете сигнализировать о конце "последовательности", поднимая IndexError (хотя StopIteration работает также...):

class foo:
    def __getitem__(self, arg):
        print('__getitem__ called with arg {}'.format(arg))
        if arg>3:
            raise IndexError
        else:    
            return arg

bar = foo()
for i in bar:
    print('__getitem__ returned {}'.format(i))   

Печать

__getitem__ called with arg 0
__getitem__ returned 0
__getitem__ called with arg 1
__getitem__ returned 1
__getitem__ called with arg 2
__getitem__ returned 2
__getitem__ called with arg 3
__getitem__ returned 3
__getitem__ called with arg 4

Цикл for ожидает либо IndexError, либо StopIteration, чтобы сигнализировать о конце последовательности.