【问题标题】:Polymorphism in Callablle under python type checking (Pylance)python类型检查(Pylance)下Callablle中的多态性
【发布时间】:2023-01-27 02:21:53
【问题描述】:

对于我的代码,我有一个聚合类,它需要为基类 BaseC 的每个子类定义一个验证方法,在本例中 InheritC 继承自 BaseC

然后通过注册方法将验证方法传递到聚合类中。

请参阅以下简单示例

from typing import Callable


class BaseC:
    def __init__(self) -> None:
        pass
    
class InheritC(BaseC):
    def __init__(self) -> None:
        super().__init__()

    @classmethod
    def validate(cls, c:'InheritC') ->bool:
        return False

class AggrC:
    def register_validate_fn(self, fn: Callable[[BaseC], bool])-> None:
        self.validate_fn = fn

ac = AggrC()
ic = InheritC()
ac.validate_fn(ic.fn)

我在注册函数的参数上添加了类型提示,它是一个 Callable 对象 Callable[[BaseC], bool],因为可能会有其他几种验证方法,这些方法是为从 BaseC 继承的每个类定义的。

但是,pylance 似乎无法识别 Callable 类型提示中的这种多态性,并发出警告(我设置我的 VScode 以对其进行类型检查)说

Argument of type "(c: InheritC) -> bool" cannot be assigned to parameter "fn" of type "(BaseC) -> bool" in function "register_fn"
  Type "(c: InheritC) -> bool" cannot be assigned to type "(BaseC) -> bool"
    Parameter 1: type "BaseC" cannot be assigned to type "InheritC"
      "BaseC" is incompatible with "InheritC" Pylance(reportGeneralTypeIssues)

我看不出我在设计中哪里犯了错误,我不想简单地忽略警告。

谁能解释为什么这是无效的? 或者它只是来自 pylance 的错误

我正在使用 python 版本 3.8.13 进行开发。

【问题讨论】:

  • 没有检查自己,我怀疑问题是 InheritC::validate 不兼容,因为它不仅采用一个 BaseC-compatible 参数,它还采用类 cls 参数。我相信一个独立的函数,称为validate,它只要BaseC-compatible 对象作为参数,就可以了。
  • 我没有检查过,但您可能还想尝试删除 @classmethod 装饰器,并将该方法声明为 def validate(self: 'InheritC'): return False,然后传递register_validate_fn。我假设这些例子是从一个真实的应用程序中剥离出来的;你没有在调用validate_fn之前调用register_validate_fn,但我认为你是故意的。
  • 很抱歉继续回复,但是...示例代码中还有另一个错误:ic.fn 未定义。我会开始用我的东西来回答思考该样本应该读起来像。
  • 我对方法参数的理解是错误的——问题更微妙,我会写一个完整的答案,因为这是一个相当抽象的问题。

标签: python type-hinting typechecking pylance


【解决方案1】:

我将使用下面的示例代码,其中修复了一些错误:

from typing import Callable


class BaseC:
    def __init__(self) -> None:
        pass

class InheritC(BaseC):
    def __init__(self) -> None:
        super().__init__()

    @classmethod
    def validate(cls, c:'InheritC') ->bool:
        return False

class AggrC:
    def register_validate_fn(self, fn: Callable[[BaseC], bool])-> None:
        self.validate_fn = fn

ac = AggrC()
ic = InheritC()
ac.register_validate_fn(ic.validate)

这个蟒蛇没有错误,但仍然产生与通过类型检查器(在我的例子中为 MyPY)运行时看到的错误相同的错误:

$ mypy stackoverflow_test.py 
stackoverflow_test.py:21: error: Argument 1 to "register_validate_fn" of "AggrC" has incompatible type "Callable[[], bool]"; expected "Callable[[BaseC], bool]"  [arg-type]
Found 1 error in 1 file (checked 1 source file)

这是一个微妙的问题,很容易被忽视:这是参数类型contravariance的问题。

这很容易被忽视的原因是因为大多数面向对象的指导都集中在类和对象上,并没有真正讨论职能作为继承类型。事实上,大多数具有面向对象语言特性的语言都不支持将一个函数声明为从另一个函数继承!

让我们尽可能地减少它:

from typing import Callable

class Parent:
    def foo(self):
        pass

class Child(Parent):
    def bar(self):
        pass
    
def takesParent(parameter: Parent):
    parameter.foo()
    
def takesChild(parameter: Child):
    parameter.bar()
    
def takesFunction(function: Callable):
    # What should the signature of `function` be to support both functions above?
    pass

你应该如何定义function: Callable以使其兼容两个功能?

让我们看看takesFunction 可以做什么,这对两个函数都有效:

def takesFunction(function: Callable):
    child = Child()
    function(child)

如果你传递其中一个函数,这个函数应该可以工作,因为takesParent 将调用 child.foo(),这是有效的; takesChild 将调用 child.bar(),这也是有效的。

好的,这个功能怎么样?

def takesFunction(function: Callable):
    parent = Parent()
    function(parent)

在这种情况下,function(parent) 只能与 takesParent 一起使用,因为如果您传递 takesChildtakesChild 将调用 parent.bar() - 这是不存在的!

因此,支持传递这两个函数的签名如下所示:

def takesFunction(function: Callable[[Child], None]):

这对许多人来说是违反直觉的。

参数函数必须是类型提示的最具体参数类型。传递一个函数不太具体参数 - 一个超类 - 是兼容的,但通过一个更具体参数不是。

这可能是一个难以理解的话题,所以如果我没有说得很清楚,我很抱歉,但我希望这个答案有所帮助。

【讨论】:

    猜你喜欢
    • 2015-01-25
    • 2023-01-25
    • 2011-05-05
    • 2011-08-26
    • 1970-01-01
    • 2019-02-11
    • 1970-01-01
    • 2021-12-25
    • 2018-01-12
    相关资源
    最近更新 更多