PyCharm提醒我关于我的元类的类型警告;mypy不同意

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

PyCharm gives me a type warning about my metaclass; mypy disagrees

问题

你的代码看起来基本正确,但PyCharm给出的警告可能是因为类型提示的问题。这并不一定意味着代码有问题,而只是PyCharm认为类型可能不匹配,但实际运行时并没有问题。

Mypy则没有发现问题,这是一个静态类型检查工具,通常可以帮助你发现类型相关的问题。

所以,从代码本身看,你的实现似乎没有问题。警告可能是PyCharm的一种类型推断方式导致的,而不一定表示代码有错误。如果你的代码能够按预期工作,你可以忽略这些警告,或者尝试在PyCharm中进行类型提示的设置以解决警告问题。

英文:

I was trying to write a metaclass named Singleton, that, of course, implement the singleton design pattern:

class Singleton(type):
	
  def __new__(cls, name, bases = None, attrs = None):
    if bases is None:
      bases = ()
		
    if attrs is None:
      attrs = {}
		
    new_class = type.__new__(cls, name, bases, attrs)
    new_class._instance = None
    return new_class
	
  def __call__(cls, *args, **kwargs):
    if cls._instance is None:
      cls._instance = cls.__new__(cls, *args, **kwargs)
      cls.__init__(cls._instance, *args, **kwargs)
		
    return cls._instance

This seems to work correctly:

class Foo(metaclass = Singleton):
  pass

foo1 = Foo()
foo2 = Foo()

print(foo1 is foo2)  # True

However, PyCharm gave me this warning for <code>cls._instance = cls.__new__(<b>cls</b>, *args, **kwargs)</code>:

Expected type &#39;Type[Singleton]&#39;, got &#39;Singleton&#39; instead

...and this for <code>cls.__init__(<b>cls._instance</b>, *args, **kwargs)</code>:

Expected type &#39;str&#39;, got &#39;Singleton&#39; instead

I ran mypy on the same file, and here's what it said:

# mypy test.py
Success: no issues found in 1 source file

I'm using Python 3.11, PyCharm 2023.1.1 and mypy 1.3.0 if that makes a difference.

So what exactly is the problem here? Am I doing this correctly? Is this a bug with PyCharm, with mypy or something else? If the error is on me, how can I fix it?

答案1

得分: 2

以下是翻译好的部分:


解决方案

Singleton.__call__ 中,没有必要手动调用 cls.__new__ 然后调用 cls.__init__。你可以直接调用 super().__call__,就像 @Grismar 在 他的回答 中所做的那样。

实际上,如果你只是想以一种类型安全的方式设置单例模式,就根本不需要自定义 Singleton.__new__ 方法。

要在你的类中使用 _instance = None 的后备方案,你只需在元类上定义并赋值该属性。

以下是所需的最小设置:

from __future__ import annotations
from typing import Any


class Singleton(type):
    _instance: Singleton | None = None

    def __call__(cls, *args: Any, **kwargs: Any) -> Singleton:
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance


class Foo(metaclass=Singleton):
    pass

这将通过 mypy --strict 并且不会导致 PyCharm 警告。你说你希望 _instance 在每个 中都被保存,而不是在 元类 中。这在这里是这样的。请尝试以下代码:

foo1 = Foo()
foo2 = Foo()

print(foo1 is foo2)           # True
print(foo1 is Foo._instance)  # True
print(Singleton._instance)    # None

如果你确实想要一个自定义元类 __new__ 方法,由于重载了 type 构造函数,它需要更多的样板代码来以一种类型安全的方式进行设置。但是以下是一个应该在所有情况下都能正常工作的模板:

from __future__ import annotations
from typing import Any, TypeVar, overload

T = TypeVar("T", bound=type)


class Singleton(type):
    _instance: Singleton | None = None

    @overload
    def __new__(mcs, o: object, /) -> type: ...

    @overload
    def __new__(
        mcs: type[T],
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        /,
        **kwargs: Any,
    ) -> T: ...

    def __new__(
        mcs,
        name: Any,
        bases: Any = (),
        namespace: Any = None,
        /,
        **kwargs: Any,
    ) -> type:
        if namespace is None:
            namespace = {}
        # 在这里做其他事情...
        return type.__new__(mcs, name, bases, namespace, **kwargs)

    def __call__(cls, *args: Any, **kwargs: Any) -> Singleton:
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance

__new__ 的重载紧密地类似于 typeshed 桩文件 中的那些。

但是再次强调,这对于 通过元类实现的单例模式 并不是必需的。

深入研究你的类型推断问题让我陷入了一个兔子洞,所以解释变得有点长了。但我认为将解决方案直接呈现给你是更有用的(因为它规避了错误)。因此,如果你对解释感兴趣,请继续阅读。


为什么 cls.__new__ 期望 type[Singleton]

我对你的代码得到了相同的 Mypy 错误:

__new__ of "Singleton" has incompatible type "Singleton"; expected "Type[Singleton]"

回想一下 __call__ 方法是一个 实例 方法。在 Singleton.__call__ 的上下文中,第一个参数(在本例中命名为 cls)的类型被推断为 Singleton

由于你定义了自己的 Singleton.__new__ 方法,它的(隐式)签名将反映在 cls.__new__ 中。你没有为 Singleton.__new__ 加上注释,但是类型检查器通常会针对特殊情况(如方法的第一个参数)退回到“标准”推断。

__new__ 是一个 静态 方法,它以类作为第一个参数,并返回该类的实例。因此,Singleton.__new__ 的第一个参数预计应该是 类型 type[Singleton],而不是 Singleton 的一个实例。

因此,从类型检查器的角度来看,通过调用 cls.__new__(cls, ...),你实际上是将 Singleton 的一个_实例_传递为参数,而实际上预期的是该类型本身(或其子类型)。


附注

这种区别可能会相当令人困惑,这也是为什么最好的做法是将 __new__ 的第一个参数命名为与普通方法不同的名称。在普通类(不继承自 type)中,正常方法的第一个参数应该叫做 self,而 __new__ 的第一个参数应该叫做 cls

然而,在 类中,惯例并没有那么普遍,但是常识告诉我们普通方法的第一个参数应该叫做 cls,而 __new__ 的第一个参数应该叫做 mcs(或 mcls 或类似的)。这非常有用,可以突出显示第一个参数的性质上的区别。当然,这些都是惯例,解释器无论如何都不在乎。


无论是否将 cls 在该上下文中解析为周围(元)类的实例,我认为期望 cls.__new__ 解析为该(元)类的 __new__ 方法是没有道理的。

英文:

Since this is an XY Problem, I'll start with the solution to X. The answers for Y are further down.


Solution

There is no need for manually calling cls.__new__ and then cls.__init__ in Singleton.__call__. You can just call super().__call__ instead, just like @Grismar did in his answer.

There is also no need for a custom Singleton.__new__ method at all, if all you want to do is set up the singleton pattern in a type safe manner.

And to have a _instance = None fallback in your classes, you can just define and assign that attribute on the meta class.

Here is the minimal setup required:

from __future__ import annotations
from typing import Any


class Singleton(type):
    _instance: Singleton | None = None

    def __call__(cls, *args: Any, **kwargs: Any) -&gt; Singleton:
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance


class Foo(metaclass=Singleton):
    pass

This passes mypy --strict and causes no PyCharm warnings. You said you wanted _instance to be saved in each class not in the meta class. This is the case here. Try the following:

foo1 = Foo()
foo2 = Foo()

print(foo1 is foo2)           # True
print(foo1 is Foo._instance)  # True
print(Singleton._instance)    # None

If you do want a custom meta class __new__ method, it requires a lot more boilerplate to set up in a type safe manner, due to the overloaded type constructor. But here is a template that should work in all situations:

from __future__ import annotations
from typing import Any, TypeVar, overload

T = TypeVar(&quot;T&quot;, bound=type)


class Singleton(type):
    _instance: Singleton | None = None

    @overload
    def __new__(mcs, o: object, /) -&gt; type: ...

    @overload
    def __new__(
        mcs: type[T],
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, Any],
        /,
        **kwargs: Any,
    ) -&gt; T: ...

    def __new__(
        mcs,
        name: Any,
        bases: Any = (),
        namespace: Any = None,
        /,
        **kwargs: Any,
    ) -&gt; type:
        if namespace is None:
            namespace = {}
        # do other things here...
        return type.__new__(mcs, name, bases, namespace, **kwargs)

    def __call__(cls, *args: Any, **kwargs: Any) -&gt; Singleton:
        if cls._instance is None:
            cls._instance = super().__call__(*args, **kwargs)
        return cls._instance

The __new__ overloads closely resemble those in the typeshed stubs.

But again, that is not necessary for the singleton-via-metaclass pattern.

Digging deeper into your type inference questions sent me down a rabbit hole, so the explanations got a little longer. But I thought it more useful to present the solution to your actual problem up front (because it circumvents the errors). So if you are interested in the explanations, keep reading.


Why does cls.__new__ expect type[Singleton]?

I get the same error from Mypy for your code:

Argument 1 to &quot;__new__&quot; of &quot;Singleton&quot; has incompatible type &quot;Singleton&quot;; expected &quot;Type[Singleton]&quot;

Recall that the __call__ method is an instance method. In the context of Singleton.__call__, the type of the first argument (in this case named cls) is inferred as Singleton.

Since you defined your own Singleton.__new__ method, its (implicit) signature will be reflected in cls.__new__. You did not annotate Singleton.__new__, but type checkers typically fall back to "standard" inferences for special cases like the first parameter of a method.

__new__ is a static method that takes a class as the first argument and returns an instance of it. The first argument to Singleton.__new__ is therefore expected to be the type type[Singleton], not an instance of Singleton.

So from the point of view of the type checker, by calling cls.__new__(cls, ...) you are passing an instance of Singleton as an argument, where a the type Singleton itself (or a subtype) is expected.


Side note:

This distinction can be quite confusing, which is one of the reasons why it is best practice to name the first parameter to __new__ differently than the first parameter to a "normal" method.

In regular classes (not inheriting from type) the first parameter to normal methods should be called self, while the first parameter to __new__ should be called cls.

In meta classes however, the conventions are not as ubiquitous, but common sense suggests that the normal methods' first parameter should be called cls, while the first parameter to __new__ should be called mcs (or mcls or something like that). It is just very useful to highlight the distinction in the nature of that first argument. But these are all conventions of course and the interpreter doesn't care either way.


Whether or not this inference of cls.__new__ as Singleton.__new__ is justified or sensible is debatable.

Since cls in that context will always be an instance of the surrounding (meta) class, I would argue that it does not make sense to expect cls.__new__ to ever resolve to the __new__ method of said (meta) class.

In fact, unless the class cls itself defines a custom __new__ method, it will fall back to object.__new__, not to Singleton.__new__:

class Singleton(type):
    def __call__(cls, *args, **kwargs):
        print(cls.__new__)
        print(cls.__new__ is object.__new__)
        print(cls.__new__ is Singleton.__new__)

class Foo(metaclass=Singleton):
    pass

foo1 = Foo()

Output:

&lt;built-in method __new__ of type object at 0x...&gt;
True
False

object.__new__ indeed does accept Singleton as its first argument because it is a class and the return type would be an instance of it. So there is nothing wrong or unsafe about the way you called cls.__new__ as far as I can tell.

We can see the wrong type inference by the type checker even more clearly, if we add a custom __new__ to Singleton and run it through the type checker:

from __future__ import annotations
from typing import Any


class Singleton(type):
    def __new__(mcs, name: str, bases: Any = None, attrs: Any = None) -&gt; Singleton:
        return type.__new__(mcs, name, bases, attrs)

    def __call__(cls, *args: Any, **kwargs: Any) -&gt; Any:
        reveal_type(cls.__new__)
        ...

Mypy wrongly infers the type as follows:

Revealed type is &quot;def (mcs: Type[Singleton], name: builtins.str, bases: Any =, attrs: Any =) -&gt; Singleton&quot;

So it clearly expects cls.__new__ to be Singleton.__new__, even though it is actually object.__new__.

As far as I understand it, the discrepancy between the actual method resolution and the one inferred by the type checker is due to the special-cased behavior for the __new__ method. It may also just have to do with meta classes being poorly supported by type checkers. But maybe someone more knowledgeable can clear this up. (Or I'll consult the issue tracker.)


That pesky __init__ call

The PyCharm message is nonsense of course. The problem seems to come down to the same faulty inference of cls.__init__ as type.__init__, as opposed to object.__init__.

Mypy has an entirely different problem, complaining about the explicit usage of __init__ on an instance with the following error:

Accessing &quot;__init__&quot; on an instance is unsound, since instance.__init__ could be from an incompatible subclass

The __init__ method is intentionally excluded from LSP conformity requirements by mypy, which means that explicitly calling it is technically unsafe.

Not much else to say. Avoid that call, unless you are sure the overrides all the way up the MRO chain for __init__ are type safe; then use # type: ignore[misc].


So to summarize, I believe these two warnings/errors are both false positives.

答案2

得分: 0

Here's the translated content:

由于您还在问“我是否做得正确?”,为什么不使用更简单的解决方案,如下所示:

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class Foo(metaclass=SingletonMeta):
    ...


foo1 = Foo()
foo2 = Foo()

print(foo1 is foo2)  # 仍然为 True

这避免了构造引起的混淆和警告。我明白我没有回答为什么PyCharm检测到您认为是不正确的类型冲突的问题 - 它们可能是不正确的,我没有深入挖掘为什么PyCharm可能会这样认为。

如果您只想摆脱警告,在确定问题没有被正确识别之后,可以这样做:

# noinspection PyTypeChecker
cls._instance = cls.__new__(cls, *args, **kwargs)

如果您找出了确切的问题,您应该向JetBrains提交一个错误报告(并在您的 #noinspection 中添加一个 #todo,以提醒自己在问题修复后删除它)。

英文:

Since you're also asking "am I doing this correctly?", why don't you use a simpler solution like this:

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__call__(*args, **kwargs)
        return cls._instances[cls]


class Foo(metaclass=SingletonMeta):
    ...


foo1 = Foo()
foo2 = Foo()

print(foo1 is foo2)  # still True

This avoids the construction causing the confusion and warning. I appreciate that I'm not answering the question why PyCharm is detecting type conflicts that you feel are incorrect - they may be, I didn't dig down deep enough to figure out why PyCharm might think this.

If you just want to get rid of the warning, after you've ascertained the problem is not correctly identified:

    # noinspection PyTypeChecker
    cls._instance = cls.__new__(cls, *args, **kwargs)

And if you find out what exactly is wrong, you should submit a bug report with JetBrains (and add a #todo to your #noinspection to remind yourself to remove it once it's fixed).

huangapple
  • 本文由 发表于 2023年5月11日 13:20:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/76224359.html
匿名

发表评论

匿名网友

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

确定