ABC与typing:能否让函数参数实现真正的可选性?
优化带可选参数的抽象类方法设计
当子类重写基类带可选默认参数的抽象方法时,比如下面的代码,pylance、mypy这类类型检查器会因为参数数量不匹配报错(基类request方法包含self、url、query_params三个参数,子类仅定义self和url两个):
from abc import ABC, abstractmethod from typing import TypedDict, Optional class QueryParams(TypedDict): param_name: str param_value: str class ApiClient(ABC): @abstractmethod def request(self, url: str, query_params: Optional[QueryParams] = None) -> None: raise NotImplementedError class ConcreteApiClient(ApiClient): def request(self, url: str): print("I don't need query params to make request!")
类型检查器会强制开发者补全缺失的参数,改成如下代码:
class ConcreteApiClient(ApiClient): def request(self, url: str, query_params: Optional[QueryParams] = None): print("I don't need query params to make request!")
但这种设计很不合理——这个参数在子类里完全没被用到。虽然这是里氏替换原则的要求,但优化这段代码的方式不止一种,你提到的“基类仅保留非可选参数,子类按需定义额外参数”是可行方案之一,下面是几种更具体的优化思路:
方案1:按功能拆分抽象接口(推荐)
基于接口隔离原则,把不同需求的客户端抽象拆分成层级化的接口,避免强制子类实现不需要的参数:
from abc import ABC, abstractmethod from typing import TypedDict, Optional class QueryParams(TypedDict): param_name: str param_value: str # 基础接口,仅定义核心请求能力 class BaseApiClient(ABC): @abstractmethod def request(self, url: str) -> None: raise NotImplementedError # 扩展接口,增加查询参数支持 class QuerySupportApiClient(BaseApiClient): @abstractmethod def request(self, url: str, query_params: Optional[QueryParams] = None) -> None: raise NotImplementedError # 不需要查询参数的子类,继承基础接口 class ConcreteApiClient(BaseApiClient): def request(self, url: str): print("I don't need query params to make request!") # 需要查询参数的子类,继承扩展接口 class QueryEnabledApiClient(QuerySupportApiClient): def request(self, url: str, query_params: Optional[QueryParams] = None): if query_params: print(f"Making request with params: {query_params}") else: print("Making request without params")
这种方式既符合里氏替换原则,又让子类只需要实现自己需要的接口,完全避免冗余参数。
方案2:用可变关键字参数兼容(适合简单场景)
如果不想拆分接口,可以让基类用**kwargs接收所有可选参数,子类按需处理:
from abc import ABC, abstractmethod from typing import TypedDict, Optional, Any class QueryParams(TypedDict): param_name: str param_value: str class ApiClient(ABC): @abstractmethod def request(self, url: str, **kwargs: Any) -> None: raise NotImplementedError class ConcreteApiClient(ApiClient): def request(self, url: str): print("I don't need query params to make request!") class QueryEnabledApiClient(ApiClient): def request(self, url: str, query_params: Optional[QueryParams] = None): if query_params: print(f"Making request with params: {query_params}") else: print("Making request without params")
缺点是基类失去了严格的类型检查能力,无法明确声明可选参数的类型,适合参数简单、类型要求不高的场景。
方案3:封装可选参数为配置类
把所有可选的请求配置(比如查询参数、请求头、超时时间等)封装成一个单独的配置类,基类只接收这个配置参数,子类可以选择忽略或使用:
from abc import ABC, abstractmethod from typing import TypedDict, Optional from dataclasses import dataclass class QueryParams(TypedDict): param_name: str param_value: str @dataclass class RequestConfig: query_params: Optional[QueryParams] = None # 后续可以添加其他可选配置,比如headers: Optional[dict] = None class ApiClient(ABC): @abstractmethod def request(self, url: str, config: Optional[RequestConfig] = None) -> None: raise NotImplementedError class ConcreteApiClient(ApiClient): def request(self, url: str, config: Optional[RequestConfig] = None): # 直接忽略配置参数 print("I don't need query params to make request!") class QueryEnabledApiClient(ApiClient): def request(self, url: str, config: Optional[RequestConfig] = None): if config and config.query_params: print(f"Making request with params: {config.query_params}") else: print("Making request without params")
这种方式既保留了类型安全,又具备扩展性——后续新增可选参数时,只需要修改配置类,不需要修改方法签名,子类也可以灵活选择是否处理配置。
内容的提问来源于stack exchange,提问作者barciewicz
相关产品推荐
相关产品推荐

