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 的类型自动生成适合这些类型的函数代码。

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

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

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 中的继承关系:

Python中的泛型编程-LMLPHP

其中 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

03-09 10:27