python:__class_getitem__使用以及cached_property源码分析
1 前言
Python中如何模拟泛型类型?
当使用类型标注时,使用 Python 的方括号标记来形参化一个 generic type 往往会很有用处。 例如,list[int] 这样的标注可以被用来表示一个 list 中的所有元素均为 int 类型。
一个类通常只有在定义了特殊的类方法 __class_getitem__() 时才能被形参化。我们知道,一个list对象,可以通过索引下标取值,即形如a[0],是因为有__getitem__方法的实现,而__class_getitem__() 即针对类的,也就是上述的类名[xx]的形式用法,调用类名[xx]时,也就会调用我们自定义的__class_getitem__()方法。
classmethod object.__class_getitem__(cls, key):
按照 key 参数指定的类型返回一个表示泛型类的专门化对象。
当在类上定义时,__class_getitem__() 会自动成为类方法。 因此,当它被定义时没有必要使用 @classmethod 来装饰。
本文基于Python3.9对__class_getitem__()进行使用的讲解和代码演示。
官方文档参考:
https://docs.python.org/zh-cn/3.9/contents.html
2 使用
官方文档参考如下:
https://docs.python.org/zh-cn/3.9/reference/datamodel.html#object.__class_getitem__
2.1 __class_getitem__ 的目的
__class_getitem__() 的目的是允许标准库泛型类的运行时形参化以更方便地对这些类应用类型提示。
要实现可以在运行时被形参化并可被静态类型检查所理解的自定义泛型类,用户应当从已经实现了 __class_getitem__() 的标准库类继承,或是从 typing.Generic 继承,这个类拥有自己的 __class_getitem__() 实现。
标准库以外的类上的 __class_getitem__() 自定义实现可能无法被第三方类型检查器如 mypy 所理解。 不建议在任何类上出于类型提示以外的目的使用 __class_getitem__()。
2.2 __class_getitem__ 与 __getitem__
通常,使用方括号语法 抽取 一个对象将会调用在该对象的类上定义的 __getitem__() 实例方法。 不过,如果被拟抽取的对象本身是一个类,则可能会调用 __class_getitem__() 类方法。 __class_getitem__() 如果被正确地定义,则应当返回一个 GenericAlias 对象。
下面先来认识GenericAlias 类型:
参考官方文档:
https://docs.python.org/zh-cn/3.9/library/stdtypes.html#types-genericalias
GenericAlias 对象通常是通过 抽取 一个类来创建的。 它们最常被用于容器类,如 list 或 dict。 举例来说,list[int] 这个 GenericAlias 对象是通过附带 int 参数抽取 list 类来创建的。 GenericAlias 对象的主要目的是用于 类型标注。
类型标注,意即:关联到某个变量、类属性、函数形参或返回值的标签,被约定作为 类型注解 来使用。局部变量的标注在运行时不可访问,但全局变量、类属性和函数的标注会分别存放模块、类和函数的 __annotations__ 特殊属性中。也就是我们所说的python的annotation注解,如下简单示例python函数使用注解的场景:
def run(x: int, y: int) -> int:
pass
上述的形参x、y以及返回值的注解都是int,存在于函数的 __annotations__ 特殊属性中。
注意:通常一个类只有在实现了特殊方法 __class_getitem__() 时才支持抽取操作,也就是形如类名A[xx]的抽取操作。
GenericAlias 对象可作为 generic type 的代理,实现了 形参化泛型。
对于一个容器类,提供给类的 抽取 操作的参数可以指明对象所包含的元素类型。 例如,set[bytes] 可在类型标注中用来表示一个 set 中的所有元素均为 bytes 类型。
对于一个定义了 __class_getitem__() 但不属于容器的类,提供给类的抽取操作的参数往往会指明在对象上定义的一个或多个方法的返回值类型。 例如,正则表达式可以被用在 str 数据类型和 bytes 数据类型上:
- 如果 x = re.search(‘foo’, ‘foo’),则 x 将为一个 re.Match 对象而 x.group(0) 和x[0] 的返回值将均为 str 类型。 我们可以在类型标注中使用 GenericAlias re.Match[str] 来代表这种对象。
- 如果 y = re.search(b’bar’, b’bar’),(注意 b 表示 bytes),则 y 也将为一个 re.Match的实例,但 y.group(0) 和 y[0] 的返回值将均为 bytes 类型。 在类型标注中,我们将使用 re.Match[bytes] 来代表这种形式的 re.Match 对象。
GenericAlias 对象是 types.GenericAlias 类的实例,该类也可被用来直接创建 GenericAlias 对象。
T[X, Y, Z…]
创建一个代表由类型 X, Y, Z来参数化的类型 T 的 GenericAlias,此类型会更依赖于所使用的 T。 例如,一个接受包含 float 元素的 list 的函数:
def average(values: list[float]) -> float:
return sum(values) / len(values)
print(average([1.3, 3, 5]))
# 3.1
另一个例子是关于 mapping 对象的,用到了 dict,泛型的两个类型参数分别代表了键类型和值类型。本例中的函数需要一个 dict,其键的类型为 str,值的类型为 int:。
def send_post_request(url: str, body: dict[str, int]) -> None:
...
内置函数 isinstance() 和 issubclass() 不接受第二个参数为 GenericAlias 类型:
isinstance([1, 2], list[str])
执行报错:
Python运行时不会强制执行类型标注。 这种行为扩展到了泛型及其类型形参。 当由 GenericAlias创建容器对象时,并不会检查容器中为元素指定的类型。 例如,以下代码虽然不被鼓励,但运行时并不会报错:
t = list[str]
print(t([1, 2, 3]))
结果:
[1, 2, 3]
或者使用GenericAlias,如下有官方文档参考:
参考官方文档,GenericAlias 对象的特殊属性:
https://docs.python.org/zh-cn/3.9/library/stdtypes.html#special-attributes-of-genericalias-objects
genericalias.__origin__:本属性指向未应用参数之前的泛型类
print(list[int].__origin__)
# <class 'list'>
genericalias.__args__:该属性是传给泛型类的原始 __class_getitem__() 的泛型所组成的 tuple (长度可能为 1):
print(dict[str, list[int]].__args__)
# (<class 'str'>, list[int])
genericalias.__parameters__:该属性是延迟计算出来的一个元组(可能为空),包含了 __args__ 中的类型变量。
from typing import TypeVar
T = TypeVar('T')
print(list[T].__parameters__)
# (~T,)
对于GenericAlias对象的特殊属性,应用参数后的泛型都实现了一些特殊的只读属性,简单示例如下:
from types import GenericAlias
print(GenericAlias)
alias = GenericAlias(list[str], [1, 4, 9])
print(alias)
print(type(alias))
# 本属性指向未应用参数之前的泛型类
print(alias.__origin__)
# list[str]
print(type(alias.__origin__))
# <class 'types.GenericAlias'>
# 该属性是传给泛型类的原始 __class_getitem__()
# 的泛型所组成的 tuple (长度可能为 1):
print(alias.__args__)
# ([1, 4, 9],)
# 该属性是延迟计算出来的一个元组(可能为空),
# 包含了 __args__ 中的类型变量。
print(alias.__parameters__)
# ()
结果如下:
除了上述对于GenericAlias的使用,我们再来举个栗子:
使用非数据描述器协议,纯Python版本的 classmethod() 实现如下:
from types import MethodType
class ClassMethod:
"Emulate PyClassMethod_Type() in Objects/funcobject.c"
def __init__(self, f):
self.f = f
def __get__(self, obj, cls=None):
if cls is None:
cls = type(obj)
if hasattr(type(self.f), '__get__'):
print("执行hasattr:")
return self.f