【问题标题】:Is there a pythonic way to decouple optional functionality from a function's main purpose?有没有一种 Pythonic 方法可以将可选功能与函数的主要目的分离?
【发布时间】:2020-02-20 22:59:07
【问题描述】:

上下文

假设我有以下 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 的简单情况下也是如此。


问题

有没有一种 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

但是编写、阅读和调试都很痛苦。

【问题讨论】:

  • 查看 python logging 模块
  • @Chris_Rands 是对的.. 使用日志记录模块.. 这样您就可以打开和关闭日志记录.. 使用以下链接。 stackoverflow.com/questions/2266646/…
  • 我看不出logging 模块在这里有什么帮助。尽管我的问题在设置上下文时使用了print 语句,但我实际上是在寻找一种解决方案,以解决如何将任何类型的可选功能与函数的主要目的分离。例如,也许我想要一个函数在运行时绘制事物。在那种情况下,我相信logging 模块甚至不适用。
  • @Pythonic 是一个形容词,描述了 Python 的语法/样式/结构/用法,以维护 Python 的哲学。这不是句法或设计规则,而是需要负责任地维护以生成干净且可维护的 Python 代码库的方法。在您的情况下,只有几行跟踪或打印语句会增加可维护性的价值,然后拥有它;不要对自己太苛刻。考虑上述任何您认为理想的方法。
  • 这个问题太笼统了。我们或许能够解决特定问题(正如使用 logging 的建议所展示的那样),但不能解决如何分离任意代码。

标签: python


【解决方案1】:

如果您需要函数外部的功能来使用函数内部的数据,那么函数内部需要一些消息传递系统来支持这一点。 没有办法解决这个问题。函数中的局部变量与外界完全隔离。

日志模块非常擅长设置消息系统。 它不仅限于打印日志消息 - 使用自定义处理程序,您可以做任何事情。

添加消息系统类似于您的回调示例,除了可以在 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。您使用logging 模块提供的代码确实比我使用printif 语句所建议的更有组织性和可维护性。但是,它不会将打印功能与example_function 函数的主要功能分离。也就是说,让example_function 同时做两件事的主要问题仍然存在,这使得它的代码比我想要的更复杂。
  • 将此与我的回调建议进行比较。使用回调,example_function 现在只有一个功能,而打印内容(或我们想要的任何其他功能)发生在它之外。
  • 嗨@JLagana。我的example_function 与打印功能分离——该功能唯一添加的功能是发送消息。它类似于您的回调示例,只是它只发送您想要的特定变量,而不是所有locals()。由日志处理程序(您将其附加到其他地方的记录器)来执行额外的功能(打印、绘图等)。您根本不需要附加任何处理程序,在这种情况下,发送消息时不会发生任何事情。我已经更新了我的帖子以使这一点更清楚。
  • 我的立场是正确的,您的示例确实将打印功能与example_function 的主要功能分离。感谢您现在说得特别清楚!我真的很喜欢这个答案,唯一付出的代价是传递消息的复杂性,就像你提到的那样,这似乎是不可避免的。还要感谢对 PyPubSub 的引用,这使我阅读了observer pattern
【解决方案2】:

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

这是一个装饰器,它将仅关键字参数和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 可以是一种函数式语言,但它并不是专门为这种方式设计的。我总是在设计代码时考虑到用户。

如果添加打印计算步骤的选项对用户有利,那么这样做没有任何问题。从设计的角度来看,您将无法在某处添加打印/日志记录命令。

【讨论】:

  • 谢谢你的回答,詹姆斯。提供的代码确实比我提出的使用printif 语句的代码更有条理和可维护性。此外,它实际上将打印的部分功能与example_function 的主要功能分离,这非常好(我也喜欢装饰器自动附加到文档字符串,很好的接触)。但是,它并没有将打印功能与example_function 的主要功能完全分离:您仍然需要将print 语句和任何随附的逻辑添加到函数的主体中。
  • 将此与我的回调建议进行比较。使用回调,example_function 现在只有一个功能,而打印内容(或我们希望拥有的任何其他功能)发生在它之外。
  • 最后,我们同意,如果打印计算步骤对用户有利,那么我将无法在某处添加打印命令。然而,我希望它们位于example_function 的主体之外,因此它的复杂性仅与其主要功能的复杂性相关。在我对所有这些的实际应用中,我有一个已经非常复杂的 main 函数。在其主体中添加打印/绘图/记录语句使其成为一个很难维护和调试的野兽。
【解决方案3】:

您可以定义一个封装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 的数值将此概念扩展到多个调试级别。

【讨论】:

  • 感谢您的回答,格德。实际上,您的解决方案无需到处使用if 语句,并且还可以更轻松地打开和关闭打印。但是,它不会将打印功能与example_function 的主要功能分离。将此与例如我的回调建议进行比较。使用回调,example_function 现在只有一个功能,而打印内容(或我们希望拥有的任何其他功能)发生在它之外。
【解决方案4】:

我已经通过简化更新了我的答案:函数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 实例并记录消息。如果您需要,您可以拥有日志记录的所有丰富性,但如果您不需要,则简单。

【讨论】:

  • 感谢您的回答,罗纳德。实际上,扩展回调思想以在函数的不同部分执行回调(并将上下文变量传递给它们)的想法似乎是最好的方法。它使编写回调变得更加容易,并且以合理的价格增加了example_function 的复杂性。
  • 使用默认值很好;这是删除大量if 语句的简单方法:)
猜你喜欢
  • 2016-05-11
  • 2020-09-02
  • 2010-10-08
  • 1970-01-01
  • 1970-01-01
  • 2012-04-30
  • 2022-11-17
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多