如何实现父类方法分派?解决shapely.Point多构造方式问题
DispatchError Traceback (most recent call last)
Cell In [461], line 1
----> 1 Point(6, 4)
File ~\anaconda3\envs\bioinfo\lib\site-packages\multimethod_init_.py:313, in multimethod.call(self, *args, **kwargs)
311 if self.pending: # check first to avoid function call
312 self.evaluate()
--> 313 func = self[tuple(func(arg) for func, arg in zip(self.type_checkers, args))]
314 try:
315 return func(*args, **kwargs)
File ~\anaconda3\envs\bioinfo\lib\site-packages\multimethod_init_.py:307, in multimethod.missing(self, types)
305 return self.setdefault(types, *funcs) # type: ignore
306 msg = f"{self.name}: {len(keys)} methods found" # type: ignore
--> 307 raise DispatchError(msg, types, keys)
DispatchError: ('init: 0 methods found', (<class 'main.Point'>, <class 'int'>), [])
请问除了`multimethod`之外,还有哪些方法可以实现父类方法的分派? # 可行替代方案 ## 1. 手动在`__init__`中做类型判断 直接在自定义`Point`类的`__init__`里区分参数类型,分别处理`AnnotationSet`和父类支持的参数格式: ```python from shapely.geometry import Point as ShapelyPoint from dendropy.datamodel.basemodel import AnnotationSet LONGITUDE_ALIAS = "lon" LATITUDE_ALIAS = "lat" class Point(ShapelyPoint): def __init__(self, *args, **kwargs): if len(args) == 1 and isinstance(args[0], AnnotationSet): annotations = args[0] try: lon = float(annotations.get_value(LONGITUDE_ALIAS)) lat = float(annotations.get_value(LATITUDE_ALIAS)) super().__init__(lon, lat) except (TypeError, ValueError): raise ValueError(f"There is no coordinates in the annotations:\n{annotations}") else: super().__init__(*args, **kwargs)
该方案简单直接,既支持Point(annotations),也能正常使用Point(6,4)或父类的其他初始化方式。
2. 使用类工厂方法
不修改__init__,新增类方法专门处理AnnotationSet的实例化,完全保留父类的初始化逻辑:
from shapely.geometry import Point as ShapelyPoint from dendropy.datamodel.basemodel import AnnotationSet LONGITUDE_ALIAS = "lon" LATITUDE_ALIAS = "lat" class Point(ShapelyPoint): @classmethod def from_annotations(cls, annotations: AnnotationSet): try: lon = float(annotations.get_value(LONGITUDE_ALIAS)) lat = float(annotations.get_value(LATITUDE_ALIAS)) return cls(lon, lat) except (TypeError, ValueError): raise ValueError(f"There is no coordinates in the annotations:\n{annotations}")
使用时,从AnnotationSet创建实例用Point.from_annotations(annotations),常规创建用Point(6,4),符合Python惯用的工厂模式设计,避免重载__init__带来的潜在问题。
3. 利用functools.singledispatchmethod
Python 3.8+自带的singledispatchmethod支持基于第一个参数的类型分派,针对__init__(第一个参数是self)的特性,可将初始化逻辑转移到辅助方法实现分派:
from functools import singledispatchmethod from shapely.geometry import Point as ShapelyPoint from dendropy.datamodel.basemodel import AnnotationSet LONGITUDE_ALIAS = "lon" LATITUDE_ALIAS = "lat" class Point(ShapelyPoint): def __init__(self, *args, **kwargs): self._init(*args, **kwargs) @singledispatchmethod def _init(self, *args, **kwargs): super().__init__(*args, **kwargs) @_init.register(AnnotationSet) def _(self, annotations: AnnotationSet): try: lon = float(annotations.get_value(LONGITUDE_ALIAS)) lat = float(annotations.get_value(LATITUDE_ALIAS)) super().__init__(lon, lat) except (TypeError, ValueError): raise ValueError(f"There is no coordinates in the annotations:\n{annotations}")
该方案实现了类似多分派的效果,同时兼容父类所有初始化方式。
4. 自定义元类实现分派
如果必须用元类方案,可以手动编写元类收集并匹配__init__方法:
import inspect from shapely.geometry import Point as ShapelyPoint from dendropy.datamodel.basemodel import AnnotationSet LONGITUDE_ALIAS = "lon" LATITUDE_ALIAS = "lat" class InitDispatchMeta(type): def __new__(cls, name, bases, namespace): init_methods = [] # 收集父类的__init__作为兜底 for base in bases: if hasattr(base, '__init__'): init_methods.append((None, base.__init__)) # 收集当前类带类型注解的__init__ if '__init__' in namespace: func = namespace['__init__'] sig = inspect.signature(func) params = list(sig.parameters.values()) if len(params) > 1: param = params[1] if param.annotation is not param.empty: init_methods.append((param.annotation, func)) # 实现分派逻辑 def dispatch_init(self, *args, **kwargs): for annot, method in init_methods: if annot is None: try: return method(self, *args, **kwargs) except TypeError: continue if len(args) >=1 and isinstance(args[0], annot): return method(self, *args, **kwargs) raise TypeError(f"No valid __init__ for arguments: {args}") namespace['__init__'] = dispatch_init return super().__new__(cls, name, bases, namespace) class Point(ShapelyPoint, metaclass=InitDispatchMeta): def __init__(self, annotations: AnnotationSet): try: lon = float(annotations.get_value(LONGITUDE_ALIAS)) lat = float(annotations.get_value(LATITUDE_ALIAS)) super().__init__(lon, lat) except (TypeError, ValueError): raise ValueError(f"There is no coordinates in the annotations:\n{annotations}")
该元类优先匹配带类型注解的__init__,失败则调用父类方法,但实现复杂度较高,仅推荐有特殊元类需求时使用。
内容的提问来源于stack exchange,提问作者Vovin

