python: 将子函数参数传递给父函数的kwargs,且IDE静态分析有类型提示的3种方法 (2025年)

方法1: TypedDict+Unpack

强烈推荐,适用于自己编写的库内使用
外部库不推荐,因为要额外维护Kwargs参数类,且无法对应不同版本的外部库
https://docs.python.org/3/library/typing.html#typing.Unpack

副作用,二次继承TypedDict

起码在vscode pylance里没有提示get,update

class T(TypedDict, total=False):
    Get: bool
    get: int
    Update: float
    update: str

class TTT(T, total=False):
    ttt: str

def t(**kwargs: Unpack[T]):... # 有get, update这2个参数,共4个参数
def ttt(**kwargs: Unpack[TTT]):... # 缺少get, update这2个参数,因为命名与dict.get()/dict.update()冲突

方法2: .pyi存根文件

stubgen -o typings

缺点:

  1. 需要额外维护.pyi,每次更新源码都需要手动更新.pyi内的函数签名
  2. 在.py内会优先使用内嵌签名,而非.pyi。
├─ src
│  ├─ app.py	# 不使用.pyi,使用.py的上下文签名
│  └─ call_app.py	# 默认使用.pyi
├─ typings
│  └─ src
│     └─ app.pyi
  1. vscode只认工作根目录下的typings,且typings需要有与src相同的文件夹结构
├─ src
│  └─ app.py
├─ typings
│  └─ src
│     └─ app.pyi
├─ requirements.txt
└─ pyproject.toml

方法3: functools.wraps

https://stackoverflow.com/questions/71968447/python-typing-copy-kwargs-from-one-function-to-another
https://github.com/python/typing/issues/270#issuecomment-1346124813

缺点:

  1. 只能复制,不能在复制的基础上添加自己的参数:https://github.com/python/cpython/issues/107001
  2. async函数使用wraps后,会丢失被装饰函数的所有信息,如docstring。

使用场景:简单包装外部库的函数;pylance静态分析会正确显示

import functools
from collections.abc import Callable
from typing import Any, Concatenate, ParamSpec, TypeVar, reveal_type
PS = ParamSpec("PS")
TV = TypeVar("TV")

#推荐
def copy_args(
    func: Callable[PS, Any]
) -> Callable[[Callable[..., TV]], Callable[PS, TV]]:
    """Decorator does nothing but returning the casted original function"""
    def return_func(func: Callable[..., TV]) -> Callable[PS, TV]:
        return cast(Callable[PS, TV], func)
    return return_func

#不推荐
def copy_callable_signature(
    source: Callable[PS, TV]
) -> Callable[[Callable[..., TV]], Callable[PS, TV]]:
    """```python
    def f(x: bool, *extra: int) -> str:
        return str(...)

    # copied signature:
    @copy_callable_signature(f)
    def test(*args, **kwargs):  # type: ignore[no-untyped-def]
        return f(*args, **kwargs)
    ```"""
    def wrapper(target: Callable[..., TV]) -> Callable[PS, TV]:
        @wraps(source)
        def wrapped(*args: PS.args, **kwargs: PS.kwargs) -> TV:
            return target(*args, **kwargs)
        return wrapped
    return wrapper

# 类内有self,使用
def copy_method_signature(
    source: Callable[Concatenate[Any, PS], TV]
) -> Callable[[Callable[..., TV]], Callable[Concatenate[Any, PS], TV]]:
    """```python
    class A:
        def foo(self, x: int, y: int, z: int) -> float:
            return float()

    class B:
        # copied signature:
        @copy_method_signature(A.foo)
        def bar(self, *args, **kwargs):  # type: ignore[no-untyped-def]
            print(*args)
    ```"""
    def wrapper(target: Callable[..., TV]) -> Callable[Concatenate[Any, PS], TV]:
        @wraps(source)
        def wrapped(self: Any, /, *args: PS.args, **kwargs: PS.kwargs) -> TV:
            return target(self, *args, **kwargs)
        return wrapped
    return wrapper

配合kwargs_filter

def kwargs_filter(funcs: List[Union[Callable, object]], kwargs, check=CHECK_KWARGS):
    """Filter out invalid kwargs to prevent Exception

    Don't use this if the funcs 
    actually parse args by `**kwargs` 
    while using `.pyi` to hint args,
    which will filter out your needed kwargs.

    ```python
    def Popen(cmd, Raise, **kwargs):
        kwargs = Kwargs([sp.Popen, Popen], kwargs)
        p = sp.Popen(cmd, **kwargs)
        return p
    ```
    """
    if not check:
        return kwargs
    from inspect import signature, isclass
    d = {}
    for f in funcs:
        if isclass(f):
            params = signature(f.__init__).parameters
        elif callable(f):
            params = signature(f).parameters
        else:
            raise TypeError(f"Invalid type: {type(f)}")
        # Log.debug(f"{funcs[0]} {params}")
        for k, v in kwargs.items():
            if k in params:
                d[k] = v
            else:
                Log.warning(f"Invalid kwarg: {k}={v}, please report to developer")
    return d
def copy_args(
    func: Callable[_PS, Any]
) -> Callable[[Callable[..., _TV]], Callable[_PS, _TV]]:
    """https://dev59.com/NnMOtIcB2Jgan1znX5jv"""
    def return_func(func: Callable[..., _TV]) -> Callable[_PS, _TV]:
        return cast(Callable[_PS, _TV], func)
    return return_func

def copy_args_with(
    func: Callable[_PS, Any], prefix: type[_TV2]
) -> Callable[[Callable[..., _TV]], Callable[Concatenate[_TV2, _PS], _TV]]:
    """https://dev59.com/NnMOtIcB2Jgan1znX5jv"""
    def return_func(func: Callable[..., _TV]) -> Callable[Concatenate[_TV2, _PS], _TV]:
        return cast(Callable[Concatenate[_TV2, _PS], _TV], func)
    return return_func
posted @ 2025-03-08 11:48  Nolca  阅读(45)  评论(0)    收藏  举报