You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决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的类型检查问题,我有两个疑问:

  1. 为何此类装饰器无法确保属性已定义并解决类型检查问题?
  2. 如何在不重复代码的前提下,系统性验证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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 10:29:55