Python中的泛型编程

目录

  • 1. 前言
    • 1.1 函数模板
    • 1.2 类模板
    • 1.3 Python中的泛型
  • 2. TypeVar
    • 2.1 函数模板与类模板
    • 2.2 构造函数
    • 2.3 约束
    • 2.4 协变与逆变
  • Ref

1. 前言

泛型编程的引入主要是为了解决代码重用的问题。在没有泛型的情况下,如果你想要实现一个功能(比如排序或查找),对于不同类型的数据(整数、浮点数、字符串等)你可能需要写多个几乎相同的函数。这不仅增加了代码量,也增加了维护成本和出错的机会。泛型编程允许你编写与类型无关的代码,从而使得一个函数或一个类可以用于多种类型,减少了代码的重复,提高了代码的复用性和可维护性。

1.1 函数模板

在 C++ 中,函数模板是实现函数泛型的机制。通过定义一个函数模板,你可以让函数对多种类型的数据进行操作。下面是一个简单的例子,说明如何使用函数模板来实现一个泛型的 swap 函数,它可以交换任意类型的两个值:

template<typename T>
void swap(T& a, T& b) {
    T temp = a;
    a = b;
    b = temp;
}

在这个例子中,template<typename T> 告诉编译器这是一个模板,其中 T 是一个类型占位符,代表任意类型。当你使用这个函数时,比如 swap(x, y),编译器会根据 xy 的类型自动生成适合这些类型的函数代码。

📝 C++ 编译器会根据函数模板在编译期间生成多个函数版本(这一过程又叫函数模板的实例化),这些函数版本分别对应不同的传入类型,也就是说,函数模板本质上是代码生成的一种方式,它会根据模板参数类型动态生成不同的函数定义。

函数模板的实例化分为隐式实例化显式实例化两种。

隐式实例化是指,当你使用一个模板函数时,编译器会根据传递给模板函数的参数类型自动生成一个特定版本的函数,这个过程称为隐式实例化。你不需要明确指出要使用的类型,编译器会根据上下文推断出来:

int x = 10, y = 20;
swap(x, y); // 隐式实例化为 swap<int>(int&, int&)

在这个例子中,编译器看到 xy 都是 int 类型,因此它会自动生成一个 swap<int> 的实例。这个过程是自动的,发生在编译时,你作为开发者不需要显式地指定类型。

有时候,你可能想要显式地告诉编译器生成特定类型的函数模板实例,这就是所谓的显式实例化。这可以通过在函数名后添加模板参数来实现:

double a = 1.1, b = 2.2;
swap<double>(a, b); // 显式实例化为 swap<double>(double&, double&)

在这里,<double> 显式指定了模板参数 T 应该是 double 类型。这样编译器就会生成一个接受 double& 类型参数的 swap 函数版本。

1.2 类模板

与函数模板类似,类模板允许你定义能够操作任意类型的类。例如,你可能想要一个能够存储任何类型元素的数组类。使用类模板,你可以这样做:

template<typename T>
class Array {
private:
    T* data;
    size_t size;
public:
    Array(size_t size) : size(size), data(new T[size]) {}
    ~Array() { delete[] data; }

    T& operator[](size_t index) {
        return data[index];
    }

    size_t getSize() const { return size; }
};

在这个 Array 类的模板中,T 代表了数组可以存储的元素的类型。这意味着你可以创建一个整数数组、浮点数数组、甚至字符串数组,只需在创建数组对象时指定类型即可,比如 Array<int> intArray(10)

同样地,类模板的实例化也分为隐式实例化和显式实例化两种。

隐式实例化发生在代码中直接使用模板类时。编译器根据模板类使用时提供的类型参数自动生成该特定类型的类定义:

Array<int> intArray(5);

这里,通过 Array<int>,我们没有明确地告诉编译器去实例化一个 int 类型的 Array 模板。编译器会根据我们提供的类型参数 int,自动进行模板实例化,生成一个操作整数的 Array 类的实例。这就是所谓的隐式实例化。

显式实例化允许你在程序的一个地方明确指定模板类的特定类型实例,让编译器生成此特定类型的实例化代码,而无需在每次使用时都进行隐式实例化。这对于减少编译时间和确保代码在不同编译单元中重用相同的实例化非常有用:

template class Array<int>;

这行代码告诉编译器:请为 int 类型生成 Array 模板的一个实例。这个实例之后可以在程序的其他部分被直接使用,而无需再次实例化。这就是显式实例化。

1.3 Python中的泛型

回到Python,它是一种动态类型语言,在运行时执行类型检查,这与静态类型语言(如C++或Java)不同,后者在编译时进行类型检查。尽管如此,Python 也支持泛型编程,主要通过一个名为 typing 的标准库模块实现。

在Python中引入泛型编程的主要目的是为了提高代码的清晰度和可维护性。类型注解使得开发者能够明确指定函数或方法期望接收的参数类型以及返回的类型。这不仅有助于开发者理解代码,也使得静态类型检查器(如 mypy)能够在代码运行之前发现潜在的类型相关错误。

2. TypeVar

TypeVar 是类型提示(Type Hints)系统的一部分,用于定义泛型(Generics),即在不指定具体类型的情况下,允许代码以通用的方式处理不同类型。TypeVar 可以让你定义一个或多个类型作为可变的占位符,这些类型将在类或函数被实例化或调用时确定。这种方式增加了代码的灵活性和重用性,同时保持了类型检查的严格性。

2.1 函数模板与类模板

我们可以通过 TypeVar 来构造函数模板,这个模板将返回传入列表的第一个元素:

from typing import List, TypeVar

T = TypeVar('T')  # T是任意类型

def first_element(lst: List[T]) -> T:
    return lst[0]

我们还可以创建一个能够存储任何类型元素的数组类模板:

from typing import TypeVar, Generic, List

T = TypeVar('T')  # T是任意类型

class GenericArray(Generic[T]):
    def __init__(self):
        self.items: List[T] = []
    
    def add(self, item: T) -> None:
        self.items.append(item)
    
    def get(self, index: int) -> T:
        return self.items[index]

# 使用泛型类创建一个可以存储整数的数组
int_array = GenericArray[int]()
int_array.add(1)
int_array.add(2)
print(int_array.get(0))

# 使用泛型类创建一个可以存储字符串的数组
str_array = GenericArray[str]()
str_array.add("hello")
str_array.add("world")
print(str_array.get(1))

对于字典也是如此:

from typing import Generic, TypeVar, Dict

K = TypeVar('K')
V = TypeVar('V')

class GenericDict(Generic[K, V]):
    def __init__(self):
        self.items: Dict[K, V] = {}
    
    def add(self, key: K, value: V) -> None:
        self.items[key] = value
    
    def get(self, key: K) -> V:
        return self.items[key]

    def __repr__(self):
        return str(self.items)

# 实例化一个键为str,值为int的GenericDict
str_int_dict = GenericDict[str, int]()
str_int_dict.add("age", 30)
str_int_dict.add("score", 100)

print(str_int_dict)
# 输出: {'age': 30, 'score': 100}

# 实例化一个键和值都为str的GenericDict
str_str_dict = GenericDict[str, str]()
str_str_dict.add("name", "Alice")
str_str_dict.add("country", "Wonderland")

print(str_str_dict)
# 输出: {'name': 'Alice', 'country': 'Wonderland'}

2.2 构造函数

在了解了TypeVar的基本用法后,我们来看一看它的构造函数:

class TypeVar( _Final, _Immutable, _TypeVarLike, _root=True):
	def __init__(self, name, *constraints, bound=None, covariant=False, contravariant=False):

name 用于指定类型变量的名称,它可以是任何字符串。例如,你可以 T = TypeVar('T'),也可以 A = TypeVar('A'),还可以 var = TypeVar('var')

*constraints 是一个可变参数,用于指定这个类型变量可以接受的类型约束。如果提供了一个或多个约束,则这个类型变量被限制只能是这些类型之一。如果不提供任何约束(即空),则这个类型变量可以是任何类型。约束提供了一种方式来限制泛型类型的使用,使得类型更加具体和安全。

bound 是一个可选参数,用于指定一个上界类型。与 constraints 不同的是,bound 不是限制类型变量必须是给定的几种类型之一,而是限制类型变量必须是指定类型的子类。这意味着所有使用这个类型变量的地方,其类型必须是这个上界类型或其子类型。

covariantcontravariant 将在稍后讲解。

2.3 约束

根据TypeVar源码可知:

  • *constraintsbound 不能同时提供。
  • 如果提供了 *constraints,那么至少要提供两个

考虑这样一个函数,它能够接受整数或浮点数(但不接受其他类型),并返回它们的总和。我们可以这样定义一个带有类型约束的 TypeVar:

from typing import TypeVar

# 只能是float或int类型
FloatOrInt = TypeVar('FloatOrInt', float, int)

def sum_numbers(a: FloatOrInt, b: FloatOrInt) -> FloatOrInt:
    return a + b

# 这些都是有效的
print(sum_numbers(1, 2))  # 使用int
print(sum_numbers(1.5, 2.5))  # 使用float

# 这将引发类型检查错误,因为'str'不是约束之一
# print(sum_numbers("a", "b"))

bound 参数用于指定一个类型变量的上界。这意味着类型变量可以是任何继承自指定上界类型的类型bound 在你希望允许泛型代码处理一系列具有共同父类的类型时非常有用,这些类型本身可能没有共同的约束类型。

假设我们有一个函数,希望它能够处理不同种类的数字类型,但这些类型都继承自一个共同的抽象基类:

from typing import TypeVar, List
from numbers import Number

# 定义一个类型变量,其被绑定到Number类
Num = TypeVar('Num', bound=Number)

def sum_of_list(numbers: List[Num]) -> Num:
    return sum(numbers)

# 这个函数现在可以接受任何Number子类的列表
print(sum_of_list([1, 2, 3]))
print(sum_of_list([1.5, 2.5, 3.5]))

numbers 中的继承关系:

其中 int 继承自 Integralfloat 继承自 Real

from numbers import Integral, Rational, Real

print(issubclass(int, Integral))
print(issubclass(float, Real))
# 均输出True

2.4 协变与逆变

协变描述了一种类型关系,其中一个类型随着另一个类型的变化而同方向变化。在泛型编程中,如果类型 B 是类型 A 的子类型,那么 Container[B] 也是 Container[A] 的子类型(这里 Container 可以是任何泛型容器,比如列表、集合等),则称 Container 类型关于其元素类型是协变的。

from typing import TypeVar, Generic

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

class Box(Generic[T_co]):
    def __init__(self, item: T_co):
        self.item = item

    def get_item(self) -> T_co:
        return self.item

# 假设我们有以下类和子类
class Fruit:
    pass

class Apple(Fruit):
    pass

# 协变允许这样的赋值
box_of_fruit: Box[Fruit] = Box[Apple](Apple())

这里,Box[Apple] 可以被认为是 Box[Fruit] 的子类型,因为 T 是协变的。这意味着你可以将一个装有苹果的盒子(Box[Apple])赋值给一个期望装有任何水果的盒子变量(Box[Fruit])。

📝 一个装着苹果的箱子当然可以被视作是一个装着水果的箱子,但反之则不行。

与协变相反,逆变描述了另一种类型关系,其中一个类型随着另一个类型的变化而反方向变化。在泛型编程中,如果类型 B 是类型 A 的子类型,那么 Processor[A] 将是 Processor[B] 的子类型,这里 Processor 代表某种泛型处理器,例如函数、接口等,它关于其参数类型是逆变的。

from typing import TypeVar, Generic, Callable

T_contra = TypeVar('T_contra', contravariant=True)

class Reader(Generic[T_contra]):
    def __init__(self, read_func: Callable[[T_contra], str]):
        self.read_func = read_func

    def read(self, input: T_contra) -> str:
        return self.read_func(input)

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

def animal_name_reader(animal: Animal) -> str:
    return animal.name

# 逆变允许这样的赋值
dog_reader: Reader[Dog] = Reader[Animal](animal_name_reader)

在这个例子中,Reader 类是关于其参数 T_contra 是逆变的。我们定义了一个 animal_name_reader 函数,它接受一个 Animal 类型的对象作为参数,并返回该动物的名字。尽管 Reader 的定义要求一个特定的输入类型,但是由于逆变的性质,我们可以将一个更通用类型 Animal 的阅读器赋值给一个期望 Dog 类型输入的阅读器变量 dog_reader。这是因为从逻辑上讲,任何能够处理 Animal 对象的函数自然也能处理 Animal 的任何子类,比如 Dog,因为子类拥有父类的所有属性和方法。


Ref

[1] https://developer.aliyun.com/article/1243714
[2] https://segmentfault.com/a/1190000042672657
[3] https://zhuanlan.zhihu.com/p/486772116
[4] https://cloud.tencent.com/developer/article/1858783

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

韦根协议刷卡原理及代码实现

一、韦根协议原理 韦根接口在门禁行业广泛使用&#xff0c;是一个门禁行业的通信标准&#xff0c;通过两条数据线 DATA0&#xff08;D0&#xff09;和 DATA1 &#xff08;D1&#xff09;发送数据。目前用的最多的是韦根 34 和韦根 26&#xff0c;二者数据格式相同&#xff0c;…

(C语言)字符分类函数

目录 字符分类函数 1. iscntrl 2. isspace 3. isdigit 4. isxdigit 5. islower 6. isupper 7. isalpha 8. isalnum 9. ispunct 10. isgraph 11. isprint 字符分类函数 C语言中有一系列的函数是专门做字符分类的 &#xff0c;也就是一个字符是属于什么类型的字符的。…

二维码门楼牌管理系统技术服务:构建智慧城市新标准

文章目录 前言一、二维码门楼牌管理系统的诞生背景二、标准地址编码的定义与作用三、二维码门楼牌管理系统的核心技术四、二维码门楼牌管理系统的应用优势五、二维码门楼牌管理系统在智慧城市建设中的作用六、结论与展望 前言 随着城市化的快速发展&#xff0c;传统的门楼牌管…

通过一篇文章带你玩转git和GitHub

Git和Github的基本用法 前言一、Git和Github的基本用法背景下载安装安装 git for windows安装 tortoise gitgit安装过程中的一些选项 tortoise git汉化教程下载tortoise git汉化安装包安装tortoise git汉化安装包 三、使用 Github 创建项目注册账号创建项目下载项目到本地 四、…

GitHub会员充值

GitHub是一个基于Web的代码托管平台&#xff0c;为开发者提供了协作、版本控制和代码管理的工具。它允许个人和团队共同协作开发软件项目&#xff0c;并提供了许多功能&#xff0c;使得代码的管理和维护更加容易 版本控制系统&#xff1a; GitHub使用Git作为其版本控制系统。Gi…

两天学会微服务网关Gateway-Gateway过滤器

锋哥原创的微服务网关Gateway视频教程&#xff1a; Gateway微服务网关视频教程&#xff08;无废话版&#xff09;_哔哩哔哩_bilibiliGateway微服务网关视频教程&#xff08;无废话版&#xff09;共计17条视频&#xff0c;包括&#xff1a;1_Gateway简介、2_Gateway工作原理、3…

python界面开发 - messagebox 提示框

文章目录 1. messagebox1.1. 示例 2. Tkinter 开发3. python图形界面开发3.1. Python图形界面开发——Tkinter3.2. Python图形界面开发——PyQt3.3. Python图形界面开发——wxPython3.4. Python图形界面开发—— PyGTK&#xff1a;基于GTK3.5. Python图形界面开发—— Kivy3.6.…

【ElasticSearch】es索引、映射、文档基本操作复杂查询

各位小伙伴们大家好&#xff0c;欢迎来到这个小扎扎的ElasticSearch专栏&#xff0c;本篇博客由B战尚硅谷的ElasticSearch视频总结而来&#xff0c;鉴于 看到就是学到、学到就是赚到 精神&#xff0c;这波依然是血赚 ┗|&#xff40;O′|┛ &#x1f306; 内容速览 1 es数据格…

金现代产品方案部部长王宁,将出席“ISIG-低代码/零代码技术与应用发展峰会”

3月16日&#xff0c;第四届「ISIG中国产业智能大会」将在上海中庚聚龙酒店拉开序幕。本届大会由苏州市金融科技协会指导&#xff0c;企智未来科技&#xff08;LowCode低码时代、RPA中国、AIGC开放社区&#xff09;主办。大会旨在聚合每一位产业成员的力量&#xff0c;深入探索低…

AD1102 小封装的3.7V锂电池转干电池使用的充放电管理芯片 替代传统干电池、镍氢电池

AD1102是一款锂电池充放电管理专用芯片。充电工作时&#xff0c;可以为 3.7V锂电池进行充电&#xff0c;电流最高可配置 1A。放电工作时&#xff0c;采用开关频率1MHz同步降压转换器进行放电&#xff0c;放电电流可以达到 3A。内部集成欠压保护、短路保护、过温保 护功能。 …

算法学习06:数组模拟:单/双链表,栈和队列,单调栈/队列

算法学习06&#xff1a;数组模拟&#xff1a;单/双链表&#xff0c;栈和队列&#xff0c;单调栈/队列 文章目录 算法学习06&#xff1a;数组模拟&#xff1a;单/双链表&#xff0c;栈和队列&#xff0c;单调栈/队列前言一、链表1.单链表2.双链表 二、栈和队列1.普通栈、队列2.单…

LeetCode Python - 42.接雨水

目录 题目答案运行结果 题目 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图&#xff0c;计算按此排列的柱子&#xff0c;下雨之后能接多少雨水。 示例1&#xff1a; 输入&#xff1a;height [0,1,0,2,1,0,1,3,2,1,2,1] 输出&#xff1a;6 解释&#xff1a;上面是由数组…

Maven基础简介

作者简介&#xff1a; zoro-1&#xff0c;目前大二&#xff0c;正在学习Java&#xff0c;数据结构&#xff0c;spring等 作者主页&#xff1a; zoro-1的主页 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f496; Maven简介 Maven是什么 Maven…

人工智能|机器学习——Canopy聚类算法(密度聚类)

1.简介 Canopy聚类算法是一个将对象分组到类的简单、快速、精确地方法。每个对象用多维特征空间里的一个点来表示。这个算法使用一个快速近似距离度量和两个距离阈值T1 > T2 处理。 Canopy聚类很少单独使用&#xff0c; 一般是作为k-means前不知道要指定k为何值的时候&#…

Java的Writer类详解

咦咦咦&#xff0c;各位小可爱&#xff0c;我是你们的好伙伴——bug菌&#xff0c;今天又来给大家普及Java SE相关知识点了&#xff0c;别躲起来啊&#xff0c;听我讲干货还不快点赞&#xff0c;赞多了我就有动力讲得更嗨啦&#xff01;所以呀&#xff0c;养成先点赞后阅读的好…

【xv6操作系统】Lec06 Isolation system call entry/exit

6.1 Trap机制 每当 1.程序执行系统调用 2.程序出现了类似page fault、运算时除以0的错误 3.一个设备触发了中断使得当前程序运行需要响应内核设备驱动 都会发生用户空间和内核空间的切换&#xff0c;通常被称为trap。trap机制要尽可能的简单。 Shell可能会执行系统调用&a…

多种方法解决Error: could not open `C:Program FilesJavajre1.8.0_311libamd64jvm.cfg‘

文章目录 1. 复现错误2. 分析错误3. 解决错误4. 补充说明1. 复现错误 今天春节后开工第一天,打开我的IDEA,却报出如下错误: 报错信息是找不到JRE,于是,通过Windows Powershell输入Java -version,如下图所示: 即Error: could not open C:\Program Files\Java\jre1.8.0_31…

外包干了5天,技术退步明显。。。。。

在湖南的一个安静角落&#xff0c;我&#xff0c;一个普通的大专生&#xff0c;开始了我的软件测试之旅。四年的外包生涯&#xff0c;让我在舒适区里逐渐失去了锐气&#xff0c;技术停滞不前&#xff0c;仿佛被时间遗忘。然而&#xff0c;生活的转机总是在不经意间降临。 与女…

7. 镜面网格

E . 镜面网格 E.镜面网格 E.镜面网格 每次测试时限&#xff1a; 2 秒 每次测试时限&#xff1a;2 秒 每次测试时限&#xff1a;2秒 每次测试的内存限制&#xff1a; 256 兆字节 每次测试的内存限制&#xff1a;256 兆字节 每次测试的内存限制&#xff1a;256兆字节 题目描述 给…

JavaScript极速入门-综合案例(3)

综合案例 猜数字 预期效果 代码实现 <button type"button" id"reset">重新开始一局游戏</button><br>请输入要猜的数字:<input type"text" id"number"><button type"button" id"button&q…