__TOC__
Занурення Ітератори це "таємний соус" мови Pythоn 3. Вони всюди, в основі всього, просто зазвичай невидимі. Вирази над структурами — лише проста форма ітераторів. Генератори — лише проста форма ітераторів. Функції що використовують yield це лише гарний, компактний спосіб побудови ітератора без побудови ітератора. Давайте я покажу що я під цим маю на увазі.
Пам'ятаєте генератор чисел Фібоначчі? Ось побудований з нуля ітератор:
class Fib: '''iterator that yields numbers in the Fibonacci sequence'''
def __init__(self, max): self.max = max
def __iter__(self): self.a = 0 self.b = 1 return self
def __next__(self): fib = self.a if fib > self.max: raise StopIteration self.a, self.b = self.b, self.a + self.b return fib
Щож давайте розбирати по рядку за раз.
class Fib:
class? Що таке клас?
Опис класів Python повністю об'єктно-орієнтований: ви можете описувати власні класи, наслідуватись від власних чи вбудованих класів, і створювати екземпляри описаних класів.
Описувати класи в Python просто. Як і з функціями, немає окремого опису інтерфейсу. Просто оголосіть клас і починайте кодити. Клас в Python починається з зарезервованого слова class, за яким іде ім'я класу. Технічно, це все що потрібно, тому що клас не обов'язково повинен наслідуватись від іншого класу.
class PapayaWhip:
Цей клас називається PapayaWhipКолір що утворюється при змішуванні пюре папаї з ванільним морозивом чи йогуртом. Або світло-оранжевий українською. :), і не наслідується від інших класів. Імена класів зазвичай пишуться з великої букви, подібно до КожнеСловоЯкУЦьомуОтут, але це лише домовленість, а не вимога.
pass
Ви можливо вже й самі здогадались, але все в класі пишеться з відступом, так само як і код всередині функцій, умовних операторів, циклів, чи інших блоків коду. Перший рядок без відступа вважається рядком за межами класу.
Наш клас PapayaWhip не описує жодних методів чи атрибутів, але синтаксично необхідно помістити що-небудь в опис, тому ми використали ключове слово pass. Це слово означає "проходьте, тут нема на що дивитись". Це команда яка нічого не робить, і її зручно підставляти коли потрібно зробити заготовку функції.
Багато класів наслідуються від інших класів, але даний ні. Багато класів описують методи, але цей ні. Взагалі немає нічого що клас в Python обов'язково повинен мати, окрім імені. Наприклад програмісти C++ можуть здивуватись що класи в Python не мають явних конструкторів та деструкторів. Щоправда хоча це й не обов'язково, класи в Python можуть мати щось подібне до конструктора: метод __init__().
Метод __init__() Цей приклад показує ініціалізацію класу Fib з використанням методу __init__.
class Fib: '''iterator that yields numbers in the Fibonacci sequence'''
Класи можуть (і повинні б) мати документацію, так само як модулі та функції.
def __init__(self, max):
Метод __init__() викликається негайно після того як створюється екземпляр класу. Спокусливо називати цей метод "конструктором" класу, але це технічно невірно. Це спокусливо тому що він виглядає як конструктор в C++ (за домовленістю, метод __init__() повинен бути першим в списку методів класу), поводиться як конструктор (це перший код що виконується в новоствореному екземплярі класу), і навіть звучить як конструктор. Але це не вірно, тому що об'єкт було створено ще до того як було викликано метод __init__(), і в нас вже є діюче посилання на новостворений екземпляр класу.
Перший аргумент будь-якого методу класу, включаючи метод __init__(), це завжди посилання на поточний екземпляр класу, за домовленістю його називають self. Цей аргумент замінює роль зарезервованого слова this в C++ чи Java, але self не є зарезервованим словом в Python, просто домовленістю з іменування. Тим не менш, будь-ласка не називайте його якось інакше ніж self, це дуже сильна домовленість.
У всіх методах класу, self посилається на екземпляр метод якого викликали. Але в особливому випадку метода __init__() екземпляр чий метод був викликаним також є новоствореним об'єктом. І хоча при описі метода необхідно описувати параметр self явно, його не потрібно передавати при виклику метода, Python передасть його для нас автоматично.
Створення екземплярів класу Створення екземплярів класу в Python досить прямолінійне. Щоб створити екземпляр класу, просто викличте клас так наче б він був функцією, передавши як аргументи все що необхідно функції __init__(). Значення яке буде повернутим і буде новоствореним об'єктом.
>>> import fibonacci2 >>> fib = fibonacci2.Fib(100)
Ми створюємо екземпляр класу Fib (описаного в модулі fibonacci2) і присвоюємо новостворений екземпляр змінній fib. Ми передаємо один параметр, 100, який буде переданий як аргумент max в метод __init__().
>>> fib <fibonacci2.Fib object at 0x00DB8810>
fib тепер екземпляр класу Fib.
>>> fib.__class__ <class 'fibonacci2.Fib'>
Кожен екземпляр класу має вбудований атрибут __class__, який посилається на клас об'єкта. Програмісти Java можливо знайомі з класом Class, який містить методи на зразок getName() та getSuperclass() щоб отримувати метаінформацію про об'єкт. В Python такі дані доступні через атрибути, але ідея загалом така сама.
>>> fib.__doc__ 'iterator that yields numbers in the Fibonacci sequence'
Можна звертатись до документації екземпляра так само як і з функціями чи модулями. Всі екземпляри класу діляться спільним рядком документації.
Змінні екземпляра І перейдемо до наступного рядка класу:
class Fib: def __init__(self, max): self.max = max
Що таке self.max? Це змінна екземпляра. Це зовсім інша змінна ніж max, яка була передана як аргумент в метод __init__(). self.max "глобальна" для екземпляра. Це означає що доступ до неї можна отримати з інших методів.
class Fib: def __init__(self, max): self.max = max
self.max створюється в методі __init__()...
. . . def __next__(self): fib = self.a if fib > self.max:
... і використовується з метода __next__().
Змінні екземпляра пов'язані лише з одним екземпляром класу. Наприклад якщо ми створимо два екземпляри класу Fib з різними максимальними значеннями, вони пам'ятатимуть кожен про свою змінну.
>>> import fibonacci2 >>> fib1 = fibonacci2.Fib(100) >>> fib2 = fibonacci2.Fib(200) >>> fib1.max 100 >>> fib2.max 200
Ітератор Фібоначчі
Тепер ви готові до того щоб навчитись створювати ітератор. Ітератор це просто клас який описує метод __iter__().
class Fib:
Щоб створити ітератор з нуля, Fib повинен бути класом а не функцією.
def __init__(self, max): self.max = max
"Виклик" Fib(max) це насправді створення екземпляру цього класу і виклик його метода __init__() з аргументом max. Метод __init__() зберігає максимальне значення як змінну екземпляру щоб інші методи могли використати її пізніше.
def __iter__(self): self.a = 0 self.b = 1 return self
Метод __iter__() викликається щоразу як хтось викликає iter(fib). (Як ви побачите незабаром, цикл for може викликати це автоматично, але ви також можете зробити це вручну.) Піcля виконання ініціалізації перед початком ітерації (в даному випадку встановлення значень лічильників self.a та self.b), метод __iter__() може повернути будь-який об'єкт що реалізує метод __next__(). В цьому (та й у більшості інших) випадку, __iter__() просто повертає self, так як цей клас реалізує власний метод __next__().
def __next__(self): fib = self.a
Метод __next__() викликається щоразу як хтось викликає next() на ітераторі чи екземплярі класу. Це стане більш зрозумілим за хвилину.
if fib > self.max: raise StopIteration
Коли метод __next__() генерує виняток StopIteration це сигналізує тому хто його викликав про те що ітерація вичерпалась. На відміну від більшості інших винятків, це не помилка. це нормальна ситуація яка просто означає що ітератор більше не має значень для генерації. Якщо тим хто викликав next() є цикл for, він просто завершить ітерації і передасть потік виконання далі. (Іншими словами він просто проковтне виняток.) Цей маленький шматочок магії насправді є ключем до використання ітераторів в циклах for.
self.a, self.b = self.b, self.a + self.b return fib
Щоб згенерувати наступне значення, метод ітератора __next__() просто повертає його за допомогою return. Він не використовує yield тому що це просто синтаксичний цукор який використовується лише в генераторах. Метод __next__ - метод ітератора а не генератор.
Вже добряче заплутались? Прекрасно. Давайте поглянемо як викликати цей ітератор:
>>> from fibonacci2 import Fib >>> for n in Fib(1000): ... print(n, end=' ') 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
Але ж цей виклик точно такий самий! Байт в байт ідентичний з тим як ми викликали генератор чисел Фібоначчі (за винятком того що тепер він називається з великої букви). Але як?
З циклами for пов'язано трішки магії. Ось що відбувається:
Цикл for викликає Fib(1000), як написано. Це дає екземпляр класу Fib. Будемо називати його fib_inst. Таємно, і дуже хитро, цикл for викликає iter(fib_inst), який повертає об'єкт ітератора. Давайте будемо називати його fib_iter. В даному випадку, fib_iter == fib_inst, тому що метод __iter__() повертає self, але цикл for про це не знає (чи взагалі не цікавиться). Щоб "проітеруватись" ітератором, цикл for викликає next(fib_iter), який викликає метод __next__() на об'єкті fib_iter який робить обчислення наступного числа Фібоначчі і повертає значення. Цикл for бере це значення і присвоює його n, після чого виконує тіло циклу з даним значенням n. Звідки цикл for знає коли зупинитись? Радий що ви спитали! Коли next(fib_iter) кидає виняток StopIteration, цикл for ковтає виняток і завершується. (Будь-який інший звичайний виняток прорветься і буде оброблятись як зазвичай.) А де ми бачили виняток StopIteration? Звичайно в методі __next__()!
Ітератор правил утворення множини
Тепер час для завершення. Давайте перепишемо генератор правил утворення множини як ітератор.
class LazyRules: rules_filename = 'plural6-rules.txt'
def __init__(self): self.pattern_file = open(self.rules_filename, encoding='utf-8') self.cache = []
def __iter__(self): self.cache_index = 0 return self
def __next__(self): self.cache_index += 1 if len(self.cache) >= self.cache_index: return self.cache[self.cache_index - 1]
if self.pattern_file.closed: raise StopIteration
line = self.pattern_file.readline() if not line: self.pattern_file.close() raise StopIteration
pattern, search, replace = line.split(None, 3) funcs = build_match_and_apply_functions( pattern, search, replace) self.cache.append(funcs) return funcs
rules = LazyRules()
Цей клас реалізує методи __iter__() та __next__(), тому він може використовуватись як ітератор. Потім ми створюємо його екземпляр та відкриваємо для нього файл з правилами. Це відбувається лише раз при імпорті.
Давайте спробуємо розібратись у всьому по кусочку за раз.
class LazyRules: rules_filename = 'plural6-rules.txt'
def __init__(self): self.pattern_file = open(self.rules_filename, encoding='utf-8')
Коли ми створюємо екземпляр класу LazyRules відкривається файл з шаблонами, але з нього нічого не читається (це буде потім).
self.cache = []
Після відкриття файла з шаблонами, ми ініціалізуємо кеш. Ми використаємо його пізніше (в методі __next__()) протягом того як будемо читати рядки з файла з шаблонами.
Перед тим як продовжити давайте поближче глянемо на змінну rules_filename. Вона не описується всередині метода __iter__(). Насправді вона не описується всередині будь-якого метода. Вона описується на рівні класу. Це змінна класу, і хоча доступ до неї подібний на доступ до змінних екземпляра (через self.rules_filename), але вона спільна для всіх екземплярів класу LazyRules.
>>> import plural6 >>> r1 = plural6.LazyRules() >>> r2 = plural6.LazyRules() >>> r1.rules_filename ① 'plural6-rules.txt' >>> r2.rules_filename 'plural6-rules.txt'
Кожен екземпляр класу успадковує атрибут rules_filename зі значенням описаним всередині класу.
>>> r2.rules_filename = 'r2-override.txt' >>> r2.rules_filename 'r2-override.txt' >>> r1.rules_filename 'plural6-rules.txt'