【问题标题】:Using signature of implemented method on Derived class for inherited method from Base class in a "template method pattern" inheritance in Python在 Python 中的“模板方法模式”继承中使用派生类上已实现方法的签名用于从基类继承的方法
【发布时间】:2026-02-18 22:40:04
【问题描述】:

假设我们有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. 我可以在类完全构建之前通过元类路径来更改签名和文档字符串属性。但是,这可能不适用于 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

【问题讨论】:

    标签: python python-typing


    【解决方案1】:

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

    目前,您的抽象类 Base 定义了一个接口,在该接口的具体实现中,方法 Base.call 可以使用 任何位置或关键字参数调用,而不会在运行。从理论的角度来看,callDerived 类中的具体实现不满足此接口。如果使用>4 个参数调用,使用url、jsontimeoutretries 的关键字参数调用,它将在运行时引发错误。

    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>]

    【讨论】:

    • 我确实想到了这种中继方法,具体来说,实现 Derived.safe_call 的特定签名只是将呼叫中继为 super().safe_call(...)。我对此有点犹豫,因为它增加了实现者的工作量。唉,这似乎是最不神奇的必要邪恶。另一方面,如果 Base 对于所有实际目的都是抽象且不可实例化的,那么在这种特殊情况下是否违反了 LSP?
    • @chiragjn — 是的,LSP 既适用于抽象接口,也适用于从具体类继承。事实上,可以说更是如此,因为抽象类——就其“不可实例化”的性质而言——除了作为接口存在之外没有任何目的,子类的“抽象模板”。如果抽象类的每个子类都违反了该模板,那么它就不是一个准确或有用的模板,不是吗?
    • 另外,请注意,如果您在子类中实现safe_call 的签名比在基类中更宽松,那将违反 LSP,因此我推荐你在每个子类中给具有准确签名的方法一个唯一的名称(Foo 类中的safe_call_specific_to_fooBar 类中的safe_call_specific_to_bar 等。显然,在现实生活中不需要那么冗长) .
    • @chiragjn -- PEP 612 (python.org/dev/peps/pep-0612) 在这里实际上可能很有用,但不幸的是 MyPy 还不完全支持它 (github.com/python/mypy/issues/8645),所以我无法查看我想象的用法是否真的在这里工作。