初窥python泛型系统与类型约束

news/2024/11/14 22:21:46/文章来源:https://www.cnblogs.com/lrj-bupt/p/18545818

对类进行索引
翻阅python源码有时会看到类似这样的实现,class Dataset(Generic[T_co]):Generic是一个类,但是可以直接对其进行索引,这需要归功于魔法方法__class_getitem__

class Box:def __class_getitem__(cls, item):print(cls, item)var = Box[int, bool, str]  # 会输出 (<class 'int'>, <class 'bool'>, <class 'str'>)

之后会看到一个更具体且复杂的应用。

使用typing.TypeVar声明类型

通过typing.TypeVar可以声明一种类型,可以将这个类型作为type hint,例如:

_F = typing.TypeVar("_F")def func():return 1data: _F = func

但是这样的代码并不具有实际意义,我们希望能够对变量进行更好地类型检查并获得更好的提示功能。因此我们可以对类型增加多种约束。但是这些约束不会强制执行,只会得到警告,这是python语言特性决定的。例如:

_F = typing.TypeVar("_F", bound=typing.Callable[..., int])

我们对类型_F增加约束,希望它是一个可以接受任意数量参数,返回值为int类型的可调用对象。再例如:

T = TypeVar("T", int, float)

我们限定T只能是int或是float类型。
实际上typing.TypeVar非常灵活,有非常多可配置项。完整init函数声明如下:

 def __init__(self, name, *constraints, bound=None,covariant=False, contravariant=False):

对于使用TypeVar声明的类型,还可以在运行时获取类型的基本信息,例如:

T = TypeVar("T", int, float)
print(T.__name__)  // T
print(T.__constraints__)  // (<class 'int'>, <class 'float'>)
// ... 更多用法

python的typing库提供了丰富的约束条件,几乎可以代表python中的所有类型特点。例如:

from typing import TypeVar, SupportsRound, SupportsAbs
SR = TypeVar("SR", bound=SupportsRound)  //希望类型SR可以支持round操作
from typing import TypeVar, Awaitable
SW = TypeVar("SW", bound=Awaitable)  // 希望类型SW是可以被await的

此外,typing库还内置了很多基本类型例如List、'Dict'、'Union'等。

T = TypeVar("T", int, float) 
TD = Dict[str, T]td: TD = {}
td["a"] = 1
td["b"] = "2"  // 会得到一个警告 值的类型不匹配

TD表示一个key类型为字符串,value类型为int或是float类型的字典。
covariant是一个不太直观的编程概念,但是有时会用到这一特性。例如:

T_co = TypeVar("T_co", covariant=True)

__init_subclass__方法
函数名称可能具有一定误导性,这个方法在声明子类时就调用而不需要实例化子类对象。并且可以在定义子类时传递参数。

class Base:def __init_subclass__(cls, config=None, **kwargs):cls.config = configprint(f"Subclass {cls.__name__} created with config: {config}")super().__init_subclass__(**kwargs)class Sub1(Base, config="config1"):passclass Sub2(Base, config="config2"):pass

Generic使用

T_co = TypeVar("T_co", covariant=True)class Dataset(Generic[T_co]):def __init__(self, data: List[T_co]):self.data = datadef get_data(self) -> List[T_co]:return self.datad: Dataset[int] = Dataset([1, 2, 3])  # 通过泛型得到类型提示
print(Dataset[int].__origin__)        # 继承自Generic类会获取该属性
print(Dataset[int].__args__)          # 继承自Generic类会获取该属性
print(Dataset[int].__parameters__)    # 继承自Generic类会获取该属性
class Generic:"""Abstract base class for generic types.A generic type is typically declared by inheriting fromthis class parameterized with one or more type variables.For example, a generic mapping type might be defined as::class Mapping(Generic[KT, VT]):def __getitem__(self, key: KT) -> VT:...# Etc.This class can then be used as follows::def lookup_name(mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:try:return mapping[key]except KeyError:return default"""__slots__ = ()_is_protocol = False@_tp_cachedef __class_getitem__(cls, params):"""Parameterizes a generic class.At least, parameterizing a generic class is the *main* thing this methoddoes. For example, for some generic class `Foo`, this is called when wedo `Foo[int]` - there, with `cls=Foo` and `params=int`.However, note that this method is also called when defining genericclasses in the first place with `class Foo(Generic[T]): ...`."""if not isinstance(params, tuple):params = (params,)params = tuple(_type_convert(p) for p in params)if cls in (Generic, Protocol):# Generic and Protocol can only be subscripted with unique type variables.if not params:raise TypeError(f"Parameter list to {cls.__qualname__}[...] cannot be empty")if not all(_is_typevar_like(p) for p in params):raise TypeError(f"Parameters to {cls.__name__}[...] must all be type variables "f"or parameter specification variables.")if len(set(params)) != len(params):raise TypeError(f"Parameters to {cls.__name__}[...] must all be unique")else:# Subscripting a regular Generic subclass.for param in cls.__parameters__:prepare = getattr(param, '__typing_prepare_subst__', None)if prepare is not None:params = prepare(cls, params)_check_generic(cls, params, len(cls.__parameters__))new_args = []for param, new_arg in zip(cls.__parameters__, params):if isinstance(param, TypeVarTuple):new_args.extend(new_arg)else:new_args.append(new_arg)params = tuple(new_args)return _GenericAlias(cls, params,_paramspec_tvars=True)def __init_subclass__(cls, *args, **kwargs):super().__init_subclass__(*args, **kwargs)tvars = []if '__orig_bases__' in cls.__dict__:error = Generic in cls.__orig_bases__else:error = (Generic in cls.__bases__ andcls.__name__ != 'Protocol' andtype(cls) != _TypedDictMeta)if error:raise TypeError("Cannot inherit from plain Generic")if '__orig_bases__' in cls.__dict__:tvars = _collect_parameters(cls.__orig_bases__)# Look for Generic[T1, ..., Tn].# If found, tvars must be a subset of it.# If not found, tvars is it.# Also check for and reject plain Generic,# and reject multiple Generic[...].gvars = Nonefor base in cls.__orig_bases__:if (isinstance(base, _GenericAlias) andbase.__origin__ is Generic):if gvars is not None:raise TypeError("Cannot inherit from Generic[...] multiple types.")gvars = base.__parameters__if gvars is not None:tvarset = set(tvars)gvarset = set(gvars)if not tvarset <= gvarset:s_vars = ', '.join(str(t) for t in tvars if t not in gvarset)s_args = ', '.join(str(g) for g in gvars)raise TypeError(f"Some type variables ({s_vars}) are"f" not listed in Generic[{s_args}]")tvars = gvarscls.__parameters__ = tuple(tvars)

我们可以看到继承了泛型类后我们自定义的Dataset类支持Dataset[int]写法,这得益于Generic类实现了__class_getitem__(cls, params)方法。
但是我们可以注意到一个反常的现象那就是Generic__class_getitem__(cls, params)方法返回了一个_GenericAlias对象,所以Generic[T]的写法应当等价于_GenericAlias(cle, T),不应该继承Generic才对。但是我们用pycharm等工具却会发现Dataset类还是继承了Generic类,这是因为_GenericAlias继承了_BaseGenericAlias类,这个类中有一个关键的魔法方法__mro_entries__,这个类可以动态修改python类的继承关系,充分体现了python编程的灵活性。具体实现如下:

def __mro_entries__(self, bases):res = []if self.__origin__ not in bases:res.append(self.__origin__)i = bases.index(self)for b in bases[i+1:]:if isinstance(b, _BaseGenericAlias) or issubclass(b, Generic):breakelse:res.append(Generic)return tuple(res)

观察这个函数的实现逻辑,显然会判断是否继承自泛型类,没有就在res中添加Generic类。

两类type hint的细微区别:

def add_module(self, name: str, module: Optional['Module']) -> None: 
def add_module(self, name: str, module: Optional[Module]) -> None:

区别只在于一个单引号,大部分场景下两种用法可以等同。前者做法的优点在于可以避免一些作用域带来的问题,例如:

from typing import Union, Optionalclass Module:def __init__(self, name: str):self.name = namedef test(self, other: Optional['Module']):if isinstance(other, Module):print(f"{self.name} and {other.name} are both modules.")Module("module1").test(Module("module2"))

此时如果去掉单引号程序会报错。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/833670.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于Java+SpringBoot+Mysql在线课程学习教育系统功能设计与实现十

审核前台用户认证信息、查看所有用户、订单、发布文章、发布常见问题等。 该系统总共24张表,代码整洁,每个功能、接口上都有注释说明。 运行环境:jdk1.8、mysql5.x、eclipse/idea、maven3.5/3.6 包远程运行的哦。 特色功能:发布课程、学习课程、分享资料、资料讨论等。 部分…

基于Java+SpringBoot+Mysql在线课程学习教育系统功能设计与实现九

该系统总共24张表,代码整洁,每个功能、接口上都有注释说明。 运行环境:jdk1.8、mysql5.x、eclipse/idea、maven3.5/3.6 包远程运行的哦。 特色功能:发布课程、学习课程、分享资料、资料讨论等。 部分功能:前台课程评论信息控制器Controller、优惠卷信息控制器Controller、…

SharePoint Online页面的一些奇怪参数

前言最近,在查找资料的时候,偶然间发现了一些非常有意思的参数,如下:?env=Embedded or ?env=WebView&wdStartOn=21.正经的SharePoint Online页面2.加了参数的SharePoint Online 页面3.加了另一个参数的SharePoint Online页面结束语相信大家看效果就已经发现了,参数是…

Alpha冲刺(2/14)——2024.11.13

目录一、团队成员分工与进度二、成员任务问题及处理方式三、冲刺会议内容记录会议内容四、GitHub签入记录及项目运行截图GitHub签入记录项目运行截图五、项目开发进展及燃尽图项目开发进展燃尽图六、团队成员贡献表 一、团队成员分工与进度成员 完成的任务 完成的任务时长 剩余…

PS端Flash固化

PS端Flash固化Vivado版本:Vivado2020.2 芯片型号:RFSoC XCZU47DR 前提条件:Vitis工程编译完成,拨码开关拨到PS JTAG模式创建引导镜像 首先右键应用工程系统,点击Create Boot Image。检查镜像工程的文件是否为固化需要的工程文件,点击创建镜像的选项即可完成创建,创建完成…

Office Word 文档格式与目录样式(毕业设计论文常用)

调整格式技巧: Word 中点击 “文件”--》"选项"--》“显示”,将高亮部分全部打钩,有利于查看格式字符、 “分页符” 和“分节符” 两个很有用, 其中分节符 前后的页码是独立的。 样式间的关系: 类比 C++ 中类的继承编写的伪代码,“正文”为基类,派生出 “论文…

想不到新版的onenote配色这么好看

原来一直在用office自带的onenote,想不到新版的onenote配色这么好看。

[豪の学习笔记] 计算机网络#001

计算机网络概念、网络协议、计算机网络结构、Internet结构、电路交换、多路复用、报文交换与分组交换1.1.1 - 什么是计算机网络 计算机网络 = 通信技术 + 计算机技术计算机网络就是一种特殊的通信网络 定义:计算机网络就是互联的、自治的计算机集合 自治:无主从关系 互联:互…

第十一次作业

1、RCE:分别实现ThinkPHP、Weblogic、Shiro漏洞的利用过程> ThinkPHP: 环境搭建前端测试是否存在pearcmd,访问路径,存在的话报错就确认存在在根目录下创建magedu3.php这个文件,文件内容为<?=phpinfo()?>,10.0.0.150:8080/public/?lang=../../../../../../../.…

Python并行编程1并行编程简介(上)高频面试题:GIL进程线程协程

1 并行编程简介 首先,我们将讨论允许在新计算机上并行执行的硬件组件,如 CPU 和内核,然后讨论操作系统中真正推动并行的实体:进程和线程。随后,将详细说明并行编程模型,介绍并发性、同步性和异步性等基本概念。 介绍完这些一般概念后,我们将讨论全局解释器锁(GIL)及其…

鸿蒙NEXT开发案例:年龄计算

​ 【引言】 本案例的目标是开发一款年龄计算器应用,该应用能够根据用户输入的出生日期,计算出用户的实际年龄、虚岁、星座、生肖等信息。同时,应用还将提供距离下次公历和农历生日的天数及星期等信息。为了实现这些功能,我们将使用ArkTS和ArkUI作为开发语言,并借助@nutpi…

Dosbox-x安装WinXP——图文教程

很多老游戏只能在win95、98或者XP中运行,因此,很多人尝试将Win95、98安装到Dosbox中,利用Dosbox来玩那些久远的情怀。有Win98自然就有人想在Dosbox中安装更高级的Win系统,于是就有人尝试在Dosnox中安装Win2000、WinXP的,其中2023-07-03在国外的fabulous.systems出现了一篇…