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

JLa*_*ana 11 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))
Run Code Online (Sandbox Code Playgroud)

example_function这里只是遍历ns列表中的每个元素,并将它们减半3次,同时累积结果。运行此脚本的输出很简单:

2.0
Run Code Online (Sandbox Code Playgroud)

由于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
Run Code Online (Sandbox Code Playgroud)

现在,当使用与以前相同的参数调用它时,将输出以下内容:

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
Run Code Online (Sandbox Code Playgroud)

这完全达到了我的预期。但是,这有点违背一个函数只能做一件事的原则,现在,该函数的代码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
Run Code Online (Sandbox Code Playgroud)

即使在我们的简单情况下,这也会导致功能过大且(不必要)不必要的复杂功能example_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
Run Code Online (Sandbox Code Playgroud)

然后定义一个回调函数来执行我想要的任何打印功能:

def print_callback(locals):
    print(locals['number'])
Run Code Online (Sandbox Code Playgroud)

并这样调用example_function

ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)
Run Code Online (Sandbox Code Playgroud)

然后输出:

0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
Run Code Online (Sandbox Code Playgroud)

这成功地将打印功能与的基本功能脱钩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)
Run Code Online (Sandbox Code Playgroud)

结果与之前完全相同:

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
Run Code Online (Sandbox Code Playgroud)

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

RPa*_*mer 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
Run Code Online (Sandbox Code Playgroud)

这指定了可以处理消息的三个位置。就其本身而言,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())
Run Code Online (Sandbox Code Playgroud)

如果你想在图表上绘制结果,那么只需定义另一个处理程序:

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())
)
Run Code Online (Sandbox Code Playgroud)

您可以定义并添加您想要的任何处理程序。它们将完全独立于 的功能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
)
Run Code Online (Sandbox Code Playgroud)