In [1]:
import sys
print(sys.version)


3.6.0 (default, Dec 24 2016, 08:01:42) 
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.42.1)]

In [2]:
# посмотрим информацию об операционной системе, на которой мы работаем.
import platform
platform.uname()


Out[2]:
uname_result(system='Darwin', node='Alexanders-MacBook-Pro.local', release='16.4.0', version='Darwin Kernel Version 16.4.0: Thu Dec 22 22:53:21 PST 2016; root:xnu-3789.41.3~3/RELEASE_X86_64', machine='x86_64', processor='i386')

Работа с директориями


In [1]:
import os

os.mkdir("/tmp/park-python")

In [2]:
try:
    os.rmdir("/tmp/park-python")
except IOError as err:
    print(err)

In [3]:
path = "/tmp/park-python/lectures/04"
if not os.path.exists(path):
    os.makedirs(path)

In [4]:
os.rmdir("/tmp/park-python")


---------------------------------------------------------------------------
OSError                                   Traceback (most recent call last)
<ipython-input-4-551c5c4b1cd6> in <module>()
----> 1 os.rmdir("/tmp/park-python")

OSError: [Errno 66] Directory not empty: '/tmp/park-python'

In [5]:
import shutil
shutil.rmtree("/tmp/park-python")

In [6]:
import pprint
pprint.pprint(list(os.walk(os.curdir)))


[('.',
  ['.ipynb_checkpoints', 'img'],
  ['debugging.py',
   'notebook.ipynb',
   'README.md',
   'server',
   'server.go',
   'slides.pdf']),
 ('./.ipynb_checkpoints', [], ['notebook-checkpoint.ipynb']),
 ('./img',
  [],
  ['g1.png',
   'g2.png',
   'g3.png',
   'g4.png',
   'g5.png',
   'loop.jpg',
   'threads.jpg'])]

Работа с файлами


In [16]:
# открываем дескриптор файла для записи
f = open("/tmp/example.txt", "w")

# записываем содержимое
f.write("Технопарк\n")

# обязательно закрываем
f.close()

In [17]:
# открываем дескриптор файла для чтения
f = open("/tmp/example.txt", "r")

# читаем содержимое полностью.
data = f.read()

# обязательно закрываем!
f.close()

print(data)


Технопарк

  • "r" – открытие на чтение (является значением по умолчанию).
  • "w" – открытие на запись, содержимое файла удаляется, если файла не существует, создается новый.
  • "x" – открытие на запись, если файла не существует, иначе исключение.
  • "a" – открытие на дозапись, информация добавляется в конец файла.
  • "b" – открытие в двоичном режиме.
  • "t" – открытие в текстовом режиме (является значением по умолчанию).
  • "+" – открытие на чтение и запись

In [18]:
# используя context-manager
with open("/tmp/example.txt", "a") as f:
    f.write("МГТУ\n")

In [19]:
with open("/tmp/example.txt", "r") as f:
    print(f.readlines())


['Технопарк\n', 'МГТУ\n']

In [20]:
# читаем файл по строке, не загружая его полность в память
with open("/tmp/example.txt", "r") as f:
    for line in f:
        print(repr(line))


'Технопарк\n'
'МГТУ\n'

In [21]:
# Чтобы проверить целостность сохраненного файла
import hashlib

def hash_file(filename):
    h = hashlib.sha1()

    # открываем файл в бинарном виде.
    with open(filename,'rb') as file:
        chunk = 0
        while chunk != b'':
            # читаем кусочками по 1024 байта
            chunk = file.read(1024)
            h.update(chunk)

    # hex-представление полученной суммы.
    return h.hexdigest()

print(hash_file("/tmp/example.txt"))
print(hash_file("/tmp/example.txt"))

with open("/tmp/example.txt", "a") as f:
    f.write("1")

print("После изменений:", hash_file("/tmp/example.txt"))


1f1e3e86acb7cdacc6b77d1629cd1a505da3e166
1f1e3e86acb7cdacc6b77d1629cd1a505da3e166
После изменений: 99dbae54cec60cd234c7d364f51a276ccf07d8cf

stdin, stdout, stderr

По умолчанию Unix-оболочки связывают файловый дескриптор 0 с потоком стандартного ввода процесса (stdin), файловый дескриптор 1 — с потоком стандартного вывода (stdout), и файловый дескриптор 2 — с потоком диагностики (stderr, куда обычно выводятся сообщения об ошибках).


In [22]:
import sys
print(sys.stdin)
print(sys.stdout)
print(sys.stderr)


<_io.TextIOWrapper name='<stdin>' mode='r' encoding='UTF-8'>
<ipykernel.iostream.OutStream object at 0x111ee79e8>
<ipykernel.iostream.OutStream object at 0x111ecff28>

In [23]:
print(sys.stdin.fileno())


0

In [24]:
print(sys.stdout.fileno())


---------------------------------------------------------------------------
UnsupportedOperation                      Traceback (most recent call last)
<ipython-input-24-ef51c9c03543> in <module>()
----> 1 print(sys.stdout.fileno())

/Users/fz/projects/course/env/lib/python3.6/site-packages/ipykernel/iostream.py in fileno(self)
    357 
    358     def fileno(self):
--> 359         raise UnsupportedOperation("IOStream has no fileno.")
    360 
    361     def write(self, string):

UnsupportedOperation: IOStream has no fileno.

Так как дескрипторы stdout и stderr переопределены в Jupyter notebook. Давайте посмотрим куда они ведут:


In [25]:
sys.stdout.write("where am I")


where am I

А ведут они как раз в этот ноутбук:)

Отладка

Показываем на примере файла debugging.py

Полезные библиотеки для отладки (debugging) - https://github.com/vinta/awesome-python#debugging-tools

Тестирование

Зачем?


In [26]:
def get_max_length_word(sentence):
    longest_word = None
    words = sentence.split()
    for word in words:
        if not longest_word or len(word) > len(longest_word):
            longest_word = word
    return longest_word

Что может пойти не так? Да все что угодно:

  • SyntaxError
  • Ошибка в логике
  • Обратная несовместимость новой версии используемой библиотеки
  • ...

Через полгода после запуска приложения без тестов изменения в код большого приложения вносить очень страшно!

Некоторые даже используют TDD - Test-Driven Development.

unittest


In [27]:
import unittest

class LongestWordTestCase(unittest.TestCase):
    
    def test_sentences(self):
        sentences = [
            ["Beautiful is better than ugly.", "Beautiful"],
            ["Complex is better than complicated.", "complicated"]
        ]
        for sentence, correct_word in sentences:
            self.assertEqual(get_max_length_word(sentence), correct_word)

# Обычно в реальных проектах использует механизм автоматического нахождения тестов (discover).
suite = unittest.defaultTestLoader.loadTestsFromTestCase(LongestWordTestCase)
unittest.TextTestRunner().run(suite)


F
======================================================================
FAIL: test_sentences (__main__.LongestWordTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-27-3662bd66bec4>", line 11, in test_sentences
    self.assertEqual(get_max_length_word(sentence), correct_word)
AssertionError: 'complicated.' != 'complicated'
- complicated.
?            -
+ complicated


----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (failures=1)
Out[27]:
<unittest.runner.TextTestResult run=1 errors=0 failures=1>
Method Checks that
assertEqual(a, b) a == b
assertNotEqual(a, b) a != b
assertTrue(x) bool(x) is True
assertFalse(x) bool(x) is False
assertIsNone(x) x is None
assertIsNotNone(x) x is not None
assertIsInstance(a, b) isinstance(a, b)
И другие... https://docs.python.org/3.5/library/unittest.html

Протестируем class.


In [32]:
class BoomException(Exception):
    pass


class Material:

    def __init__(self, name, reacts_with=None):
        self.name = name
        self.reacts_with = reacts_with or []

    def __repr__(self):
        return self.name


class Alchemy:

    def __init__(self):
        self.materials = []

    def add(self, material):
        for existing_material in self.materials:
            if material.name not in existing_material.reacts_with:
                continue
            self.materials = []
            raise BoomException("{0} + {1}".format(existing_material.name, material.name))
        self.materials.append(material)

    def remove(self, material):
        self.materials.remove(material)


# 2Na + 2H2O = 2NaOH + H2 (Не повторять дома!!! Щелочь чрезвычайно опасна!)
alchemy = Alchemy()

material_ca = Material("Ca", reacts_with=[])
material_h20 = Material("H2O", reacts_with=["Na"])
material_na = Item("Na", reacts_with=["H2O"])


alchemy.add(material_ca)
alchemy.add(material_h20)

try:
    alchemy.add(material_na)
except BoomException:
    print("We are alive! But all items lost!")


We are alive! But all items lost!

In [33]:
import unittest

class AlchemyTest(unittest.TestCase):

    def setUp(self):
        self.alchemy = Alchemy()
    
    def test_add(self):
        self.alchemy.add(Material("C"))
        self.alchemy.add(Material("F"))
        self.assertEqual(len(self.alchemy.materials), 2)

    def test_remove(self):
        material_c = Material("C")
        self.alchemy.add(material_c)
        self.assertEqual(len(self.alchemy.materials), 1)
        self.alchemy.remove(material_c)
        self.assertEqual(len(self.alchemy.materials), 0)

    def test_boom(self):
        material_na = Material("Na", reacts_with=["H2O"])
        material_h20 = Material("H2O", reacts_with=["Na"])
        self.alchemy.add(material_na)
        self.assertRaises(BoomException, self.alchemy.add, material_h20)
        self.assertEqual(len(self.alchemy.materials), 0)


# Обычно в реальных проектах использует механизм автоматического нахождения тестов (discover).
suite = unittest.defaultTestLoader.loadTestsFromTestCase(AlchemyTest)
unittest.TextTestRunner().run(suite)


...
----------------------------------------------------------------------
Ran 3 tests in 0.003s

OK
Out[33]:
<unittest.runner.TextTestResult run=3 errors=0 failures=0>

unittest.mock

Как тестировать код, выполняющий внешние вызовы: чтение файла, запрос содержимого URL?


In [35]:
import requests

def get_location_city(ip):
    data = requests.get("https://freegeoip.net/json/{ip}".format(ip=ip)).json()
    return data["city"]

def get_ip():
    data = requests.get("https://httpbin.org/ip").json()
    return data["origin"]

get_location_city(get_ip())


Out[35]:
'Moscow'

Для начала посмотрим что такое monkey patching.


In [36]:
import math

def fake_sqrt(num):
    return 42

original_sqrt = math.sqrt
math.sqrt = fake_sqrt

# вызываем ф-ю, которую мы запатчили.
print(math.sqrt(16))

math.sqrt = original_sqrt


42

In [37]:
math.sqrt(16)


Out[37]:
4.0

In [38]:
import unittest
from unittest.mock import patch, Mock


class FakeIPResponse:

    def json(self):
        return {"origin": "127.0.0.1"}


class LongestWordTestCase(unittest.TestCase):

    @patch('requests.get', Mock(return_value=FakeIPResponse()))
    def test_get_ip(self):
        self.assertEqual(get_ip(), "127.0.0.1")

suite = unittest.defaultTestLoader.loadTestsFromTestCase(LongestWordTestCase)
unittest.TextTestRunner().run(suite)


.
----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
Out[38]:
<unittest.runner.TextTestResult run=1 errors=0 failures=0>

In [39]:
from unittest.mock import Mock

mock = Mock()
mock.method(1, 2, 3, test='wow')

mock.method.assert_called_with(1, 2, 3, test='wow')
mock.non_existing_method.assert_not_called()

https://docs.python.org/3/library/unittest.mock.html

Библиотека coverage позволяет оценить степень покрытия кода тестами.

Помимо unit-тестирования существует масса других типов тестов:

  • Интеграционные (как разные компоненты взаимодействуют друг с другом)
  • Функциональные (напр. Selenium)
  • Тестирование производительности (бенчмарки)
  • ...

Полезные библиотеки для тестирования - https://github.com/vinta/awesome-python#testing

Многопоточность

Что такое процесс?

UNIX является многозадачной операционной системой. Это означает, что одновременно может быть запущена более чем одна программа. Каждая программа, работающая в некоторый момент времени, называется процессом.

http://www.kharchuk.ru/%D0%A1%D1%82%D0%B0%D1%82%D1%8C%D0%B8/15-unix-foundations/80-unix-processes


In [41]:
STEPS = 50000000

# Простая программа, складывающая числа.
def worker(steps):
    count = 0
    for i in range(steps):
        count += 1
    return count

%timeit -n1 -r1 worker(STEPS)

print("Напомните преподавателю показать actvity monitor")


3.38 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
Напомните преподавателю показать actvity monitor
![title](img/g3.png)

Что такое поток?

Многопоточность является естественным продолжением многозадачности. Каждый из процессов может выполнятся в несколько потоков. Программа выше исполнялась в одном процессе в главном потоке.

![title](img/threads.jpg)

http://www.cs.miami.edu/home/visser/Courses/CSC322-09S/Content/UNIXProgramming/UNIXThreads.shtml

Логичный шаг предположить, что 2 потока выполнят программу выше быстрее. Проверим?


In [49]:
import threading
import queue

result_queue = queue.Queue()

STEPS = 50000000
NUM_THREADS = 2

def worker(steps):
    count = 0
    for i in range(steps):
        count += 1
    result_queue.put(count)


def get_count_threaded():    
    count = 0
    threads = []

    for i in range(NUM_THREADS):
        t = threading.Thread(target=worker, args=(STEPS//NUM_THREADS,))
        threads.append(t)
        t.start()

    for i in range(NUM_THREADS):
        count += result_queue.get()

    return count

%timeit -n1 -r1 get_count_threaded()


3.5 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
![title](img/g4.png)

GIL

https://jeffknupp.com/blog/2012/03/31/pythons-hardest-problem/

Ок. Неужели выхода нет? Есть - multiprocessing

Мультипроцессинг


In [60]:
import multiprocessing

NUM_PROCESSES = 2
STEPS = 50000000

result_queue = multiprocessing.Queue()

def worker(steps):
    count = 0
    for i in range(steps):
        count += 1

    result_queue.put(count)


def get_count_in_processes():    
    count = 0
    processes = []
    for i in range(NUM_PROCESSES):
        p = multiprocessing.Process(target=worker, args=(STEPS//NUM_PROCESSES,))
        processes.append(p)
        p.start()

    for i in range(NUM_PROCESSES):
        count += result_queue.get()

    return count

%timeit -n1 -r1 get_count_in_processes()


1.88 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

Зачем тогда нужны потоки?

Все потому что не все задачи CPU-bound. Есть IO-bound задачи, которые прекрасно параллелятся на несколько CPU. Кто приведет пример?

В качестве примера поднимем HTTP-сервер на порту 8000 (server.go). По адресу http://localhost:8000 будет отдаваться небольшой кусочек текста. Наша задача - скачивать контент по этому адресу.


In [105]:
import requests

STEPS = 100

def download():
    requests.get("http://127.0.0.1:8000").text

# Простая программа, загружающая контент URL-странички. Типичная IO-bound задача.
def worker(steps):
    for i in range(steps):
        download()

%timeit -n1 -r1 worker(STEPS)


10.7 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
![title](img/g1.png)

In [75]:
import threading

STEPS = 100
NUM_THREADS = 2

def worker(steps):
    count = 0
    for i in range(steps):
        download()

def run_worker_threaded():    
    threads = []

    for i in range(NUM_THREADS):
        t = threading.Thread(target=worker, args=(STEPS//NUM_THREADS,))
        threads.append(t)
        t.start()

    for t in threads:
        t.join()


%timeit -n1 -r1 run_worker_threaded()


5.49 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
![title](img/g2.png)

Ради интереса попробуем мультипроцессинг для этой задачи:


In [80]:
import multiprocessing

NUM_PROCESSES = 2

def worker(steps):
    count = 0
    for i in range(steps):
        download()

def run_worker_in_processes():    
    processes = []

    for i in range(NUM_PROCESSES):
        p = multiprocessing.Process(target=worker, args=(STEPS//NUM_PROCESSES,))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

%timeit -n1 -r1 run_worker_in_processes()


5.39 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

Как мы видим - треды позволили получить лучший результат (Macbook Pro 2016 - 64 треда).

Чтобы упростить работу с тредами в Python есть модуль concurrent.futures: он предоставляет доступ к 2-м высокоуровневым объектам: ThreadPoolExecutor и ProcessPoolExecutor


In [86]:
import concurrent.futures
import requests

STEPS = 100

def download():
    return requests.get("http://127.0.0.1:8000").text

def run_in_executor():
    executor = concurrent.futures.ThreadPoolExecutor(max_workers=64)

    future_to_url = {executor.submit(download): i for i in range(STEPS)}
    for future in concurrent.futures.as_completed(future_to_url):
        i = future_to_url[future]
        try:
            data = future.result()
        except Exception as exc:
            print('%d generated an exception: %s' % (i, exc))
        else:
            pass
            #print('%d page is %d bytes' % (i, len(data)))

    executor.shutdown()

%timeit -n1 -r1 run_in_executor()


459 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

Аналогично можно использовать ProcessPoolExecutor, чтобы вынести работу в пул процессов.

Сложность многопоточных приложений


In [87]:
counter = 0

def worker(num):
    global counter
    for i in range(num):
        counter += 1

worker(1000000)

print(counter)


1000000

In [90]:
import threading

counter = 0

def worker(num):
    global counter
    for i in range(num):
        counter += 1

threads = []
for i in range(10):
    t = threading.Thread(target=worker, args=(100000,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

#print(counter)

In [91]:
import threading

counter = 0
lock = threading.Lock()

def worker(num):
    global counter
    for i in range(num):
        lock.acquire()
        counter += 1
        lock.release()

threads = []
for i in range(10):
    t = threading.Thread(target=worker, args=(100000,))
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print(counter)


1000000

In [79]:
# deadlock example
import threading

counter = 0
lock = threading.Lock()

def print_counter():
    lock.acquire()
    print(counter)
    lock.release()

def worker():
    global counter
    lock.acquire()
    print_counter()
    counter += 1
    lock.release()
    
worker()


---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-79-8c0ff98f22b4> in <module>()
     17     lock.release()
     18 
---> 19 worker()

<ipython-input-79-8c0ff98f22b4> in worker()
     13     global counter
     14     lock.acquire()
---> 15     print_counter()
     16     counter += 1
     17     lock.release()

<ipython-input-79-8c0ff98f22b4> in print_counter()
      6 
      7 def print_counter():
----> 8     lock.acquire()
      9     print(counter)
     10     lock.release()

KeyboardInterrupt: 

Вернемся к примеру с загрузкой URL: можем ли мы сделать еще лучше?

  • загружать странички еще быстрее
  • потреблять меньше памяти, не расходуя ее на создание потоков
  • не задумываться о синхронизации потоков

Асинхронное программирование

Мотивация - IO-операции очень медленные, нужно заставить программу выполнять полезную работу во время ожидания ввода-вывода.

Сравнение Latency некоторых операций (https://gist.github.com/jboner/2841832)

L1 CPU cache reference 0.5 ns
Main memory reference 100 ns 200x L1 cache
Read 1 MB sequentially from memory 250,000 ns (250 us)
Round trip within same datacenter 500,000 ns (500 us)
Read 1 MB sequentially from SSD* 1,000,000 ns (1,000 us, 1ms)
Read 1 MB sequentially from disk 20,000,000 ns (20,000 us, 20 ms) 80x memory, 20X SSD
Send packet CA->Netherlands->CA 150,000,000 ns (150,000 us, 150 ms)

Вернемся к упрощенному варианту нашей программы, загружавшей URL в одном потоке синхронно.


In [84]:
import time

def request(i):
    print(f"Sending request {i+1}")
    time.sleep(1)
    print(f"Got response from request {i+1}")
    print()

for i in range(5):
    request(i)


Sending request 1
Got response from request 1

Sending request 2
Got response from request 2

Sending request 3
Got response from request 3

Sending request 4
Got response from request 4

Sending request 5
Got response from request 5

На запрос тратится 1 секунда, и мы ждем 5 секунд на 5 запросов - а ведь могли бы отправить их друг за другом и через секунду получить результаты для всех и обработать.

Подход с callback-ами:


In [92]:
import time

def request(i):
    print("Sending request %d" % i)

    def on_data(data):
        print("Got response from request %d" % i)

    return on_data

callbacks = []

for i in range(5):
    cb = request(i)
    callbacks.append(cb)

time.sleep(1)

for cb in callbacks:
    cb("data")


Sending request 0
Sending request 1
Sending request 2
Sending request 3
Sending request 4
Got response from request 0
Got response from request 1
Got response from request 2
Got response from request 3
Got response from request 4

Генераторы

Это функция, которая генерирует последовательность значений используя ключевое слово yield

Самый простой пример:


In [86]:
def simple_gen():
    yield 1
    yield 2

gen = simple_gen()
print(next(gen))
print(next(gen))
print(next(gen))


1
2
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-86-a356859e67b4> in <module>()
      6 print(next(gen))
      7 print(next(gen))
----> 8 print(next(gen))

StopIteration: 

In [87]:
gen = simple_gen()
for i in gen:
    print(i)


1
2

Первый плюс: получить значения, не загружая все элементы в память. Яркий пример - range.

Чуть посложнее (с состоянием):


In [88]:
def fib():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

gen = fib()
for i in range(6):
    print(next(gen))


0
1
1
2
3
5

Второй плюс: получить значения сразу после того как они были вычислены

Корутины на основе генераторов:


In [89]:
def coro():
    next_value = yield "Hello"
    yield next_value

c = coro()
print(next(c))
print(c.send("World"))


Hello
World

Можно работать с бесконечным потоком данных. Можно обмениваться результатами между отдельными генераторами по мере готовности - то есть иметь дело с несколькими параллельными задачами. При этом не обязательно эти задачи зависят друг от друга.

Для более глубокого понимания и изучения других особенностей - http://www.dabeaz.com/finalgenerator/


In [90]:
import time

def request(i):
    print("Sending request %d" % i)
    data = yield
    print("Got response from request %d" % i)

generators = []

for i in range(5):
    gen = request(i)
    generators.append(gen)
    next(gen)

time.sleep(1)

for gen in generators:
    try:
        gen.send("data")
    except StopIteration:
        pass


Sending request 0
Sending request 1
Sending request 2
Sending request 3
Sending request 4
Got response from request 0
Got response from request 1
Got response from request 2
Got response from request 3
Got response from request 4

В контексте лекции важно понять, что выполнение функции-генератора в Python можно приостановить, дождаться нужных данных, а затем продолжить выполнение с места прерывания. При этом сохраняется локальный контекст выполнения и пока мы ждем данных интерпретатор может заниматься другой полезной работой

Asyncio

Асинхронное программирование с использованием библиотеки asyncio строится вокруг понятия Event Loop - "цикл событий". Event loop является основным координатором в асинхронных программах на Python. Он отвечает за:

  • шедулинг корутин и коллбеков
  • регистрацию отложенных вызовов
  • регистрацию таймеров

Это позволяет писать программы так, что в момент блокирующих IO операций контекст выполнения будет переключаться на другие задачи, ждущие выполнения.


In [91]:
import asyncio

loop = asyncio.get_event_loop()
loop.run_forever()


---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-91-820a8cbd667d> in <module>()
      2 
      3 loop = asyncio.get_event_loop()
----> 4 loop.run_forever()

/usr/local/Cellar/python3/3.6.0/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py in run_forever(self)
    419             events._set_running_loop(self)
    420             while True:
--> 421                 self._run_once()
    422                 if self._stopping:
    423                     break

/usr/local/Cellar/python3/3.6.0/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py in _run_once(self)
   1387                            timeout * 1e3, dt * 1e3)
   1388         else:
-> 1389             event_list = self._selector.select(timeout)
   1390         self._process_events(event_list)
   1391 

/usr/local/Cellar/python3/3.6.0/Frameworks/Python.framework/Versions/3.6/lib/python3.6/selectors.py in select(self, timeout)
    575             ready = []
    576             try:
--> 577                 kev_list = self._kqueue.control(None, max_ev, timeout)
    578             except InterruptedError:
    579                 return ready

KeyboardInterrupt: 
![title](img/loop.jpg)

In [92]:
import asyncio

def cb():
    print("callback called")
    loop.stop()

loop = asyncio.get_event_loop()
loop.call_later(delay=3, callback=cb)
print("start event loop")
loop.run_forever()


start event loop
callback called

В Python 3.4 вызов результата корутины выполнялся с помощью конструкции yield from (https://www.python.org/dev/peps/pep-0380/), а функции-корутины помечались декоратором @asyncio.coroutine


In [93]:
import asyncio

@asyncio.coroutine
def return_after_delay():
    yield from asyncio.sleep(3)
    print("return called")

loop = asyncio.get_event_loop()
print("start event loop")
loop.run_until_complete(return_after_delay())


start event loop
return called

В версии 3.5 появились специальные ключевые слова, позволяющие программировать в асинхронном стиле: async и await

async - ключевое слово, позволяющее обозначить функцию как асинхронную (корутина, coroutine). Такая функция может прервать свое выполнение в определенной точке (на блокирующей операции), а затем, дождавшись результата этой операции, продолжить свое выполнение.

await позволяет запустить такую функцию и дождаться результата.


In [95]:
import asyncio

async def return_after_delay():
    await asyncio.sleep(3)
    print("return called")

loop = asyncio.get_event_loop()
print("start event loop")
loop.run_until_complete(return_after_delay())


start event loop
return called

Чтобы программа стала работать асинхронно нужно использовать примитивы, которые есть в библиотеке asyncio:


In [94]:
import asyncio

async def get_data():
    await asyncio.sleep(1)
    return "boom"

async def request(i):
    print(f"Sending request {i+1}")
    data = await get_data()
    print(f"Got response from request {i+1}: {data}")

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*[request(i) for i in range(5)]))


Sending request 5
Sending request 3
Sending request 1
Sending request 4
Sending request 2
Got response from request 5: boom
Got response from request 3: boom
Got response from request 1: boom
Got response from request 4: boom
Got response from request 2: boom
Out[94]:
[None, None, None, None, None]

Исключения при работе с корутинами работают точно так же как и в синхронном коде:


In [95]:
import asyncio

async def get_data():
    await asyncio.sleep(1)
    raise ValueError

async def request(i):
    print("Sending request %d" % i)
    try:
        data = await get_data()
    except ValueError:
        print("Error in request %d" % i)
    else:
        print("Got response from request %d" % i)

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*[request(i) for i in range(5)]))


Sending request 2
Sending request 3
Sending request 0
Sending request 4
Sending request 1
Error in request 2
Error in request 3
Error in request 0
Error in request 4
Error in request 1
Out[95]:
[None, None, None, None, None]

Примеры других реализаций Event Loop'ов:

Мы еще вернемся к asyncio в лекции про интернет и клиент-серверные приложения. В том числе подробно посмотрим на сетевые операции - неблокирующее чтение и запись в сокеты.

Список библиотек, написанных поверх asyncio - https://github.com/python/asyncio/wiki/ThirdParty

А пока вернемся к нашему примеру и перепишем его, используя асинхронный подход и asyncio в частности.


In [106]:
import aiohttp
import asyncio

STEPS = 100

async def download(loop):
    async with aiohttp.ClientSession(loop=loop) as session:
        async with session.get("http://127.0.0.1:8000") as response:
            return await response.text()

async def worker(steps, loop):
    await asyncio.gather(*[download(loop) for x in range(steps)])

loop = asyncio.get_event_loop()
%timeit -n1 -r1 loop.run_until_complete(worker(STEPS, loop))


195 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)


In [97]:
# future example.
import asyncio

async def slow_operation(future):
    try:
        await asyncio.wait_for(asyncio.sleep(1), 2) 
    except asyncio.TimeoutError:
        future.set_exception(ValueError("Error sleeping"))
    else:
        future.set_result('Future is done!')

def got_result(future):
    if future.exception():
        print("Exception:", type(future.exception()))
    else:
        print(future.result())
    loop.stop()

loop = asyncio.get_event_loop()

future = asyncio.Future()
future.add_done_callback(got_result)

asyncio.ensure_future(slow_operation(future))

loop.run_forever()


Future is done!

In [121]:
# выносим блокирующие вызовы в пул тредов
import asyncio
import requests

async def main():
    loop = asyncio.get_event_loop()
    future1 = loop.run_in_executor(None, requests.get, 'http://127.0.0.1:8000')
    future2 = loop.run_in_executor(None, requests.get, 'http://127.0.0.1:8000')
    response1 = await future1
    response2 = await future2
    print(response1.text)
    print(response2.text)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())


Hello, world!

Request method is "GET"
RequestURI is "/"
Requested path is "/"
Host is "127.0.0.1:8000"
Query string is ""
User-Agent is "python-requests/2.11.1"
Connection has been established at 2017-03-23 09:13:47 +0300 MSK
Request has been started at 2017-03-23 09:13:47 +0300 MSK
Serial request number for the current connection is 1
Your ip is "127.0.0.1"


Hello, world!

Request method is "GET"
RequestURI is "/"
Requested path is "/"
Host is "127.0.0.1:8000"
Query string is ""
User-Agent is "python-requests/2.11.1"
Connection has been established at 2017-03-23 09:13:47 +0300 MSK
Request has been started at 2017-03-23 09:13:47 +0300 MSK
Serial request number for the current connection is 1
Your ip is "127.0.0.1"


Завершающий пример (asyncio + multiprocessing)


In [108]:
# asyncio + multiprocessing
import aiohttp
import asyncio
import multiprocessing

NUM_PROCESSES = 2

STEPS = 100

async def download(loop):
    async with aiohttp.ClientSession(loop=loop) as session:
        async with session.get("http://127.0.0.1:8000") as response:
            return await response.text()

async def worker(steps, loop):
    await asyncio.gather(*[download(loop) for x in range(steps)], loop=loop)

def run(steps):
    loop = asyncio.new_event_loop()
    loop.run_until_complete(worker(steps, loop))

def run_in_processes():    
    processes = []

    for i in range(NUM_PROCESSES):
        p = multiprocessing.Process(target=run, args=(STEPS//NUM_PROCESSES,))
        processes.append(p)
        p.start()

    for p in processes:
        p.join()

%timeit -n1 -r1 run_in_processes()


226 ms ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)

Subprocess. Если останется время...


In [109]:
import subprocess
import os

result = subprocess.run(["ls", "-l", os.getcwd()], stdout=subprocess.PIPE)
print(result.stdout)


b'total 13760\n-rw-r--r--  1 fz  staff        0 Mar  1  2017 README.md\n-rw-r--r--  1 fz  staff      600 Mar 23  2017 debugging.py\ndrwxr-xr-x  9 fz  staff      306 Mar  1  2017 img\n-rw-r--r--  1 fz  staff    82363 Oct 19 09:42 notebook.ipynb\n-rwxr-xr-x  1 fz  staff  5960316 Mar 23  2017 server\n-rw-r--r--  1 fz  staff     1190 Mar 23  2017 server.go\n-rw-r--r--  1 fz  staff   983707 Mar  1  2017 slides.pdf\n'

In [110]:
# используя shell
result = subprocess.run(
    "ls -l " + os.getcwd() + "|grep debug",
    stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
)
print(result.stdout)


b'-rw-r--r--  1 fz  staff      600 Mar 23  2017 debugging.py\n'

In [ ]: