Как заставить некоторые параметры функции быть только позиционными - PullRequest
1 голос
/ 18 марта 2020

Я хочу имитировать c это поведение Python3 .8 в Python3 .7

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

#Python3.8
def f(a,b,/,**kwargs):
    print(a,b,kwargs)

>>> f(1,2,**{'a':100,'b':200,'c':300})
# 1 2 {'a': 100, 'b': 200, 'c': 300}

a, b используются только как позиционные параметры.

Как я делаю то же самое в Python3 .7

#Python3.7

def f(a,b,**kwargs):
    print(a,b,kwargs)

>>> f(1,2,**{'a':1,'b':2})
# TypeError: f() got multiple values for argument 'a'

Как сделать a, b только позиционными параметрами. / не работает ниже с Python3 .8

Возможно ли имитировать синтаксис c / в Python3 .7?

1 Ответ

1 голос
/ 18 марта 2020

Вы можете создать собственный декоратор, который объявляет только позиционные аргументы, возвращая оболочку, которая анализирует свои собственные *args, **kwargs так, чтобы они соответствовали сигнатуре декорированной функции. Из-за возможных конфликтов имен между позиционными аргументами и аргументами ключевых слов невозможно использовать упаковку ключевых слов (**) для этого подхода (это единственное ограничение). Аргументы упакованных ключевых слов должны быть объявлены либо как последний параметр positional-or-keyword, либо как первый параметр только для ключевых слов. Вот два примера:

def foo(a, b, kwargs):  # last positional-or-keyword parameter
    pass

def foo(a, *args, kwargs):  # first keyword-only parameter
    pass

Переменная kwargs получит оставшиеся **kwargs от функции-оболочки, т. Е. Ее можно использовать аналогично тому, как если бы **kwargs использовался непосредственно в декорированной функции (как в Python 3.8 +).

Следующая реализация декоратора в значительной степени основана на реализации inspect.Signature.bind с несколькими незначительными изменениями для обработки только позиционных параметров посредством объявленные декоратором имена и для обработки дополнительного (искусственного) параметра kwargs.

import functools
import inspect
import itertools


def positional_only(*names, kwargs_name='kwargs'):
    def decorator(func):
        signature = inspect.signature(func)

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            new_args = []
            new_kwargs = {}            

            parameters = iter(signature.parameters.values())
            parameters_ex = ()
            arg_vals = iter(args)

            while True:
                try:
                    arg_val = next(arg_vals)
                except StopIteration:
                    try:
                        param = next(parameters)
                    except StopIteration:
                        break
                    else:
                        if param.name == kwargs_name or param.kind == inspect.Parameter.VAR_POSITIONAL:
                            break
                        elif param.name in kwargs:
                            if param.name in names:
                                msg = '{arg!r} parameter is positional only, but was passed as a keyword'
                                msg = msg.format(arg=param.name)
                                raise TypeError(msg) from None
                            parameters_ex = (param,)
                            break
                        elif param.default is not inspect.Parameter.empty:
                            parameters_ex = (param,)
                            break
                        else:
                            msg = 'missing a required argument: {arg!r}'
                            msg = msg.format(arg=param.name)
                            raise TypeError(msg) from None
                else:
                    try:
                        param = next(parameters)
                    except StopIteration:
                        raise TypeError('too many positional arguments') from None
                    else:
                        if param.name == kwargs_name or param.kind == inspect.Parameter.KEYWORD_ONLY:
                            raise TypeError('too many positional arguments') from None

                        if param.kind == inspect.Parameter.VAR_POSITIONAL:
                            new_args.append(arg_val)
                            new_args.extend(arg_vals)
                            break

                        if param.name in kwargs and param.name not in names:
                            raise TypeError(
                                'multiple values for argument {arg!r}'.format(
                                    arg=param.name)) from None

                        new_args.append(arg_val)

            for param in itertools.chain(parameters_ex, parameters):
                if param.name == kwargs_name or param.kind == inspect.Parameter.VAR_POSITIONAL:
                    continue

                try:
                    arg_val = kwargs.pop(param.name)
                except KeyError:
                    if (param.kind != inspect.Parameter.VAR_POSITIONAL
                            and param.default is inspect.Parameter.empty):
                        raise TypeError(
                            'missing a required argument: {arg!r}'.format(
                                arg=param.name)) from None
                else:
                    if param.name in names:
                        raise TypeError(
                            '{arg!r} parameter is positional only, '
                            'but was passed as a keyword'.format(arg=param.name))

                    new_kwargs[param.name] = arg_val

            new_kwargs.update(kwargs=kwargs)
            return func(*new_args, **new_kwargs)
        return wrapper
    return decorator

Вот пример того, как его можно использовать:

@positional_only('a')
def foo(a, *args, kwargs, b=9, c):
    print(a, args, b, c, kwargs)

foo(1, **dict(a=2), c=3)  # ok
foo(1, 2, 3, 4, 5, c=6)  # ok
foo(1, b=2, **dict(a=3), c=4)  # ok
foo(a=1, c=2)  # error
foo(c=1)  # error
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...