3 стъпки към основен ремонт на Python async

Python е един от многото езици, които поддържат някакъв начин за писане на асинхронни програми - програми, които се превключват свободно между множество задачи, всички изпълнявани наведнъж, така че никоя задача не спира напредъка на останалите.

Шансовете са обаче, че сте писали главно синхронни програми на Python - програми, които правят само едно нещо в даден момент, чакайки всяка задача да завърши, преди да стартирате друга. Преминаването към асинхронизация може да бъде смущаващо, тъй като изисква изучаване не само на нов синтаксис, но и на нови начини за мислене за кода. 

В тази статия ще изследваме как съществуваща синхронна програма може да бъде превърната в асинхронна. Това включва нещо повече от просто декориране на функции с асинхронен синтаксис; изисква също да се мисли по различен начин за това как работи нашата програма и да се реши дали async е дори добра метафора за това, което прави. 

[Също така: Научете съвети и трикове на Python от видеоклиповете на Serdar Yegulalp за Smart Python]

Кога да се използва асинхрон в Python

Програмата на Python е най-подходяща за асинхронизация, когато има следните характеристики:

  • Опитва се да направи нещо, което е обвързано най-вече от I / O или от изчакване на завършване на някакъв външен процес, като продължително четене от мрежата.
  • Опитва се да изпълнява едно или повече от тези видове задачи наведнъж, като в същото време се справя и с потребителските взаимодействия.
  • Въпросните задачи не са изчислително тежки.

Програма на Python, която използва резби, обикновено е добър кандидат за използване на асинхрон. Темите в Python са съвместни; те се поддават един на друг при нужда. Асинхронните задачи в Python работят по същия начин. Освен това async предлага определени предимства пред нишките:

  • В async/ awaitсинтаксис го прави лесно да се идентифицират асинхронни части на вашата програма. За разлика от това често е трудно с един поглед да се каже кои части на приложението се изпълняват в нишка. 
  • Тъй като асинхронните задачи споделят една и съща нишка, всички данни, до които имат достъп, се управляват автоматично от GIL (родния механизъм на Python за синхронизиране на достъпа до обекти). Нишките често изискват сложни механизми за синхронизация. 
  • Асинхронните задачи са по-лесни за управление и анулиране от нишките.

Използването на async не се препоръчва, ако вашата програма Python има следните характеристики:

  • Задачите имат високи изчислителни разходи - например, те извършват тежко разбиване на числа. С тежката изчислителна работа се справя най-добре multiprocessing, което ви позволява да отделите цяла хардуерна нишка за всяка задача.
  • Задачите не се възползват от преплитането. Ако всяка задача зависи от последната, няма смисъл да ги карате да се изпълняват асинхронно. Въпреки това, ако програмата включва  набори от серийни задачи, можете да стартирате всеки набор асинхронно.

Стъпка 1: Идентифицирайте синхронните и асинхронните части на вашата програма

Асинхронният код на Python трябва да бъде стартиран и управляван от синхронните части на вашето приложение на Python. За тази цел първата ви задача при конвертиране на програма в асинхронна е да начертаете линия между синхронизиращата и асинхронната части на вашия код.

В предишната ни статия за асинхронизацията използвахме приложение за уеб скрепер като прост пример. Асинхронните части на кода са подпрограмите, които отварят мрежовите връзки и четат от сайта - всичко, което искате да преплетете. Но частта от програмата, която стартира всичко това, не е асинхронна; той стартира асинхронните задачи и след това ги затваря грациозно, когато приключат.

Също така е важно да отделите евентуално  блокираща операция от асинхронизация и да я запазите в частта за синхронизиране на приложението си. Четенето на потребителски вход от конзолата например блокира всичко, включително асинхронния цикъл на събитието. Следователно искате да обработите въвеждането от потребителя или преди да стартирате асинхронни задачи, или след като ги завършите. (Възможно е асинхронно да се обработва въвеждането от потребителя чрез многообработка или резба, но това е усъвършенствано упражнение, в което няма да навлизаме тук.)

Някои примери за блокиращи операции:

  • Конзолен вход (както току-що описахме).
  • Задачи, включващи тежко използване на процесора.
  • Използване time.sleepза налагане на пауза. Имайте предвид, че можете да спите в асинхронна функция, като използвате asyncio.sleepкато заместител на time.sleep.

Стъпка 2: Преобразувайте подходящите функции за синхронизиране в асинхронни функции

След като разберете кои части от вашата програма ще работят асинхронно, можете да ги разделите на функции (ако още не сте го направили) и да ги превърнете в асинхронни функции с asyncключовата дума. След това ще трябва да добавите код към синхронната част на приложението си, за да стартирате асинхронния код и да съберете резултати от него, ако е необходимо.

Забележка: Ще искате да проверите веригата на обажданията на всяка функция, която сте направили асинхронна, и да се уверите, че те не извикват потенциално продължителна или блокираща операция. Асинхронните функции могат директно да извикват синхронизиращи функции и ако тази синхронизираща функция блокира, тогава това прави и асинхронната функция, която го извиква.

Нека разгледаме опростен пример за това как може да работи преобразуването синхронизиране в асинхронно. Ето нашата програма „преди“:

def a_function (): # някакво асинхронно съвместимо действие, което отнема известно време def another_function (): # някаква функция за синхронизиране, но не и блокираща def do_stuff (): a_function () another_function () def main (): for _ in range (3): do_stuff () main () 

Ако искаме три екземпляра do_stuffда се изпълняват като асинхронни задачи, трябва да превърнем do_stuff (и потенциално всичко, до което се докосне) в асинхронен код. Ето първото преминаване при преобразуването:

импортиране на asyncio async def a_function (): # някакво асинхронно съвместимо действие, което отнема известно време def another_function (): # някаква функция за синхронизиране, но не и блокиране на async def do_stuff (): изчакайте a_function () another_function () async def main ( ): задачи = [] за _ в обхват (3): task.append (asyncio.create_task (do_stuff ())) await asyncio.gather (задачи) asyncio.run (main ()) 

Обърнете внимание на промените, които направихме  main. Сега main използва asyncioза стартиране на всеки екземпляр do_stuffкато едновременна задача, след което изчаква резултатите ( asyncio.gather). Също така преобразувахме a_functionв асинхронна функция, тъй като искаме всички екземпляри a_functionда работят едно до друго и заедно с всички други функции, които се нуждаят от асинхронно поведение.

Ако искахме да отидем стъпка по-нататък, бихме могли да конвертираме и another_functionв асинхронно

async def another_function (): # някаква функция за синхронизиране, но не и блокираща async def do_stuff (): await a_function () await another_function () 

Изработването на  another_function асинхрон обаче би било прекалено, тъй като (както отбелязахме) не прави нищо, което да блокира напредъка на нашата програма. Също така, ако се извикат някакви синхронни части от нашата програма  another_function, ще трябва да ги преобразуваме и в асинхронни, което може да направи нашата програма по-сложна, отколкото трябва.

Стъпка 3: Тествайте добре вашата асинхронна програма Python

Всяка асинхронно конвертирана програма трябва да бъде тествана, преди да влезе в производство, за да гарантира, че работи както се очаква.

If your program is modest in size — say, a couple of dozen lines or so — and doesn’t need a full test suite, then it shouldn’t be difficult to verify that it works as intended. That said, if you’re converting the program to async as part of a larger project, where a test suite is a standard fixture, it makes sense to write unit tests for async and sync components alike.

Both of the major test frameworks in Python now feature some kind of async support. Python’s own unittest framework includes test case objects for async functions, and pytest offers pytest-asyncio for the same ends.

Finally, when writing tests for async components, you’ll need to handle their very asynchronousness as a condition of the tests. For instance, there is no guarantee that async jobs will complete in the order they were submitted. The first one might come in last, and some might never complete at all. Any tests you design for an async function must take these possibilities into account.

How to do more with Python

  • Get started with async in Python
  • How to use asyncio in Python
  • How to use PyInstaller to create Python executables
  • Cython tutorial: How to speed up Python
  • How to install Python the smart way
  • How to manage Python projects with Poetry
  • How to manage Python projects with Pipenv
  • Virtualenv and venv: Python virtual environments explained
  • Python virtualenv and venv do’s and don’ts
  • Python threading and subprocesses explained
  • How to use the Python debugger
  • Как да използвам timeit за профилиране на Python код
  • Как да използвам cProfile за профилиране на Python код
  • Как да конвертирате Python в JavaScript (и обратно)