如何正确为Python的classonlymethod描述符添加类型注解?
问题背景
我实现了一个类专属方法装饰器classonlymethod,功能类似内置classmethod,但限制只能通过类本身调用,不能在实例上调用。代码如下:
from typing import Concatenate, Self from collections.abc import Callable from functools import partial class classonlymethod[C, **P, R]: def __init__(self, func: Callable[Concatenate[type[C], P], R], /): self._func = func def __get__(self, instance: None, owner: type[C], /) -> Callable[P, R]: if instance is not None: raise AttributeError('class-only method') return partial(self._func, owner) class Foo: @classonlymethod def bar(cls: type[Self]) -> None: raise NotImplementedError()
但Pyright在Foo.bar的定义处抛出如下报错:
参数“cls”的类型必须是其类“Foo”的超类型(reportGeneralTypeIssues)
这里的注解格式和typeshed中classmethod的写法一致,我无法理解:Self指代的Foo本身难道不是自身的非严格超类型吗?尝试过其他引用Foo的方式,均无效。想搞清楚这是什么情况,该怎么修复,是否属于Pyright的bug?
问题原因
Pyright对内置classmethod做了特殊逻辑处理,能自动识别cls参数应为类本身或其子类的类型。但自定义的classonlymethod作为泛型描述符,Pyright无法自动推导泛型参数C和Self的关联关系,导致它误将类专属方法的cls参数按照实例方法的逻辑校验——要求参数类型是类的超类型,而type[Foo](即Self对应的类对象类型)并不符合这个校验逻辑。
修复方案
方案1:显式指定装饰器的泛型参数
在使用classonlymethod时,明确指定泛型参数C为当前类,让Pyright明确类型关联:
class Foo: @classonlymethod[Foo] def bar(cls: type[Self]) -> None: raise NotImplementedError()
方案2:调整装饰器的泛型定义
修改classonlymethod的泛型参数绑定,让它适配类对象类型的推断:
from typing import Concatenate, Self, TypeVar, Generic from collections.abc import Callable from functools import partial # 绑定泛型参数为类类型 C = TypeVar('C', bound=type) class classonlymethod(Generic[C]): def __init__(self, func: Callable[Concatenate[C, ...], None], /): self._func = func def __get__(self, instance: None, owner: C, /) -> Callable[..., None]: if instance is not None: raise AttributeError('class-only method') return partial(self._func, owner) class Foo: @classonlymethod def bar(cls: type[Self]) -> None: raise NotImplementedError()
这种方式通过TypeVar绑定到type,让Pyright能正确识别cls的类型为Foo的类对象类型。
方案3:直接使用类名前向引用替代Self
用字符串字面量做类的前向引用,避免Self带来的推断问题:
class Foo: @classonlymethod def bar(cls: type['Foo']) -> None: raise NotImplementedError()
是否是Pyright的bug?
不算严格意义上的bug。因为内置classmethod是Pyright特殊优化的类型,而自定义泛型描述符需要更明确的类型提示才能触发正确的推断逻辑。如果认为这种场景应该被支持,可以在Pyright的GitHub仓库提交issue,提议优化自定义类方法装饰器的类型推断逻辑。
内容的提问来源于stack exchange,提问作者yuri kilochek

