Асинхронное программирование
Введение
Асинхронное программирование - это способ организации программного кода, который позволяет выполнять несколько задач одновременно в рамках одного процесса. Это достигается путем использования неблокирующих операций ввода-вывода (I/O), которые позволяют программе продолжать работу, не ожидая завершения операции ввода-вывода.
Для лучшего понимания, можно сравнить синхронное и асинхронное программирование. В синхронном программировании, задачи выполняются последовательно, одна за другой. Когда одна задача завершается, начинается выполнение следующей. Это означает, что если задача занимает много времени, все остальные задачи будут ожидать ее завершения, прежде чем начнутся.
С другой стороны, в асинхронном программировании, задачи выполняются параллельно, каждая задача может быть запущена и работать параллельно с другими задачами. Это делает выполнение задач более эффективным и быстрым, поскольку программе не приходится ждать завершения операций ввода-вывода, прежде чем продолжить выполнение задач.
В Python для асинхронного программирования используется модуль asyncio, который предоставляет различные инструменты и средства для упрощения организации асинхронных операций. Он основан на принципе сопрограмм, которые являются легковесными процессами, работающими в рамках одного потока выполнения. Вместо создания нового потока для каждой задачи, сопрограммы позволяют запускать несколько задач в одном потоке, что позволяет экономить ресурсы системы.
В следующем разделе мы рассмотрим преимущества асинхронного программирования и почему его использование может быть важно для вашего проекта.
Применение
Асинхронное программирование может быть полезным во многих случаях, особенно в приложениях, которые обрабатывают большое количество запросов одновременно. Вот несколько причин, почему использование асинхронного программирования может быть важным для вашего проекта:
1. Увеличение производительности: благодаря параллельному выполнению задач, асинхронное программирование позволяет увеличить производительность приложения. Это особенно важно для приложений, которые обрабатывают множество запросов одновременно, например, веб-серверы.
2. Экономия ресурсов: поскольку асинхронное программирование позволяет запускать несколько задач в одном потоке, оно может помочь экономить ресурсы системы и снижать нагрузку на процессор.
3. Улучшение отзывчивости: благодаря асинхронной обработке запросов, приложение может быстро отвечать на запросы пользователей, что улучшает общую отзывчивость системы.
4. Работа с сетью: асинхронное программирование может быть полезным при работе с сетью, поскольку большинство операций ввода-вывода в сети являются блокирующими. Использование асинхронных операций ввода-вывода позволяет избежать блокировки приложения и продолжать работу в то время, пока запросы обрабатываются.
5. Упрощение кода: асинхронное программирование может помочь упростить код приложения и сделать его более понятным. Благодаря использованию асинхронных операций, можно избежать создания большого количества потоков и упростить синхронизацию задач.
В следующих разделах мы рассмотрим примеры использования модуля asyncio и объясним, как его можно использовать в вашем проекте.
Модуль asyncio в Python
Модуль asyncio - это библиотека Python, которая предоставляет возможности для асинхронного программирования. Он был добавлен в стандартную библиотеку Python начиная с версии 3.4. Этот модуль предоставляет средства для создания асинхронных сетевых приложений, обработки событий и многого другого.
Основными возможностями модуля asyncio являются:
Корутины: асинхронное программирование в Python основано на корутинах. Корутины - это функции, которые используют ключевое слово "async" и позволяют выполнять задачи асинхронно. Корутины используются в asyncio для выполнения асинхронных операций ввода-вывода, таких как чтение и запись в сетевые сокеты.
Event Loop: asyncio использует цикл обработки событий (Event Loop) для обработки событий и запуска задач. Этот цикл обработки событий позволяет организовать асинхронное выполнение кода и избежать блокировки приложения.
Команды управления задачами: asyncio предоставляет набор команд для управления задачами, которые выполняются асинхронно. Эти команды позволяют запускать задачи, приостанавливать их выполнение, возобновлять работу задач и отменять их выполнение.
Сетевые протоколы: asyncio предоставляет набор классов для создания асинхронных сетевых протоколов, таких как TCP и UDP. Эти классы позволяют разработчикам создавать сетевые приложения, которые работают асинхронно и не блокируют выполнение программы.
Планировщик задач: asyncio использует планировщик задач для определения того, какие задачи должны выполняться в данный момент времени. Планировщик задач позволяет оптимизировать работу приложения и обеспечивает более эффективное использование ресурсов.
Классы для работы с файловой системой: asyncio также предоставляет набор классов для работы с файловой системой. Эти классы позволяют асинхронно выполнять операции чтения и записи файлов, что может быть особенно полезным для сетевых приложений.
Некоторые из функций asynco
Модуль asyncio содержит ряд основных функций, которые позволяют управлять выполнением асинхронных задач. Рассмотрим некоторые из них:
Async/await языковые конструкции, которые используются в асинхронном программировании на основе asyncio модуля в Python.
Ключевое слово async следует вместе с оператором def в определении функции и указывает, что функция является корутиной.
Пример:
async def my_coroutine():
# Ваш код здесь
return result
Ключевое слово await указывает на то, что программа должна ждать завершения выполнения другой корутины, не блокируя при этом выполнение других частей программы.
Пример:
async def do_something():
result1 = await my_coroutine()
result2 = await another_coroutine()
return result1 + result2
Здесь функция do_something() вызывает две других корутины: my_coroutine() и another_coroutine(). Ключевое слово await указывает на то, что она должна ждать завершения выполнения каждой из них и затем сложить полученные результаты.
Если в основной части программы вы хотите запустить корутину, то можно использовать метод `run()` из asyncio модуля.
Пример:
import asyncio
async def my_coroutine():
print("Hello, world!")
asyncio.run(my_coroutine())
asyncio.sleep(seconds): Функция приостанавливает выполнение текущей задачи на указанное количество секунд.
asyncio.get_event_loop(): Функция возвращает цикл событий (event loop), который обрабатывает события в асинхронном коде. Этот цикл обрабатывает все задачи (coroutines) в очереди, приоритезирует их и запускает синхронно.
Пример использования:
import asyncio
async def hello():
print("Hello")
await asyncio.sleep(1)
print("World")
loop = asyncio.get_event_loop()
loop.run_until_complete(hello())
В этом примере мы создали асинхронную функцию hello(), которая выводит "Hello", затем ждет одну секунду с помощью asyncio.sleep(1) и затем выводит "World". Далее мы получаем цикл событий с помощью asyncio.get_event_loop() и запускаем нашу асинхронную функцию в этом цикле с помощью loop.run_until_complete(hello()).
Заметьте, что если вы попытаетесь запустить асинхронную функцию без цикла событий, то вы получите исключение RuntimeError: no running event loop. Также, каждый цикл событий может обрабатывать только одну задачу за раз, так что если у вас есть множество асинхронных функций, то их нужно запускать одну за другой или использовать функции, которые могут запускаться параллельно, такие как asyncio.gather().
asyncio.gather(): Функция запускает несколько корутин одновременно и ожидает их завершения. Возвращает результаты выполнения корутин в виде списка. Если в процессе выполнения произошла ошибка, то возвращается исключение.
Её синтаксис следующий:
import asyncio
async def foo():
await asyncio.sleep(1)
return 1
async def bar():
await asyncio.sleep(2)
return 2
async def main():
results = await asyncio.gather(foo(), bar())
print(results)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
В этом примере мы определяем две корутины foo() и bar(), которые имитируют долгие операции с помощью функции asyncio.sleep(). Затем мы определяем корутину main(), которая запускает эти две корутины параллельно с помощью asyncio.gather() и дожидается их завершения.
Функция asyncio.gather() возвращает список результатов, в данном случае [1, 2], который мы выводим на экран.
Кроме того, функция asyncio.gather() позволяет передавать ей аргумент return_exceptions, который определяет, следует ли возвращать исключения из запущенных корутин. Если return_exceptions=True, то исключения будут включены в список результатов. Если return_exceptions=False (по умолчанию), то исключения будут подняты вверх по стеку вызовов и остановят выполнение программы.
Вот как выглядит пример с использованием аргумента return_exceptions:
...
async def baz():
raise Exception('Something went wrong!')
async def main():
results = await asyncio.gather(foo(), bar(), baz(), return_exceptions=True)
print(results)
...
Здесь мы добавили корутину baz(), которая всегда бросает исключение. Мы также передали аргумент return_exceptions=True в функцию asyncio.gather(). Результат выполнения этого примера будет следующим:
[1, 2, Exception('Something went wrong!')]
Исключение от корутины baz() было включено в список результатов.
Функция asyncio.gather() также может использоваться для отмены выполнения корутин, если одна из них бросит исключение. Для этого нужно передать ей параметр return_exceptions=False и обернуть вызов asyncio.gather() в блок try/except. В случае возникновения исключения в одной из корутин, все остальные будут отменены.
Вот пример:
...
async def baz():
await asyncio.sleep(1)
raise Exception('Something went wrong!')
async def main():
tasks = [foo(), bar(), baz()]
try:
results = await asyncio.gather(*tasks)
except Exception as e:
for task in tasks:
task.cancel()
raise e
print(results)
...
loop.create_task(): Функция создает новую задачу и помещает ее в очередь на выполнение в EventLoop.
Вот пример:
import asyncio
async def say_hello(name):
await asyncio.sleep(1)
print(f"Hello, {name}!")
async def main():
task1 = asyncio.create_task(say_hello("Alice"))
task2 = asyncio.create_task(say_hello("Bob"))
print("Before awaiting tasks")
await task1
await task2
print("After awaiting tasks")
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Здесь мы определяем две задачи, task1 и task2, которые вызывают функцию say_hello с разными именами. Затем мы выводим сообщение "Before awaiting tasks" и ожидаем выполнения обеих задач с помощью await task1 и await task2. После этого мы выводим сообщение "After awaiting tasks".
В результате выполнения этого кода вы увидите что-то вроде следующего:
Before awaiting tasks
Hello, Alice!
Hello, Bob!
After awaiting tasks
Здесь мы используем asyncio.create_task() для создания задачи для каждого вызова say_hello. Затем мы ожидаем выполнения обеих задач с помощью await, прежде чем продолжить выполнение кода.
Также мы используем функцию asyncio.get_event_loop() для получения ссылки на цикл событий (event loop). В конце мы запускаем цикл событий с помощью loop.run_until_complete() и передаем ему нашу основную функцию main().
Использование loop.create_task() позволяет добавить задачу в цикл событий и продолжить выполнение кода без ожидания завершения этой задачи. Это может быть полезно в случаях, когда вы хотите выполнить несколько задач параллельно, но не хотите блокировать выполнение кода до завершения каждой задачи.
asyncio.wait(): Функция ожидает завершения всех задач, переданных в виде списка или множества.
Рассмотрим пример использования:
import asyncio
async def coro(n):
print(f"Starting coroutine {n}")
await asyncio.sleep(1)
print(f"Coroutine {n} is finished")
loop = asyncio.get_event_loop()
tasks = [loop.create_task(coro(i)) for i in range(5)]
loop.run_until_complete(asyncio.wait(tasks))
В этом примере мы определяем асинхронную функцию coro(n), которая принимает аргумент n и выводит сообщение о своем запуске и завершении. Затем мы создаем список задач tasks, где каждая задача представляет вызов функции coro(n) с соответствующим аргументом n.
Далее мы запускаем цикл событий asyncio, создаем задачи для каждой асинхронной функции в списке tasks с помощью метода loop.create_task(), и ждем их завершения с помощью функции asyncio.wait(). Метод asyncio.wait() принимает список задач и возвращает кортеж, содержащий два множества: множество задач, которые завершились успешно, и множество задач, которые завершились неуспешно или были отменены.
В результате выполнения этого кода мы увидим следующий вывод:
Starting coroutine 0
Starting coroutine 1
Starting coroutine 2
Starting coroutine 3
Starting coroutine 4
Coroutine 0 is finished
Coroutine 1 is finished
Coroutine 2 is finished
Coroutine 3 is finished
Coroutine 4 is finished
Здесь мы видим, что все корутины выполнились параллельно, но при этом были управляемы циклом событий asyncio, что позволило избежать блокировки и повысило производительность нашего кода.
Конечной задачей в асинхронном программировании является создание корутин (coroutine), которые представляют собой функции, которые могут быть приостановлены и возобновлены позже. Для выполнения корутин используются функции из модуля asyncio, такие как create_task()
и ensure_future()
.
asyncio.create_task(): Это функция, которая запускает корутину в новом задании и возвращает объект Task. Это удобно, когда нужно запустить корутину в фоновом режиме и не ждать ее завершения.
Вот пример использования функции asyncio.create_task():
import asyncio
async def my_coroutine():
print("Starting my coroutine")
await asyncio.sleep(1)
print("Finishing my coroutine")
async def main():
# создаем задание и запускаем его
task = asyncio.create_task(my_coroutine())
print("Task created")
await asyncio.sleep(0.5)
print("Task running")
await task
print("Task finished")
asyncio.run(main())
В этом примере мы определяем асинхронную корутину my_coroutine(), которая просто засыпает на одну секунду и затем выводит сообщение в консоль. Затем мы определяем асинхронную корутину main(), которая создает задание с помощью функции asyncio.create_task() и выводит сообщения в консоль.
Мы вызываем asyncio.run(main()), чтобы запустить main() в событийном цикле.
Когда мы запускаем main(), мы создаем задание, но не ждем его завершения. Вместо этого мы выводим сообщение в консоль и засыпаем на полсекунды, а затем выводим еще одно сообщение и ждем завершения задания, используя await task.
Результат выполнения этого кода:
Task created
Starting my coroutine
Task running
Finishing my coroutine
Task finished
Как видно из вывода, задание создается, затем корутина my_coroutine() запускается и выполняется в фоновом режиме, пока мы не ждем его завершения с помощью await task. После завершения задания мы выводим сообщение в консоль о его завершении.
asyncio.ensure_future(): Создает и возвращает объект Task, который представляет собой асинхронную задачу для выполнения корутины. Она позволяет запускать корутину без использования цикла событий.
Рассмотрим пример использования функции ensure_future():
import asyncio
async def my_coroutine():
print("Starting coroutine")
await asyncio.sleep(1)
print("Coroutine resumed")
return "Result"
async def main():
print("Creating task")
task = asyncio.ensure_future(my_coroutine())
print("Waiting for result")
result = await task
print(f"Got result: {result}")
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
В этом примере мы создаем корутину my_coroutine(), которая приостанавливает свое выполнение на одну секунду, а затем возобновляет его и возвращает строковый результат. Затем мы создаем объект задачи, используя ensure_future(), и ожидаем ее выполнения внутри корутины main(), используя await. В результате мы получаем ожидаемый результат "Result".
Важно отметить, что ensure_future() и create_task() являются взаимозаменяемыми, но рекомендуется использовать create_task() в большинстве случаев, так как он более универсален и позволяет легче управлять задачами в цикле событий. Однако, в некоторых случаях ensure_future() может быть полезнее, особенно если вам нужно запустить корутину вне цикла событий или использовать другой способ управления задачами.
asyncio.Lock(): Является асинхронной версией традиционного механизма блокировки (lock) в многопоточном программировании. Он используется для управления доступом к ресурсам в асинхронной среде, где множество асинхронных задач могут пытаться одновременно получить доступ к одному и тому же ресурсу. Она гарантирует, что только одна асинхронная задача может получить доступ к блокированному сегменту кода в любой момент времени. Это предотвращает возникновение гонок (race conditions), когда две или более задач пытаются одновременно изменить общий ресурс.
Пример использования в FastAPI сервисе. Метод GET /test должен асинхронно выполнять функцию work(), которая "спит" 3 секунды. Необходимо обеспечить, чтобы функция work() не выполнялась одновременно в нескольких экземплярах. Метод возвращает время, затраченное на обработку запроса.
from fastapi import FastAPI
from pydantic import BaseModel
from time import monotonic
import asyncio
app = FastAPI()
class TestResponse(BaseModel):
elapsed: float
lock = asyncio.Lock()
async def work():
await asyncio.sleep(3)
@app.get("/test", response_model=TestResponse)
async def handler() -> TestResponse:
ts1 = monotonic()
async with lock:
await work()
ts2 = monotonic()
return TestResponse(elapsed=ts2 - ts1)
Когда задача пытается получить блокировку через await lock.acquire() или async with lock, и блокировка уже захвачена другой задачей, ожидающая задача будет асинхронно приостановлена (не блокируя выполнение других задач). Она возобновит своё выполнение только после того, как блокировка будет освобождена.
По завершении работы внутри блока async with, блокировка автоматически освобождается. Это позволяет другой ожидающей задаче (если таковая имеется) захватить блокировку и выполнить свою работу.
Это только некоторые из функций, доступных в модуле asyncio. Рекомендуется ознакомиться с полным списком функций и их описанием в официальной документации Python.
Примеры с использованием asyncio
В этом разделе мы рассмотрим несколько примеров использования модуля asyncio для создания асинхронных приложений в Python.
Пример 1: Асинхронный HTTP-сервер
Один из самых распространенных примеров использования asyncio - это создание асинхронного HTTP-сервера. Для этого мы можем использовать модуль aiohttp, который предоставляет средства для создания асинхронных HTTP-серверов и клиентов.
Вот пример простого HTTP-сервера, который использует aiohttp:
import asyncio
from aiohttp import web
async def handle(request):
name = request.match_info.get('name', "Anonymous")
text = "Hello, " + name
return web.Response(text=text)
async def init():
app = web.Application()
app.add_routes([web.get('/', handle),
web.get('/{name}', handle)])
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, 'localhost', 8080)
await site.start()
print("Server started at http://localhost:8080")
return runner, site
loop = asyncio.get_event_loop()
runner, site = loop.run_until_complete(init())
try:
loop.run_forever()
except KeyboardInterrupt:
pass
loop.run_until_complete(runner.cleanup())
loop.run_until_complete(site.stop())
В этом примере мы используем async/await для создания асинхронной функции handle, которая обрабатывает HTTP запросы на корневой URL и на URL с параметром имени. Мы создаем объект Application и передаем ему наши маршруты. Затем мы запускаем приложение и запускаем сервер на порту 8080.
В последней части мы запускаем бесконечный цикл с помощью loop.run_forever, который будет обрабатывать запросы до тех пор, пока мы не прервем выполнение программы.
Пример 2: Асинхронный клиент для работы с API
Другим распространенным примером использования asyncio является создание асинхронного клиента для работы с API. Для этого мы можем использовать модуль aiohttp.
Вот пример асинхронного клиента, который получает данные из API с помощью библиотеки aiohttp:
import aiohttp
import asyncio
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
html = await fetch(session, 'https://jsonplaceholder.typicode.com/posts/1')
print(html)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
В этом примере мы определяем функцию fetch, которая получает данные из URL с помощью библиотеки aiohttp. Затем мы создаем асинхронный клиент ClientSession и вызываем функцию fetch для получения данных. Наконец, мы выводим данные в консоль.
Пример 3: Асинхронный параллельный запуск задач
Ещё одним примером асинхронного программирования с помощью модуля asyncio является параллельный запуск нескольких задач. Возьмем задачу поиска подстроки в тексте и задачу проверки, является ли число простым. Предположим, что у нас есть функции search_substring и is_prime для выполнения этих задач.
Можно запустить эти функции последовательно в цикле, что займет время выполнения каждой задачи, а итоговое время будет равно сумме времен каждой задачи. Но мы можем ускорить выполнение этих задач, используя асинхронное программирование.
Начнем с создания корутины для каждой задачи и добавления их в список корутин с помощью asyncio.gather(). Затем мы можем запустить все корутины параллельно с помощью asyncio.run().
import asyncio
async def search_substring(text, substring):
print(f"Searching for '{substring}' in '{text}'")
await asyncio.sleep(2) # имитация поиска
if substring in text:
print(f"Found '{substring}' in '{text}'")
else:
print(f"'{substring}' not found in '{text}'")
async def is_prime(num):
print(f"Checking if {num} is prime")
await asyncio.sleep(2) # имитация проверки
if num > 1:
for i in range(2, int(num/2)+1):
if (num % i) == 0:
print(f"{num} is not a prime number")
break
else:
print(f"{num} is a prime number")
else:
print(f"{num} is not a prime number")
async def run_tasks():
tasks = []
tasks.append(asyncio.create_task(search_substring("Hello, world!", "world")))
tasks.append(asyncio.create_task(is_prime(17)))
tasks.append(asyncio.create_task(search_substring("This is a test string", "test")))
tasks.append(asyncio.create_task(is_prime(21)))
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(run_tasks())
В этом примере мы создали четыре корутины, две для каждой задачи. Затем мы добавили эти корутины в список задач с помощью asyncio.create_task() и запустили их параллельно с помощью asyncio.gather(). Каждая корутина будет выполняться в отдельном потоке, что позволит выполнить обе задачи параллельно.
Результат выполнения этого кода:
Searching for 'world' in 'Hello, world!'
Checking if 17 is prime
Searching for 'test' in 'This is a test string'
Checking if 21 is prime
Found 'world' in 'Hello, world!'
17 is a prime number
Found 'test' in 'This is a test string'
21 is not a prime number
Пример 4: с использованием asyncio для параллельной обработки запросов к нескольким эндпоинтам
import asyncio
import aiohttp
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
async with aiohttp.ClientSession() as session:
urls = ['https://jsonplaceholder.typicode.com/posts',
'https://jsonplaceholder.typicode.com/comments',
'https://jsonplaceholder.typicode.com/albums']
tasks = []
for url in urls:
tasks.append(fetch(session, url))
results = await asyncio.gather(*tasks)
print(results)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
Здесь мы создаём функцию fetch(), которая отправляет GET-запрос к указанному URL и возвращает ответ в виде текста.
Затем мы определяем функцию main(), которая создаёт сессию aiohttp, формирует список URL-адресов, для которых нужно сделать запрос, и создаёт список задач, каждая из которых запускает функцию fetch(). Затем мы используем asyncio.gather() для запуска всех задач и ожидаем, пока они завершатся.
Для запуска программы мы используем функцию asyncio.get_event_loop() и метод run_until_complete().
В этом примере мы отправляем запросы к трем разным эндпоинтам, но вы можете изменить список urls и использовать любой другой список адресов эндпоинтов, которые должны быть обработаны параллельно.
Рекомендации по использованию
При использовании асинхронного программирования следует учитывать некоторые рекомендации, чтобы извлечь максимальную пользу из этой технологии.
1. Используйте асинхронные библиотеки
Чтобы добиться максимальной производительности и эффективности, используйте асинхронные библиотеки, которые уже имеют реализованную поддержку асинхронных операций. Некоторые из таких библиотек: aiohttp, asyncio-redis, aiofiles и другие.
2. Используйте асинхронные функции
Используйте асинхронные функции в своих проектах вместо синхронных функций, где это возможно. Это позволит избежать блокировок и увеличит производительность вашего приложения.
3. Используйте корутины
Корутины - это основной инструмент для реализации асинхронного программирования в Python. Используйте их для организации асинхронных операций и обработки событий.
4. Избегайте блокировок
Избегайте блокировок в своем коде, которые могут привести к задержкам и ухудшению производительности. Используйте асинхронные операции, которые не блокируют поток выполнения, для выполнения задач.
5. Будьте осторожны при работе с сетью и вводом/выводом
При работе с сетью и операциями ввода/вывода, которые могут блокировать поток выполнения, необходимо быть осторожными и использовать асинхронные операции. Используйте специальные асинхронные библиотеки, которые позволяют работать с сетью и вводом/выводом в асинхронном режиме.
6. Не злоупотребляйте асинхронностью
Несмотря на все преимущества, асинхронное программирование не подходит для всех задач. Некоторые задачи лучше решать с использованием традиционных синхронных методов.
7. Тестируйте свой код
Тестируйте свой код на асинхронность, чтобы убедиться, что он работает правильно и эффективно. Используйте библиотеки для тестирования асинхронного кода, такие как pytest-asyncio.
Заключение
Асинхронное программирование является мощным инструментом для разработки быстрых и эффективных приложений. Модуль asyncio в Python предоставляет удобный и гибкий способ реализации асинхронных операций и обработки событий.
Надеемся, что данная статья помогла вам лучше понять, что такое асинхронное программирование и как его использовать с помощью модуля asyncio в Python. Если у вас есть какие-либо вопросы или комментарии, не стесняйтесь задавать их в комментариях. Спасибо за внимание!