Mkdir700's Note

Mkdir700's Note

Python中的泛型

2022-04-17

不知道从什么时候起,我写Python代码习惯给变量加上类型注解。这虽然降低了Python的灵活性,但确确实实在后续维护的过程中提供了很好的帮助。

第一次听到泛型这个词,是在Java中了解到的,但具体做什么,有什么作用,都是比较模糊的。因为我本身对Java只是了解并且也没有用Java做过什么实际项目,所以一直对泛型不太明白。

在官方文档中描述了泛型的使用方法,我将从小例子开始,看看泛型是如何帮助我们的。

类型提示

当前有一个get函数,它会返回一个int类型的值

def get():
	return 1

这样一个函数,我们很快就写完了,而等其他人来调用这个函数时,他就不知道这个get返回值究竟是什么数据类型。

在Python3.4版本之后,支持类型提示

我们可以为变量增加类型注解,虽然这个类型注解并不能对强制约束作用,但这可以帮助开发人员更好的维护代码,类型提示使得Pycharm、vscode这样的开发工具更加智能。

def get() -> int:
	return 1

int属于基础数据类型,我们也可以自定义类,例如:

class My(object):  
    pass  
  
  
def get() -> My:  
    return My()  
  
  
get()

了解标准库typing

https://docs.python.org/zh-cn/3/library/typing.html#

还是以上面的get函数举例,现在我想让其返回值的类型可能是int,也可能是My的实例。

此时,就可用到typing库了。

Union类型,表示

例如下方的例子就是,get返回值的就是[My, int]中的任意一个。

from typing import Union  
  
  
def get() -> Union[My, int]:  
    return My()

在3.10版本后,支持更加精简的写法:
My | int

初见泛型

为后续讲解泛型,在typing库中,我拿出List来讲讲。

这是定义了一个列表,但是我怎么知道这个容器内装的是什么玩意呢?

my_list = []

ok,那我提前规定好嘛,my_list里面全装的My的实例对象,那就可以这样写:

from typing import List

my_list: List[My] = []

不仅如此,我们还可以让List装其他东西,例如:

a: List[int] = [] # 声明内部全是int
b: List[str] = [] # 声明内部全是str
c: List[Model] = [] # 声明内部全是Model类的对象

这么能装的吗?

小试泛型

为了更好的理解泛型的作用,我们基于list来实现一个简单的MyList

class MyList(object):

    def __init__(self, val: list):
        self.list: list = val

    def __getitem__(self, item) -> int:
        return self.list[item]


a: MyList[int] = MyList([1])
b = a[0] # 调用__getitem__方法

编辑器提示,b的数据类型是int,这是因为指定了__getitem__返回值类型是int.

现在换成str

c: MyList[str] = MyList(["xy"])

Pycharm还是提示为int类型,因为我们把__getitem__返回值类型写死了,所以这里依然返回的int类型,和我们所设想不一致。

接下来,就让泛型出场吧。

其实,在当前这个需求下,我们只是想__getitem__返回值的类型随着外界的变化而变化。

这个外界指的就是 MyList[类型][]中的内容。

使用泛型,我们会经常使用到typing.Generictyping.TypeVar

  1. 声明通用数据类型
    所谓泛型,就是通用的数据类型,我们通过typing.TypeVar先定义一个通用的数据类型.
MyType = TypeVar("MyType", int, str)

str, int 都属于这个通用数据类型下,类型名称是MyType

  1. 使用泛型
    使用这个通用数据类型也很简单,我们只需要在原本的类上继承至Generic[MyType]即可。
class MyList(object, Generic[MyType]):

    def __init__(self, val: list):
        self.list: list = val

    def __getitem__(self, item) -> MyType:
        return self.list[item]

__getitem__返回值类型就可以设置为MyType了。

看看效果,这里提示d的类型是MyType

然后,也可以看到pycharm有str类型的方法提示了,如图:

泛型示例

在写Web应用时,我们会经常写crud,这一小节就演示在实际项目中如何应用泛型。

本节示例代码来自于FastAPI示例项目 https://github.com/tiangolo/full-stack-fastapi-postgresql

定义两个模型,此处用到了pydantic这个第三方库。

用户类

class User(BaseModel):
	id: int
	username: str
	password: str
	email: str
	create_time: datetime
	update_time: datetime

文章类

class Post(BaseModel):
	id: int
	title: str
	content: str
	owner: int
	create_time: datetime
	update_time: datetime

CURD基类

class CRUDBase(object):
    
    def create(self, obj: CreateSchema) -> ???:
        pass

其他类继承至CRUDBase,例如,我要编写针对于UserCRUD类,就可以像这样:

class CRUDUser(CRUDBase):
	pass

对于create这样最基础的方法,在子类中只有很少情况会进行重写。

所以,完全可以在CRUDBase中实现好,子类就无需再实现了。

关键点就在于create接受的参数,在本例中,接受了一个CreateSchema,你可以理解为这是个专门用于创建的数据结构。

例如:
对于User的Schema是:

class UserCreateSchema(BaseModel):
	username: str
	password: str
	email: str
	create_time: datetime

对于文章类Post的Schema是:

class PostCreateSchema(BaseModel):
	title: str
	content: str
	create_time: str
	owner: int

对于CRUDcreate方法而言,它只关心传入的schema,并将schema的内容插入数据库。

对于子类create的返回值类型,我们无法提前预知,所以泛型就登场了。

定义两个泛型

from typing import TypeVar, Generic

from pydantic import BaseModel

ModelType = TypeVar("ModelType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)

重写CRUDBase

class CRUDBase(Generic[ModelType, CreateSchemaType]):
	def create(self, obj: CreateSchemaType) -> ModelType:
		pass

继承CRUDBase

class CRUDUser(CRUDBase[User, UserCreateSchema]):
	pass

看看实际效果如何:

  1. 查看 CRUDUserCRUDPostcreate方法返回值类型。

  2. crud_post.create传入的UserCreateSchema
    可见在静态检查时,抛出了类型异常。

完整代码

from typing import TypeVar, Generic
from datetime import datetime

from pydantic import BaseModel

ModelType = TypeVar("ModelType", bound=BaseModel)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)


class User(BaseModel):
    id: int
    username: str
    password: str
    email: str
    create_time: datetime
    update_time: datetime


class UserCreateSchema(BaseModel):
    username: str
    password: str
    email: str
    create_time: datetime


class Post(BaseModel):
    id: int
    title: str
    content: str
    owner: int
    create_time: datetime
    update_time: datetime


class PostCreateSchema(BaseModel):
    title: str
    content: str
    create_time: str
    owner: int


class CRUDBase(Generic[ModelType, CreateSchemaType]):
    
    def create(self, obj: CreateSchemaType) -> ModelType:
        pass


class CRUDUser(CRUDBase[User, UserCreateSchema]):
    pass


class CRUDPost(CRUDBase[Post, PostCreateSchema]):
    pass


crud_post = CRUDPost()

obj = UserCreateSchema(username="mkdir700",
                       password="xxx",
                       email="mkdir700@gmail.com",
                       create_time=datetime.now())
crud_post.create(obj)