为什么这个 mypy、slots 和 abstract class hack 有效?
Why does this mypy, slots, and abstract class hack work?
我有一个相对较大的 Python 项目,为了尽量减少调试时间,我尝试模拟 lower-level 语言的几个方面。具体
- 类型转换的能力(静态类型)
- 防止向 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 中的变量(即只将它们设置为 None
或 int
),那么它们似乎不会被带入 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 中分配默认值是没有意义的,因为并非所有子级都具有 f
或 b
属性。如果所有子项都必须有一个 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
的情况下(您 知道 类型,因为某些索引保证是 withf
或 withb
),你可以 explicitly cast
the type,但请注意,这会丢弃 mypy
的检查能力;列表旨在作为同质数据结构,并且使位置变得重要(a la tuple
,旨在作为异构容器)正在滥用它们。
我有一个相对较大的 Python 项目,为了尽量减少调试时间,我尝试模拟 lower-level 语言的几个方面。具体
- 类型转换的能力(静态类型)
- 防止向 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 中的变量(即只将它们设置为 None
或 int
),那么它们似乎不会被带入 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 中分配默认值是没有意义的,因为并非所有子级都具有 f
或 b
属性。如果所有子项都必须有一个 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
的情况下(您 知道 类型,因为某些索引保证是 withf
或 withb
),你可以 explicitly cast
the type,但请注意,这会丢弃 mypy
的检查能力;列表旨在作为同质数据结构,并且使位置变得重要(a la tuple
,旨在作为异构容器)正在滥用它们。