我试图编写一个名为Singleton的元类,当然,它实现了单例设计模式:

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

这似乎工作正常:

class Foo(metaclass = Singleton):
  pass

foo1 = Foo()
foo2 = Foo()

print(foo1 is foo2)  # True

然而,PyCharm给了我这样一个cls._instance = cls.__new__(cls, *args, **kwargs)岁的警告:

Expected type 'Type[Singleton]', got 'Singleton' instead

...这是cls.__init__(cls._instance, *args, **kwargs)美元:

Expected type 'str', got 'Singleton' instead

我在同一文件上运行了mypy,它是这样写的:

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

我使用的是Python3.11、PyCharm2023.1.1和mypy 1.3.0,如果它们有区别的话.

那么,这里到底有什么问题呢?我做得对吗?这是一个与PyCharm,与mypy或其他什么的错误?如果错误是我的错,我怎么才能改正呢?

推荐答案

由于这是一个XY Problem,我将从X的解开始.Y的答案更低.


不需要手动呼叫cls.__new__,然后在Singleton.__call__中呼叫cls.__init__.你可以直接拨打super().__call__,就像@Grismar拨打his answer一样.

如果您想要做的只是以类型安全的方式设置单例模式,那么也根本不需要定制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保存在每个class中,而不是meta class中.这里就是这种情况.try 以下操作:

foo1 = Foo()
foo2 = Foo()

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

如果您do想要一个定制的元类__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 = {}
        # do other things here...
        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 stubs的重载非常相似.

但同样,这对于singleton-via-metaclass%的模式来说并不是必要的.

更深入地挖掘你的类型推理问题让我陷入了困境,所以解释变得更长了.但我认为提前展示actual个问题的解决方案更有用(因为它避免了错误).因此,如果您对这些解释感兴趣,请继续阅读.


Why does cls.__new__ expect type[Singleton]?

对于您的代码,我从Mypy那里得到了相同的错误:

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

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

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

__new__是一个static方法,它将一个类作为第一个参数,并返回它的一个实例.因此,Singleton.__new__的第一个自变量预计是the type type[Singleton],而不是Singletonan instance.

因此,从类型判断器的Angular 来看,通过调用cls.__new__(cls, ...),您传递的是Singletoninstance作为参数,其中预期的是类型Singleton本身(或子类型).


Side note:

这种区别可能非常令人困惑,这就是为什么最佳实践之一是将第一个参数命名为__new__ differently,而不是将第一个参数命名为"正常"方法.

在常规类中(不是从type继承),普通方法的第一个参数应该调用self,而__new__的第一个参数应该调用cls.

然而,在meta个类中,约定并不普遍,但常识表明,普通方法的第一个参数应该被称为cls,而__new__的第一个参数应该被称为mcs(或mcls或类似的东西).强调第一个论点性质上的区别是非常有用的.但这些当然都是约定,解释器并不关心这两种方式.


这种cls.__new__等于Singleton.__new__的推论是否合理或合理,是值得商榷的.

由于该上下文中的cls将始终是周围(元)类的instance,我认为期望cls.__new__解析为所述(元)类的__new__方法是没有意义的.

事实上,除非类cls本身定义了一个定制的__new__方法,否则它将回退到object.__new__,而不是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()

输出:

<built-in method __new__ of type object at 0x...>
True
False

object.__new__实际上does接受Singleton作为它的第一个参数,因为它是一个类,返回类型将是它的一个实例.因此,据我所知,你拨打cls.__new__的方式没有任何错误或不安全之处.

如果我们将自定义__new__添加到Singleton并通过类型判断器运行它,我们可以更清楚地看到类型判断器做出的错误类型推断:

from __future__ import annotations
from typing import Any


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

    def __call__(cls, *args: Any, **kwargs: Any) -> Any:
        reveal_type(cls.__new__)
        ...

Mypy错误地推断出类型如下:

Revealed type is "def (mcs: Type[Singleton], name: builtins.str, bases: Any =, attrs: Any =) -> Singleton"

因此,它显然预计cls.__new__Singleton.__new__,尽管它实际上是object.__new__.

据我所知,实际的方法解析与类型判断器推断的解析之间的差异是由于__new__方法的特殊行为造成的.这也可能只是与类型判断器对元类的支持不佳有关.但也许更有见识的人可以澄清这一点.(或者我会咨询问题跟踪器.)


That pesky __init__ call

当然,关于PyCharm的消息是无稽之谈.这个问题似乎归结为同样的错误推理,即cls.__init__type.__init__,而不是object.__init__.

Mypy有一个完全不同的问题,它抱怨实例上显式使用__init__,并出现以下错误:

Accessing "__init__" on an instance is unsound, since instance.__init__ could be from an incompatible subclass

Mypy故意将__init__方法排除在LSP个一致性要求之外,这意味着显式调用它是technically不安全的.

没什么可说的了.避免调用该调用,除非您确定__init__MRO链上的所有重写都是类型安全的;然后使用# type: ignore[misc].


综上所述,我认为这两个警告/错误都是假阳性.

Python相关问答推荐

在Python中对分层父/子列表进行排序

2维数组9x9,不使用numpy.数组(MutableSequence的子类)

即使在可见的情况下也不相互作用

如何使用Python将工作表从一个Excel工作簿复制粘贴到另一个工作簿?

如何过滤包含2个指定子字符串的收件箱列名?

Python—从np.array中 Select 复杂的列子集

将输入聚合到统一词典中

如果满足某些条件,则用另一个数据帧列中的值填充空数据帧或数组

Pandas Loc Select 到NaN和值列表

用砂箱开发Web统计分析

合并与拼接并举

如何按row_id/row_number过滤数据帧

使用字典或列表的值组合

如何在Django模板中显示串行化器错误

分解polars DataFrame列而不重复其他列值

用由数据帧的相应元素形成的列表的函数来替换列的行中的值

在任何要保留的字段中添加引号的文件,就像在Pandas 中一样

根据过滤后的牛郎星图表中的数据计算新系列

递归链表反转与打印语句挂起

FileNotFoundError:[WinError 2]系统找不到指定的文件:在os.listdir中查找扩展名