繁体   English   中英

Python:将方法标记为实现/覆盖

[英]Python: mark method as implementing/overriding

给定我想要实现的各种“合同”,我希望代码

  1. 告诉读者意图是什么
  2. 允许类型检查器纠正我(脆弱的基类问题)

例如,在 C++ 中,您可以

class X: public Somethingable {
  int get_something() const override
  { return 10; }
};

现在,当我重命名Somethingable::get_something (例如,简单的something )时,编译器将在我的X::get_something上出错,因为它不再是覆盖(不再)。

在 C# 中,读者可以获得更多信息:

class X : Somethingable {
  int GetSomething() implements Somethingable.GetSomething { return 10; }
}

在 Python 中,我们可以使用abc.ABC@abstractmethod来注释子类必须定义 this 和 that 成员,但是在实现站点上是否有标准化的方法来注释这种关系?

class X(Somethingable):
  @typing.implements(Somethingable.get_something) # does not exist
  def get_something(self):
     return 10

我高估了这种解决方案的复杂性,它更短:

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 替换原则)是一个非常重要的概念,特别是它假设任何父类都可以被任何子类替换而没有接口不兼容。 _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': next((p for p in params.values() 
                          if p.kind == Parameter.VAR_POSITIONAL), 
                         None) is not None,
            'kwonly': [(name, p.default is Parameter.empty) 
                       for name, p in params.items() 
                       if p.kind == Parameter.KEYWORD_ONLY],
            'kwargs': next((p for p in params.values() 
                            if p.kind == Parameter.VAR_KEYWORD), 
                           None) is not None,
        }
    
    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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM