Есть ли питонный способ отделить дополнительную функциональность от основного назначения функции?

11

контекст

Предположим, у меня есть следующий код 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

но это боль писать, читать и отлаживать.

JLagana
источник
6
Проверьте loggingмодуль Python
Chris_Rands
@Chris_Rands прав ... используйте модуль регистрации ... таким образом, вы можете включать и выключать вход в систему ... используйте следующую ссылку. stackoverflow.com/questions/2266646/…
Ятиш Кадам
2
Я не вижу, как loggingмодуль поможет здесь. Хотя мой вопрос использует printоператоры при настройке контекста, я на самом деле ищу решение о том, как отделить любой тип необязательной функциональности от основной цели функции. Например, может быть, я хочу, чтобы функция отображала вещи во время работы. В этом случае я считаю, что loggingмодуль даже не будет применим.
JLagana
3
@Pythonic - это прилагательное, которое описывает синтаксис / стиль / структуру / использование python для поддержки философии Python. Это не синтаксическое или дизайнерское правило, а подход, который необходимо поддерживать ответственно для создания чистой и поддерживаемой базы кода Python. В вашем случае наличие нескольких строк операторов трассировки или печати добавляет значения к удобству сопровождения, а затем имеет их; не будь жестким с самим собой. Рассмотрим любой из вышеупомянутых подходов, которые вы считаете идеальными.
Наир
1
Этот вопрос слишком широк. Мы могли бы ответить на конкретные вопросы (как loggingдемонстрируют предложения по использованию ), но не на то, как отделить произвольный код.
Чепнер

Ответы:

4

Если вам нужна функциональность вне функции для использования данных внутри функции, то внутри функции должна быть какая-то система обмена сообщениями для поддержки этого. Обойти это невозможно. Локальные переменные в функциях полностью изолированы снаружи.

Модуль регистрации довольно хорош в настройке системы сообщений. Он не ограничен только распечаткой сообщений журнала - с помощью пользовательских обработчиков вы можете делать все что угодно.

Добавление системы сообщений аналогично вашему примеру обратного вызова, за исключением того, что места, где обрабатываются «обратные вызовы» (обработчики журналов), могут быть указаны где угодно внутри example_function (путем отправки сообщений в регистратор). Любые переменные, которые нужны обработчикам журналирования, могут быть указаны при отправке сообщения (вы все еще можете использовать locals(), но лучше явно объявить переменные, которые вам нужны).

Новый example_functionможет выглядеть так:

import logging

# Helper function
def send_message(logger, level=logging.DEBUG, **kwargs):
  logger.log(level, "", extra=kwargs)

# Your example function with logging information
def example_function(numbers, n_iters):
    logger = logging.getLogger("example_function")
    # If you have a logging system set up, then we don't want the messages sent here to propagate to the root logger
    logger.propagate = False
    sum_all = 0
    for number in numbers:
        send_message(logger, action="processing", number=number)
        for i_iter in range(n_iters):
            number = number/2
            send_message(logger, action="division", i_iter=i_iter, number=number)
        sum_all += number
        send_message(logger, action="sum", sum=sum_all)
    return sum_all

Это указывает три места, где сообщения могут быть обработаны. Само example_functionпо example_functionсебе это не будет делать ничего, кроме функциональности самого. Он не будет ничего распечатывать или выполнять какие-либо другие функции.

Чтобы добавить дополнительные функции в example_function, вам нужно будет добавить обработчики в логгер.

Например, если вы хотите выполнить некоторую печать из отправленных переменных (аналогично вашему debuggingпримеру), то вы определяете пользовательский обработчик и добавляете его в example_functionрегистратор:

class ExampleFunctionPrinter(logging.Handler):
    def emit(self, record):
        if record.action == "processing":
          print("Processing number {}".format(record.number))
        elif record.action == "division":
          print(record.number)
        elif record.action == "sum":
          print("sum_all: {}".format(record.sum))

example_function_logger = logging.getLogger("example_function")
example_function_logger.setLevel(logging.DEBUG)
example_function_logger.addHandler(ExampleFunctionPrinter())

Если вы хотите отобразить результаты на графике, просто определите другой обработчик:

class ExampleFunctionDivisionGrapher(logging.Handler):
    def __init__(self, grapher):
      self.grapher = grapher

    def emit(self, record):
      if record.action == "division":
        self.grapher.plot_point(x=record.i_iter, y=record.number)

example_function_logger = logging.getLogger("example_function")
example_function_logger.setLevel(logging.DEBUG)
example_function_logger.addHandler(
    ExampleFunctionDivisionGrapher(MyFancyGrapherClass())
)

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

Хотя ведение журнала можно использовать в качестве системы обмена сообщениями, может быть, лучше перейти к полноценной системе обмена сообщениями, такой как PyPubSub , чтобы она не мешала выполнению каких-либо действий по ведению журнала:

from pubsub import pub

# Your example function
def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        pub.sendMessage("example_function.processing", number=number)
        for i_iter in range(n_iters):
            number = number/2
            pub.sendMessage("example_function.division", i_iter=i_iter, number=number)
        sum_all += number
        pub.sendMessage("example_function.sum", sum=sum_all)
    return sum_all

# If you need extra functionality added in, then subscribe to the messages.
# Otherwise nothing will happen, other than the normal example_function functionality.
def handle_example_function_processing(number):
    print("Processing number {}".format(number))

def handle_example_function_division(i_iter, number):
    print(number)

def handle_example_function_sum(sum):
    print("sum_all: {}".format(sum))

pub.subscribe(
    "example_function.processing",
    handle_example_function_processing
)
pub.subscribe(
    "example_function.division",
    handle_example_function_division
)
pub.subscribe(
    "example_function.sum",
    handle_example_function_sum
)
RPalmer
источник
Спасибо за ответ, RPalmer. Код, который вы предоставили с помощью loggingмодуля, действительно более организован и понятен, чем то, что я предложил, используя операторы printи ifвыражения. Тем не менее, он не отделяет функциональность печати от основной функциональности example_functionфункции. То есть основная проблема, связанная с одновременным example_functionвыполнением двух действий , делает его код более сложным, чем хотелось бы.
JLagana
Сравните это, например, с моим предложением обратного вызова. Используя обратные вызовы, example_functionтеперь есть только одна функциональность, а печать (или любая другая функциональность, которую мы хотели бы иметь) происходит вне ее.
JLagana
Привет @JLagana. Мой example_functionотделен от функции печати - единственная добавленная функция - отправка сообщений. Он похож на ваш пример обратного вызова, за исключением того, что он отправляет только определенные переменные, которые вы хотите, а не все locals(). Это зависит от обработчиков журналов (которые вы подключаете к регистратору где-то еще) для выполнения дополнительных функций (печать, создание графиков и т. Д.). Вам не нужно вообще прикреплять обработчики, и в этом случае ничего не произойдет при отправке сообщений. Я обновил свой пост, чтобы сделать это более понятным.
RPalmer
Я исправлюсь, ваш пример отделил функции печати от основных функций example_function. Спасибо за разъяснение сейчас! Мне очень нравится этот ответ, единственная цена, которую платят, это дополнительная сложность передачи сообщений, которая, как вы упомянули, кажется неизбежной. Спасибо также за ссылку на PyPubSub, которая привела меня к чтению паттерна наблюдателя .
JLagana
1

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

Вот декоратор, который добавляет аргумент только для ключевого слова и значение по умолчанию verbose=Falseдля любой функции, обновляет строку документации и подпись. Вызов функции как есть возвращает ожидаемый результат. Вызов функции с verbose=Trueвключит операторы печати и вернет ожидаемый результат. Это дает дополнительное преимущество, заключающееся в том, что не нужно вводить каждый отпечаток if debug:блоком.

from functools import wraps
from inspect import cleandoc, signature, Parameter
import sys
import os

def verbosify(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        def toggle(*args, verbose=False, **kwargs):
            if verbose:
                _stdout = sys.stdout
            else:
                _stdout = open(os.devnull, 'w')
            with redirect_stdout(_stdout):
                return func(*args, **kwargs)
        return toggle(*args, **kwargs)
    # update the docstring
    doc = '\n\nOption:\n-------\nverbose : bool\n    '
    doc += 'Turns on/off print lines in the function.\n '
    wrapper.__doc__ = cleandoc(wrapper.__doc__ or '\n') + doc
    # update the function signature to include the verbose keyword
    sig = signature(func)
    param_verbose = Parameter('verbose', Parameter.KEYWORD_ONLY, default=False)
    sig_params = tuple(sig.parameters.values()) + (param_verbose,)
    sig = sig.replace(parameters=sig_params)
    wrapper.__signature__ = sig
    return wrapper

Обертывание вашей функции теперь позволяет включать / выключать функции печати с помощью verbose.

@verbosify
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

Примеры:

example_function([1,3,12], 3)
# returns:
2.0

example_function([1,3,12], 3, verbose=True)
# returns/prints:
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
2.0

При осмотре example_functionвы увидите обновленную документацию. Поскольку ваша функция не имеет строки документации, это просто то, что находится в декораторе.

help(example_function)
# prints:
Help on function example_function in module __main__:

example_function(numbers, n_iters, *, verbose=False)
    Option:
    -------
    verbose : bool
        Turns on/off print lines in the function.

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

Если добавление опции распечатывать этапы расчета является преимуществом для пользователя, то с этим НИЧЕГО не так. С точки зрения дизайна, вы будете застревать с добавлением где-нибудь команд печати / регистрации.

Джеймс
источник
Спасибо за ответ, Джеймс. Предоставленный код действительно более организован и поддерживается, чем тот, который я предложил, который использует printи ifоператоры. Кроме того, ему удается фактически отделить часть функциональности печати от example_functionосновных функциональных возможностей России, что было очень приятно (мне также понравилось, что декоратор автоматически добавляет строку документации, приятное прикосновение). Тем не менее, он не полностью отделяет функциональность печати от основной функциональности example_function: вам все равно нужно добавить printоператоры и любую сопутствующую логику в тело функции.
JLagana
Сравните это, например, с моим предложением обратного вызова. Используя обратные вызовы, у example_function теперь есть только одна функциональность, а материал для печати (или любой другой функционал, который мы хотели бы иметь) происходит вне его.
JLagana
И наконец, мы согласны с тем, что если печать шагов вычисления приносит пользу пользователю, то я застряну с добавлением команд печати где-нибудь. Однако я хочу, чтобы они были вне example_functionтела, так что его сложность остается связанной только со сложностью его основной функциональности. В моем реальном применении всего этого у меня есть основная функция, которая уже значительно сложнее. Добавление операторов печати / печати / записи в его тело делает его чудовищем, которое было довольно сложно поддерживать и отлаживать.
JLagana
1

Вы можете определить функцию, инкапсулирующую debug_modeусловие, и передать нужную необязательную функцию и ее аргументы этой функции (как предлагается здесь ):

def DEBUG(function, *args):
    if debug_mode:
        function(*args)

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        DEBUG(print, 'Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            DEBUG(print, number)
        sum_all += number
        DEBUG(print, 'sum_all:', sum_all)
    return sum_all

ns = [1, 3, 12]
debug_mode = True
print(example_function(ns, 3))

Обратите внимание, что, debug_modeочевидно, должно быть присвоено значение перед вызовом DEBUG.

Конечно, можно вызывать функции, отличные от print.

Вы также можете расширить эту концепцию до нескольких уровней отладки, используя числовое значение для debug_mode.

Герд
источник
Спасибо за ответ, Герд. Действительно, ваше решение избавляет от необходимости ifповсеместного использования выписок, а также облегчает включение и выключение печати. Тем не менее, он не отделяет функции печати от основных функций example_function. Сравните это, например, с моим предложением обратного вызова. Используя обратные вызовы, у example_function теперь есть только одна функциональность, а материал для печати (или любой другой функционал, который мы хотели бы иметь) происходит вне его.
JLagana
1

Я обновил свой ответ с упрощением: функции example_functionпередается одиночный обратный вызов или ловушка со значением по умолчанию, так что example_functionбольше не нужно проверять, прошел ли он или нет:

hook=lambda *args, **kwargs: None

Выше приведено лямбда-выражение, которое возвращает Noneи example_functionможет вызывать это значение по умолчанию для hookлюбой комбинации позиционных и ключевых параметров в различных местах внутри функции.

В приведенном ниже примере, я заинтересован только в "end_iteration"и "result"событий.

def example_function(numbers, n_iters, hook=lambda *args, **kwargs: None):
    hook("init")
    sum_all = 0
    for number in numbers:
        for i_iter in range(n_iters):
            hook("start_iteration", number)
            number = number/2
            hook("end_iteration", number)
        sum_all += number
    hook("result", sum_all)
    return sum_all

if __name__ == '__main__':
    def my_hook(event_type, *args):
        if event_type in ["end_iteration", "result"]:
            print(args[0])

    print('sum = ', example_function([1, 3, 12], 3))
    print('sum = ', example_function([1, 3, 12], 3, my_hook))

Печать:

sum =  2.0
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
sum =  2.0

Функция ловушки может быть настолько простой или сложной, как вы хотите. Здесь выполняется проверка типа события и простая печать. Но он может получить loggerэкземпляр и зарегистрировать сообщение. Вы можете иметь все богатство регистрации, если вам это нужно, но простота, если вы этого не делаете.

Бубу
источник
Спасибо за ответ, Рональд. Идея расширения идеи обратного вызова для выполнения обратных вызовов в различных частях функции (и передачи им переменной контекста), кажется, действительно лучший путь. Это значительно облегчает написание обратных вызовов и по разумной цене в усложнении example_function.
JLagana
Приятное прикосновение со значением по умолчанию; это простой способ удалить много ifутверждений :)
JLagana