在 Python 中的 "template method pattern" 继承中,对 Derived class 上从 Base class 继承的方法使用已实现方法的签名
Using signature of implemented method on Derived class for inherited method from Base class in a "template method pattern" inheritance in Python
考虑我们有 Template method pattern 继承场景。我正在使用带有请求库的示例。
import abc
import json
import requests
from typing import Dict, Any
class Base(abc.ABC)
@abc.abstractmethod
def call(self, *args, **kwargs) -> requests.Response:
raise NotImplementedError
def safe_call(self, *args, **kwargs) -> Dict[str, Any]:
try:
response = self.call(*args, **kwargs)
response.raise_for_status()
return response.json()
except json.JSONDecodeError as je: ...
except requests.exceptions.ConnectionError as ce: ...
except requests.exceptions.Timeout as te: ...
except requests.exceptions.HTTPError as he: ...
except Exception as e: ...
return {"success": False}
class Derived(Base):
def call(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> requests.Response:
# actual logic for making the http call
response = requests.post(url, json=json, timeout=timeout, ...)
return response
将像
一样使用
executor = Derived()
response = executor.safe_call(url='https://example.com', json={"key": "value"}, ...)
有没有办法将 Derived.call
的签名附加到 Derived.safe_call
以便现代 IDE 自动完成和类型检查器工作?
因为这看起来非常像装饰器模式,所以我尝试在 __init__
中使用 functools.update_wrapper
但失败了
AttributeError: 'method' object has no attribute '__module__'
我确实有几个选择,但我找不到关于这个问题的任何建议
- 在 class 完全构建之前,我可以使用 metaclass 路由来更改签名和文档字符串属性。但是,这可能不适用于 IDEs.
- 我可以定义一个存根文件
.pyi
并将其与主要代码一起维护。
class Derived(Base):
def safe_call(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> Dict[str, Any]: ...
完全改变设计
(编辑:添加 post 第一个答案)(Ab)使用 @typing.overload
class Derived(Base):
@typing.overload
def safe_call(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> Dict[str, Any]:
...
def call(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> requests.Response:
# actual logic for making the http call
response = requests.post(url, json=json, timeout=timeout, ...)
return response
我明白你想做什么,但我认为走那条路可能是个错误,因为它会破坏 Liskov Substitution Principle。
目前,您的抽象 class Base
定义了一个接口,其中方法 Base.call
可以在接口的具体实现中以 任何位置调用或关键字参数 而不会在运行时引发错误。从理论上讲,你的Derived
class中call
的具体实现不满足这个接口。如果使用 >4 个参数调用、使用 <4 个参数调用或使用非 url
、json
、timeout
或 retries
的关键字参数调用,它将在运行时引发错误。
Python 仍然会让你在运行时 实例化 个 Derived
的实例,因为它只检查是否存在与Base
中定义的抽象方法。它不会在具体实现实例化时检查抽象方法的签名兼容性,并且尝试这样做将非常困难。但是,某些类型检查器可能会抱怨派生 class 中的签名不如基础 class 中的签名宽松。此外,理论原则本身也很重要。
我认为要走的路可能是这样的。此处注意两点:
- 基础 class 中的
safe_call
和 call
方法都变成了私有方法,因为它们是实现细节,不能直接使用。
- 我更改了
call
和 safe_call
基 class 中的签名,以便它们只接受关键字参数。
import abc
import json
import requests
from typing import Dict, Any
class Base(abc.ABC)
@abc.abstractmethod
def _call(self, **kwargs: Any) -> requests.Response:
raise NotImplementedError
def _safe_call(self, **kwargs: Any) -> Dict[str, Any]:
try:
response = self.call(**kwargs)
response.raise_for_status()
return response.json()
except json.JSONDecodeError as je: ...
except requests.exceptions.ConnectionError as ce: ...
except requests.exceptions.Timeout as te: ...
except requests.exceptions.HTTPError as he: ...
except Exception as e: ...
return {"success": False}
class Derived(Base):
def _call(self, **kwargs: Any) -> requests.Response:
url: str = kwargs['url']
json: Dict[str, Any] = kwargs['json']
timeout: int = kwargs['timeout']
retries: int = kwargs['retries']
# actual logic for making the http call
response = requests.post(url, json=json, timeout=timeout, ...)
return response
def safe_call_specific_to_derived(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> Dict[str, Any]:
return self._safe_call(url=url, json=json, timeout=timeout, retries=retries)
如果您想在 _call
的具体实现中使用默认参数值,请注意您可以使用 kwargs.get(<arg_name>, <default_val>)
而不是 kwargs[<arg_name>]
考虑我们有 Template method pattern 继承场景。我正在使用带有请求库的示例。
import abc
import json
import requests
from typing import Dict, Any
class Base(abc.ABC)
@abc.abstractmethod
def call(self, *args, **kwargs) -> requests.Response:
raise NotImplementedError
def safe_call(self, *args, **kwargs) -> Dict[str, Any]:
try:
response = self.call(*args, **kwargs)
response.raise_for_status()
return response.json()
except json.JSONDecodeError as je: ...
except requests.exceptions.ConnectionError as ce: ...
except requests.exceptions.Timeout as te: ...
except requests.exceptions.HTTPError as he: ...
except Exception as e: ...
return {"success": False}
class Derived(Base):
def call(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> requests.Response:
# actual logic for making the http call
response = requests.post(url, json=json, timeout=timeout, ...)
return response
将像
一样使用executor = Derived()
response = executor.safe_call(url='https://example.com', json={"key": "value"}, ...)
有没有办法将 Derived.call
的签名附加到 Derived.safe_call
以便现代 IDE 自动完成和类型检查器工作?
因为这看起来非常像装饰器模式,所以我尝试在 __init__
中使用 functools.update_wrapper
但失败了
AttributeError: 'method' object has no attribute '__module__'
我确实有几个选择,但我找不到关于这个问题的任何建议
- 在 class 完全构建之前,我可以使用 metaclass 路由来更改签名和文档字符串属性。但是,这可能不适用于 IDEs.
- 我可以定义一个存根文件
.pyi
并将其与主要代码一起维护。
class Derived(Base):
def safe_call(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> Dict[str, Any]: ...
完全改变设计
(编辑:添加 post 第一个答案)(Ab)使用
@typing.overload
class Derived(Base):
@typing.overload
def safe_call(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> Dict[str, Any]:
...
def call(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> requests.Response:
# actual logic for making the http call
response = requests.post(url, json=json, timeout=timeout, ...)
return response
我明白你想做什么,但我认为走那条路可能是个错误,因为它会破坏 Liskov Substitution Principle。
目前,您的抽象 class Base
定义了一个接口,其中方法 Base.call
可以在接口的具体实现中以 任何位置调用或关键字参数 而不会在运行时引发错误。从理论上讲,你的Derived
class中call
的具体实现不满足这个接口。如果使用 >4 个参数调用、使用 <4 个参数调用或使用非 url
、json
、timeout
或 retries
的关键字参数调用,它将在运行时引发错误。
Python 仍然会让你在运行时 实例化 个 Derived
的实例,因为它只检查是否存在与Base
中定义的抽象方法。它不会在具体实现实例化时检查抽象方法的签名兼容性,并且尝试这样做将非常困难。但是,某些类型检查器可能会抱怨派生 class 中的签名不如基础 class 中的签名宽松。此外,理论原则本身也很重要。
我认为要走的路可能是这样的。此处注意两点:
- 基础 class 中的
safe_call
和call
方法都变成了私有方法,因为它们是实现细节,不能直接使用。 - 我更改了
call
和safe_call
基 class 中的签名,以便它们只接受关键字参数。
import abc
import json
import requests
from typing import Dict, Any
class Base(abc.ABC)
@abc.abstractmethod
def _call(self, **kwargs: Any) -> requests.Response:
raise NotImplementedError
def _safe_call(self, **kwargs: Any) -> Dict[str, Any]:
try:
response = self.call(**kwargs)
response.raise_for_status()
return response.json()
except json.JSONDecodeError as je: ...
except requests.exceptions.ConnectionError as ce: ...
except requests.exceptions.Timeout as te: ...
except requests.exceptions.HTTPError as he: ...
except Exception as e: ...
return {"success": False}
class Derived(Base):
def _call(self, **kwargs: Any) -> requests.Response:
url: str = kwargs['url']
json: Dict[str, Any] = kwargs['json']
timeout: int = kwargs['timeout']
retries: int = kwargs['retries']
# actual logic for making the http call
response = requests.post(url, json=json, timeout=timeout, ...)
return response
def safe_call_specific_to_derived(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> Dict[str, Any]:
return self._safe_call(url=url, json=json, timeout=timeout, retries=retries)
如果您想在 _call
的具体实现中使用默认参数值,请注意您可以使用 kwargs.get(<arg_name>, <default_val>)
而不是 kwargs[<arg_name>]