Программно создавать функцию спецификации
Для собственного развлечения мне было интересно, как добиться следующего:
functionA = make_fun(['paramA', 'paramB'])
functionB = make_fun(['arg1', 'arg2', 'arg3'])
эквивалентно
def functionA(paramA, paramB):
print(paramA)
print(paramB)
def functionB(arg1, arg2, arg3):
print(arg1)
print(arg2)
print(arg3)
Это означает, что требуется следующее поведение:
functionA(3, paramB=1) # Works
functionA(3, 2, 1) # Fails
functionB(0) # Fails
В центре внимания вопроса находится переменная argspec - мне удобно создавать тело функции, используя обычные методы декоратора.
Для тех, кто заинтересован, я пытался программно создавать классы, подобные следующим. Опять же, трудность заключается в генерации метода __init__
с программными параметрами - остальная часть класса выглядит просто, используя декоратор или, возможно, метакласс.
class MyClass:
def __init__(self, paramA=None, paramB=None):
self._attr = ['paramA', 'paramB']
for a in self._attr:
self.__setattr__(a, None)
def __str__(self):
return str({k:v for (k,v) in self.__dict__.items() if k in self._attributes})
Ответы
Ответ 1
Вы можете использовать exec
для создания объекта функции из строки, содержащей код Python:
def make_fun(parameters):
exec("def f_make_fun({}): pass".format(', '.join(parameters)))
return locals()['f_make_fun']
Пример:
>>> f = make_fun(['a', 'b'])
>>> import inspect
>>> print(inspect.signature(f).parameters)
OrderedDict([('a', <Parameter at 0x1024297e0 'a'>), ('b', <Parameter at 0x102429948 'b'>)])
Если вам нужна дополнительная функциональность (например, значения аргументов по умолчанию), необходимо адаптировать строку, содержащую код, и сделать так, чтобы она представляла нужную сигнатуру функции.
Отказ от ответственности: как указано ниже, важно, чтобы вы проверяли содержимое parameters
и чтобы полученная строка кода Python была безопасна для передачи в exec
. Вы должны создать parameters
самостоятельно или установить ограничения, чтобы пользователь не мог создать вредоносное значение для parameters
.
Ответ 2
Одно из возможных решений с использованием класса:
def make_fun(args_list):
args_list = args_list[:]
class MyFunc(object):
def __call__(self, *args, **kwargs):
if len(args) > len(args_list):
raise ValueError('Too many arguments passed.')
# At this point all positional arguments are fine.
for arg in args_list[len(args):]:
if arg not in kwargs:
raise ValueError('Missing value for argument {}.'.format(arg))
# At this point, all arguments have been passed either as
# positional or keyword.
if len(args_list) - len(args) != len(kwargs):
raise ValueError('Too many arguments passed.')
for arg in args:
print(arg)
for arg in args_list[len(args):]:
print(kwargs[arg])
return MyFunc()
functionA = make_fun(['paramA', 'paramB'])
functionB = make_fun(['arg1', 'arg2', 'arg3'])
functionA(3, paramB=1) # Works
try:
functionA(3, 2, 1) # Fails
except ValueError as e:
print(e)
try:
functionB(0) # Fails
except ValueError as e:
print(e)
try:
functionB(arg1=1, arg2=2, arg3=3, paramC=1) # Fails
except ValueError as e:
print(e)
Ответ 3
Вот еще один способ сделать это с помощью functools.wrap
, который сохраняет подпись и docstring, по крайней мере, на python 3. Трюк заключается в создании подписи и документации в фиктивных функциях, которые никогда не вызываются. Вот несколько примеров.
Основной пример
import functools
def wrapper(f):
@functools.wraps(f)
def template(common_exposed_arg, *other_args, common_exposed_kwarg=None, **other_kwargs):
print("\ninside template.")
print("common_exposed_arg: ", common_exposed_arg, ", common_exposed_kwarg: ", common_exposed_kwarg)
print("other_args: ", other_args, ", other_kwargs: ", other_kwargs)
return template
@wrapper
def exposed_func_1(common_exposed_arg, other_exposed_arg, common_exposed_kwarg=None):
"""exposed_func_1 docstring: this dummy function exposes the right signature"""
print("this won't get printed")
@wrapper
def exposed_func_2(common_exposed_arg, common_exposed_kwarg=None, other_exposed_kwarg=None):
"""exposed_func_2 docstring"""
pass
exposed_func_1(10, -1, common_exposed_kwarg='one')
exposed_func_2(20, common_exposed_kwarg='two', other_exposed_kwarg='done')
print("\n" + exposed_func_1.__name__)
print(exposed_func_1.__doc__)
И результат:
>> inside template.
>> common_exposed_arg: 10 , common_exposed_kwarg: one
>> other_args: (-1,) , other_kwargs: {}
>>
>> inside template.
>> common_exposed_arg: 20 , common_exposed_kwarg: two
>> other_args: () , other_kwargs: {'other_exposed_kwarg': 'done'}
>>
>> exposed_func_1
>> exposed_func_1 docstring: this dummy function exposes the right signature
Вызов inspect.signature(exposed_func_1).parameters
возвращает требуемую подпись. Однако использование inspect.getfullargspec(exposed_func_1)
все равно возвращает подпись template
. По крайней мере, если вы поместите какие-либо аргументы, общие для всех функций, которые вы хотите сделать в определении template
, они появятся.
Если по какой-то причине это плохая идея, пожалуйста, дайте мне знать!
Более сложный пример
И вы можете стать намного сложнее, чем это, накладывая больше оболочек и определяя более четкое поведение во внутренней функции:
import functools
def wrapper(inner_func, outer_arg, outer_kwarg=None):
def wrapped_func(f):
@functools.wraps(f)
def template(common_exposed_arg, *other_args, common_exposed_kwarg=None, **other_kwargs):
print("\nstart of template.")
print("outer_arg: ", outer_arg, " outer_kwarg: ", outer_kwarg)
inner_arg = outer_arg * 10 + common_exposed_arg
inner_func(inner_arg, *other_args, common_exposed_kwarg=common_exposed_kwarg, **other_kwargs)
print("template done")
return template
return wrapped_func
# Build two examples.
def inner_fcn_1(hidden_arg, exposed_arg, common_exposed_kwarg=None):
print("inner_fcn, hidden_arg: ", hidden_arg, ", exposed_arg: ", exposed_arg, ", common_exposed_kwarg: ", common_exposed_kwarg)
def inner_fcn_2(hidden_arg, common_exposed_kwarg=None, other_exposed_kwarg=None):
print("inner_fcn_2, hidden_arg: ", hidden_arg, ", common_exposed_kwarg: ", common_exposed_kwarg, ", other_exposed_kwarg: ", other_exposed_kwarg)
@wrapper(inner_fcn_1, 1)
def exposed_function_1(common_exposed_arg, other_exposed_arg, common_exposed_kwarg=None):
"""exposed_function_1 docstring: this dummy function exposes the right signature """
print("this won't get printed")
@wrapper(inner_fcn_2, 2, outer_kwarg="outer")
def exposed_function_2(common_exposed_arg, common_exposed_kwarg=None, other_exposed_kwarg=None):
""" exposed_2 doc """
pass
Это немного многословно, но дело в том, что существует много гибкости в том, где возникают динамические входы от вас (программиста) при использовании этого для создания функций, и так с тем, где открытые входные данные (от пользователя функция).