是否有Python方式将可选功能与功能的主要用途脱钩?

时间:2019-10-24 15:03:29

标签: python

上下文

假设我有以下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语句以及所有变量与此功能相关,这个过程既繁琐又容易出错。

如果我希望在函数执行期间始终可以打印或不打印,情况将变得更糟,这导致我要么声明了两个极其相似的函数(一个带有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的简单情况下,也会导致function肿的(希望)不必要的复杂功能。


问题

是否有Python方法将打印功能与example_function的原始功能“分离”?

更笼统地说,有没有一种Python方式将可选功能与功能的主要用途脱钩?


到目前为止我尝试过的事情:

我目前发现的解决方案是使用回调进行解耦。例如,可以这样重写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

但是写,读和调试很麻烦。

4 个答案:

答案 0 :(得分: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
)

答案 1 :(得分:1)

我用一个简化的方法更新了答案:函数example_function被传递了一个带有默认值的回调或钩子,使得example_function不再需要测试以查看是否被传递:

hook=lambda *args, **kwargs: None

上面是一个返回None的lambda表达式,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实例并记录该消息。如果需要,您可以拥有所有丰富的日志记录,如果不需要,则可以简单得多。

答案 2 :(得分:1)

您可以定义一个封装debug_mode条件的函数,并将所需的可选函数及其参数传递给该函数(如建议的here):

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使用数字值将此概念扩展到几个调试级别。

答案 3 :(得分:1)

如果您只想使用打印语句,则可以使用装饰器,该装饰器添加一个参数,以打开/关闭打印到控制台。

这里是一个装饰器,它将仅关键字参数和默认值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 / logging命令。