如何冻结多个相关 class 方法的一些参数

How to freeze some arguments over multiple related class methods

什么是 "best" 方法来获取使用一些通用参数名称(假设意思相同)的函数集合,并创建一个包含这些函数但固定了一些关键参数值的对象,或者至少他们的默认值是固定的。

如果我要有一组函数来处理由一组属性定义的某些特定数据,我通常会使用 class,提供必须在__init__

但有时从函数开始更有意义,或者您没有选择,因为您正在使用其他人的代码。然而,您希望方便地固定一些参数的值,并且在操作时不必重复指定这些值,这很无聊且容易出错。这是 DRY 的一种形式。

如果只有一个函数,您只需使用 functools.partial。但是,当您拥有大量函数时,有什么好的方法可以做到这一点。

这是我如何使用一个参数进行操作的示例:

import inspect
from functools import partial

def mk_func_uses_arg_filt(argname):
    def func_uses_arg(obj):
        if callable(obj):
            try:
                if argname in inspect.signature(obj).parameters:
                    return True
            except ValueError:  # some functions don't have signatures (!?!)
                pass
        return False
    return func_uses_arg

class FixedArgFuncs(object):
    def __init__(self, argname, argval, funcs, only_if_func_uses_arg=True):
        func_uses_arg = mk_func_uses_arg_filt(argname)
        for func in funcs:
            if func_uses_arg(func):
                setattr(self, func.__name__, partial(func, **{argname: argval}))
            elif not only_if_func_uses_arg:
                setattr(self, func.__name__, func)

这是一个使用所有具有 "path" 参数的 os.path 函数的示例(我们将修复到本地主文件夹)。

import os.path
faf = FixedArgFuncs(argname='path', argval=os.path.expanduser('~'), 
                    funcs=filter(callable, os.path.__dict__.values()))
assert faf.exists() == True
assert faf.isfile() == False
print(list(faf.__dict__.keys()))

给我 ['exists', 'isfile', '_get_sep', 'islink', 'lexists', 'ismount', 'expanduser', 'expandvars', 'normpath', 'abspath', '_joinrealpath', 'relpath']

这并不完全令人满意,因为 (1) 根据我正在修复的参数的位置,我将被迫使用仅关键字调用,(2) 我想要一些看起来更像正常 class,具有我固定的属性的 self,随后由函数使用,(3) 它只是一个参数示例。

我猜巧妙地使用装饰器 and/or 描述符可以做一些不错的事情。

这是一个 class 装饰器的例子,它在 class 方法中搜索想要的参数,并用它们的部分方法版本替换这些方法。我正在冻结值为 2 的 y arg,以表明它不会触及未使用 y 的方法。

'''Freeze args in multiple functions wrapped as class methods,
   using a class decorator'''

import math
from functools import partialmethod
import inspect

class Calc:
    '''An imaginary Calc class with related methods that might share some args
    between them'''
    def add(self, x, y):
        return x + y
    def sub(self, x, y):
        return x - y
    def sqrt(self, x):
        return math.sqrt(x)

def partial_cls_arg_pairs(cls, arg_pairs):
    '''A class decorator to freeze arguments in class methods given
    as an arg_pairs iterable of argnames with argvalues'''
    cls_attrs = dict(cls.__dict__)
    freezed_cls_attrs = dict()
    for name, value in cls_attrs.items():
        if inspect.isfunction(value):
            for argname, argvalue in arg_pairs:
                if argname in inspect.signature(value).parameters:
                    print('Freezing args in {}.'.format(name))
                    value = partialmethod(value, **{argname:argvalue})
        freezed_cls_attrs[name] = value

    return type(cls.__name__, (object,), freezed_cls_attrs)

c1 = Calc()
print(c1.add(1,2))
print(c1.sub(3,2))
print(c1.sqrt(2))

print()

CalcY2 = partial_cls_arg_pairs(Calc, [('y', 2)])
c2 = CalcY2()
print(c2.add(1))
print(c2.sub(3))
print(c2.sqrt(2))

输出:

3
1
1.4142135623730951

Freezing args in add.
Freezing args in sub.
3
1
1.4142135623730951