У цій частині циклу статей про синтаксичний цукор Python ми розглянемо оператор for. Ми поринемо у C-код CPython, але розуміння або навіть читання цих частин допису не обов'язкове, щоб зрозуміти, як це все працює.
Байт-код
Почнімо з простого оператора for
:
for a in b:
c
Приклад з оператором for
Передача коду через модуль dis дає нам:
2 0 LOAD_GLOBAL 0 (b)
2 GET_ITER
>> 4 FOR_ITER 8 (to 14)
6 STORE_FAST 0 (a)
3 8 LOAD_GLOBAL 1 (c)
10 POP_TOP
12 JUMP_ABSOLUTE 4
>> 14 LOAD_CONST 0 (None)
16 RETURN_VALUE
Розбір for a in b: c
Якщо ви подивитесь на вивід, то можете помітити, що інструкції opcode GET_ITER
та FOR_ITER
здаються специфічними для оператора for
, тоді як всі інші інструкції opcode є загальними.
Вивчаючи GET_ITER у нескінченному циклі
, здається, що здебільшого він викликає PyObject_GetIter()
. Зазвичай (але не завжди) це еквівалентно вбудованій функції iter()
. Ми також знаємо, що, мабуть, це буде щось спільне з next()
, тож надамо визначення обох функцій далі в цьому дописі.
Якщо говорити про next()
, opcode FOR_ITER
— це семантично виклик __next__()
для типу об'єкта (як представлено (*iter->ob_type->tp_iternext)(iter)
у коді C). Це означає, що для розуміння роботи операторів потрібно розуміти і те, як працює iter()
і next()
.
Вбудовані
Перш ніж ознайомлюватися з роботою iter()
і next()
, ми повинні розібрати два важливі терміни. Ітерований (iterable)
— це контейнер, який може повертати вміщені елементи по одному. Ітератор — це об'єкт, який передає ці елементи. Отже, хоча кожен ітератор є концептуально ітерованим (і ви завжди повинні робити свої ітератори також ітерованими; це не дуже багато роботи), зворотне не буде істинним і не всі ітеровані є ітераторами самі по собі.
iter()
Ми зупинилися на визначенні цих слів, адже завдання iter()
— взяти ітерований об'єкт і повернути його ітератор. Тепер те, що iter()
вважає ітерованим об'єктом залежить від того, чи передаєте ви йому один або два аргументи.
iter(iterable)
Починаючи з семантики функції, коли є один аргумент, ми бачимо, що реалізація викликає PyObject_GetIter()
, щоб отримати ітератор. Псевдокод для цієї функції, який ми зараз пояснимо, такий:
def iter(iterable):
\ttype_ = type(iterable)
if hasattr(type_, "__iter__"): # type_->tp_iter != NULL
iterator = type_.__iter__(iterable)
if hasattr(type(iterator), "__next__"): # PyIter_Check
return iterator
else:
raise TypeError
elif hasattr(type_, "__getitem__"): # PySequence_Check
return SeqIter(iterable) # PySeqIter_New
else:
raise TypeError
Псевдокод iter(iterable)
Перший крок — пошук спеціального методу __iter__()
для ітерованого. Виклик цього призначений для повернення ітератора (який явно перевіряється на повернене значення). На рівні С визначення «ітератор» є об'єктом, який визначає __next__()
; протоколи як ітерованого
, так і ітератора
також можна перевірити за допомогою їхніх необхідних класів collections.abc
та issubclass()
(цього не зроблено так у псевдокоді просто тому, що перевірка hasattr()
ближча до того, як пишеться код С; у коді Python, мабуть, краще застосувати абстрактні базові класи).
Є примітка щодо __iter__()
, яка повідомляє, що якщо для атрибуту класу встановлено значення None
, він не буде застосовуватися (хоча авторові цих рядків не траплялося жодного явного коду, що виконує цю перевірку). Схоже, що така поведінка неявно підтримується завдяки тому, як це імплементовано, тобто None
не викликається і не має відповідних методів, які перевіряються.
Другий крок — що ж відбувається, якщо __iter__()
недоступний? У цьому разі перевіряється, чи маємо ми справу з послідовністю, шукаючи спеціальний метод __getitem__()
. Якщо об'єкт виявляється послідовністю, повертається екземпляр PySeqIter_Type
, чиєю наближеною реалізацією Python буде:
def _seq_iter(seq):
"""Yield the items of the sequence starting at 0."""
index = 0
while True:
try:
yield seq[index]
index += 1
except (IndexError, StopIteration):
return
Реалізація PySeqIterType
«Наближеною», оскільки версія CPython підтримує pickling, і це спрощує життя.😁
Якщо все зазначене раніше не вдається виконати, тоді виникає TypeError
.
iter(callable, sentinel)
Якщо ви запам'ятали раніше, ми зазначали, що iter()
мав двоаргументну версію. У цьому разі iter()
відрізняється від того, що ми вже обговорювали:
def iter(callable, sentinel):
if not hasattr(callable, "__call__"): # PyCallable_Check
raise TypeError
return CallIter(callable, sentinel) # PyCallIter_Type
Псевдокод для iter(callable, sentinel)
Як бачите, є перевірка щодо можливості викликати перший аргумент, і якщо так, тоді повертається екземпляр ітератора PyCallIter_Type
. Оскільки функція настільки коротка, важливим питанням є те, що робить ітератор PyCallIter_Type
?
Ця форма iter()
застосовується, коли у вас є викликаний об'єкт без аргументу, який ви постійно викликаєте, доки не буде повернуто певне значення. Це означатиме, що більше нічого не надходить від викликаного.
def _call_iter(callable, sentinel):
while True:
val = callable()
if val == sentinel:
return
else:
yield val
Приклад того, де це може бути корисним, якщо ви читаєте файл частинами, зупиняючись, коли повертається b""
:
with open(path, "rb") as file:
for chunk in iter(lambda: file.read(chunk_size), b""):
...
Якщо ми об'єднаємо все разом і зробимо це правильно, виразом iter()
буде:
def iter(obj, /, sentinel=_NOTHING,):
"""Return an iterator for the object.
If 'sentinel' is unspecified, the first argument must either be an iterable
or a sequence. If the argument is a sequence, an iterator will be returned
which will index into the argument starting at 0 and continue until
IndexError or StopIteration is raised.
With 'sentinel' specified, the first argument is expected to be a callable
which takes no arguments. The returned iterator will execute the callable on
each iteration until an object equal to 'sentinel' is returned.
"""
# Python/bltinmodule.c:builtin_iter
obj_type = builtins.type(obj)
if sentinel is _NOTHING:
# Python/abstract.c:PyObject_GetIter
try:
__iter__ = _mro_getattr(obj_type, "__iter__")
except AttributeError:
try:
_mro_getattr(obj_type, "__getitem__")
except AttributeError:
raise TypeError(f"{obj_type.__name__!r} is not iterable")
else:
return _seq_iter(obj)
else:
iterator = __iter__(obj)
# Python/abstract.c:PyIter_Check
iterator_type = builtins.type(iterator)
try:
_mro_getattr(iterator_type, "__next__")
except AttributeError:
raise TypeError(
f"{obj_type.__name__!r}.__iter__() returned a non-iterator of type {builtins.type(__iter__)!r}"
)
else:
return __iter__(obj)
else:
# Python/object.c:PyCallable_Check
try:
_mro_getattr(obj_type, "__call__")
except AttributeError:
raise TypeError(f"{obj_type.__name__!r} must be callable")
else:
return _call_iter(obj, sentinel)
Реалізація для iter()
next()
Щоб отримати наступне значення з ітератора, ми передаємо його вбудованій функції next()
. Вона приймає ітератор і додатково може приймати типове значення. Якщо ітератор має значення для повернення, воно повертається. Якщо всі ітерації виконано, тоді або виконується StopIteration
, або повертається типове значення, якщо воно було надане.
def next(iterator, /, default=_NOTHING):
"""Return the next value from the iterator by calling __next__().
If a 'default' argument is provided, it is returned if StopIteration is
raised by the iterator.
"""
# Python/bltinmodule.c:builtin_next
iterator_type = builtins.type(iterator)
try: # Python/abstract.c:PyIter_Check
__next__ = _mro_getattr(iterator_type, "__next__")
except AttributeError:
raise TypeError(f"{iterator_type.__name__!r} is not an iterator")
else:
try:
val = __next__(iterator)
except StopIteration:
if default is _NOTHING:
raise
else:
return default
else:
return val
Реалізація для next()
Семантика
Кожне значення, що повертається ітератором ітерованого об'єкта, присвоюється цілі циклу (іноді це називають варіантом циклу). Оператори у тілі циклу запускаються і це повторюється, поки не виконаються всі ітерації. Якщо є умова else
для циклу for
, тоді він виконується, якщо не настає умова спрацювання оператора break
. Переривання роботи циклу за допомогою break дає нам змогу:
- Отримати ітератор ітерованого.
- Присвоїти значення
next
ітератора цілі циклу. - Виконати умову тіла циклу.
- Повторювати виконання умови, доки не виконаються всі ітерації.
- Якщо є умова
else
, а умова спрацювання оператораbreak
не настає, виконувати умовуelse
.
Схоже на цикл while, чи не так?
- Отримати ітератор за допомогою
iter()
. - Викликати
next()
та визначати результат цілі циклу. - Поки виклик next не спричиняє спрацювання
StopIteration
, виконувати умову тіла циклу. - Повторювати, доки необхідно.
- Запустити умову
else
за необхідності.
for
без умови else
Почнемо з простішого випадку без умови else
. У цьому разі ми можемо перенести:
for a in b:
c
Приклад циклу for
до:
_iter = iter(b)
while True:
try:
a = next(_iter)
except StopIteration:
break
else:
c
del _iter
Приклад циклу for
, розгорнутий
Якщо подивитись уважно, то це такий собі приклад циклу for. Це, безумовно, значно більше писанини, та було б головним болем писати це все щоразу, коли потрібно виконати ітерації, але цикл працюватиме.
for
з умовою else
Ви могли зауважити, що ми застосовували оператор break
у нашому розв'язанні вище, через що умова else
циклу while
завжди пропускається. Як зробити, щоб умова else спрацьовувала?
Спочатку оновімо наш приклад:
for a in b:
c
else:
d
Приклад циклу for
з умовою else
Нам потрібно виключити застосування break
в оригінальному розв'язанні. Отже, ми маємо придумати інший спосіб позначати, коли закінчуються елементи для ітерації. Для цього скористаємось змінною, яка просто відстежує, чи залишилися ще значення, — цього має бути достатньо.
_iter = iter(b)
_looping = True
while _looping:
try:
a = next(_iter)
except StopIteration:
_looping = False
continue
else:
c
else:
d
del _iter, _looping
Таке розв'язання має досить зручний побічний ефект. Ми можемо покладатись на власну умову else
у циклі while
та його семантику, щоб отримати те, що ми шукаємо для циклу for
!
І це все! Ми можемо застосувати цикл while
для впровадження циклів for
без помітних семантичних змін (крім тимчасових змінних).
Ще немає коментарів