解决Optional类属性的mypy类型检查问题:装饰器失效原因与方案
我有一段代码,Main类以Child类实例作为属性,这些属性被标记为Optional类型——因为应用启动时未定义,后续会通过initialise方法初始化。Main类的多个方法依赖这些Child对象的属性与方法,简化代码如下:
import typing class Child(): def print_foo(self) -> None: print("foo") class Main(): def __init__(self) -> None: self.child: typing.Optional[Child] = None def initialise(self) -> None: if not self.child: self.child = Child() def foo(self) -> None: self.child.print_foo() main = Main() main.initialise() main.foo()
使用mypy进行类型检查时,由于这些属性是Optional类型且初始为None,mypy报错:
Item "None" of "Optional[Child]" has no attribute "print_foo" [union-attr]
我可以在每个方法中检查属性是否已定义,示例代码如下:
def foo(self) -> None: if self.child: self.child.print_foo() else: raise Exception("child not defined")
但这种方式会产生大量重复代码,因此尝试使用装饰器解决:
import typing def check_defined(func): def wrapper(*args, **kwargs): _self = args[0] if not _self.child: raise Exception("child not defined") else: return func(*args, **kwargs) return wrapper class Child(): def print_foo(self) -> None: print("foo") class Main(): def __init__(self) -> None: self.child: typing.Optional[Child] = None def initialise(self) -> None: if not self.child: self.child = Child() @check_defined def foo(self) -> None: self.child.print_foo() main = Main() main.initialise() main.foo()
然而该装饰器并未解决mypy的类型检查问题,我有两个疑问:
- 为何此类装饰器无法确保属性已定义并解决类型检查问题?
- 如何在不重复代码的前提下,系统性验证Optional属性已定义?
疑问1:装饰器无法解决类型检查问题的原因
mypy是静态类型检查工具,它不会执行你的代码,也无法自动推断出装饰器的运行时逻辑会确保self.child不为None。
在被装饰的foo方法中,mypy看到的self.child依然是Optional[Child]类型——它不知道装饰器的wrapper函数已经提前做了非空检查,因此会继续抛出union-attr错误。
疑问2:系统性验证Optional属性的方案
方案1:使用类型守卫(Type Guard)
定义一个类内的检查方法,用TypeGuard注解告诉mypy:当该方法返回True时,self.child一定不为None。这样既避免重复代码,又能让mypy识别类型。
import typing from typing import TypeGuard class Child(): def print_foo(self) -> None: print("foo") class Main(): def __init__(self) -> None: self.child: typing.Optional[Child] = None def initialise(self) -> None: if not self.child: self.child = Child() def _ensure_child_initialised(self) -> TypeGuard[self]: """类型守卫:确保child已初始化,否则抛出异常""" if self.child is None: raise Exception("child not defined") return True def foo(self) -> None: self._ensure_child_initialised() self.child.print_foo() # mypy此时明确知道child是Child类型 main = Main() main.initialise() main.foo()
方案2:结合装饰器与类型断言
如果坚持使用装饰器,可以在被装饰的方法中用cast做类型断言,明确告诉mypyself.child此时不为None。也可以给装饰器添加类型注解,让mypy理解装饰器的作用:
import typing from typing import TypeVar, Callable T = TypeVar('T', bound='Main') Func = Callable[[T], None] def check_defined(func: Func[T]) -> Func[T]: def wrapper(self: T, *args, **kwargs): if self.child is None: raise Exception("child not defined") return func(self, *args, **kwargs) return wrapper class Child(): def print_foo(self) -> None: print("foo") class Main(): def __init__(self) -> None: self.child: typing.Optional[Child] = None def initialise(self) -> None: if not self.child: self.child = Child() @check_defined def foo(self) -> None: # 用cast断言child不为None child = typing.cast(Child, self.child) child.print_foo() main = Main() main.initialise() main.foo()
方案3:重构初始化逻辑
如果业务允许,将初始化逻辑改为工厂模式,确保返回的Main实例已经完成child的初始化,这样可以直接把self.child的类型从Optional[Child]改为Child:
import typing class Child(): def print_foo(self) -> None: print("foo") class Main(): def __init__(self) -> None: # 这里不再用Optional,因为工厂方法会确保初始化 self.child: Child @classmethod def create(cls) -> "Main": """工厂方法:返回已初始化完成的Main实例""" instance = cls() instance.initialise() return instance def initialise(self) -> None: self.child = Child() def foo(self) -> None: self.child.print_foo() # mypy无报错 main = Main.create() main.foo()
内容的提问来源于stack exchange,提问作者Mathieu Roze

