контекст
Предположим, у меня есть следующий код Python:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
for _ in range(n_iters):
number = halve(number)
sum_all += number
return sum_all
ns = [1, 3, 12]
print(example_function(ns, 3))
example_function
Здесь мы просто просматриваем каждый из элементов в ns
списке и делим их пополам по 3 раза, одновременно накапливая результаты. Результат выполнения этого сценария просто:
2.0
Так как 1 / (2 ^ 3) * (1 + 3 + 12) = 2.
Теперь предположим, что (по любой причине, возможно, отладке или ведению журнала) я хотел бы отобразить некоторую информацию о промежуточных шагах, которые example_function
выполняет. Может быть, я бы тогда переписал эту функцию примерно так:
def example_function(numbers, n_iters):
sum_all = 0
for number in numbers:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
print(number)
sum_all += number
print('sum_all:', sum_all)
return sum_all
который теперь при вызове с теми же аргументами, что и раньше, выдает следующее:
Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0
Это достигает именно того, что я намеревался. Однако это немного противоречит принципу, согласно которому функция должна выполнять только одно действие, и теперь код для example_function
нее несколько длиннее и сложнее. Для такой простой функции это не проблема, но в моем контексте у меня есть довольно сложные функции, вызывающие друг друга, и операторы печати часто включают более сложные шаги, чем показано здесь, что приводит к существенному увеличению сложности моего кода (например, из моих функций было больше строк кода, связанных с ведением журнала, чем было строк, связанных с его фактическим назначением!).
Кроме того, если позже я решу, что больше не хочу печатать операторы в своей функции, мне придется пройти example_function
и удалить все print
операторы вручную вместе с любыми переменными, связанными с этой функцией, процесс, который является утомительным и ошибочным. -prone.
Ситуация становится еще хуже, если я хотел бы всегда иметь возможность печатать или не печатать во время выполнения функции, что приводит меня к объявлению двух чрезвычайно похожих функций (одна с print
операторами, одна без), что ужасно для поддержки, или определить что-то вроде:
def example_function(numbers, n_iters, debug_mode=False):
sum_all = 0
for number in numbers:
if debug_mode:
print('Processing number', number)
for i_iter in range(n_iters):
number = number/2
if debug_mode:
print(number)
sum_all += number
if debug_mode:
print('sum_all:', sum_all)
return sum_all
что приводит к раздутой и (мы надеемся) излишне сложной функции, даже в простом случае нашего example_function
.
Вопрос
Есть ли питонный способ «отделить» функциональность печати от оригинальной функциональности example_function
?
В более общем смысле, есть ли питонный способ отделить необязательную функциональность от основного назначения функции?
Что я уже пробовал:
Решение, которое я нашел на данный момент, использует обратные вызовы для развязки. Например, можно переписать example_function
так:
def example_function(numbers, n_iters, callback=None):
sum_all = 0
for number in numbers:
for i_iter in range(n_iters):
number = number/2
if callback is not None:
callback(locals())
sum_all += number
return sum_all
и затем определение функции обратного вызова, которая выполняет любую функцию печати, которую я хочу:
def print_callback(locals):
print(locals['number'])
и звонит example_function
так:
ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)
который затем выводит:
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
Это успешно отделяет функциональность печати от базовой функциональности example_function
. Однако основная проблема этого подхода заключается в том, что функция обратного вызова может быть запущена только в определенной части example_function
(в данном случае сразу после деления текущего числа на два), и вся печать должна выполняться именно там. Это иногда вынуждает проект функции обратного вызова быть довольно сложным (и делает невозможным выполнение некоторых действий).
Например, если бы вы хотели добиться точно такого же типа печати, как я делал в предыдущей части вопроса (показывая, какое число обрабатывается, вместе с соответствующими ему половинками), результирующий обратный вызов будет выглядеть так:
def complicated_callback(locals):
i_iter = locals['i_iter']
number = locals['number']
if i_iter == 0:
print('Processing number', number*2)
print(number)
if i_iter == locals['n_iters']-1:
print('sum_all:', locals['sum_all']+number)
что приводит к тому же результату, что и раньше:
Processing number 1.0
0.5
0.25
0.125
sum_all: 0.125
Processing number 3.0
1.5
0.75
0.375
sum_all: 0.5
Processing number 12.0
6.0
3.0
1.5
sum_all: 2.0
но это боль писать, читать и отлаживать.
logging
модуль Pythonlogging
модуль поможет здесь. Хотя мой вопрос используетprint
операторы при настройке контекста, я на самом деле ищу решение о том, как отделить любой тип необязательной функциональности от основной цели функции. Например, может быть, я хочу, чтобы функция отображала вещи во время работы. В этом случае я считаю, чтоlogging
модуль даже не будет применим.logging
демонстрируют предложения по использованию ), но не на то, как отделить произвольный код.Ответы:
Если вам нужна функциональность вне функции для использования данных внутри функции, то внутри функции должна быть какая-то система обмена сообщениями для поддержки этого. Обойти это невозможно. Локальные переменные в функциях полностью изолированы снаружи.
Модуль регистрации довольно хорош в настройке системы сообщений. Он не ограничен только распечаткой сообщений журнала - с помощью пользовательских обработчиков вы можете делать все что угодно.
Добавление системы сообщений аналогично вашему примеру обратного вызова, за исключением того, что места, где обрабатываются «обратные вызовы» (обработчики журналов), могут быть указаны где угодно внутри
example_function
(путем отправки сообщений в регистратор). Любые переменные, которые нужны обработчикам журналирования, могут быть указаны при отправке сообщения (вы все еще можете использоватьlocals()
, но лучше явно объявить переменные, которые вам нужны).Новый
example_function
может выглядеть так:Это указывает три места, где сообщения могут быть обработаны. Само
example_function
поexample_function
себе это не будет делать ничего, кроме функциональности самого. Он не будет ничего распечатывать или выполнять какие-либо другие функции.Чтобы добавить дополнительные функции в
example_function
, вам нужно будет добавить обработчики в логгер.Например, если вы хотите выполнить некоторую печать из отправленных переменных (аналогично вашему
debugging
примеру), то вы определяете пользовательский обработчик и добавляете его вexample_function
регистратор:Если вы хотите отобразить результаты на графике, просто определите другой обработчик:
Вы можете определить и добавить любые обработчики, которые вы хотите. Они будут полностью отделены от функциональности
example_function
и могут использовать только те переменные, которые имexample_function
дает.Хотя ведение журнала можно использовать в качестве системы обмена сообщениями, может быть, лучше перейти к полноценной системе обмена сообщениями, такой как PyPubSub , чтобы она не мешала выполнению каких-либо действий по ведению журнала:
источник
logging
модуля, действительно более организован и понятен, чем то, что я предложил, используя операторыprint
иif
выражения. Тем не менее, он не отделяет функциональность печати от основной функциональностиexample_function
функции. То есть основная проблема, связанная с одновременнымexample_function
выполнением двух действий , делает его код более сложным, чем хотелось бы.example_function
теперь есть только одна функциональность, а печать (или любая другая функциональность, которую мы хотели бы иметь) происходит вне ее.example_function
отделен от функции печати - единственная добавленная функция - отправка сообщений. Он похож на ваш пример обратного вызова, за исключением того, что он отправляет только определенные переменные, которые вы хотите, а не всеlocals()
. Это зависит от обработчиков журналов (которые вы подключаете к регистратору где-то еще) для выполнения дополнительных функций (печать, создание графиков и т. Д.). Вам не нужно вообще прикреплять обработчики, и в этом случае ничего не произойдет при отправке сообщений. Я обновил свой пост, чтобы сделать это более понятным.example_function
. Спасибо за разъяснение сейчас! Мне очень нравится этот ответ, единственная цена, которую платят, это дополнительная сложность передачи сообщений, которая, как вы упомянули, кажется неизбежной. Спасибо также за ссылку на PyPubSub, которая привела меня к чтению паттерна наблюдателя .Если вы хотите использовать только операторы print, вы можете использовать декоратор, который добавляет аргумент, который включает / выключает печать на консоли.
Вот декоратор, который добавляет аргумент только для ключевого слова и значение по умолчанию
verbose=False
для любой функции, обновляет строку документации и подпись. Вызов функции как есть возвращает ожидаемый результат. Вызов функции сverbose=True
включит операторы печати и вернет ожидаемый результат. Это дает дополнительное преимущество, заключающееся в том, что не нужно вводить каждый отпечатокif debug:
блоком.Обертывание вашей функции теперь позволяет включать / выключать функции печати с помощью
verbose
.Примеры:
При осмотре
example_function
вы увидите обновленную документацию. Поскольку ваша функция не имеет строки документации, это просто то, что находится в декораторе.С точки зрения философии кодирования. Наличие функции, которая не вызывает побочных эффектов, является парадигмой функционального программирования. Python может быть функциональным языком, но он не предназначен исключительно для этого. Я всегда проектирую свой код, ориентируясь на пользователя.
Если добавление опции распечатывать этапы расчета является преимуществом для пользователя, то с этим НИЧЕГО не так. С точки зрения дизайна, вы будете застревать с добавлением где-нибудь команд печати / регистрации.
источник
print
иif
операторы. Кроме того, ему удается фактически отделить часть функциональности печати отexample_function
основных функциональных возможностей России, что было очень приятно (мне также понравилось, что декоратор автоматически добавляет строку документации, приятное прикосновение). Тем не менее, он не полностью отделяет функциональность печати от основной функциональностиexample_function
: вам все равно нужно добавитьprint
операторы и любую сопутствующую логику в тело функции.example_function
тела, так что его сложность остается связанной только со сложностью его основной функциональности. В моем реальном применении всего этого у меня есть основная функция, которая уже значительно сложнее. Добавление операторов печати / печати / записи в его тело делает его чудовищем, которое было довольно сложно поддерживать и отлаживать.Вы можете определить функцию, инкапсулирующую
debug_mode
условие, и передать нужную необязательную функцию и ее аргументы этой функции (как предлагается здесь ):Обратите внимание, что,
debug_mode
очевидно, должно быть присвоено значение перед вызовомDEBUG
.Конечно, можно вызывать функции, отличные от
print
.Вы также можете расширить эту концепцию до нескольких уровней отладки, используя числовое значение для
debug_mode
.источник
if
повсеместного использования выписок, а также облегчает включение и выключение печати. Тем не менее, он не отделяет функции печати от основных функцийexample_function
. Сравните это, например, с моим предложением обратного вызова. Используя обратные вызовы, у example_function теперь есть только одна функциональность, а материал для печати (или любой другой функционал, который мы хотели бы иметь) происходит вне его.Я обновил свой ответ с упрощением: функции
example_function
передается одиночный обратный вызов или ловушка со значением по умолчанию, так чтоexample_function
больше не нужно проверять, прошел ли он или нет:Выше приведено лямбда-выражение, которое возвращает
None
иexample_function
может вызывать это значение по умолчанию дляhook
любой комбинации позиционных и ключевых параметров в различных местах внутри функции.В приведенном ниже примере, я заинтересован только в
"end_iteration"
и"result
"событий.Печать:
Функция ловушки может быть настолько простой или сложной, как вы хотите. Здесь выполняется проверка типа события и простая печать. Но он может получить
logger
экземпляр и зарегистрировать сообщение. Вы можете иметь все богатство регистрации, если вам это нужно, но простота, если вы этого не делаете.источник
example_function
.if
утверждений :)