Я починаю вивчати Python, і я натрапив на функції генератора, ті, які мають в них заяву про вихід. Хочу знати, які типи проблем, які ці функції справді добре вирішують.
Я починаю вивчати Python, і я натрапив на функції генератора, ті, які мають в них заяву про вихід. Хочу знати, які типи проблем, які ці функції справді добре вирішують.
Відповіді:
Генератори дають вам ледачу оцінку. Ви використовуєте їх, повторюючи їх, або явно з 'за', або неявно, передаючи його будь-якій функції або конструкції, яка повторюється. Ви можете вважати генераторів поверненням декількох елементів, як ніби вони повертають список, але замість того, щоб повернути їх відразу, вони повертають їх один за одним, і функція генератора призупиняється, поки не буде запропоновано наступний елемент.
Генератори корисні для обчислення великих наборів результатів (зокрема, обчислень із залученням самих циклів), де ви не знаєте, чи потрібні вам всі результати, або де ви не хочете одночасно виділяти пам'ять для всіх результатів. . Або для ситуацій, коли генератор використовує інший генератор або споживає якийсь інший ресурс, і зручніше, якщо це сталося якомога пізніше.
Ще одне використання для генераторів (це дійсно те саме) - це заміна зворотних викликів ітерацією. У деяких ситуаціях ви хочете, щоб функція багато працювала і періодично звітувала перед тим, хто телефонує. Традиційно для цього використовується функція зворотного дзвінка. Ви передаєте цей зворотний виклик робочій функції, і він періодично буде викликати цей зворотний виклик. Підхід генератора полягає в тому, що робоча функція (зараз це генератор) нічого не знає про зворотний виклик і просто дає результат, коли хоче щось повідомити. Користувач, замість того, щоб записати окремий зворотний дзвінок і передати його робочій функції, виконує всі звітні роботи в циклі навколо "генератора".
Наприклад, скажіть, що ви написали програму "Пошук файлової системи". Ви можете виконати пошук у повному обсязі, зібрати результати, а потім відобразити їх по одному. Усі результати повинні бути зібрані до того, як ви показали перший, і всі результати були б одночасно в пам’яті. Або ви можете відображати результати, коли ви їх знайдете, що було б більш ефективною пам’яттю та значно дружнішою для користувача. Останнє можна зробити, передавши функцію друку результатів функції функції пошуку файлових систем, або це можна зробити, просто зробивши функцію пошуку генератором і ітерацією над результатом.
Якщо ви хочете побачити приклад двох останніх підходів, див. Os.path.walk () (стара функція ходіння файлової системи із зворотним викликом) та os.walk () (новий генератор ходіння файлової системи.) Звичайно, якщо ви дуже хотіли зібрати всі результати у списку, підхід генератора тривіальний, щоб перетворитись на підхід у великому списку:
big_list = list(the_generator)
yield
та join
після них, щоб отримати наступний результат, він не виконується паралельно (і це не робить жоден стандартний генератор бібліотек; таємно запускаються потоки нахмурені). Генератор робить паузу при кожному, yield
поки не буде запропоновано наступне значення. Якщо генератор завершує введення / виведення, ОС може проактивно кешувати дані з файлу за умови, що він буде запитаний незабаром, але це ОС, Python не задіяний.
Однією з причин використання генератора є зробити рішення більш зрозумілим для деяких рішень.
Інша - обробляти результати поодинці, уникаючи складання величезних списків результатів, які ви б так чи інакше обробляли.
Якщо у вас є така функція підвищення рівня n:
# function version
def fibon(n):
a = b = 1
result = []
for i in xrange(n):
result.append(a)
a, b = b, a + b
return result
Ви можете легше записати функцію так:
# generator version
def fibon(n):
a = b = 1
for i in xrange(n):
yield a
a, b = b, a + b
Функція чіткіша. І якщо ви використовуєте цю функцію:
for x in fibon(1000000):
print x,
у цьому прикладі, якщо використовується версія генератора, весь список елементів 1000000 взагалі не буде створений, лише одне значення за один раз. Це не було б при використанні списку версії, де список буде створений спочатку.
list(fibon(5))
Дивіться розділ "Мотивація" в PEP 255 .
Неочевидне використання генераторів - це створення переривних функцій, що дозволяє робити такі речі, як оновлення інтерфейсу користувача або виконувати декілька завдань "одночасно" (перемежовуватися фактично), не використовуючи потоки.
Я вважаю це пояснення, яке знімає сумніви. Тому що існує можливість, про яку не знає Generators
і людина, яка не знаєyield
Повернення
Повернення оператора - це те, коли всі локальні змінні знищуються, а отримане значення повертається (повертається) абоненту. Якщо таку ж функцію буде викликано через деякий час, функція отримає новий свіжий набір змінних.
Вихід
Але що робити, якщо локальні змінні не викидаються, коли ми виходимо з функції? Це означає, що ми можемо resume the function
там, де зупинилися. Саме generators
тут вводиться концепція , і yield
заява поновлюється там, де function
зупинено.
def generate_integers(N):
for i in xrange(N):
yield i
In [1]: gen = generate_integers(3)
In [2]: gen
<generator object at 0x8117f90>
In [3]: gen.next()
0
In [4]: gen.next()
1
In [5]: gen.next()
Отже, в цьому різниця між return
і yield
твердженнями в Python.
Оперативність виходу - це те, що робить функцію функцією генератора.
Тож генератори - простий і потужний інструмент для створення ітераторів. Вони записуються як звичайні функції, але вони використовують yield
оператор, коли хочуть повернути дані. Кожен раз, коли викликається next (), генератор поновлюється там, де він зупинився (він запам'ятовує всі значення даних та те, що оператор востаннє виконувався).
Скажімо, у вашій таблиці MySQL є 100 мільйонів доменів, і ви хочете оновити рейтинг Alexa для кожного домену.
Перше, що вам потрібно - це вибрати доменні імена з бази даних.
Скажімо, назва вашої таблиці - це ім'я domains
та стовпець domain
.
Якщо ви використовуєте, SELECT domain FROM domains
це поверне 100 мільйонів рядків, що забирає багато пам'яті. Так ваш сервер може вийти з ладу.
Тож ви вирішили запускати програму партіями. Скажімо, наш розмір партії - 1000.
У нашій першій партії ми запитаємо перші 1000 рядків, перевіримо рейтинг Alexa для кожного домену та оновимо рядок бази даних.
У нашій другій партії ми будемо працювати над наступними 1000 рядками. У нашій третій партії це буде з 2001 по 3000 і так далі.
Тепер нам потрібна функція генератора, яка генерує наші партії.
Ось наша функція генератора:
def ResultGenerator(cursor, batchsize=1000):
while True:
results = cursor.fetchmany(batchsize)
if not results:
break
for result in results:
yield result
Як бачите, наша функція зберігає yield
результати. Якщо ви використовували ключове слово return
замість yield
, тоді вся функція буде закінчена, як тільки вона досягне повернення.
return - returns only once
yield - returns multiple times
Якщо функція використовує ключове слово, yield
то це генератор.
Тепер ви можете повторити так:
db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
doSomethingWith(result)
db.close()
Буферизація. Коли дані можуть отримувати дані великими фрагментами, але обробляти їх невеликими шматками, то генератор може допомогти:
def bufferedFetch():
while True:
buffer = getBigChunkOfData()
# insert some code to break on 'end of data'
for i in buffer:
yield i
Наведене вище дозволяє легко відокремити буферизацію від обробки. Функція споживача тепер може просто отримувати значення по одному, не турбуючись про буферизацію.
Я виявив, що генератори дуже корисні для очищення вашого коду і надають вам унікальний спосіб інкапсуляції та модуляції коду. У ситуації , коли вам потрібно що - то постійно випльовувати цінності на основі своєї власної внутрішньої обробки , і коли що - то має бути викликана з будь-якого місця в коді (а не тільки в межах циклу або блоку, наприклад), генератори особливість в використання.
Абстрактним прикладом може бути генератор чисел Фібоначчі, який не живе в циклі, і коли він викликається з будь-якого місця, завжди повертає наступне число у послідовності:
def fib():
first = 0
second = 1
yield first
yield second
while 1:
next = first + second
yield next
first = second
second = next
fibgen1 = fib()
fibgen2 = fib()
Тепер у вас є два об'єкти генератора чисел Фібоначчі, на які можна зателефонувати з будь-якого місця вашого коду, і вони завжди повертатимуть все більші числа Фібоначчі в послідовності наступним чином:
>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5
Чудова річ у генераторах полягає в тому, що вони інкапсулюють стан, не проходячи обручі створення об'єктів. Один із способів мислення про них - це "функції", які запам'ятовують їх внутрішній стан.
Я отримав приклад Фібоначчі від генераторів Python - Що вони? і, трохи уявивши, ви можете придумати безліч інших ситуацій, коли генератори створюють чудову альтернативу for
петлям та іншим традиційним ітераційним конструкціям.
Просте пояснення: Розгляньте for
твердження
for item in iterable:
do_stuff()
Багато часу, всі предмети в них iterable
не повинні бути там з самого початку, але можуть бути створені на льоту, як потрібно. Це може бути набагато ефективніше в обох
В іншому випадку ви навіть не знаєте всіх предметів достроково. Наприклад:
for command in user_input():
do_stuff_with(command)
Ви не можете заздалегідь знати всі команди користувача, але ви можете використовувати приємний цикл, як цей, якщо у вас генератор передає команди:
def user_input():
while True:
wait_for_command()
cmd = get_command()
yield cmd
З генераторами ви також можете мати ітерацію над нескінченними послідовностями, що, звичайно, неможливо при ітерації над контейнерами.
Мої улюблені використання - це операції "фільтрація" та "зменшення".
Скажімо, ми читаємо файл і хочемо лише рядки, які починаються з "##".
def filter2sharps( aSequence ):
for l in aSequence:
if l.startswith("##"):
yield l
Потім ми можемо використовувати функцію генератора у відповідному циклі
source= file( ... )
for line in filter2sharps( source.readlines() ):
print line
source.close()
Приклад зменшення подібний. Скажімо, у нас є файл, де нам потрібно знаходити блоки <Location>...</Location>
рядків. [Не теги HTML, а рядки, схожі на теги.]
def reduceLocation( aSequence ):
keep= False
block= None
for line in aSequence:
if line.startswith("</Location"):
block.append( line )
yield block
block= None
keep= False
elif line.startsWith("<Location"):
block= [ line ]
keep= True
elif keep:
block.append( line )
else:
pass
if block is not None:
yield block # A partial block, icky
Знову ж таки, ми можемо використовувати цей генератор у відповідному циклі.
source = file( ... )
for b in reduceLocation( source.readlines() ):
print b
source.close()
Ідея полягає в тому, що функція генератора дозволяє нам фільтрувати або зменшувати послідовність, виробляючи іншу послідовність по одному значенню.
fileobj.readlines()
буде читати весь файл до списку в пам'яті, перемагаючи мету використання генераторів. Оскільки файлові об’єкти вже ітерабельні, ви можете використовувати for b in your_generator(fileobject):
замість цього. Таким чином ваш файл буде читатися один рядок, щоб уникнути читання цілого файлу.
Практичний приклад, де ви могли б скористатися генератором, це якщо у вас є якась форма і ви хочете перебрати його кути, краї та будь-яку іншу. Для мого власного проекту (вихідний код тут ) у мене був прямокутник:
class Rect():
def __init__(self, x, y, width, height):
self.l_top = (x, y)
self.r_top = (x+width, y)
self.r_bot = (x+width, y+height)
self.l_bot = (x, y+height)
def __iter__(self):
yield self.l_top
yield self.r_top
yield self.r_bot
yield self.l_bot
Тепер я можу створити прямокутник і петлю за його кутами:
myrect=Rect(50, 50, 100, 100)
for corner in myrect:
print(corner)
Замість цього __iter__
ви можете мати метод iter_corners
і викликати це за допомогою for corner in myrect.iter_corners()
. Це просто більш елегантно використовувати __iter__
з того часу, ми можемо використовувати ім'я екземпляра класу безпосередньо у for
виразі.
Деякі хороші відповіді тут, однак я також рекомендую повністю прочитати навчальний посібник з функціональним програмуванням Python, який допомагає пояснити деякі найпотужніші випадки використання генераторів.
Оскільки спосіб відправлення генератора не згадувався, ось приклад:
def test():
for i in xrange(5):
val = yield
print(val)
t = test()
# Proceed to 'yield' statement
next(t)
# Send value to yield
t.send(1)
t.send('2')
t.send([3])
Він показує можливість відправити значення працюючому генератору. Більш просунутий курс на генераторів у відео нижче (в тому числі yield
від експлікації, генераторів для паралельної обробки, виходу з межі рекурсії тощо)
Купи речей. Кожен раз, коли ви хочете генерувати послідовність елементів, але не хочуть їх "матеріалізувати" відразу в список. Наприклад, у вас може бути простий генератор, який повертає прості числа:
def primes():
primes_found = set()
primes_found.add(2)
yield 2
for i in itertools.count(1):
candidate = i * 2 + 1
if not all(candidate % prime for prime in primes_found):
primes_found.add(candidate)
yield candidate
Потім ви можете використовувати це для створення продуктів наступних праймерів:
def prime_products():
primeiter = primes()
prev = primeiter.next()
for prime in primeiter:
yield prime * prev
prev = prime
Це досить тривіальні приклади, але ви можете бачити, як це може бути корисно для обробки великих (потенційно нескінченних!) Наборів даних, не створюючи їх заздалегідь, що є лише одним із найбільш очевидних застосувань.
Також добре для друку простих чисел до n:
def genprime(n=10):
for num in range(3, n+1):
for factor in range(2, num):
if num%factor == 0:
break
else:
yield(num)
for prime_num in genprime(100):
print(prime_num)