How to freeze some arguments over multiple related class methods

為{幸葍}努か 提交于 2021-01-28 11:29:49

问题


What's the "best" method to take a collection of functions that use some common argument names (assumed to mean the same thing), and make an object that holds these functions, but with some key argument values fixed, or at least their defaults fixed.

If I'm going to have a set of functions meant to work on some specific data defined by a set of attributes, I'd usually use a class, providing the attributes that must be set in the __init__.

But sometimes it makes more sense to start with functions, or you didn't have the choice because you're using someone else's code. Yet you'd like the convenience to fix some argument's values, and operate without having to specify those values repeatedly as you operate, which is boring and prone to error. It's a form of DRY.

If you had one function, you'd just use functools.partial. But what are good ways to do this when you have a big collection of functions.

Here's an example of how I do it with one argument:

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)

Here's an example using all os.path functions that have a "path" argument (which we'll fix to the local home folder).

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()))

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

This is not totally satisfactory because (1) depending on where the argument I'm fixing is, I'd be forced to use keyword-only calls, (2) I'd want something that looks more like a normal class, with a self having the attributes I fixed, subsequently used by the functions, (3) it's only a one argument example.

I'm guessing clever use of decorators and/or descriptors could do something nice.


回答1:


This is an example of a class decorator that searches the class methods for wanted args and replaces the methods with their partialmethod versions. I am freezing the y arg with value 2 to show also that it doesn't touch methods where y is not used.

'''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))

Output:

3
1
1.4142135623730951

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


来源:https://stackoverflow.com/questions/56611213/how-to-freeze-some-arguments-over-multiple-related-class-methods

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!