【问题标题】:Why does this mypy, slots, and abstract class hack work?为什么这个 mypy、slots 和 abstract class hack 有效?
【发布时间】:2018-02-13 01:45:38
【问题描述】:

我有一个相对较大的 Python 项目,为了尽量减少调试时间,我正在尝试模拟低级语言的一些方面。具体

  1. 能够进行类型转换(静态类型)
  2. 防止向类添加动态属性。

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

在某一时刻,我需要一个列表,其中包含两个不同的子类(它们具有相同的父类),它们的属性略有不同。 mypy 不喜欢所有列表项中都不存在的列表项的属性调用这一事实。但是,使父对象过于笼统意味着不会阻止动态添加另一个子对象中存在的变量。

为了解决这个问题,我调试/强制自己使用以下似乎可行的代码示例:

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

这行得通。但我不确定为什么。如果我不初始化父级中的变量(即仅将它们设置为Noneint),那么它们似乎不会被带入子级。但是,如果我给他们一个占位符值,例如f:int = 0 在父母中,然后他们进入孩子,我的支票不再起作用。

谁能向像我这样的白痴解释这种行为?我想知道这样我就不会搞砸实现某些东西并引入更多错误!

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

【问题讨论】:

    标签: python python-3.x slots mypy abc


    【解决方案1】:

    将名称设置为父级中的值会创建一个类属性。即使实例受到__slots__ 的限制,类本身也可以具有非插槽名称,并且当实例缺少属性时,始终会检查其类的类级别属性(这就是您可以在实例上调用方法的方式完全)。

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

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

    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
    

    如果目标是根据子类的类型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 的检查能力;列表旨在用作同质数据结构,而使位置变得重要(例如tuple,旨在用作异构容器)正在滥用它们。

    【讨论】:

    • 我现在无法运行它,因为我在我的手机上,但我很确定我测试了一些非常相似的东西,mypy 会在我原来的列表结构之类的东西上出现错误例子。
    • 是的,刚刚运行它。 mypy 提出error: "parentclass" has no attribute "f"
    • @ashgetstazered:这不是故意的吗?代码中的 cmets 明确表示,当使用混合的 parentclass 子代的 list 时,您希望分配给 .f 并从中读取失败。
    • 对不起!我已经更新了这个问题,以更好地反映我所追求的。如果您只访问具有withb 类的列表部分,如果withf 类在列表中,mypy 仍然会抛出错误,因为它们没有属性b,即使它们在运行时永远不会被调用。
    • @ashgetstazered:这感觉越来越像an XY problem。你有一个list,其中类型实际上是异构的(它们以明确遵守公共接口的方式使用),所以你需要跟踪哪些索引是有效的@987654354 @ 与withb 并以完全不同的方式使用它们正是mypy 应该 防止的。 mypydoes understand isinstance checks,所以如果只在类型检查后使用attr,是合法的。
    猜你喜欢
    • 1970-01-01
    • 2011-07-16
    • 2016-10-05
    • 2021-05-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多