如何编写动态创建的类方法

huangapple go评论78阅读模式
英文:

How to type dynamically created class methods

问题

我有一个用于向服务器发送请求的类。

有一个ROUTES字典,它是类方法名称到request函数中使用的服务器端点的映射。

这些方法在__init__中使用setattr添加到类中。我本可以直接将这些方法添加到类中,但有很多方法,而且我不喜欢代码重复。

类看起来像这样:

ROUTES = {
    "send_test": ["GET", "/test/"],
    "send_start": ["GET", "/start/"]
}

class Response(TypedDict):
    ...

class Client(object):
    def __init__(self) -> None:
        for mname, endpoint in ROUTES.items():
            setattr(self, mname, self.make_request_func(endpoint[0], endpoint[1]))

    def make_request_func(self, method, path): 
        def _func(*args, **kwargs):
            return self.request(method, path, *args, **kwargs)
        return _func

    def request(self, method, path, data: dict = {}, files: Optional[dict] = None) -> Response:
        ...

我基本上需要这些send_testsend_start方法只指向request函数的别名,但不带methodpath参数,只有datafiles

我对Python的类型系统了解非常基础,我会感激任何有关这个类型的额外解释!

英文:

I have a class which is used to send requests to server.

There's a ROUTES dictionary which is a map of class method names to the server endpoint used in request function.

These methods are added to class in __init__ with setattr. I could've just added these methods to the class, but there are a lot of them and I hate code duplication.

Class looks like this:

ROUTES = {
    "send_test": ["GET", "/test/"],
    "send_start": ["GET", "/start/"]
}

class Response(TypedDict):
    ...

class Client(object):
    def __init__(self) -> None:
        for mname, endpoint in ROUTES.items():
            setattr(self, mname, self.make_request_func(endpoint[0], endpoint[1]))

    def make_request_func(self, method, path): 
        def _func(*args, **kwargs):
            return self.request(method, path, *args, **kwargs)
        return _func

    def request(self, method, path, data: dict = {}, files: Optional[dict] = None) -> Response:
        ...

I basically need these send_test and send_start methods to just point to an alias of request function, but without method and path arguments, only data and files.

I have a very basic knowledge of python's typing, I will appreciate any additional explanation of typing this!

答案1

得分: 1

如果你愿意稍微减少 DRY 原则并显式定义实例方法,以下代码应该可以工作。(面对现实吧,对 makerequest 的重复调用实际上并不比 ROUTES 的原始定义更重复。)

from typing import Optional, Callable

def makerequest(method: str, path: str) -> Callable[[Client, Optional[dict], Optional[dict]], Response]:
    def wrapper(self: Client, data: Optional[dict] = None, files: Optional[dict] = None) -> Response:
        return self.request(method, path, data, files)
    return wrapper

class Client(object):
    def __init__(self) -> None:
        ...

    send_test = makerequest("GET", "/test/")
    send_start = makerequest("GET", "/start/")

    def request(self, method, path, data: Optional[dict] = None, files: Optional[dict] = None) -> Response:
        ...

(我冒昧地用 None 替换了你的默认 dict 值,以避免可变默认值的问题。)

有了这个,mypy 报告了 Client.send_test 的揭示类型:

tmp.py:32: note: Revealed type is "def (tmp.Client, Union[builtins.dict[Any, Any], None], Union[builtins.dict[Any, Any], None]) -> TypedDict('tmp.Response', {})"

如果类型注释不是问题,我会建议使用一个类装饰器来定义这些方法。

ROUTES = {...}

def add_aliases(d: dict):
    def _(cls):
        for name, args in d.items():
            setattr(cls, name, makerequest(*args))
        return cls
    return _

@add_aliases(ROUTES)
class Client:
    def request(self, ...):
        ...

问题在于如何对 add_aliases_ 进行注释,以捕获 _ 返回的类具有所需类型的方法,因为仅从 add_aliases 的主体中无法推断出任何信息。

英文:

If you are willing to be a little less DRY and define instance methods explicitly, the following should work. (Let's face it, the repeated calls to makerequest aren't really any more repetitive than the original definition of ROUTES.)

from typing import Optional, Callable

def makerequest(method: str, path: str) -> Callable[[Client, Optional[dict], Optional[dict]], Response]:
    def wrapper(self: Client, data: Optional[dict] = None, files: Optional[dict] = None) -> Response:
        return self.request(method, path, data, files)
    return wrapper

class Client(object):
    def __init__(self) -> None:
        ...

    send_test = makerequest("GET", "/test/")
    send_start = makerequest("GET", "/start/")

    def request(self, method, path, data: Optional[dict] = None, files: Optional[dict] = None) -> Response:
        ...

(I took the liberty of replacing your default dict value with None, to avoid problems with mutable default values.)

With this, mypy reports the revealed type of Client.send_test as

tmp.py:32: note: Revealed type is "def (tmp.Client, Union[builtins.dict[Any, Any], None], Union[builtins.dict[Any, Any], None]) -> TypedDict('tmp.Response', {})"

Adapting this to allow defining the aliases by iterating over ROUTES will be at best, I think, a mess, and at worst not possible, due to the class attributes send_test etc not being statically defined.


If typing weren't an issue, I would recommend a class decorator to define the methods.

ROUTES = {...}

def add_aliases(d: dict):
    def _(cls):
        for name, args in d.items():
            setattr(cls, name, makereqeust(*args))
        return cls
    return _


def makerequest(method, path):
    ...


@add_aliases(ROUTES)
class Client:
    def request(self, ...):
        ...

The problem is annotating add_aliases and _ in a way that captures the idea of the class that _ returns having methods of the desired type, since nothing can be inferred from the body of add_aliases alone.

huangapple
  • 本文由 发表于 2023年2月8日 22:29:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/75387243.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定