For type checking, can I use decorators to check optional typed class attributes are defined to prevent None object has no attribute errors?

huangapple go评论65阅读模式
英文:

For type checking, can I use decorators to check optional typed class attributes are defined to prevent None object has no attribute errors?

问题

I understand your questions:

  1. The decorator you've created doesn't change the outcome of type checking because mypy analyzes code statically, and it may not fully understand the runtime logic involved in your decorator. The decorator doesn't provide enough information to mypy to infer that self.child is guaranteed to be defined before calling the decorated method.

  2. To systematically verify that optional attributes are defined without repeating code, you can use a combination of the @check_defined decorator and type hints to inform mypy of the intended behavior. Here's a modified version of your code that addresses this:

import typing

def check_defined(func):
    def wrapper(self, *args, **kwargs):
        if not self.child:
            raise Exception("child not defined")
        else:
            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:
        self.child.print_foo()

main = Main()
main.initialise()
main.foo()

By specifying self as the first parameter in the wrapper function and including type hints, you provide mypy with more information about the code's behavior. This should help mypy correctly understand that self.child is guaranteed to be defined when calling the decorated method.

英文:

I have a code where a Main class receives instances of a Child class as attributes.
Those attributes are typed as Optional, because they are undefined at application startup, in which case they get initialised.

Methods of the Main instance rely in many places on methods and attributes of those Child objects.

Here is a simplified example of my code, which is synchronous but in my actual app it's all async hencewhy the child does not get instantiated in __init__, and I have a separate initialise method:

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()

I am trying to enforce proper type checking in my application pipeline using mypy.

My problem is that since those child attributes are Optional and initially undefined, mypy complains that

Item "None" of "Optional[Child]" has no attribute "print_foo"  [union-attr]

I could check in every method that the relevant attribute is defined, such as

def foo(self) -> None:
    if self.child:
        self.child.print_foo()
    else:
        raise Exception("child not defined")

but I do not want to do that because that would mean repeating this code for every method that is encountering that problem, and there are many in my application.

One way I tried to solve it is using a decorator:

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()

However this does not change the outcome of the type checking.

Therefore my question is twofold:

  • why can't I use such a decorator to enforce that the attribute is defined and fix my type checking issue?
  • what is a good solution to systematically verify optional attributes are defined without repeating code?

答案1

得分: 1

Your Main类似乎尝试着同时具备两个角色:

  1. 具有 foo 方法的东西
  2. 构建具有可用 foo 方法的对象

这些功能可能应该分成两个独立的类。

from dataclasses import dataclass
from typing import Optional

class Child:
    def print_foo(self) -> None:
        print("foo")

@dataclass
class Main:
    child: Child

    def foo(self) -> None:
        self.child.print_foo()

@dataclass
class MainBuilder:
    child: Optional[Child] = None
        
    def set_child(self) -> None:
        self.child = Child()

    def build(self) -> Main:
        if self.child is None:
            raise ValueError("Child not yet available")

        # 类型缩小:如果达到这一行,
        # 我们可以假定 self.child 不为 None,
        # 因此可以假定其类型为 Child,而不是 Optional[Child]
        return Main(self.child)

现在,您可以通过实例化一个 MainBuilder 对象开始,使用 set_child 替代旧的 initialise 方法。

mb = MainBuilder()
mb.set_child()

您不再直接调用 Main 来创建一个 Main 对象,而是通过调用 mb.build 来创建,如果还没有一个 child,则会引发异常。一旦您拥有了一个 Main 对象,那么它就保证可以调用 foo

main = mb.build()
main.foo()

可以直接实例化 Main,但现在 Main.__init__ 需要一个 Child 参数,且不可选。

main2 = Main(Child())
英文:

Your Main class seems to be trying to be two things:

  1. Something that has a foo method
  2. Something that builds an object that has a working foo method.

These should probably be two separate classes.

from dataclasses import dataclass
from typing import Optional


class Child:
    def print_foo(self) -> None:
        print("foo")


@dataclass
class Main:
    child: Child

    def foo(self) -> None:
        self.child.print_foo()


@dataclass
class MainBuilder:
    child: Optional[Child] = None
    
    def set_child(self) -> None:
        self.child = Child()

    def build(self) -> Main:
        if self.child is None:
            raise ValueError("Child not yet available")

        # Type narrowing: if we reach this line,
        # we can assume self.child is not None,
        # and thus assume its type is Child, not Optional[Child]
        return Main(self.child)

Now you can start by instantiating a MainBuilder object, with set_child replacing the old initialise method.

mb = MainBuilder()
mb.set_child()

You create a Main object not by calling Main directly, but by calling mb.build, which will raise an exception if you don't yet have a child. Once you have a Main object, then it is guaranteed to be ready to call foo.

main = mb.build()
main.foo()

You can instantiate Main directly, but now a Child argument to Main.__init__ is defined and not optional.

main2 = Main(Child())

huangapple
  • 本文由 发表于 2023年4月19日 17:36:25
  • 转载请务必保留本文链接:https://go.coder-hub.com/76052961.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定