Как заморозить некоторые аргументы для нескольких связанных методов класса - PullRequest
1 голос
/ 15 июня 2019

Каков «лучший» метод для сбора набора функций, использующих некоторые общие имена аргументов (предполагается, что они означают одно и то же), и создания объекта, который содержит эти функции, но с фиксированными значениями некоторых ключевых аргументов или, по крайней мере,их значения по умолчанию исправлены.

Если у меня будет набор функций, предназначенных для работы с некоторыми конкретными данными, определенными набором атрибутов, я обычно использую класс, предоставляющий атрибуты, которые должны быть установлены в __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-функции, которые имеют аргумент «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) я бы хотел что-то, что больше похоже на обычный класс, с атрибутом self, имеющим фиксированные атрибуты,впоследствии используется функциями, (3) это только один аргумент в качестве примера.

Я предполагаю, что умное использование декораторов и / или дескрипторов может сделать что-то хорошее.

1 Ответ

0 голосов
/ 15 июня 2019

Это пример декоратора класса, который ищет методы класса для искомых аргументов и заменяет методы их частичными версиями метода.Я замораживаю аргумент y со значением 2, чтобы показать, что он не касается методов, в которых 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
...