Python 键入:将“**kwargs”从一个函数复制到另一个函数

Kou*_*und 6 python python-typing

这是 Python 扩展或包装函数中的常见模式,用于**kwargs将所有关键字参数传递给扩展函数。

即采取

class A:
    def bar(self, *, a: int, b: str, c: float) -> str:
       return f"{a}_{b}_{c}"
   

class B(A):
    def bar(self, **kwargs):
        return f"NEW_{super().bar(**kwargs)}"


def base_function(*, a: int, b: str, c: float) -> str:
    return f"{a}_{b}_{c}"


def extension(**kwargs) -> str:
    return f"NEW_{base_function(**kwargs)}"
Run Code Online (Sandbox Code Playgroud)

现在调用extension(not_existing="a")orB().bar(not_existing="a")会导致TypeError, 可以被静态类型检查器检测到。

如何在运行代码之前注释我的extensionB.bar以便检测到此问题?

此注释也有助于 IDE 为我提供有关extensionor 的正确建议B.bar

Kou*_*und 13

解决方案

\n

PEP 612引入了ParamSpec(请参阅文档)类型。

\n

我们可以利用它来生成一个装饰器,告诉我们的类型检查器,装饰函数与给定函数具有相同的参数:

\n
from typing import (\n   Callable, ParamSpec, TypeVar, cast, Any, Type, Literal,\n)\n\n# Define some specification, see documentation\nP = ParamSpec("P")\nT = TypeVar("T")\n\n# For a help about decorator with parameters see \n# https://stackoverflow.com/questions/5929107/decorators-with-parameters\ndef copy_kwargs(\n    kwargs_call: Callable[P, Any]\n) -> Callable[[Callable[..., T]], Callable[P, T]]:\n    """Decorator does nothing but returning the casted original function"""\n\n    def return_func(func: Callable[..., T]) -> Callable[P, T]:\n        return cast(Callable[P, T], func)\n\n    return return_func\n\n
Run Code Online (Sandbox Code Playgroud)\n

这将定义一个装饰器,可用于将完整的ParameterSpec定义复制到我们的新函数,并保留它的返回值。

\n

让我们测试一下(另请参阅MyPy Playground

\n
# Our test function for kwargs\ndef source_func(foo: str, bar: int, default: bool = True) -> str:\n    if not default:\n        return "Not Default!"\n    return f"{foo}_{bar}"\n\n@copy_kwargs(source_func)\ndef kwargs_test(**kwargs) -> float:\n    print(source_func(**kwargs))\n    return 1.2\n\n# define some expected return values\nokay: float\nbroken_kwargs: float\nbroken_return: str\n\nokay = kwargs_test(foo="a", bar=1)\nbroken_kwargs = kwargs_test(foo=1, bar="2")\nbroken_return = kwargs_test(foo="a", bar=1)\n
Run Code Online (Sandbox Code Playgroud)\n

这与Pyre 1.1.310mypy 1.2.0和 PyCharm 2023.1.1的预期一致。\n这三个版本都会抱怨损坏的 kwargs 和损坏的返回值。\n只有 PyCharm 无法检测参数,default因为PEP 612 支持是尚未完全实施

\n

\xe2\x9a\xa0\xef\xb8\x8f 限制

\n

我们仍然需要非常小心地应用这个函数。\n假设以下调用

\n
runtime_error = kwargs_test("a", 1)\n
Run Code Online (Sandbox Code Playgroud)\n

将导致运行时错误 \xe2\x80\x9c kwargs_test1() 采用 0 个位置参数,但给出了 2 个\xe2\x80\x9d 而没有任何类型检查器抱怨。

\n

因此,如果您像这样复制**kwargs,请确保将所有位置参数放入函数中。\n定义参数的函数应仅使用关键字参数

\n

因此,最佳实践source_func如下所示:

\n
def source_func(*, foo: str, bar: int, default: bool = True) -> str:\n    if not default:\n        return "Not Default!"\n    return f"{foo}_{bar}"\n
Run Code Online (Sandbox Code Playgroud)\n

但由于这可能经常用于库函数,因此我们并不总是能够控制source_func,因此请记住这个问题!

\n

您还可以添加*args到目标函数来防止此问题:

\n
def source_func(*, foo: str, bar: int, default: bool = True) -> str:\n    if not default:\n        return "Not Default!"\n    return f"{foo}_{bar}"\n
Run Code Online (Sandbox Code Playgroud)\n

MyPy 和 PyCharm 的 PEP 612 简介的历史

\n

ParamSpec创建此答案时,MyPy 和 PyCharm 使用时遇到问题。问题似乎已解决,但链接仍保留作为历史参考:

\n\n

使用连接

\n

如果您想复制 kwargs 但又想允许其他参数,您需要将 kwargs 与Concanate结合使用:

\n
# Our test function for args and kwargs\ndef source_func_a(\n  a: Literal["a"], b: Literal["b"], c: Literal["c"], d: Literal["d"], default: bool =True\n) -> str:\n    if not default:\n        return "Not Default!"\n    return f"{a}_{b}_{c};{d}"\n\n\n\n@copy_kwargs(source_func_a)\ndef args_test(a: Literal["a"], *args, c: Literal["c"], **kwargs) -> float:\n    kwargs["c"] = c\n    # Note the correct types of source_func are not checked for kwargs and args,\n    # if args_test doesn\'t define them (at least for mypy)\n    print(source_func(a, *args, **kwargs))\n    return 1.2\n\n# define some expected return values\nokay_args: float\nokay_kwargs: float\nbroken_kwargs: float\nbroken_args: float\n\nokay_args = args_test("a", "b", "c", "d")\nokay_kwargs = args_test(a="a", b="b", c="c", d="d")\nborken_args = args_test("not", "not", "not", "not")\nbroken_kwargs = args_test(a="not", b="not", c="not", d="not")\n
Run Code Online (Sandbox Code Playgroud)\n

有关详细信息,请参阅MyPy Play

\n

注意:目前,您需要为要添加的每个变量定义装饰器,并且由于 Concanate 的性质,它们也可以作为参数添加在前面。

\n

  • 的确。但甚至可能具有更多功能。即自动检测返回值,并允许使用 [Concatenate](https://docs.python.org/3/library/typing.html#typing.Concatenate) 组合多个函数的参数,也许这应该进入 tpying而不是 functools ?但恕我直言,可能值得 PEP 请求。 (2认同)