Щоб зрозуміти, що yield
робить, ви повинні зрозуміти, що таке генератори . І перш ніж ви зможете зрозуміти генератори, ви повинні зрозуміти ітерабелі .
Ітерабелі
Створюючи список, ви можете читати його елементи по одному. Читання предметів один за одним називається ітерацією:
>>> mylist = [1, 2, 3]
>>> for i in mylist:
... print(i)
1
2
3
mylist
є ітерабельним . Коли ви використовуєте розуміння списку, ви створюєте список і такий ітерабельний:
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
... print(i)
0
1
4
Все, на чому можна використовувати " for... in...
" - це ітерабельне; lists
, strings
, Файли ...
Ці ітерабелі зручні, тому що ви можете їх читати скільки завгодно, але ви зберігаєте всі значення в пам’яті, і це не завжди те, що ви хочете, коли маєте багато значень.
Генератори
Генератори - це ітератори, які ви можете повторити лише один раз . Генератори не зберігають усі значення в пам'яті, вони генерують цілі на льоту :
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
... print(i)
0
1
4
Це те саме, що ви використовували ()
замість цього []
. Але НЕ ви можете виконати for i in mygenerator
вдруге, оскільки генератори можуть бути використані лише один раз: вони обчислюють 0, потім забудьте про це і обчисліть 1, і закінчіть обчислення 4, по одному.
Вихід
yield
- це ключове слово, яке використовується як return
, за винятком того, що функція поверне генератор.
>>> def createGenerator():
... mylist = range(3)
... for i in mylist:
... yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
Ось це марний приклад, але це зручно, коли ви знаєте, що ваша функція поверне величезний набір значень, які вам потрібно буде прочитати лише один раз.
Щоб засвоїти yield
, ви повинні зрозуміти, що при виклику функції код, написаний у тілі функції, не працює. Функція повертає лише об'єкт генератора, це трохи хитро :-)
Потім ваш код буде продовжуватися з того місця, де він припиняється кожного разу, коли for
використовує генератор.
Тепер важка частина:
Перший раз, коли for
викликає об’єкт-генератор, створений з вашої функції, він запускатиме код у вашій функції з самого початку, поки він не потрапить yield
, тоді він поверне перше значення циклу. Потім кожен наступний виклик запустить ще одну ітерацію циклу, який ви записали у функцію, і поверне наступне значення. Це триватиме до тих пір, поки генератор не вважатиметься порожнім, що відбувається, коли функція працює без удару yield
. Це може бути тому, що цикл закінчився, або тому, що ви більше не задовольняєте "if/else"
.
Ваш код пояснив
Генератор:
# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):
# Here is the code that will be called each time you use the generator object:
# If there is still a child of the node object on its left
# AND if the distance is ok, return the next child
if self._leftchild and distance - max_dist < self._median:
yield self._leftchild
# If there is still a child of the node object on its right
# AND if the distance is ok, return the next child
if self._rightchild and distance + max_dist >= self._median:
yield self._rightchild
# If the function arrives here, the generator will be considered empty
# there is no more than two values: the left and the right children
Абонент:
# Create an empty list and a list with the current object reference
result, candidates = list(), [self]
# Loop on candidates (they contain only one element at the beginning)
while candidates:
# Get the last candidate and remove it from the list
node = candidates.pop()
# Get the distance between obj and the candidate
distance = node._get_dist(obj)
# If distance is ok, then you can fill the result
if distance <= max_dist and distance >= min_dist:
result.extend(node._values)
# Add the children of the candidate in the candidate's list
# so the loop will keep running until it will have looked
# at all the children of the children of the children, etc. of the candidate
candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result
Цей код містить декілька розумних частин:
Цикл повторюється в списку, але список розширюється під час ітерації циклу :-) Це стислий спосіб пройти всі ці вкладені дані, навіть якщо це трохи небезпечно, оскільки ви можете закінчитись нескінченним циклом. У цьому випадку candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
вичерпайте всі значення генератора, але while
продовжуйте створювати нові об'єкти генератора, які даватимуть різні значення від попередніх, оскільки вони не застосовуються на одному вузлі.
extend()
Метод є методом об'єкта списку , який очікує , що ітератор і додає його значення в список.
Зазвичай ми передаємо йому список:
>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]
Але у вашому коді він отримує генератор, що добре, тому що:
- Значення не потрібно читати двічі.
- У вас може бути багато дітей, і ви не хочете, щоб вони все зберігалися в пам'яті.
І це працює, тому що Python байдуже, аргумент методу є списком чи ні. Python очікує ітерабелів, тому він буде працювати з рядками, списками, кортежами та генераторами! Це називається качиним типом і є однією з причин, чому Python настільки крутий. Але це вже інша історія, для іншого питання ...
Ви можете зупинитися тут або почитати трохи, щоб побачити вдосконалене використання генератора:
Контроль виснаження генератора
>>> class Bank(): # Let's create a bank, building ATMs
... crisis = False
... def create_atm(self):
... while not self.crisis:
... yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
... print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...
Примітка. Для Python 3 використовуйте print(corner_street_atm.__next__())
абоprint(next(corner_street_atm))
Це може бути корисно для різних речей, таких як контроль доступу до ресурсу.
Itertools, твій найкращий друг
Модуль itertools містить спеціальні функції для управління ітерабелями. Ви хочете дублювати генератор? Ланцюг двох генераторів? Згрупуйте значення у вкладеному списку за допомогою одного вкладиша? Map / Zip
без створення іншого списку?
Тоді просто import itertools
.
Приклад? Давайте подивимось на можливі замовлення прибуття для чотирьох коней:
>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
(1, 2, 4, 3),
(1, 3, 2, 4),
(1, 3, 4, 2),
(1, 4, 2, 3),
(1, 4, 3, 2),
(2, 1, 3, 4),
(2, 1, 4, 3),
(2, 3, 1, 4),
(2, 3, 4, 1),
(2, 4, 1, 3),
(2, 4, 3, 1),
(3, 1, 2, 4),
(3, 1, 4, 2),
(3, 2, 1, 4),
(3, 2, 4, 1),
(3, 4, 1, 2),
(3, 4, 2, 1),
(4, 1, 2, 3),
(4, 1, 3, 2),
(4, 2, 1, 3),
(4, 2, 3, 1),
(4, 3, 1, 2),
(4, 3, 2, 1)]
Розуміння внутрішніх механізмів ітерації
Ітерація - це процес, що передбачає ітерабелі (реалізація __iter__()
методу) та ітератори (реалізація __next__()
методу). Інтерабелі - це будь-які об'єкти, з яких можна отримати ітератор. Ітератори - це об'єкти, які дозволяють повторити ітерабелі.
Про це докладніше в цій статті про те, як for
працюють петлі .
yield
не така магічна ця відповідь. Коли ви зателефонуєте до функції, яка міститьyield
оператор де завгодно, ви отримаєте об’єкт генератора, але не запускається код. Потім кожен раз, коли ви виймаєте об'єкт з генератора, Python виконує код у функції, поки не надійде доyield
оператора, а потім призупиняє і доставляє об'єкт. Коли ви виймаєте інший об'єкт, Python поновлюється відразу післяyield
та продовжує, поки не досягне іншогоyield
(часто того самого, але пізніше ітерації). Це продовжується до тих пір, поки функція не закінчиться, і в цей момент генератор визнається вичерпаним.