Python 泛型 - 如何在实例方法中获取泛型参数T的类型?

news2024/11/24 14:58:13

先上解决方法:https://stackoverflow.com/questions/57706180/generict-base-class-how-to-get-type-of-t-from-within-instance

再来简单分析下源码。

talk is cheap, show me the code.

from typing import Dict
Dict[str, int]

Dict只是一个类型,并不是字典类,但是我们可以通过一些方法,拿到其真正意义上的类。

typing 库提供了 get_argsget_origin 函数。

get_args

顾名思义,获取参数。这里的获取参数指的是获取类的泛型参数。

什么意思?

看一看 dict 的代码注解,可以看到 dict 支持泛型的,接受两个泛型参数:_KT_VT
这就是我们可以通过 Dict[str, int] 的方式对字典内键值对的类型进行更加具体标注原因。

所以,Dict[str, int]str, int 就是 Dict 的泛型参数。

通过 get_args 就可以获取到内部的泛型参数,就像这样:


get_origin

获取原始,原始什么?就是获取类型的原始类。

Dict 本身仅仅一个类型,它并不支持去实例化一个字典对象。

如何通过 Dict 而拿到 dict 呢,则就需要使用 get_origin 获取原始类。

就像这样:

值得注意的是:get_args get_origin 仅支持内置的类型

这里再贴一下,官方文档的描述

get_args、get_origin 为泛型类型与特殊类型形式提供了基本的内省功能。
对于 X[Y, Z, ...] 形式的类型对象,这些函数返回 X(Y, Z, ...)。如果 X 是内置对象或 collections class 的泛型别名, 会将其标准化为原始类。如果 X 是包含在其他泛型类型中的联合类型或 Literal(Y, Z, ...) 的顺序会因类型缓存,而与原始参数 [Y, Z, ...] 的顺序不同。对于不支持的对象会相应地返回 None()

在实例方法中获取原始类及泛型参数的数据类型

接下来看另一种情况,我希望在 Demo 类内部,获取 T 它所对应的真实类,例如:

from typing import Generic, TypeVar, get_args

T = TypeVar("T")
class Demo(Generic[T]):
	def __init__(self):
		print(get_args(self.__class__))

Demo[int]()

看着没什么问题,其实这里会打印空内容。

为什么呢?

self.__class__ 确实获取到了 Demo 类,但是这个 Demo 类并不是最原始的样子。

get_args(self.__class__) 就等同于 get_args(Demo),自然拿不到泛型参数。

我们想要的语句是长这个样子的 get_args(Demo[int]),那么,现在的问题就转换成了如何在 Demo 类内部获取到 Demo[int],我暂且就叫它原始类吧。

先上解决方法,在方法内部调用 self.__orig_class__ 即可获取到原始类。

from typing import Generic, TypeVar, get_args  
  
T = TypeVar("T")  
  
  
class Demo(Generic[T]):  
    def __init__(self):  
        pass  
  
    def test(self):
        c = get_args(self.__orig_class__)[0]
        assert c is int  
  
  
demo = Demo[int]()  
demo.test()

在上面这个示例中,当调用 test 方法时,在该方法内部即可知道泛型参数 T 所对应的类型是什么了。

这里有一个疑问, 为什么`get_args(self.__orig_class__)[0]`写在了`test`方法内,而不是`__init__`初始化方法内。

先说结论:通过以上方法获取泛型参数的类型,只能在该泛型类初始化完成之后才可以使用,即必须在`__init__, __new__`执行后才可调用。

简要分析 Generic 源码

接下来,让我一个人墨迹一会儿,我会简单分析 Generic 的源码,看一看为什么必须在 __init__, __new__ 之后才可以使用。

再多提一句,对于类本身是没有 __orig_class__ 这个属性的,但是为什么我们又可以使用它。

简单点说就是,__orig_class__ 是后来加上的,最初并没有做初始化,如下图 Pycharm 提示了该类不存在 __orig_class__ 属性。

在分析代码之前,可以再看下 Generic[T] 这个写法,它有这么一个中括号的。这个符号在 Python 就是一个语法糖。我们知道,列表对象可以通过 lst[0] 获取到对应下标的元素,字典对象可以通过 d[key] 获取到对应 key 的值,这都是因为列表类和字典类了实现了 __getitem__ 魔术方法。

对于列表和字典,它们都是已经被实例化的对象,而 Generic 是一个类,所以对于类同样支持 [] 语法糖的魔术方法,叫做 __class_getitem__ ,方法名也是比较好记住的,无非就是加了 __class__ 前缀。

现在我们就跳到 Generic 类里面,找到 __class__getitem__ 方法,为了方便浏览,我在以下代码中写注释了。

# 缓存泛型类
@_tp_cache
def __class_getitem__(cls, params):
	# params 很好理解,就是我们传入的泛型参数,`Generic[T]`中 T 就是这个 params
	if not isinstance(params, tuple):
		params = (params,)
	if not params and cls is not Tuple:
		raise TypeError(
			f"Parameter list to {cls.__qualname__}[...] cannot be empty")
	msg = "Parameters to generic types must be types."
	# 类型检查
	params = tuple(_type_check(p, msg) for p in params

	# 只有 Generic 极其子类才可以使用泛型 TypeVar
	if cls in (Generic, Protocol):
		# Generic and Protocol can only be subscripted with unique type variables.
		# 判断所有泛型参数都是 TypeVar 的实例
		if not all(isinstance(p, TypeVar) for p in params):
			raise TypeError(
				f"Parameters to {cls.__name__}[...] must all be type variables")
		if len(set(params)) != len(params):
			raise TypeError(
				f"Parameters to {cls.__name__}[...] must all be unique")
	else:
		# Subscripting a regular Generic subclass.
		_check_generic(cls, params)

	# 重点
	return _GenericAlias(cls, params)

我们关注 __class_getitem__ 的返回结果,返回了 _GenericAlias 的实例,接收两个参数:clsparams。这里的 cls 指的是 Generic 或其子类,params 就是泛型参数。

重点来了,对于 class Demo(Generic[T]) 而言,我们并不是继承至 Generic 而是 _GenericAlias()

_GenericAlias 是什么?看看它的初始化方法,如下:

def __init__(self, origin, params, *, inst=True, special=False, name=None):
	self._inst = inst
	self._special = special
	if special and name is None:
		orig_name = origin.__name__
		name = _normalize_alias.get(orig_name, orig_name)
	self._name = name
	if not isinstance(params, tuple):
		params = (params,)
	# origin 就是 Generic 或继承自它的子类
	self.__origin__ = origin

	self.__args__ = tuple(... if a is _TypingEllipsis else
						  () if a is _TypingEmpty else
						  a for a in params)
	# parmas 转换成了 self.__parameters__
	self.__parameters__ = _collect_type_vars(params)
	self.__slots__ = None  # This is not documented.
	if not name:
		self.__module__ = origin.__module__

其它参数,我们就不了解了,在 Generic 也只传了两个参数,对应这里面的 originparams

class Demo(Generic[T]):
	pass

对于上面代码,换一种写法就是:

class Demo(_GenericAlias(Generic, T)):
	pass

一个类是不是会用到 () 来实例化一个对象,如下:

Demo()

在 Python 中的 () 也是一个语法糖,对应的是 __call__ 方法,所以 Demo() 等同于 Demo.__call__(),本质上就是调用父类的 _GenericAlias(Generic, T).__call__ 方法,所以我们应该去找 _GenericAlias__call__ 方法。

_GenericAlias 没有实现 __call__,而是它继承的父类实现的 _BaseGenericAlias,如下:

def __call__(self, *args, **kwargs):
    if not self._inst:
        raise TypeError(f"Type {self._name} cannot be instantiated; "
                        f"use {self.__origin__.__name__}() instead")
    result = self.__origin__(*args, **kwargs)
    try:
        result.__orig_class__ = self
    except AttributeError:
        pass
    return result

self.__origin__ 就是本例中的 Demo 类,可以看到这里先是进行实例化了,然后再将 self 绑定在了 result 上。注意,这里的 self 指的就是 _GenericAlias 对象。

会到上文讲到的 get_orgs

get_args(self.__orig_class__)[0]

这里获取的 self.__orig_class__ 就是 _GenericAlias 的对象,get_orgs 源码如下:

def get_args(tp):
    """Get type arguments with all substitutions performed.

    For unions, basic simplifications used by Union constructor are performed.
    Examples::
        get_args(Dict[str, int]) == (str, int)
        get_args(int) == ()
        get_args(Union[int, Union[T, int], str][int]) == (int, str)
        get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
        get_args(Callable[[], T][int]) == ([], int)
    """
    if isinstance(tp, _AnnotatedAlias):
        return (tp.__origin__,) + tp.__metadata__
    if isinstance(tp, (_GenericAlias, GenericAlias)):
        res = tp.__args__  # 访问了 __args__
        if _should_unflatten_callable_args(tp, res):
            res = (list(res[:-1]), res[-1])
        return res
    if isinstance(tp, types.UnionType):
        return tp.__args__
    return ()

显而易见,就是访问了 _GenericAlias__args__ 成员。我们再看下 __args__ 是什么?

    def __init__(self, origin, args, *, inst=True, name=None,
                 _paramspec_tvars=False):
        super().__init__(origin, inst=inst, name=name)
        if not isinstance(args, tuple):
            args = (args,)
        self.__args__ = tuple(... if a is _TypingEllipsis else
                              a for a in args)
        self.__parameters__ = _collect_parameters(args)
        self._paramspec_tvars = _paramspec_tvars
        if not name:
            self.__module__ = origin.__module__

__args__ 来自参数传递 args ,让我们回到 Generic.__class_getitem__ 方法,如下:

    @_tp_cache
    def __class_getitem__(cls, params):
        if not isinstance(params, tuple):
            params = (params,)
        # 中间省略大部分内容,都是为了组装 params
        return _GenericAlias(cls, params,
                             _paramspec_tvars=True)

显而易见,就是将 [] 中的泛型参数传了进来,并实例化了 _GenericAlias 对象,并在泛型类实例化时(即调用 __call__ 时)将其绑定在该实例的 __orig_class__ 成员上。

这也解释了为什么只能在非 __init__ 实例方法中访问 __orig_class__,因为泛型类实际上是实例化之后才被绑定的 __orig_class__

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java数组基础详解

目录java数组基础详解一、引言二、声明数组三、初始化数组3.1 静态初始化3.2 动态初始化四、访问数组元素五、遍历数组六、分析数组内存七、数组常见异常7.1 索引越界异常ArrayIndexOutOfBoundsException7.2 空指针异常NullPointerExceptionjava数组基础详解 一、引言 数组定…

裸辞了,面试了几十家软件测试公司,终于找到想要的工作

上半年裁员,下半年裸辞,有不少人高呼裸辞后躺平真的好快乐!但也有很多人,裸辞后的生活五味杂陈。 面试了几十家终于找到心仪工作 因为工作压力大、领导PUA等各种原因,今年2月下旬我从一家互联网小厂裸辞,没…

【C语言进阶】结构体、位段、枚举、以及联合(共用体)的相关原理与使用

​ ​📝个人主页:Sherry的成长之路 🏠学习社区:Sherry的成长之路(个人社区) 📖专栏链接:C语言进阶 🎯长路漫漫浩浩,万事皆有期待 文章目录1.结构体1.1 概述&a…

代码随想录【Day22】| 235. 二叉搜索树的最近公共祖先、701. 二叉搜索树中的插入操作、450. 删除二叉搜索树中的节点

235. 二叉搜索树的最近公共祖先 题目链接 题目描述: 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 …

别担心ChatGPT距离替代程序猿还有距离

经过多天对chat-GPT在工作的使用,我得出一个结论,它睁眼瞎说就算了,它还积极认错,绝不改正,错误答案极具误导性,啥也不说了,请看图。 经过N次较量它固执的认为 0011 1101 0110 0101在最高位是左…

【python】考前复习,python基础语法知识点整理

文章目录1.常量与表达式2.变量和数据类型创建变量数据类型动态类型数据类型的转换3.注释4.字符串字符串的定义方式字符串的拼接字符串的格式化①字符串格式化的精度控制字符串的格式化②对表达式进行格式化5.从控制台输入(input)6.运算符算术运算符赋值运算符布尔类型和比较运算…

【Spring Cloud Alibaba】006-OpenFeign

【Spring Cloud Alibaba】006-OpenFeign 文章目录【Spring Cloud Alibaba】006-OpenFeign一、概述1、Java 项目实现接口调用的方法HttpclientOkhttpHttpURLConnectionRestTemplate WebClient2、Feign 概述二、Spring Cloud Alibaba 快速整合 OpenFeign1、添加依赖2、启动类加注…

STM32开发(12)----CubeMX配置WWDG

CubeMX配置窗口看门狗(WWDG)前言一、窗口看门狗的介绍二、实验过程1.STM32CubeMX配置窗口看门狗2.代码实现3.硬件连接4.实验结果总结前言 本章介绍使用STM32CubeMX对窗口看门狗定时器进行配置的方法。门狗本质上是一个定时器,提供了更高的安…

物联网在物流行业中的应用

物流管理需要同时监控供应链、仓储、运输等多项活动,然而许多因素会影响物流流程本身并导致延迟。为了简化流程和提高客户满意度,一些行业领导者和决策者积极创新,不断评估并使用物联网对物流流程的成本效益进行深入优化。在本文中&#xff0…

初识MySQL下载与安装【快速掌握知识点】

目录 前言 MySQL版本 MySQL类型 MySQL官网有.zip和.msi两种安装形式; MySQL 下载 1、MySQL 属于 Oracle 旗下产品,进入Oracle官网下载 2、点击产品,找到MySQL 3、进入MySQL页面 4、点击Download(下载)&#x…

PHP面向对象03:命名空间

PHP面向对象03:命名空间一、命名空间基础二、子空间三、命名空间访问1. 非限定名称2. 限定名称3. 完全限定名称四、全局空间五、命名空间应用六、命名空间引入一、命名空间基础 namespace,是指人为的将内存进行分隔,让不同内存区域的同名结构…

在uniapp 中使用Ucharts 进行可视化图表开发,折线统计图。

首先我们得 在uniapp 插件市场中找到Ucharts 这款插件&#xff0c;我这里是使用uni_modules导入这款插件案例1:我们这时可以在页面中使用组件的方式进行使用<qiun-data-charts type"area" :chartData"chartData" :opts"opts"/>Js逻辑的代…

基于vue考研助手网站

可定制框架:ssm/Springboot/vue/python/PHP/小程序/安卓均可开发目录 1 绪论 1 1.1课题背景 1 1.2课题研究现状 1 1.3初步设计方法与实施方案 2 1.4本文研究内容 2 2 系统开发环境 4 3 系统分析 6 3.1系统可行性分析 6 3.1.1经济可行性 6 3.1.2技术可行性 6 3.1.3运行可行性 6 …

我的 System Verilog 学习记录(3)

引言 本文简单介绍 System Verilog 语言的 TestBench。 前文链接&#xff1a; 我的 System Verilog 学习记录&#xff08;1&#xff09; 我的 System Verilog 学习记录&#xff08;2&#xff09; testbench 的目的何在 &#xff1f; testbench 可以让我们通过仿真验证设计…

Qt 工程 pro文件

工作中&#xff0c;感觉pro文件的有些内容真不太懂&#xff0c;现系统性的学习一下。于此备录&#xff0c;分享共勉。 为了更好的理解&#xff0c;先创建一个简单的工程作为实践。 【1】创建一个pro文件 1.1 新建proDemo工程。步骤如下&#xff1a;Qt Creator--->New Pro…

数据结构初阶——时间复杂度与空间复杂度

时间复杂度与空间复杂度1. 算法效率1.1 如何衡量一个算法的好坏1.2算法的复杂度2.时间复杂度2.1 时间复杂度的概念2.2 大O的渐进表示法2.3常见时间复杂度计算举例实列1&#xff1a;实列2&#xff1a;实列3&#xff1a;实列4&#xff1a;实列5&#xff1a;实列6&#xff1a;实列…

k8s service的底层实现

承接上文同一个node中pod之间如何通信&#xff1f;当前的集群中给2个apache pod注册了一个service&#xff0c;这个地址是10.152.183.151&#xff0c;在ubuntu的pod中测试这个ip是可以通信的&#xff0c;it work来源于本机的pod&#xff0c;多访问几次发现会随机的把请求定向到…

远程控制详细教程,同时支持手机控制

​“我需要一些帮助&#xff0c;目前我因为休假旅游去了&#xff0c;需要临时远程办公。我工作的电脑运行的是Windows 10系统&#xff0c;我如何操作才能远程控制公司的电脑进行远程办公&#xff1f;我之前没用过远程控制相关的工具&#xff0c;什么简单的方法可以远程控制另一…

别只会搜日志了,求你懂点检索原理吧

别只会搜日志了&#xff0c;求你懂点检索原理吧 本篇主要内容如下&#xff1a; 前言 项目中我们总是用 Kibana 界面来搜索测试或生产环境下的日志&#xff0c;来看下有没有异常信息。Kibana 就是 我们常说的 ELK 中的 K。 Kibana 界面如下图所示&#xff1a; 但这些日志检索…

内网渗透(五十一)之域控安全和跨域攻击-跨域攻击介绍

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…