为什么这个 mypy、slots 和 abstract class hack 有效?

Why does this mypy, slots, and abstract class hack work?

我有一个相对较大的 Python 项目,为了尽量减少调试时间,我尝试模拟 lower-level 语言的几个方面。具体

  1. 类型转换的能力(静态类型)
  2. 防止向 classes 添加动态属性。

我一直在使用 mypy 来捕获类型转换错误,并且我一直在我的 class 实例中定义 __slots__ 以防止动态添加。

有一次我需要一个列表,其中包含两个不同的 children class(它们具有相同的 parent),它们的属性略有不同。 mypy 不喜欢这样的事实,即对所有列表项中都不存在的列表项的属性进行调用。但是,然后使 parent object 过于笼统意味着不会阻止动态添加其他 child 中存在的变量。

为了解决这个问题,我 debugged/brute-forced 自己使用了以下似乎有效的代码示例:

from abc import ABCMeta
from typing import List

class parentclass(metaclass=ABCMeta):
    __slots__:List[str] = []
    name: None

class withb(parentclass):
    __slots__ = ['b','name']
    def __init__(self):
        self.b: int = 0 
        self.name: str = "john"

class withf(parentclass):
    __slots__ = ['f','name']
    def __init__(self):
        self.name: str = 'harry'
        self.f: int = 123


bar = withb()
foo = withf()

ls: List[parentclass] = [bar, foo]

ls[0].f = 12 ## Needs to fail either in Python or mypy

for i in range(1):
    print(ls[i].name)
    print(ls[i].b) ## This should NOT fail in mypy

这行得通。但我不确定为什么。如果我不初始化 parent 中的变量(即只将它们设置为 Noneint),那么它们似乎不会被带入 children .但是,如果我给他们一个占位符值,例如f:int = 0 在 parent 然后他们进入了 children,我的支票不再有效。

任何人都可以向像我这样的白痴解释这种行为吗?我想知道,这样我就不会搞砸实施某些事情并引入更多错误!

顺便说一句:我确实尝试过 List[Union[withb, withf]] 但这也没有用!

将名称设置为父项中的值会创建 class 属性。即使实例受到 __slots__ 的限制,class 本身也可以有非槽名称,并且当实例缺少属性时,它的 class 总是会被检查是否有 class 级属性(这就是您可以在实例上调用方法的方式)。

尝试通过实例分配给 class 属性并不会替换 class 属性。 instance.attr = someval 将始终尝试在实例上创建不存在的属性(隐藏 class 属性)。当层次结构中的所有 class 都使用 __slots__(没有 __dict__ 插槽)时,这将失败(因为插槽不存在)。

当你只是为了 f: None 时,你已经注释了名称 f,但实际上并没有创建 class 属性;实际创建它的是默认值的分配。当然,在您的示例中,在父级 class 中分配默认值是没有意义的,因为并非所有子级都具有 fb 属性。如果所有子项都必须有一个 name,那应该是父项 class 的一部分,例如:

class parentclass(metaclass=ABCMeta):
    # Slot for common attribute on parent
    __slots__:List[str] = ['name']
    def __init__(self, name: str):
        # And initializer for parent sets it (annotation on argument covers attribute type)
        self.name = name

class withb(parentclass):
    # Slot for unique attributes on child
    __slots__ = ['b']
    def __init__(self):
        super().__init__("john")  # Parent attribute initialized with super call
        self.b: int = 0  # Child attribute set directly

class withf(parentclass):
    __slots__ = ['f']
    def __init__(self):
        super().__init__('harry')
        self.f: int = 123

如果目标是根据子class、mypy understands isinstance checks的类型动态选择是使用f还是b,那么可以更改使用它的代码:

if isinstance(ls[0], withf):  # Added to ensure `ls[0]` is withf before using it
    ls[0].f = 12 ## Needs to fail either in Python or mypy

for x in ls:
    print(x.name)
    if isinstance(x, withb):  # Added to only print b for withb instances in ls
        print(x.b) ## This should NOT fail in mypy

在不需要 isinstance 的情况下(您 知道 类型,因为某些索引保证是 withfwithb),你可以 explicitly cast the type,但请注意,这会丢弃 mypy 的检查能力;列表旨在作为同质数据结构,并且使位置变得重要(a la tuple,旨在作为异构容器)正在滥用它们。