我高估了这种解决方案的复杂性,它更短:
import warnings
def override(func):
if hasattr(func, 'fget'): # We see a property, go to actual callable
func.fget.__overrides__ = True
else:
func.__overrides__ = True
return func
class InterfaceMeta(type):
def __new__(mcs, name, bases, attrs):
for name, a in attrs.items():
f = getattr(a, 'fget', a)
if not getattr(f, '__overrides__', None): continue
f = getattr(f, '__wrapped__', f)
try:
base_class = next(b for b in bases if hasattr(b, name))
ref = getattr(base_class, name)
if type(ref) is not type(a):
warnings.warn(f'Overriding method {name} messes with class/static methods or properties')
continue
if _check_lsp(f, ref):
warnings.warn(f'LSP violation for method {name}')
continue
except StopIteration:
warnings.warn(f'Overriding method {name} does not have parent implementation')
return super().__new__(mcs, name, bases, attrs)
override装饰器可以标记覆盖方法,InterfaceMeta确认这些方法确实存在于超类中。 _check_lsp 是其中最复杂的部分,我将在下面进行解释。
到底发生了什么?首先,我们从装饰器中获取一个可调用对象并为其添加一个属性。然后元类寻找带有这个标记的方法并且:
- 确认,至少有一个基类实现了它
- 检查,
property仍然是财产,classmethod仍然是classmethod,staticmethod仍然是staticmethod
- 检查,该实现没有违反 Liskov 替换原则。
用法
def stupid_decorator(func):
"""Stupid, because doesn't use `wrapt` or `functools.wraps`."""
def inner(*args, **kwargs):
return func(*args, **kwargs)
return inner
class IFoo(metaclass=InterfaceMeta):
def foo(self): return 'foo'
@property
def bar(self): return 'bar'
@classmethod
def cmethod(cls): return 'classmethod'
@staticmethod
def smethod(): return 'staticmethod'
def some_1(self): return 1
def some_2(self): return 2
def single_arg(self, arg): return arg
def two_args_default(self, arg1, arg2): return arg1
def pos_only(self, arg1, /, arg2, arg3=1): return arg1
def kwonly(self, *, arg1=1): return arg1
class Foo(IFoo):
@override
@stupid_decorator # Wrong signature now: "self" not mentioned. With "self" in decorator won't fail.
def foo(self): return 'foo2'
@override
@property
def baz(self): return 'baz'
@override
def quak(self): return 'quak'
@override
@staticmethod
def cmethod(): return 'Dead'
@override
@classmethod
def some_1(cls): return None
@override
def single_arg(self, another_arg): return 1
@override
def pos_only(self, another_arg, / , arg2, arg3=1): return 1
@override
def two_args_default(self, arg1, arg2=1): return 1
@override
def kwonly(self, *, arg2=1): return 1
这警告:
LSP violation for method foo
Overriding method baz does not have parent implementation
Overriding method quak does not have parent implementation
Overriding method cmethod messes with class/static methods or properties
Overriding method some_1 messes with class/static methods or properties
LSP violation for method single_arg
LSP violation for method kwonly
您也可以在 Foo 上设置元类,结果相同。
语言服务提供商
LSP (Liskov substitution principle) 是一个非常重要的概念,特别是假设任何父类都可以用任何子类代替,而不会出现接口不兼容。 _check_lsp 只执行非常简单的检查,忽略类型注释(这是mypy 区域,我不会碰它!)。它证实了
-
*args和**kwargs不会消失
- 仅位置参数计数相同
- 所有父级的常规(位置或关键字)参数都具有相同的名称,不会丢失默认值(但可能会更改)并且所有添加的都有默认值
- 仅关键字参数相同
实现如下:
from inspect import signature, Parameter
from itertools import zip_longest, chain
def _check_lsp(child, parent):
child = signature(child).parameters
parent = signature(parent).parameters
def rearrange(params):
return {
'posonly': sum(p.kind == Parameter.POSITIONAL_ONLY for p in params.values()),
'regular': [(name, p.default is Parameter.empty)
for name, p in params.items()
if p.kind == Parameter.POSITIONAL_OR_KEYWORD],
'args': any(p.kind == Parameter.VAR_POSITIONAL
for p in params.values()),
'kwonly': [(name, p.default is Parameter.empty)
for name, p in params.items()
if p.kind == Parameter.KEYWORD_ONLY],
'kwargs': any(p.kind == Parameter.VAR_KEYWORD
for p in params.values()),
}
child, parent = rearrange(child), rearrange(parent)
if (
child['posonly'] != parent['posonly']
or not child['args'] and parent['args']
or not child['kwargs'] and parent['kwargs']
):
return True
for new, orig in chain(zip_longest(child['regular'], parent['regular']),
zip_longest(child['kwonly'], parent['kwonly'])):
if new is None and orig is not None:
return True
elif orig is None and new[1]:
return True
elif orig[0] != new[0] or not orig[1] and new[1]:
return True