英文:
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_test
和send_start
方法只指向request
函数的别名,但不带method
和path
参数,只有data
和files
。
我对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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论