Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет, Хаброжители.
По плану у нас руководство по Python.
Разнообразие возможностей современного Python становится испытанием для разработчиков всех уровней. Как программисту на старте карьеры понять, с чего начать, чтобы это испытание не стало для него непосильным? Как опытному разработчику Python понять, эффективен или нет его стиль программирования? Как перейти от изучения отдельных возможностей к мышлению на Python на более глубоком уровне? «Python. Исчерпывающее руководство» отвечает на эти, а также на многие другие актуальные вопросы.
Эта книга делает акцент на основополагающих возможностях Python (3.6 и выше), а примеры кода демонстрируют «механику» языка и учат структурировать программы, чтобы их было проще читать, тестировать и отлаживать. Дэвид Бизли знакомит нас со своим уникальным взглядом на то, как на самом деле работает этот язык программирования.
Перед вами практическое руководство, в котором компактно изложены такие фундаментальные темы программирования, как абстракции данных, управление программной логикой, структура программ, функции, объекты и модули, лежащие в основе проектов Python любого масштаба.
Одна серьезная проблема с функциями обратного вызова связана с передачей аргументов передаваемой функции. Возьмем написанную ранее функцию after():
В этом коде func() фиксируется для вызова без аргументов. Если вы захотите передать дополнительные аргументы, можно попытаться сделать так:
Здесь функция add(2, 3) выполняется немедленно, возвращая 5. Потом after() вызывает ошибку, пытаясь выполнить 5(). Это определенно не то, чего вы ожидали. Но на первый взгляд нет очевидного способа заставить программу работать при вызове add() с нужными аргументами.
Это указывает на более масштабную проблему проектирования, связанную с использованием функций и функциональным программированием вообще, — композицию функций. Когда функции по-разному сочетаются, нужно думать, как соединяются их входные и выходные данные. Это не всегда просто.
В нашем случае одно из возможных решений основано на упаковке вычисления в функцию с нулем аргументов при помощи lambda:
Такие маленькие функции с нулем аргументов иногда называют преобразователями (thunk). Это выражение, которое будет вычислено позднее, когда будет вызвано как функция с нулем аргументов. Этот способ может стать приемом общего назначения, позволяющим отложить вычисление любого выражения на будущее. Поместите выражение в lambda и вызовите функцию, когда вам понадобится значение.
Вместо использования лямбда-функции можно воспользоваться вызовом functools.partial() для создания частично вычисленной функции:
partial() создает вызываемый объект, один или несколько аргументов которого уже были заданы и кешированы. Это может быть удобным способом привести неподходящие функции в соответствие с ожидаемыми сигнатурами в обратных вызовах и других возможных применениях. Несколько примеров использования partial():
partial() и lambda могут использоваться для похожих целей. Но между этими двумя решениями есть важные семантические различия. С partial() вычисление и связывание аргументов происходит при первом определении частичной функции. При использовании лямбда-функции с нулем аргументов вычисление и связывание аргументов выполняется во время фактического выполнения лямбда-функции (все вычисления откладываются):
Частичные выражения вычисляются полностью, поэтому вызов partial() создает объекты, способные сериализоваться в последовательности байтов, сохраняться в файлах и даже передаваться по сети (например, средствами модуля стандартной библиотеки pickle). С лямбда-функциями это невозможно. Поэтому в приложениях с передачей функций (возможно, с интерпретаторами Python, работающими в разных процессах или на разных компьютерах) решение c partial() оказывается более гибким.
Замечу, что применение частичных функций тесно связано с концепцией, называемой каррированием (currying). Это прием функционального программирования, где функция с несколькими аргументами выражается цепочкой вложенных функций с одним аргументом:
Этот прием не относится к общепринятому стилю программирования Python, и причины для его практического применения встречаются редко. Но иногда слово «каррирование» мелькает в разговорах с программистами, которые провели много времени, разбираясь в таких вещах, как лямбда-счисление. Этот метод обработки нескольких аргументов был назван в честь знаменитого логика Хаскелла Карри. Полезно знать, что это такое, например, если вы столкнетесь с группой функциональных программистов, ожесточенно спорящих на каком-нибудь светском мероприятии.
Вернемся к исходной проблеме передачи аргументов. Другой вариант передачи аргументов функции обратного вызова основан на их передаче в отдельных аргументах внешней вызывающей функции. Рассмотрим следующую версию функции after():
Заметьте, что передача ключевых аргументов func() не поддерживается. Это было сделано намеренно. Одна из проблем ключевых аргументов в том, что имена аргументов заданной функции могут конфликтовать с уже используемыми именами (то есть seconds и func). Ключевые аргументы могут быть зарезервированы для передачи параметров самой функции after():
Но не все потеряно. Задать ключевые аргументы для func() можно при помощи partial():
Если вы хотите, чтобы функция after() получала ключевые аргументы, безопасным решением может стать использование только позиционных аргументов:
Есть и другой настораживающий факт: after() представляет два разных вызова функций, объединенных вместе. Возможно, проблема передачи аргументов может быть решена декомпозицией на две функции:
Теперь конфликты между аргументами after() и func полностью исключены. Но такие решения могут породить конфликты с вашими коллегами, которые будут читать ваш код.
В прошлом разделе не упоминалась еще одна проблема: возвращение результатов вычислений. Рассмотрим измененную функцию after():
Она работает, но есть неочевидные граничные случаи, возникающие из-за того, что в ней задействованы две разные функции: сама функция after() и переданный обратный вызов func.
Одна из сложностей связана с обработкой исключений. Опробуйте следующие два примера:
В обоих случаях выдается ошибка TypeError, но по разным причинам и в разных функциях. Первая ошибка обусловлена проблемой в самой функции after(): функции time.sleep() передается неправильный аргумент. Вторая ошибка возникает из-за проблемы с выполнением функции обратного вызова func(*args).
Есть несколько вариантов, чтобы различить эти два случая. В одном из них используются цепочки исключений. Идея в том, чтобы упаковать ошибки из обратного вызова особым способом, позволяющим обрабатывать их отдельно от других ошибок:
Измененный код отделяет ошибки от переданного обратного вызова в отдельную категорию исключений. Он используется примерно так:
При возникновении проблемы с выполнением самой функции after() это исключение будет распространяться наружу без перехвата. С другой стороны, проблемы, связанные с выполнением переданной функции обратного вызова, будут перехватываться, и программа будет сообщать о них исключением CallbackError.
Вся эта схема неочевидна. Но на практике обработка ошибок — достаточно сложная тема. Такой подход позволяет точнее управлять распределением ответственности и упрощает документирование поведения after(). Если с обратным вызовом возникают проблемы, программа всегда сообщает о ней в виде CallbackError.
Другой вариант — упаковка результата функции обратного вызова в экземпляр-результат, содержащий и значение, и ошибку. Например, класс можно определить так:
Далее используйте этот класс для возвращения результатов из функции after():
Второй способ основан на выделении выдачи результата функции обратного вызова в отдельный шаг. При возникновении проблемы с after() о ней будет сообщено немедленно. Если возникнет проблема с обратным вызовом func(), уведомление о ней будет отправлено при попытке пользователя получить результат вызовом метода result().
Этот стиль упаковки результата в специальный экземпляр для распаковки в будущем все чаще встречается в современных языках программирования. Одна из причин в том, что он упрощает проверку типов. Если вам понадобится включить аннотацию типа в after(), ее поведение полностью определено — она всегда возвращает Result и ничего другого:
Этот паттерн еще не так часто встречается в коде Python, но он регулярно возникает при работе с примитивами синхронизации (потоками и процессами). Например, экземпляры Future ведут себя так при работе с пулами потоков:
Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Python
По плану у нас руководство по Python.
Разнообразие возможностей современного Python становится испытанием для разработчиков всех уровней. Как программисту на старте карьеры понять, с чего начать, чтобы это испытание не стало для него непосильным? Как опытному разработчику Python понять, эффективен или нет его стиль программирования? Как перейти от изучения отдельных возможностей к мышлению на Python на более глубоком уровне? «Python. Исчерпывающее руководство» отвечает на эти, а также на многие другие актуальные вопросы.
Эта книга делает акцент на основополагающих возможностях Python (3.6 и выше), а примеры кода демонстрируют «механику» языка и учат структурировать программы, чтобы их было проще читать, тестировать и отлаживать. Дэвид Бизли знакомит нас со своим уникальным взглядом на то, как на самом деле работает этот язык программирования.
Перед вами практическое руководство, в котором компактно изложены такие фундаментальные темы программирования, как абстракции данных, управление программной логикой, структура программ, функции, объекты и модули, лежащие в основе проектов Python любого масштаба.
5.16. ПЕРЕДАЧА АРГУМЕНТОВ ФУНКЦИЯМ ОБРАТНОГО ВЫЗОВА
Одна серьезная проблема с функциями обратного вызова связана с передачей аргументов передаваемой функции. Возьмем написанную ранее функцию after():
import time
def after(seconds, func):
time.sleep(seconds)
func()
В этом коде func() фиксируется для вызова без аргументов. Если вы захотите передать дополнительные аргументы, можно попытаться сделать так:
def add(x, y):
print(f'{x} + {y} -> {x+y}')
return x + y
after(10, add(2, 3)) # Ошибка: add() вызывается немедленно
Здесь функция add(2, 3) выполняется немедленно, возвращая 5. Потом after() вызывает ошибку, пытаясь выполнить 5(). Это определенно не то, чего вы ожидали. Но на первый взгляд нет очевидного способа заставить программу работать при вызове add() с нужными аргументами.
Это указывает на более масштабную проблему проектирования, связанную с использованием функций и функциональным программированием вообще, — композицию функций. Когда функции по-разному сочетаются, нужно думать, как соединяются их входные и выходные данные. Это не всегда просто.
В нашем случае одно из возможных решений основано на упаковке вычисления в функцию с нулем аргументов при помощи lambda:
after(10, lambda: add(2, 3))
Такие маленькие функции с нулем аргументов иногда называют преобразователями (thunk). Это выражение, которое будет вычислено позднее, когда будет вызвано как функция с нулем аргументов. Этот способ может стать приемом общего назначения, позволяющим отложить вычисление любого выражения на будущее. Поместите выражение в lambda и вызовите функцию, когда вам понадобится значение.
Вместо использования лямбда-функции можно воспользоваться вызовом functools.partial() для создания частично вычисленной функции:
from functools import partial
after(10, partial(add, 2, 3))
partial() создает вызываемый объект, один или несколько аргументов которого уже были заданы и кешированы. Это может быть удобным способом привести неподходящие функции в соответствие с ожидаемыми сигнатурами в обратных вызовах и других возможных применениях. Несколько примеров использования partial():
def func(a, b, c, d):
print(a, b, c, d)
f = partial(func, 1, 2) # Зафиксировать a=1, b=2
f(3, 4) # func(1, 2, 3, 4)
f(10, 20) # func(1, 2, 10, 20)
g = partial(func, 1, 2, d=4) # Зафиксировать a=1, b=2, d=4
g(3) # func(1, 2, 3, 4)
g(10) # func(1, 2, 10, 4)
partial() и lambda могут использоваться для похожих целей. Но между этими двумя решениями есть важные семантические различия. С partial() вычисление и связывание аргументов происходит при первом определении частичной функции. При использовании лямбда-функции с нулем аргументов вычисление и связывание аргументов выполняется во время фактического выполнения лямбда-функции (все вычисления откладываются):
>>> def func(x, y):
... return x + y
...
>>> a = 2
>>> b = 3
>>> f = lambda: func(a, b)
>>> g = partial(func, a, b)
>>> a = 10
>>> b = 20
>>> f() # Использует текущие значения a, b
30
>>> g() # Использует текущие значения a, b
5
>>>
Частичные выражения вычисляются полностью, поэтому вызов partial() создает объекты, способные сериализоваться в последовательности байтов, сохраняться в файлах и даже передаваться по сети (например, средствами модуля стандартной библиотеки pickle). С лямбда-функциями это невозможно. Поэтому в приложениях с передачей функций (возможно, с интерпретаторами Python, работающими в разных процессах или на разных компьютерах) решение c partial() оказывается более гибким.
Замечу, что применение частичных функций тесно связано с концепцией, называемой каррированием (currying). Это прием функционального программирования, где функция с несколькими аргументами выражается цепочкой вложенных функций с одним аргументом:
# Функция с тремя аргументами
def f(x, y, z):
return x + y + z
# Каррированная версия
def fc(x):
return lambda y: (lambda z: x + y + z)
# Пример использования
a = f(2, 3, 4) # Функция с тремя аргументами
b = fc(2)(3)(4) # Каррированная версия
Этот прием не относится к общепринятому стилю программирования Python, и причины для его практического применения встречаются редко. Но иногда слово «каррирование» мелькает в разговорах с программистами, которые провели много времени, разбираясь в таких вещах, как лямбда-счисление. Этот метод обработки нескольких аргументов был назван в честь знаменитого логика Хаскелла Карри. Полезно знать, что это такое, например, если вы столкнетесь с группой функциональных программистов, ожесточенно спорящих на каком-нибудь светском мероприятии.
Вернемся к исходной проблеме передачи аргументов. Другой вариант передачи аргументов функции обратного вызова основан на их передаче в отдельных аргументах внешней вызывающей функции. Рассмотрим следующую версию функции after():
def after(seconds, func, *args):
time.sleep(seconds)
func(*args)
after(10, add, 2, 3) # Вызывает add(2, 3) через 10 секунд
Заметьте, что передача ключевых аргументов func() не поддерживается. Это было сделано намеренно. Одна из проблем ключевых аргументов в том, что имена аргументов заданной функции могут конфликтовать с уже используемыми именами (то есть seconds и func). Ключевые аргументы могут быть зарезервированы для передачи параметров самой функции after():
def after(seconds, func, *args, debug=False):
time.sleep(seconds)
if debug:
print('About to call', func, args)
func(*args)
Но не все потеряно. Задать ключевые аргументы для func() можно при помощи partial():
after(10, partial(add, y=3), 2)
Если вы хотите, чтобы функция after() получала ключевые аргументы, безопасным решением может стать использование только позиционных аргументов:
def after(seconds, func, debug=False, /, *args, **kwargs):
time.sleep(seconds)
if debug:
print('About to call', func, args, kwargs)
func(*args, **kwargs)
after(10, add, 2, y=3)
Есть и другой настораживающий факт: after() представляет два разных вызова функций, объединенных вместе. Возможно, проблема передачи аргументов может быть решена декомпозицией на две функции:
def after(seconds, func, debug=False):
def call(*args, **kwargs):
time.sleep(seconds)
if debug:
print('About to call', func, args, kwargs)
func(*args, **kwargs)
return call
after(10, add)(2, y=3)
Теперь конфликты между аргументами after() и func полностью исключены. Но такие решения могут породить конфликты с вашими коллегами, которые будут читать ваш код.
5.17. ВОЗВРАЩЕНИЕ РЕЗУЛЬТАТОВ ИЗ ОБРАТНЫХ ВЫЗОВОВ
В прошлом разделе не упоминалась еще одна проблема: возвращение результатов вычислений. Рассмотрим измененную функцию after():
def after(seconds, func, *args):
time.sleep(seconds)
return func(*args)
Она работает, но есть неочевидные граничные случаи, возникающие из-за того, что в ней задействованы две разные функции: сама функция after() и переданный обратный вызов func.
Одна из сложностей связана с обработкой исключений. Опробуйте следующие два примера:
after("1", add, 2, 3) # Ошибка: TypeError (ожидается целое число)
after(1, add, "2", 3) # Ошибка: TypeError (конкатенация int
# со str невозможна)
В обоих случаях выдается ошибка TypeError, но по разным причинам и в разных функциях. Первая ошибка обусловлена проблемой в самой функции after(): функции time.sleep() передается неправильный аргумент. Вторая ошибка возникает из-за проблемы с выполнением функции обратного вызова func(*args).
Есть несколько вариантов, чтобы различить эти два случая. В одном из них используются цепочки исключений. Идея в том, чтобы упаковать ошибки из обратного вызова особым способом, позволяющим обрабатывать их отдельно от других ошибок:
class CallbackError(Exception):
pass
def after(seconds, func, *args):
time.sleep(seconds)
try:
return func(*args)
except Exception as err:
raise CallbackError('Callback function failed') from err
Измененный код отделяет ошибки от переданного обратного вызова в отдельную категорию исключений. Он используется примерно так:
try:
r = after(delay, add, x, y)
except CallbackError as err:
print("It failed. Reason", err.__cause__)
При возникновении проблемы с выполнением самой функции after() это исключение будет распространяться наружу без перехвата. С другой стороны, проблемы, связанные с выполнением переданной функции обратного вызова, будут перехватываться, и программа будет сообщать о них исключением CallbackError.
Вся эта схема неочевидна. Но на практике обработка ошибок — достаточно сложная тема. Такой подход позволяет точнее управлять распределением ответственности и упрощает документирование поведения after(). Если с обратным вызовом возникают проблемы, программа всегда сообщает о ней в виде CallbackError.
Другой вариант — упаковка результата функции обратного вызова в экземпляр-результат, содержащий и значение, и ошибку. Например, класс можно определить так:
class Result:
def __init__(self, value=None, exc=None):
self._value = value
self._exc = exc
def result(self):
if self._exc:
raise self._exc
else:
return self._value
Далее используйте этот класс для возвращения результатов из функции after():
def after(seconds, func, *args):
time.sleep(seconds)
try:
return Result(value=func(*args))
except Exception as err:
return Result(exc=err)
# Пример использования:
r = after(1, add, 2, 3)
print(r.result()) # Выводит 5
s = after("1", add, 2, 3) # Немедленно выдает TypeError -
# недопустимый аргумент sleep().
t = after(1, add, "2", 3) # Возвращает "Result"
print(t.result()) # Выдает TypeError
Второй способ основан на выделении выдачи результата функции обратного вызова в отдельный шаг. При возникновении проблемы с after() о ней будет сообщено немедленно. Если возникнет проблема с обратным вызовом func(), уведомление о ней будет отправлено при попытке пользователя получить результат вызовом метода result().
Этот стиль упаковки результата в специальный экземпляр для распаковки в будущем все чаще встречается в современных языках программирования. Одна из причин в том, что он упрощает проверку типов. Если вам понадобится включить аннотацию типа в after(), ее поведение полностью определено — она всегда возвращает Result и ничего другого:
def after(seconds, func, *args) -> Result:
...
Этот паттерн еще не так часто встречается в коде Python, но он регулярно возникает при работе с примитивами синхронизации (потоками и процессами). Например, экземпляры Future ведут себя так при работе с пулами потоков:
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(16)
r = pool.submit(add, 2, 3) # Возвращает Future
print(r.result()) # Распаковывает результат Future
Об авторе
Дэвид Бизли — автор книг Python Essential Reference, 4-е издание (Addison-Wesley, 2010) и Python Cookbook, 3-е издание (O’Reilly, 2013). Сейчас ведет учебные курсы повышения квалификации в своей компании Dabeaz LLC (www.dabeaz.com). Он пишет на Python и преподает его с 1996 года.
Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Python