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


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语句以及与此功能相关的所有变量,这既繁琐又出错-易于。

如果我想在函数执行期间始终有可能打印或不打印,情况会变得更糟,这导致我要么声明两个非常相似的函数(一个带有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

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


6
签出python logging模块
— Chris_Rands

@Chris_Rands是正确的..使用日志记录模块..那样您可以打开和关闭日志记录..使用以下链接。stackoverflow.com/questions/2266646/...
— Yatish噶当派

2
我看不到该logging模块将如何帮助您。尽管我的问题是print在设置上下文时使用语句,但实际上我在寻找一种解决方案,该方法如何将任何类型的可选功能与功能的主要用途脱钩。例如,也许我想要一个函数在运行时绘制事物。在那种情况下,我认为该logging模块甚至不适用。
— JLagana '19

3
@Pythonic是形容词,描述了python语法/样式/结构/用法,以维护Python的哲学。这不是语法或设计规则,而是需要负责任地坚持以产生干净且可维护的python代码库的方法。在您的情况下,跟踪或打印语句的行很少会增加可维护性的价值,然后再具有可维护性;别对自己强硬。考虑您认为理想的上述任何方法。
— Nair

1
这个问题太广泛了。我们也许能够解决特定的问题(如所使用的建议logging所示),但不能解决如何分离任意代码的问题。
— chepner

Answers:


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。logging实际上,您使用该模块提供的代码比我提议使用print和if声明的代码确实更具组织性和可维护性。但是,它不会使打印功能与功能的主要功能脱钩example_function。也就是说,example_function一次完成两件事的主要问题仍然存在,这使得它的代码比我想要的复杂。
— JLagana

将此与例如我的回调建议进行比较。使用回调,example_function现在仅具有一种功能,而打印内容(或我们希望具有的其他任何功能)发生在其外部。
— JLagana '19

嗨@JLagana。My example_function与打印功能脱钩了-该功能唯一添加的功能是发送消息。它与您的回调示例类似,不同之处在于它仅发送所需的特定变量,而不是所有locals()。日志处理程序(您将其附加到其他地方的记录器上)要执行额外的功能(打印,绘图等)。您根本不需要附加任何处理程序,在这种情况下,发送消息时不会发生任何事情。我已经更新了我的帖子,以使其更加清晰。
— RPalmer

经纠正后,您的示例确实使的打印功能与的主要功能脱钩example_function。感谢您让它变得更加清晰!我真的很喜欢这个答案,唯一要付出的代价就是传递消息的复杂性,就像您提到的那样,这似乎是不可避免的。也感谢您对PyPubSub的引用,这使我阅读了观察者模式。
— JLagana '19

1

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

这是一个装饰器,它将仅关键字参数和的默认值添加verbose=False到任何函数,更新文档字符串和签名。按原样调用该函数将返回预期的输出。调用with 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命令。


谢谢你的回答,詹姆斯。所提供的代码确实比我提出的使用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 '19

1

我已经简化了答案:函数example_function通过单个回调或带有默认值的钩子传递,这样example_function就不再需要测试是否通过了:

hook=lambda *args, **kwargs: None

上面是一个lambda表达式,该表达式返回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 '19

带有默认值的触摸效果很好;这是删除许多if陈述的简单方法:)
— JLagana
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.