Установить сигнатуру функции в Python - PullRequest
26 голосов
/ 11 сентября 2009

Предположим, у меня есть общая функция f. Я хочу программно создать функцию f2, которая ведет себя так же, как f, но имеет настроенную подпись.

Подробнее

Учитывая список l и словарь d, я хочу иметь возможность:

  • Установить аргументы без ключевого слова для f2 в строки в l
  • Установите ключевые аргументы f2 для ключей в d и значения по умолчанию для значений d

т. Предположим, у нас есть

l=["x", "y"]
d={"opt":None}

def f(*args, **kwargs):
    #My code

Тогда я бы хотел функцию с подписью:

def f2(x, y, opt=None):
    #My code

Особый вариант использования

Это просто упрощенная версия моего конкретного варианта использования. Я приведу это только в качестве примера.

Мой фактический вариант использования (упрощенный) выглядит следующим образом. У нас есть общая функция инициации:

def generic_init(self,*args,**kwargs):
    """Function to initiate a generic object"""
    for name, arg in zip(self.__init_args__,args):
        setattr(self, name, arg)
    for name, default in self.__init_kw_args__.items():
        if name in kwargs:
            setattr(self, name, kwargs[name])
        else:
            setattr(self, name, default)

Мы хотим использовать эту функцию в ряде классов. В частности, мы хотим создать функцию init , которая ведет себя как generic_init, но имеет сигнатуру, определенную некоторыми переменными класса в время создания :

class my_class:
    __init_args__=["x", "y"]
    __kw_init_args__={"my_opt": None}

__init__=create_initiation_function(my_class, generic_init)
setattr(myclass, "__init__", __init__)

Мы хотим, чтобы create_initiation_function создала новую функцию с сигнатурой, определенной с использованием init_args и kw_init_args . Можно ли написать функцию create_initiation_function?

Обратите внимание:

  • Если бы я просто хотел улучшить помощь, я мог бы установить doc .
  • Мы хотим установить сигнатуру функции при создании. После этого его не нужно менять.
  • Вместо создания функции, такой как generic_init, но с другой сигнатурой, мы могли бы создать новую функцию с нужной сигнатурой, которая просто вызывает generic_init
  • Мы хотим определить функцию create_initiation_function. Мы не хотим указывать новую функцию вручную!

Относящиеся

Ответы [ 8 ]

22 голосов
/ 13 октября 2015

Начиная с PEP-0362 , действительно существует способ установить подпись в py3.3 +, используя атрибут fn.__signature__:

def shared_vars(*shared_args):
    """Decorator factory that defines shared variables that are
       passed to every invocation of the function"""

    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            full_args = shared_args + args
            return f(*full_args, **kwargs)

        # Override signature
        sig = signature(f)
        sig = sig.replace(parameters=tuple(sig.parameters.values())[1:])
        wrapper.__signature__ = sig

        return wrapper
    return decorator

Тогда:

>>> @shared_vars({"myvar": "myval"})
>>> def example(_state, a, b, c):
>>>     return _state, a, b, c
>>> example(1,2,3)
({'myvar': 'myval'}, 1, 2, 3)
>>> str(signature(example))
'(a, b, c)'

Примечание: ПКП не совсем верен; Signature.replace переместило параметры из позиционного аргумента в аргумент только для квт.

16 голосов
/ 11 сентября 2009

Для вашего варианта использования наличие строки документации в классе / функции должно сработать - это будет хорошо отображаться в help () и может быть установлено программно (func .__ doc__ = "stuff").

Я не вижу способа установить фактическую подпись. Я бы подумал, что модуль functools сделал бы это, если бы это было выполнимо, но это не так, по крайней мере в py2.5 и py2.6.

Вы также можете вызвать исключение TypeError, если получите неверный ввод.

Хм, если вы не против быть по-настоящему мерзким, вы можете использовать compile () / eval () для этого. Если желаемая подпись указана с помощью arglist = ["foo", "bar", "baz"], а вашей действительной функцией является f (* args, ** kwargs), вы можете управлять:

argstr = ", ".join(arglist)
fakefunc = "def func(%s):\n    return real_func(%s)\n" % (argstr, argstr)
fakefunc_code = compile(fakefunc, "fakesource", "exec")
fakeglobals = {}
eval(fakefunc_code, {"real_func": f}, fakeglobals)
f_with_good_sig = fakeglobals["func"]

help(f)               # f(*args, **kwargs)
help(f_with_good_sig) # func(foo, bar, baz)

Изменение docstring и func_name должно дать вам полное решение. Но, э-э ...

5 голосов
/ 25 мая 2018

Я написал пакет с именем forge, который решает именно эту проблему для Python 3.5+:

Ваш текущий код выглядит следующим образом:

l=["x", "y"]
d={"opt":None}

def f(*args, **kwargs):
    #My code

И ваш нужный код выглядит так:

def f2(x, y, opt=None):
    #My code

Вот как это можно решить, используя forge:

f2 = forge.sign(
    forge.arg('x'),
    forge.arg('y'),
    forge.arg('opt', default=None),
)(f)

Поскольку forge.sign является оберткой, вы также можете использовать ее напрямую:

@forge.sign(
    forge.arg('x'),
    forge.arg('y'),
    forge.arg('opt', default=None),
)
def func(*args, **kwargs):
    # signature becomes: func(x, y, opt=None)
    return (args, kwargs)

assert func(1, 2) == ((), {'x': 1, 'y': 2, 'opt': None})
1 голос
/ 11 сентября 2009

Вы не можете сделать это с живым кодом.

То есть вы, кажется, хотите использовать реальную, живую функцию, которая выглядит следующим образом:

def f(*args, **kwargs):
    print args[0]

и замените его следующим:

def f(a):
    print a

Причина, по которой это невозможно сделать - по крайней мере, без изменения действительного байт-кода Python - в том, что они компилируются по-разному.

В первом случае получается функция, которая получает два параметра: список и dict, и код, который вы пишете, работает с этим списком и dict. Второе приводит к функции, которая получает один параметр и доступ к которой осуществляется как локальная переменная напрямую. Если бы вы изменили функцию «подпись», так сказать, это привело бы к такой функции:

def f(a):
    print a[0]

что явно не сработает.

Если вы хотите больше подробностей (хотя это вам не очень помогает), функция, которая принимает * args или * kwargs, имеет один или два бита, установленные в f.func_code.co_flags; Вы можете проверить это самостоятельно. Функция, которая принимает обычный параметр, имеет f.func_code.co_argcount, установленное в 1; версия * args равна 0. Это то, что Python использует, чтобы выяснить, как настроить фрейм стека функции при ее вызове, проверить параметры и т. д.

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

Тем не менее, вы можете динамически изменять строку документации функции. Просто присвойте func.__doc__. Обязательно делайте это только во время загрузки (из глобального контекста или - скорее всего - декоратора); если вы сделаете это позже, инструменты, загружающие модуль для проверки строк документации, никогда его не увидят.

0 голосов
/ 14 марта 2019

Посмотрите на makefun, это было сделано для этого (раскрытие вариантов функций с более или менее параметрами и точной сигнатурой) и работает в python 2 и 3.

Ваш пример будет написан так:

try:  # python 3.3+
    from inspect import signature, Signature, Parameter
except ImportError:
    from funcsigs import signature, Signature, Parameter

from makefun import create_function

def create_initiation_function(cls, gen_init):
    # (1) check which signature we want to create
    params = [Parameter('self', kind=Parameter.POSITIONAL_OR_KEYWORD)]
    for mandatory_arg_name in cls.__init_args__:
        params.append(Parameter(mandatory_arg_name, kind=Parameter.POSITIONAL_OR_KEYWORD))
    for default_arg_name, default_arg_val in cls.__opt_init_args__.items():
        params.append(Parameter(default_arg_name, kind=Parameter.POSITIONAL_OR_KEYWORD, default=default_arg_val))
    sig = Signature(params)

    # (2) create the init function dynamically
    return create_function(sig, generic_init)

# ----- let's use it

def generic_init(self, *args, **kwargs):
    """Function to initiate a generic object"""
    assert len(args) == 0
    for name, val in kwargs.items():
        setattr(self, name, val)

class my_class:
    __init_args__ = ["x", "y"]
    __opt_init_args__ = {"my_opt": None}

my_class.__init__ = create_initiation_function(my_class, generic_init)

и работает как положено:

# check 
o1 = my_class(1, 2)
assert vars(o1) == {'y': 2, 'x': 1, 'my_opt': None}

o2 = my_class(1, 2, 3)
assert vars(o2) == {'y': 2, 'x': 1, 'my_opt': 3}

o3 = my_class(my_opt='hello', y=3, x=2)
assert vars(o3) == {'y': 3, 'x': 2, 'my_opt': 'hello'}
0 голосов
/ 11 сентября 2009

Возможно, я не очень хорошо понял проблему, но если речь идет о том же поведении при изменении сигнатуры функции, то вы можете сделать что-то вроде:

# define a function
def my_func(name, age) :
    print "I am %s and I am %s" % (name, age)

# label the function with a backup name
save_func = my_func

# rewrite the function with a different signature
def my_func(age, name) :
    # use the backup name to use the old function and keep the old behavior
    save_func(name, age)

# you can use the new signature
my_func(35, "Bob")

Это выводит:

I am Bob and I am 35
0 голосов
/ 11 сентября 2009

Мы хотим create_initiation_function изменить подпись

Пожалуйста, не делайте этого.

Мы хотим использовать эту функцию в ряде классов

Пожалуйста, используйте обычное наследство.

Нет смысла в том, чтобы подпись "изменялась" во время выполнения.

Вы создаете кошмар обслуживания. Никто больше не будет пытаться понять, что вы делаете. Они просто вырвут его и заменит наследством.

Сделайте это вместо этого. Это просто и очевидно и делает ваш общий init доступным во всех подклассах очевидным, простым Pythonic способом.

class Super( object ):
    def __init__( self, *args, **kwargs ):
        # the generic __init__ that we want every subclass to use

class SomeSubClass( Super ):
    def __init__( self, this, that, **kwdefaults ):
        super( SomeSubClass, self ).__init__( this, that, **kwdefaults )

class AnotherSubClass( Super ):
    def __init__( self, x, y, **kwdefaults ):
        super( AnotherSubClass, self ).__init__( x, y, **kwdefaults )
0 голосов
/ 11 сентября 2009

Редактировать 1: Отвечая на новый вопрос:

Вы спрашиваете, как создать функцию с такой подписью:

def fun(a, b, opt=None):
    pass

Правильный способ сделать это в Python таков:

def fun(a, b, opt=None):
    pass

Редактировать 2: Объяснение ответа:

«Предположим, у меня есть общая функция f. Я хочу программно создать функцию f2, которая ведет себя так же, как f, но имеет настроенную подпись.»

def f(*args, **kw):
    pass

ОК, тогда f2 выглядит так:

def f2(a, b, opt=None):
    f(a, b, opt=opt)

Опять же, ответ на ваш вопрос настолько тривиален, что вы, очевидно, хотите знать нечто иное, чем то, что вы спрашиваете. Вам действительно нужно перестать задавать абстрактные вопросы и объяснить свою конкретную проблему.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...