英文:
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:
-
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. -
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
类似乎尝试着同时具备两个角色:
- 具有
foo
方法的东西 - 构建具有可用
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:
- Something that has a
foo
method - 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())
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论