Python sleep(): Как выполнить код с задержкой?

9880

Python sleep(): Как выполнить код с задержкой?

Python sleep(): Как выполнить код с задержкой?
Python sleep(): Как выполнить код с задержкой?

Знакома ли вам ситуация, когда программа Python должна выполняться не сразу? В большинстве случаев требуется, чтобы код запускался как можно скорее. Однако порой перед работой оптимальнее будет дать программе немного поспать.

В Python есть возможность вызвать функцию sleep() для симуляции задержки в выполнении программы. Быть может, вам нужно дождаться загрузки, скачивания или появления графического объекта на экране. Также может потребоваться сделать паузу между вызовами к веб API или запросами к базе данных. В таких случаях поможет добавление вызова функции sleep() в программу.

Главные аспекты данного руководства по вызову sleep() в Python:

  • time.sleep();
  • Декораторы;
  • Потоки;
  • Async IO;
  • Графический пользовательский интерфейс GUI.

Данная статья предназначена для разработчиков Python среднего уровня, что стремятся повысить свою квалификацию. Если это похоже на вас, приступим!

Вызов sleep() через time.sleep()

В Python есть встроенная поддержка для погружения программы в сон. У модуля time есть функция sleep(), что позволяет отсрочить выполнение вызываемого потока на указанное количество секунд.

Мы собрали ТОП Книг для Python программиста которые помогут быстро изучить язык программирования Python Скачать книги для Python программиста от новичка до профи.

Далее дан пример использования time.sleep():

import time
time.sleep(3) # Сон в 3 секунды
На заметку: В Python 3.5 разработчики слегка изменили поведение time.sleep(). Благодаря новой системе вызова sleep() эффект отсрочки будет длиться как минимум на продолжении указанного количества секунд, даже в том случае, если сон прерывается сигналом. Однако, это не касается случаев, если сигнал является признаком вызова исключения.

Вы можете протестировать, как долго продлиться сон с помощью модуля Python timeit:

$ python3 -m timeit -n 3 "import time; time.sleep(3)"
3 loops, best of 3: 3 sec per loop

Здесь модуль timeit запускается с параметром -n, что указывает timeit, сколько раз выполнять последующий оператор. Можно заметить, что timeit выполнил оператор 3 раза, а лучшее время длилось 3 секунды, чего и следовало ожидать.

По умолчанию timeit будет запускать код миллион раз. Если бы вы запустили вышеуказанный код, оставив значение -n по умолчанию, тогда при 3 секундах на итерацию код завис бы примерно на 34 дня! У модуля timeit есть несколько других настроек для командной строки, с которыми можно ознакомиться в документации.

Создадим что-то более практичное. Системному администратору всегда нужно быть в курсе, если какой-то из сайтов упал. Вы бы хотели иметь возможность проверить код состояния сайта регулярно, но запрашивать веб сервер постоянно нельзя, ведь это сильно повлияет на производительность. В Python одним из простых способов совершить такую проверку является использование системного вызова sleep():

import time
import urllib.request
import urllib.error
 
def uptime_bot(url):
    while True:
        try:
            conn = urllib.request.urlopen(url)
        except urllib.error.HTTPError as e:
            # Отправка admin / log
            print(f'HTTPError: {e.code} для {url}')
        except urllib.error.URLError as e:
            # Отправка admin / log
            print(f'URLError: {e.code} для {url}')
        else:
            # Сайт поднят
            print(f'{url} поднят')
        time.sleep(60)
 
if __name__ == '__main__':
    url = 'http://www.google.com/py'
    uptime_bot(url)

Здесь создается uptime_bot(), что принимает URL в качестве аргумента. Затем функция пытается открыть данный URL c urllib. При возникновении HTTPError или URLError программа перехватывает ошибку и выводит на экран. На практике вам, скорее всего, придется зафиксировать ошибку и отправить письмо веб-мастеру или системному администратору.

Если ошибок нет, код спокойно выполняется. Вне зависимости от того, что произойдет, программа уходит в сон на 60 секунд. Это значит, что доступ к сайту будет раз за минуту. URL, используемый в примере, содержит ошибки. Ежеминутный вывод на консоли выглядит следующим образом:

HTTPError: 404 для http://www.google.com/py

Попробуйте обновить код, используя проверенный хороший URL, к примеру https://www.google.com/. После этого вы можете перезапустить программу и проверить, что изменилось. Также можно попробовать обновить код для отправки сообщения или записи об ошибке. Для получения более подробной информации можете ознакомиться со статьями отправка писем smtp и логирование.

Вызов sleep() с декораторами

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

Другим возможным случаем использования sleep() является необходимость проверки состояния пользовательского интерфейса во время автоматического теста. В зависимости от компьютера, на котором запускается тест, пользовательский интерфейс может грузиться быстрее или медленнее обычного. Это может изменить отображаемое на экране во время проверки программой чего-то.

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

Для добавления системного вызова sleep() в Python можно использовать декоратор в каждом из данных случаев. Разберем следующий пример:

import time
import urllib.request
import urllib.error
 
def sleep(timeout, retry=3):
    def the_real_decorator(function):
        def wrapper(*args, **kwargs):
            retries = 0
            while retries < retry:
                try:
                    value = function(*args, **kwargs)
                    if value is None:
                        return
                except:
                    print(f'Сон на {timeout} секунд')
                    time.sleep(timeout)
                    retries += 1
        return wrapper
    return the_real_decorator

sleep() является вашим декоратором. Он принимает значение timeout и количество раз для повтора retry, что по умолчанию равняется 3. Внутри sleep() есть другая функцияthe_real_decorator(), которая принимает декорируемую функцию.

В конечном итоге самая внутренняя функция wrapper() принимает аргументы и ключевые слова, которые вы передаете декорируемой функции. Здесь все и происходит! Используется цикл while, чтобы повторить вызов функции. Если возникла ошибка, вызывается time.sleep(), увеличивается счетчик попыток retries и повторяется попытка запуска функции.

Теперь переписывается uptime_bot()  для использования нового декоратора:

@sleep(3)
def uptime_bot(url):
    try:
        conn = urllib.request.urlopen(url)
    except urllib.error.HTTPError as e:
        # Отправка admin / log
        print(f'HTTPError: {e.code} для {url}')
        # Повторное поднятие ошибки исключения для декоратора
        raise urllib.error.HTTPError
    except urllib.error.URLError as e:
        # Отправка admin / log
        print(f'URLError: {e.code} для {url}')
        # Повторное поднятие ошибки исключения для декоратора
        raise urllib.error.URLError
    else:
        # Сайт поднят
        print(f'{url} поднят')
 
if __name__ == '__main__':
    url = 'http://www.google.com/py'
    uptime_bot(url)

Здесь вы декорируете uptime_bot() с помощью sleep() в 3 секунды. Вы также удалили оригинальный цикл while и старый вызов sleep(60)Декоратор теперь позаботится об этом.

Другое изменение состоит в добавлении raise внутри блоков, отвечающих за обработку исключений. Это нужно для правильной работы декоратора. Можно также написать декоратор, чтобы он отвечал за ошибки, однако ввиду того, что исключения касаются только urllib, может быть лучше сохранить декоратор в текущем состоянии. В таком случае он будет работать c более широким ассортиментом функций.

Декоратору можно добавить несколько улучшений. Если число попыток заканчивается, и он по-прежнему проваливается, тогда можно сделать так, чтобы он повторно вызвал последнюю ошибку. Декоратор подождет 3 секунды после последней неудачи, что не всегда нужно. Можете попробовать поэкспериментировать самостоятельно.

Вызов sleep() в потоках

Могут возникнуть ситуации, когда в Python требуется добавить вызов sleep() для потока. К примеру, запуск скрипта миграции для базы данных с миллионами записей. Здесь важно избежать простоя, а также не ждать дольше необходимого для завершения миграции, поэтому можно использовать потоки.

На заметку: Потоки являются одним из методов использования конкурентности в Python. Можно запустить несколько потоков одновременно, чтобы увеличить производительность приложения.

Чтобы клиенты не замечали какого-либо замедления, каждый поток должен работать в течение короткого периода времени, а затем уходить в сон. Есть два способа сделать это:

  1. Использовать time.sleep() как ранее;
  2. Использовать Event.wait() из модуля threading;

Начнем с разбора time.sleep().

Использование time.sleep() в threading

Python Logging Cookbook является хорошим примером использования time.sleep(). Модуль логирования logging является потоко-безопасным, поэтому в данном примере он будет полезнее, чем операторы print(). В основе следующего кода лежит данный пример:

import logging
import threading
import time
 
def worker(arg):
    while not arg["stop"]:
        logging.debug("рабочий поток вносится")
        time.sleep(1)
 
def main():
    logging.basicConfig(
        level=logging.DEBUG,
        format="%(relativeCreated)6d %(threadName)s %(message)s"
    )
    info = {"stop": False}
    thread = threading.Thread(target=worker, args=(info,))
    thread_two = threading.Thread(target=worker, args=(info,))
    thread.start()
    thread_two.start()
 
    while True:
        try:
            logging.debug("Добавление из главного потока")
            time.sleep(0.75)
        except KeyboardInterrupt:
            info["stop"] = True
            logging.debug('Остановка')
            break
    thread.join()
    thread_two.join()
 
if __name__ == "__main__":
    main()

Здесь для создания двух потоков используется модуль Python threading. Также создается объект входа, что будет вводить threadName в stdout. Затем начинаются оба потока и инициируется цикл для каждого входа из главного потока. Для фиксирования пользователя используется KeyboardInterrupt при нажатии ^Ctrl+C.

Попробуйте запустить вышеуказанный код в терминале. Ваш вывод должен походить на следующий:

2 Thread-1 рабочий поток вносится
     4 Thread-2 рабочий поток вносится
     4 MainThread Добавление из главного потока
   755 MainThread Добавление из главного потока
  1004 Thread-1 рабочий поток вносится
  1006 Thread-2 рабочий поток вносится
  1506 MainThread Добавление из главного потока
  2005 Thread-1 рабочий поток вносится
  2007 Thread-2 рабочий поток вносится
  2257 MainThread Добавление из главного потока
  3007 Thread-1 рабочий поток вносится
  3008 MainThread Добавление из главного потока

Когда каждый поток работает, а затем уходит в сон, выходные данные выводятся в консоль. Теперь, разобрав пример, вы сможете использовать данные концепции в своем собственном коде.

Использование Event.wait() в многопоточности Python

Модуль threading предоставляет Event(), которого можно использовать как time.sleep(). Однако преимущество Event() в том, что он более отзывчив. Причина в том, что когда событие установлено, программа сразу выходит из цикла. В Python с time.sleep() коду надо будет подождать завершения вызова sleep() до выхода из потока.

Причина, по которой здесь лучше использовать wait() в том, что он не блокируется, в то время, как time.sleep() блокируется. Это значит, что при использовании time.sleep() вы заблокируете выполнение основного потока, пока тот будет ждать завершения вызова sleep()wait() решает данную проблему. Более подробнее прочитать о принципах работы потоков можно в документации.

Далее показан пример добавления в Python вызова sleep() с Event.wait():

import logging
import threading
 
def worker(event):
    while not event.isSet():
        logging.debug("рабочий поток вносится")
        event.wait(1)
 
def main():
    logging.basicConfig(
        level=logging.DEBUG,
        format="%(relativeCreated)6d %(threadName)s %(message)s"
    )
    event = threading.Event()
 
    thread = threading.Thread(target=worker, args=(event,))
    thread_two = threading.Thread(target=worker, args=(event,))
    thread.start()
    thread_two.start()
 
    while not event.isSet():
        try:
            logging.debug("Добавление из главного потока")
            event.wait(0.75)
        except KeyboardInterrupt:
            event.set()
            break
 
if __name__ == "__main__":
    main()

В данном примере создается threading.Event() и передается к worker(). Вспомните, что в предыдущем примере вместо этого передавался словарь.

Затем устанавливаются циклы для проверки, настроено ли событие event. Если это не так, тогда код выведет сообщение и немного подождет перед повторной проверкой. Для установки события можно воспользоваться комбинацией ^Ctrl+C. Как только событие установлено, worker() вернется, и цикл оборвется, завершив программу.

Рассмотрите подробнее код выше. Как бы вы передали разное время сна каждому работающему потоку? Справитесь с задачей? Не бойтесь экспериментировать!

Вызов sleep() с Async IO на примерах

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

Модуль asyncio позволяет добавлять в Python вызов sleep() асинхронно.

Вот пример, данный в официальной документации Python:

import asyncio
 
async def main():
    print('Hello ...')
    await asyncio.sleep(1)
    print('... World!')
 
# Python 3.7+
asyncio.run(main())

В данном примере запускается main(), что погружается в сон на секунду между двумя вызовами print().

Вот более подробный пример из раздела о Сопрограммах и задачах документации asyncio:

import asyncio
import time
 
async def output(sleep, text):
    await asyncio.sleep(sleep)
    print(text)
 
async def main():
    print(f"Started: {time.strftime('%X')}")
    await output(1, 'First')
    await output(2, 'Second')
    await output(3, 'Third')
    print(f"Ended: {time.strftime('%X')}")
 
# Python 3.7+
asyncio.run(main())

В данном коде создается рабочий поток output(), что принимает количество секунд для sleep и выводит text. После этого используется ключевое слово Python await для ожидания запуска кода output(). Здесь требуется await, так как output() был отмечен как функция async, и теперь не может вызываться как обычная функция.

При запуске кода программа выполнит await 3 раза. Код будет ждать 1, 2 и 3 секунды, общее время ожидания равно 6 секундам. Можно также переписать код таким образом, чтобы задачи выполнялись параллельно:

import asyncio
import time
 
async def output(text, sleep):
    while sleep > 0:
        await asyncio.sleep(1)
        print(f'{text} counter: {sleep} seconds')
        sleep -= 1
 
async def main():
    task_1 = asyncio.create_task(output('First', 1))
    task_2 = asyncio.create_task(output('Second', 2))
    task_3 = asyncio.create_task(output('Third', 3))
    print(f"Started: {time.strftime('%X')}")
    await task_1
    await task_2
    await task_3                                 
    print(f"Ended: {time.strftime('%X')}")
 
if __name__ == '__main__':
    asyncio.run(main())

Теперь вы используете концепт задач, что можно создать через create_task(). При использовании задач в asyncio Python будет запускать задачи асинхронно. Таким образом, выполнение программы завершится через 3 секунды вместо 6.

Вызов sleep() в Tkinter и wxPython

Вызовы sleep() в Python можно добавить не только для приложений командной строки. При создании графического пользовательского интерфейса (GUI) периодически нужно добавлять отсрочки. К примеру, при создании приложения FTP для скачивания около миллиона файлов будет разумно добавить вызов sleep() между партиями, чтобы снизить нагрузку на сервер.

GUI код выполнит всю обработку в основном потоке, называемом циклом обработки событий, или event loop. При использовании time.sleep() внутри кода GUI заблокируется цикл обработки событий.

К счастью, помимо time.sleep(), можно использовать некоторые другие методы специально для этой задачи. Далее мы рассмотрим, как добавить вызовы sleep() в Tkinter и wxPython.

Метод after() — Погружение в сон для Tkinter

tkinter является частью стандартной библиотеки Python. В случае, если вы используете заранее установленную версию Python на Linux или Mac, он может быть вам недоступен. При получении ошибки ImportError стоит самостоятельно добавить его в систему. В том случае, если вы ранее установили Python сами, tkinter должен быть доступен.

Начнем с разбора примера, где используется time.sleep(). Запустите следующий код и посмотрите, что произойдет при неправильном добавлении вызова sleep() в Python:

import tkinter
import time
 
class MyApp:
    def __init__(self, parent):
        self.root = parent
        self.root.geometry("400x400")
        self.frame = tkinter.Frame(parent)
        self.frame.pack()
        b = tkinter.Button(text="click me", command=self.delayed)
        b.pack()
 
    def delayed(self):
        time.sleep(3)
 
if __name__ == "__main__":
    root = tkinter.Tk()
    app = MyApp(root)
    root.mainloop()

После запуска кода нажмите кнопку в GUI. Кнопка не будет реагировать три секунды, ожидая завершения sleep(). Если в приложении есть другие кнопки, на них тоже нельзя будет нажать. Закрыть приложение во время сна нельзя, так как оно не будет откликаться на событие закрытия.

Для должного погружения tkinter в сон потребуется использовать after():

import tkinter
 
class MyApp:
    def __init__(self, parent):
        self.root = parent
        self.root.geometry("400x400")
        self.frame = tkinter.Frame(parent)
        self.frame.pack()
        self.root.after(3000, self.delayed)
 
    def delayed(self):
        print('Я задержался')
 
if __name__ == "__main__":
    root = tkinter.Tk()
    app = MyApp(root)
    root.mainloop()

Здесь создается приложение, высота которого 400 пикселей, и ширина также 400 пикселей. На нем нет виджетов. Оно только показывает фрейм. Затем вызывается self.root.after(), где self.root является отсылкой к объекту Tk()after() принимает два аргумента:

  1. Количество миллисекунд для сна;
  2. Метод который вызовется после завершения сна.

В данном случае приложение выведет строку в стандартный поток вывода (stdout) через 3 секунды. Можно рассматривать after() как Tkinter-версию того же time.sleep(), только он добавляет способность вызова функции после завершения сна.

Данную функциональность можно использовать для улучшения работы пользователя. Добавив в Python вызов sleep(), можно ускорить процесс загрузки приложения, после чего начать какой-то длительный процесс. В таком случае пользователю не придется ждать открытия приложения.

Метод CallLater() — Погружение в сон для wxPython Python

Между wxPython и Tkinter есть два важных различия:

  1. В wxPython намного больше виджетов;
  2. wxPython стремится выглядеть нативным на любой платформе.

Фреймворк wxPython не поставляется с Python вместе, поэтому его нужно установить wxPython самостоятельно. Если вы не знакомы с wxPython, можете изучить курс из 55 уроков по wxPython.

Для добавления вызова sleep() в wxPython можно использовать wx.CallLater():

import wx
 
class MyFrame(wx.Frame):
    def __init__(self):
        super().__init__(parent=None, title='Привет, мир!')
        wx.CallLater(4000, self.delayed)
        self.Show()
 
    def delayed(self):
        print('Я задержался')
 
if __name__ == '__main__':
    app = wx.App()
    frame = MyFrame()
    app.MainLoop()

Здесь напрямую создается подкласс wx.Frame и затем вызывается wx.CallLater(). Данная функция принимает такие же параметры, что и after() в Tkinter:

  1. Количество миллисекунд для сна;
  2. Метод который вызовется сразу после завершения сна.

При запуске данного кода появится небольшое пустое окно без виджетов. Через 4 секунды в стандартном потоке вывода (stdout) появится строка 'Я задержался'.

Одним из преимуществ wx.CallLater() является его поточная безопасность. Данный метод можно использовать внутри потока для вызова функции, что в основном приложении wxPython.

Заключение

В данном руководстве вы познакомились с новой полезной техникой для работы в Python. Теперь вы знаете, как добавить задержку для ускорения работы приложений и предотвращения использование ими системных ресурсов. Вы также можете использовать вызовы Python sleep(), чтобы помочь графическому интерфейсу GUI работать более эффективно. Все это может усовершенствовать опыт пользователя при работе в приложении.

Подведем итоги. Основные, рассмотренные в статье инструменты, для добавления вызовов sleep() в Python:

  • time.sleep();
  • Декораторы;
  • Потоки;
  • asyncio;
  • Tkinter;
  • wxPython;

Теперь вы можете использовать полученные знания и погружать ваш код на Python в сон.

источник