Fluent Python: coroutine
Сопрограммы (coroutine) на основе генератора.
Следующий раздел Fluent Python - сопрограммы.
В книге Fluent Python используется Python 3.4. В версиях 3.5 и 3.6 были внесены изменения связанные с сопрограммами. В этой заметке вся информация основывается на разделе coroutines в книге Fluent Python.
Сопрограмма
Сопрограмма - часть программы, которая взаимодействует с вызывающем ее кодом, генерируя и получая данные.
Более полное определение в википедии
С точки зрения синтаксиса, сопрограмма выглядит как генератор.
Однако, в сопрограмме, yield как правило находится с правой стороны выражения: command = yield
.
При этом генерация значения опциональна:
- Если после yield находится выражение, генератор отдает его
- иначе, отдается значение None
Сопрограмма может не только генерировать данные, но и принимать их. Для этого используется метод send.
Базовый пример сопрограммы. Передача данных сопрограмме
Пример сопрограммы:
In [1]: def basic_coroutine1(start):
...: print('Start value:', start)
...: first = yield
...: print('First received:', first)
...: second = yield
...: print('Second received:', second)
...:
Вызываем функцию, чтобы получить генератор:
In [2]: bc1 = basic_coroutine1(100)
In [3]: bc1
Out[3]: <generator object basic_coroutine1 at 0xb598abfc>
Теперь, чтобы получить возможность отправлять данные сопрограмме, надо ее инициировать. Это делается вызовом next:
In [4]: next(bc1)
Start value: 100
Сопрограмма выполнила весь код до первого yield и остановилась. Теперь у нас есть возможность отправить данные, используя метод send:
In [5]: bc1.send(200)
First received: 200
Сопрограмма получила данные, присвоила их в переменную first, вывела сообщение “First received: 200” и остановилась на следующем yield.
Теперь можно снова отправить данные:
In [6]: bc1.send(300)
Second received: 300
---------------------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-5-62309180db70> in <module>()
----> 1 bc1.send(300)
StopIteration:
Тут аналогично: сопрограмма получила данные, присвоила их в переменную second, вывела сообщение “Second received: 300”. Но, так как на этом функция-генератор закончилась, возвращается исключение StopIteration.
Вернемся к загадочной строке next(bc1)
.
Эта инициация необходима, чтобы отправлять данные сопрограмме.
Если снова вызвать функцию и сразу попытаться отправить данные, возникнет исключение:
In [8]: bc1 = basic_coroutine1(100)
In [9]: bc1.send(200)
------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-9-11b40b565caa> in <module>()
----> 1 bc1.send(200)
TypeError: can't send non-None value to a just-started generator
По описанию исключения понятно, что инициировать сопрограмму можно и по-другому, отправив send(None)
:
In [10]: bc1 = basic_coroutine1(100)
In [11]: bc1.send(None)
Start value: 100
Базовый пример сопрограммы. Оператор return
В сопрограмме можно использовать оператор return для завершения работы генератора и возврата данных.
Например:
In [50]: def basic_coroutine2():
...: collection = []
...: while True:
...: item = yield
...: if item is None:
...: return collection
...: collection.append(item)
...:
Инициация генератора и отправка данных выполняется аналогично:
In [51]: bc2 = basic_coroutine2()
In [52]: next(bc2)
In [53]: bc2.send(100)
In [54]: bc2.send(200)
При отправке None, сопрограмма завершает работу. В этом случае, по-прежнему генерируется исключение StopIteration, но, кроме этого, данные возвращаются как атрибут исключения:
In [55]: bc2.send(None)
------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-55-ef77f9d8836c> in <module>()
----> 1 bc2.send(None)
StopIteration: [100, 200]
Для получения данных в переменную, надо получить значение атрибута value:
In [56]: bc2 = basic_coroutine2()
In [57]: next(bc2)
In [58]: bc2.send(100)
In [59]: bc2.send(200)
In [60]: bc2.send(300)
In [61]: try:
...: bc2.send(None)
...: except StopIteration as e:
...: result = e.value
...:
In [62]: result
Out[62]: [100, 200, 300]
Базовый пример сопрограммы. Получение данных с yield
В этом примере yield не просто приостанавливает выполнение сопрограммы, а еще и возвращает данные:
In [68]: def basic_coroutine3(items):
...: collection = [i for i in items]
...: while True:
...: item = yield collection
...: collection.append(item)
...:
Еще одно небольшое изменение - сопрограмма создана с параметром items. Это значит, что ей можно передавать аргументы:
In [70]: bc3 = basic_coroutine3([1,2,3])
In [71]: next(bc3)
Out[71]: [1, 2, 3]
После инициации сопрограммы, ей можно передавать данные. Теперь после каждой передачи данных, возвращается содержимое списка collection:
In [72]: bc3.send(100)
Out[72]: [1, 2, 3, 100]
In [73]: bc3.send(200)
Out[73]: [1, 2, 3, 100, 200]
Раз содержимое возвращается, его можно присвоить в переменную:
In [74]: result = bc3.send(300)
In [75]: result
Out[75]: [1, 2, 3, 100, 200, 300]
В этой сопрограмме нет условия для завершения цикла while. Это значит, что она будет принимать данные и возвращать результат до тех пор, пока сопрограмма используется. Но у сопрограммы есть метод close, который позволяет в любой момент завершить ее:
In [76]: bc3.close()
Теперь, при обращении к сопрограмме, будет возвращаться исключение StopIteration:
In [77]: bc3.send(400)
------------------------------------------------------------
StopIteration Traceback (most recent call last)
<ipython-input-77-1d704bd03fe8> in <module>()
----> 1 bc3.send(400)
StopIteration:
Хоть я уже и не сетевой инженер, а никуда не денешься от того, что при изучении новых тем, в голове рождаются примеры использования для сетей :)
Пример использования сопрограммы с netmiko
С помощью сопрограммы можно создать соединение SSH с устройством, которое ожидает команды. А, после получения команды, возвращает результат.
Первый вариант сопрограммы:
In [1]: def send_show_command(device_params):
...: print('Opening connection to IP: {}'.format(device_params['ip']))
...: conn = netmiko.ConnectHandler(**device_params)
...: conn.enable()
...: result = None
...: while True:
...: command = yield result
...: result = conn.send_command(command)
...:
Для подключения по SSH с помощью netmiko, надо создать словарь с параметрами подключения:
In [2]: import netmiko
In [3]: r1 = {'device_type': 'cisco_ios',
...: 'ip': '192.168.100.1',
...: 'username': 'cisco',
...: 'password': 'cisco',
...: 'secret': 'cisco' }
...:
Теперь можно вызывать сопрограмму и инициировать ее:
In [5]: ssh = send_show_command(r1)
In [6]: next(ssh)
Opening connection to IP: 192.168.100.1
При инициации сопрограммы, выполняется весь код до yield - выводится сообщение, выполняется подключение к устройству и netmiko переходит в режим enable. После этого, управление останавливается на yield.
Теперь, если передать сопрограмме команду, она запишет ее в переменную command, выполнит ее с помощью метода send_command и, так как цикл пошел на следующую итерацию, вернет результат выполнения команды и остановится:
In [7]: ssh.send('sh ip arp')
Out[7]: 'Protocol Address Age (min) Hardware Addr Type Interface\nInternet 19.1.1.1 - aabb.cc00.6520 ARPA Ethernet0/2\nInternet 192.168.100.1 - aabb.cc00.6500 ARPA Ethernet0/0\nInternet 192.168.100.100 37 aabb.cc80.c900 ARPA Ethernet0/0\nInternet 192.168.200.1 - 0203.e800.6510 ARPA Ethernet0/1\nInternet 192.168.200.100 28 0800.27ac.1b91 ARPA Ethernet0/1\nInternet 192.168.230.1 - aabb.cc00.6530 ARPA Ethernet0/3'
Отправка еще одной команды, но только теперь результат сохраняется в переменную:
In [8]: result = ssh.send('sh ip int br')
In [9]: print(result)
Interface IP-Address OK? Method Status Protocol
Ethernet0/0 192.168.100.1 YES NVRAM up up
Ethernet0/1 192.168.200.1 YES NVRAM up up
Ethernet0/2 19.1.1.1 YES NVRAM up up
Ethernet0/3 192.168.230.1 YES NVRAM up up
Если сопрограмма больше не нужна, можно остановить ее, с помощью метода close:
In [10]: ssh.close()
Но, в данном случае, после завершения работы сопрограммы, сессия SSH остается открытой на оборудовании. Более корректно было бы завершать сессию, когда сопрограмма завершает работу.
Это достаточно легко сделать, так как вызов метода close, генерирует внутри сопрограммы исключение GeneratorExit. А значит, можно перехватить его и закрыть сессию.
Финальный пример сопрограммы send_show_command с закрытием сессии (файл netmiko_coroutine.py):
import netmiko
def send_show_command(device_params):
print('Open connection to: '.rjust(40, '#'),
device_params['ip'])
conn = netmiko.ConnectHandler(**device_params)
conn.enable()
result = None
while True:
try:
command = yield result
result = conn.send_command(command)
except GeneratorExit:
conn.disconnect()
print('Connection closed'.rjust(40, '#'))
break
r1 = {'device_type': 'cisco_ios',
'ip': '192.168.100.1',
'username': 'cisco',
'password': 'cisco',
'secret': 'cisco' }
commands = ['sh ip int br', 'sh ip arp']
ssh = send_show_command(r1)
next(ssh)
for c in commands:
result = ssh.send(c)
print(result)
ssh.close()
Результат выполнения:
$ python netmiko_coroutine.py
####################Open connection to: 192.168.100.1
Interface IP-Address OK? Method Status Protocol
Ethernet0/0 192.168.100.1 YES NVRAM up up
Ethernet0/1 192.168.200.1 YES NVRAM up up
Ethernet0/2 19.1.1.1 YES NVRAM up up
Ethernet0/3 192.168.230.1 YES NVRAM up up
Protocol Address Age (min) Hardware Addr Type Interface
Internet 19.1.1.1 - aabb.cc00.6520 ARPA Ethernet0/2
Internet 192.168.100.1 - aabb.cc00.6500 ARPA Ethernet0/0
Internet 192.168.100.100 54 aabb.cc80.c900 ARPA Ethernet0/0
Internet 192.168.200.1 - 0203.e800.6510 ARPA Ethernet0/1
Internet 192.168.200.100 2 0800.27ac.1b91 ARPA Ethernet0/1
Internet 192.168.230.1 - aabb.cc00.6530 ARPA Ethernet0/3
#######################Connection closed
yield from
Выражение yield from может использоваться, как и yield, в генераторе и в сопрограммах.
yield from в генераторе
При использовании в генераторе, yield from может помочь упростить использование yield в цикле for:
In [1]: def generate():
...: for i in range(5):
...: yield i
...:
In [2]: list(generate())
Out[2]: [0, 1, 2, 3, 4]
Аналогичный вариант с yield from:
In [3]: def generate():
...: yield from range(5)
...:
In [4]: list(generate())
Out[4]: [0, 1, 2, 3, 4]
Пример использования yield from для получения плоского списка из списка списков с разной вложенностью (упрощенный вариант примера 4.14 из книги Python Cookbook:
In [5]: def flatten_list(alist):
...: for item in alist:
...: if type(item) is list:
...: yield from flatten_list(item)
...: else:
...: yield item
...:
In [6]: example = [0, 1, [2, 3], 4, [5, 6, [7, 8]], 9]
In [7]: list(flatten_list(example))
Out[7]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
yield from в сопрограммах
Основная функциональность yield from - открытие двухстороннего канала между кодом, который вызвает сопрограмму и вложенным генератором, при этом значения передаются между ними напрямую.
Пример использования yield from:
from pprint import pprint
#subgenerator
def power():
print('Start subgenerator')
result = {}
while True:
num = yield
if num is None:
print('Finish subgenerator')
break
print(num)
result[num] = num**2
return result
#delegating generator
def del_gen(results, set_id):
while True:
print('*'*40)
print('Start delegating generator')
results[set_id] = yield from power()
print(results.keys())
print('Finish delegating generator')
print('*'*40)
#caller
def main(num_sets):
results = {}
for set_id, num_set in num_sets.items():
collect = del_gen(results, set_id)
next(collect)
for num in num_set:
collect.send(num)
collect.send(None)
return results
all_numbers = {'set1': [1, 2, 3, 4, 5],
'set2': [10, 20, 30, 40, 50]}
result = main(all_numbers)
pprint(result)
Функция main перебирает наборы чисел и для каждого набора вызывает сопрограмму del_gen и инициирует генератор:
for set_id, num_set in num_sets.items():
collect = del_gen(results, set_id)
next(collect)
После этого, генератор останавливается на yield и вызывается вложенный генератор power. Теперь send из функции main попадает напрямую во вложенный генератор power, а yield из вложенного генератора power, попадает в функцию main.
Но, так как во вложеном генераторе после yield нет никакого значения, возвращается None.
После перебора всех чисел в наборе, функция main отправляет значение None, чтобы завершить работу вложенного генератора. Как только вложенный генератор завершил своб работу, результат, который он возвращает записывается в сопрограмме del_gen в словарь results:
results[set_id] = yield from power()
Аналогично все повторяется для следующего набора чисел.
Результат выполнения:
$ python basic_subgenerator.py
****************************************
Start delegating generator
Start subgenerator
1
2
3
4
5
Finish subgenerator
dict_keys(['set1'])
Finish delegating generator
****************************************
****************************************
Start delegating generator
Start subgenerator
****************************************
Start delegating generator
Start subgenerator
10
20
30
40
50
Finish subgenerator
dict_keys(['set1', 'set2'])
Finish delegating generator
****************************************
****************************************
Start delegating generator
Start subgenerator
{'set1': {1: 1, 2: 4, 3: 9, 4: 16, 5: 25},
'set2': {10: 100, 20: 400, 30: 900, 40: 1600, 50: 2500}}
Я пока не до конца разобралась с этими промежуточными повторными вызовами, которые появляются после нормального вызова
Пример использования yield from
Пример в целом аналогичен прошлому, но немного изменен код во вложенном генераторе send_show_command, чтобы соединение устанавливалось только, когда это действительно нужно (чтобы не было было подключения из-за промежуточных вызовов):
import netmiko
from pprint import pprint
#subgenerator
def send_show_command3(device_params):
command = yield
print('Opening connection to IP: {}'.format(device_params['ip']))
conn = netmiko.ConnectHandler(**device_params)
conn.enable()
command_result = {}
while True:
if command is None:
conn.disconnect()
print('Connection closed')
break
print(command)
output = conn.send_command(command)
command_result[command] = output
command = yield
return command_result
#delegating generator
def collect_output(results, device_params):
while True:
print('*'*40)
results[device_params['ip']] = yield from send_show_command3(device_params)
#caller
def main(devices, commands):
results = {}
for device in devices:
collect = collect_output(results, device)
next(collect)
for command in commands:
collect.send(command)
collect.send(None)
return results
r1 = {'device_type': 'cisco_ios',
'ip': '192.168.100.1',
'username': 'cisco',
'password': 'cisco',
'secret': 'cisco' }
r2 = {'device_type': 'cisco_ios',
'ip': '192.168.100.2',
'username': 'cisco',
'password': 'cisco',
'secret': 'cisco' }
all_devices = [r1, r2]
commands = ['sh ip arp', 'sh ip int br']
result = main(all_devices, commands)
for device, command in result.items():
print(' IP: {} '.format(device).center(50,'#'))
for c, output in command.items():
print(' Command: {} '.format(c).center(50,'#'))
print(output)
Результат выполнения:
$ python netmiko_subgenerator.py
****************************************
Opening connection to IP: 192.168.100.1
sh ip arp
sh ip int br
Connection closed
****************************************
****************************************
Opening connection to IP: 192.168.100.2
sh ip arp
sh ip int br
Connection closed
****************************************
############### IP: 192.168.100.1 ################
############### Command: sh ip arp ###############
Protocol Address Age (min) Hardware Addr Type Interface
Internet 19.1.1.1 - aabb.cc00.6520 ARPA Ethernet0/2
Internet 192.168.100.1 - aabb.cc00.6500 ARPA Ethernet0/0
Internet 192.168.100.2 71 aabb.cc00.6600 ARPA Ethernet0/0
Internet 192.168.100.100 72 aabb.cc80.c900 ARPA Ethernet0/0
Internet 192.168.200.1 - 0203.e800.6510 ARPA Ethernet0/1
Internet 192.168.200.100 71 0800.27ac.1b91 ARPA Ethernet0/1
Internet 192.168.230.1 - aabb.cc00.6530 ARPA Ethernet0/3
############# Command: sh ip int br ##############
Interface IP-Address OK? Method Status Protocol
Ethernet0/0 192.168.100.1 YES NVRAM up up
Ethernet0/1 192.168.200.1 YES NVRAM up up
Ethernet0/2 19.1.1.1 YES NVRAM up up
Ethernet0/3 192.168.230.1 YES NVRAM up up
############### IP: 192.168.100.2 ################
############### Command: sh ip arp ###############
Protocol Address Age (min) Hardware Addr Type Interface
Internet 192.168.100.1 72 aabb.cc00.6500 ARPA Ethernet0/0
Internet 192.168.100.2 - aabb.cc00.6600 ARPA Ethernet0/0
Internet 192.168.100.100 72 aabb.cc80.c900 ARPA Ethernet0/0
############# Command: sh ip int br ##############
Interface IP-Address OK? Method Status Protocol
Ethernet0/0 192.168.100.2 YES NVRAM up up
Ethernet0/1 unassigned YES NVRAM administratively down down
Ethernet0/2 unassigned YES NVRAM administratively down down
Ethernet0/3 unassigned YES NVRAM administratively down down
Дополнительные материалы
Документация:
- Generator-iterator methods
- PEP 342 – Coroutines via Enhanced Generators
- PEP 380 – Syntax for Delegating to a Subgenerator
Статьи:
Ответ на stackoverflow:
- What does the “yield” keyword do? и перевод этого ответа
- what’s the difference between yield from and yield in python 3.3.2+
Fluent Python:
Презентация: