首页 > 解决方案 > 如何构建 python 装饰函数以使其更易于维护?

问题描述

真的厌倦了通过在我的代码中装饰函数来管道值,虽然我认为使用 splat (**) 或编写装饰器可能有适合我的 python 解决方案,但我不确定最好的类型检查器友好的解决方案是。我对维护代码感到沮丧

@dataclass
class FooBar:
    foo: FooOut
    bar: BarOut

def foo(arg1: str, arg2: int) -> FooOut:
    do_something()
    return inner_func(arg1=arg1, arg2=arg2).foo

def bar(arg1: str, arg2: int) -> BarOut:
    bar = inner_func(arg1=arg1, arg2=arg2).bar
    bar2 = do_some_mutation(bar, arg1)
    return bar2

def inner_func(arg1: str, arg2: int) -> FooBar:
    # do something with the arguments
    return FooBar(foo=some_value, bar=some_other_value)

现在问题来了,什么inner_func时候需要一个烦人的新arg3: Optional[str]参数。我想做的最后一件事是arg3: Optional[str] = None到处添加,然后手动传递 arg3。

一种解决方案是将参数字典转换为对象。这没关系,只是它需要修改所有呼叫站点。一个有点混乱的变体是创建新函数foo_new(in: InputParams),这些函数接受输入对象并将旧函数委托给新函数。然后我们最终会弃用旧功能。

另一种解决方案是将签名转换为要使用的每个函数**kwargs。除了让静态类型检查器满意之外,这将起作用。为了让某人知道有哪些有效参数,需要深入研究inner_func以查看使用了什么。

我认为理想的解决方案是如果我可以做类似的事情

@dataclass
class InputParams:
    arg1: str
    arg2: int
    arg3: Optional[str]

def bar(input: **InputParams) -> BarOut:
    bar = inner_func(input)
    bar2 = do_some_mutation(bar, input.arg1)
    return bar

你会怎么做?非常感谢!

标签: python

解决方案


似乎您几乎已经回答了您的问题,您的所有功能似乎都充当inner_func. 您在 bar 中使用的事实arg1有点令人难过,并且使事情变得比实际情况更复杂(例如,您不能忽略这些论点)。

处理这个问题的尝试通常有点失控,但至少应该是可重用的(希望对此的罐装解决方案可以替换我的代码块)。查看了包装因子、生成器和类,最终得到了这个。

示例用法:

def inner(a, b=2, c=3):
    return a * b + c


@preprocessor(inner)
def double_a(arguments):
    arguments.arguments['a'] *= 2

@postprocessor(inner)
def add_b_to_result(res, arguments):
    return res + arguments.arguments['b']


def clean_double_a(arguments):
    arguments.arguments['a'] *= 2


def clean_add_b_to_result(res, arguments):
    return res + arguments.arguments['b']


both = processor(inner, pre=clean_double_a, post=clean_add_b_to_result)

@preprocessor(inner)
def both_alt(arguments):
    return clean_double_a(arguments)

@both_alt.postprocessor
def both_alt_post(res, arguments):
    return clean_add_b_to_result(res, arguments)


def self_tests():
    assert inner(1)           == 1 * 2 + 3
    assert double_a(1)        == 2 * 2 + 3
    assert add_b_to_result(1) == 1 * 2 + 3 + 2
    assert both(1)            == 2 * 2 + 3 + 2
    assert both_alt(1)        == 2 * 2 + 3 + 2
    print('Tests complete')

self_tests()  # Tests complete

帮助库:

from functools import partial, update_wrapper
from inspect import signature


class Processor(object):
    def __init__(self, func, pre=None, post=None):
        self.__func = func
        self.__sig = signature(func)
        self.__preprocessor = pre
        self.__postprocessor = post
        if pre or post:
            # Make this instance look like the first of pre/post
            self.__wrapped_by_pre_post = True
            update_wrapper(self, pre or post)
        else:
            # Make this instance look like the function
            self.__wrapped_by_pre_post = False
            update_wrapper(self, func)

    def __call__(self, *args, **kwargs):
        arguments = self.__sig.bind(*args, **kwargs)
        arguments.apply_defaults()
        if self.__preprocessor is not None:
            new_args = self.__preprocessor(arguments)
            if new_args is not None:
                arguments = new_args
        res = self.__func(*arguments.args, **arguments.kwargs)
        if self.__postprocessor is not None:
            res = self.__postprocessor(res, arguments)
        return res

    def preprocessor(self, func):
        if self.__preprocessor is not None:
            raise RuntimeError('defined multiple preprocessors')
        self.__preprocessor = func
        self.__wrap_if_first(func)
        return self

    def postprocessor(self, func):
        if self.__postprocessor is not None:
            raise RuntimeError('defined multiple postprocessors')
        self.__postprocessor = func
        self.__wrap_if_first(func)
        return self

    def __wrap_if_first(self, func):
        # Make it look like the first to process, like getter/setters
        if not self.__wrapped_by_pre_post:
            update_wrapper(self, func)
            self.__wrapped_by_pre_post = True


processor = Processor


def preprocessor(func):
    return Processor(func).preprocessor


def postprocessor(func):
    return Processor(func).postprocessor

推荐阅读