在Python中的“模板方法模式”继承中,为基类继承的方法在派生类上使用已实现方法的签名

2024-04-29 15:01:13 发布

您现在位置: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__'

我确实有一些选择,但我找不到关于这个问题的任何建议

  1. 在类完全构造之前,我可以通过元类的方法更改签名和docstring属性。但是,这可能不适合IDE
  2. 我可以定义存根文件.pyi,并将其与主代码一起维护
class Derived(Base):
    def safe_call(self, url: str, json: Dict[str, Any], timeout: int, retries: int) -> Dict[str, Any]: ...
  1. 将设计完全更改为其他设计

  2. (编辑:添加后第一个答案)(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

Tags: selfjsonurlresponsedeftimeoutanycall
1条回答
网友
1楼 · 发布于 2024-04-29 15:01:13

我知道你想做什么,但我认为沿着这条路走可能是个错误,因为它会破坏Liskov Substitution Principle

目前,抽象类Base定义了一个接口,在接口的具体实现中,方法Base.call可以使用任何位置或关键字参数调用,而不会在运行时引发错误。从理论上看,在Derived类中call的具体实现不满足此接口。如果使用>;4个参数,用<;4个参数,或使用非urljsontimeoutretries的关键字参数调用,将在运行时引发错误

Python仍然允许您在运行时实例化Derived实例,因为它只检查是否存在与Base中定义的抽象方法同名的方法。它不会在实例化具体实现时检查抽象方法的签名兼容性,并且尝试这样做将非常困难。但是,某些类型检查器可能会抱怨派生类中的签名比基类中的签名更不允许。此外,理论原则本身也很重要

我想应该是这样的。这里要注意两件事:

  1. 基类中的safe_callcall方法都已成为私有方法,因为它们是设计为不直接使用的实现细节
  2. 我已经修改了基类中callsafe_call的签名,以便它们只接受关键字参数
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>]

相关问题 更多 >