跳至主要內容

Python 类型体操训练(二)-- 中级篇

bbruceyuan大约 9 分钟python-type-challengepython-typing-tutorialpython-type-tutorial

阅读提示

  • 面向读者群体
    • 有一定Python基础,需要进阶开发中大型项目
    • 有其他静态类型语言开发经验的人,需要快速了解 Python 类型注释(type hint)
    • 如果没有太多基础,可以先阅读 Python 类型体操训练(一)-- 基础篇
  • 你能学到什么?
    • Python 类变量如何写类型注释(type hint)?
    • Python 字典类型如何写类型注释?
    • Python 通用类型(Generic) 如何写类型注释?
    • Python 类型中的一些高级关键字(Literal, Callable)?
  • 结论
    • 完成这篇文章的阅读,基本上已经可以适应 Python 日常项目的开发需求
    • 强烈推荐自己进行类型训练

这篇文章按照 Python-Type-Challengesopen in new window[1]库的划分,一共分为四个部分。

类相关类型

基础类类型

在 Python 里面,任何一个类本质上都是对象,因此无论是 Python 内部类还是自定义类,都需要进行类型注释。

  • Example 1, Python 内部类
# 以 Python pathlib 内部类 为例
from pathlib import Path

def get_cur_path() -> Path:
    cur_path: Path = Path('.')
    return cur_path

pwd_path = get_cur_path()

# 这里 pwd_path 的类型可能是 pathlib.PosixPath 或者 pathlib.WindowsPath, 但是两者都是 Path 的子类。

# 注意:int/str/float等也是内部的类
  • Example 2,自定义类
class Person:
    name: str
    age: int

class FakePerson:
    name: str
    age: int

def print_user_info(user: Person) -> None:
    print(user.name, str(user.age))

user1 = Person('chaofa', '28')
user2 = FakePerson('chaofa', '25')

print_user_info(user1)   # 通过检查
print_user_info(user2)   # 无法通过检查(失败)

类变量 (ClassVar)

在 Python 内部,class 属性分为类变量和成员变量,类变量可以同时被类和实例反问,因此类变量可能被实例修改,但一般情况下,我们不希望实例修改类变量的值,

from typing import ClassVar

class Person:
    # 类变量,只能被类修改,不能被实例修改
    name: ClassVar[str] = "chaofa"
    # 实例变量,instance variable,可以被实例修改
    age: int = 28

p = Person()   # Person 类的实例

Person.name = "bbruceyuan"  # ok, 成功
p.age = 25                  # ok, 成功,instace var 可以被实例修改
p.name = "other name"       # error, 失败(因为这是一个类变量,不能被实例修改)

Self 类型

self 在 Python 里面是一个特殊的名字,尽管可以写成其他的名字,但是一般会按照约定把 Python Class Method 里面的第一个参数设置为 self

  • 用法 1: 希望 current class 和 sub class 返回不同的类型
from typing import Self, reveal_type

class Foo:
    def return_self(self) -> Self:
        ...
        return self

class SubclassOfFoo(Foo):
    pass

reveal_type(Foo().return_self())  # 最终类型是 "Foo"
reveal_type(SubclassOfFoo().return_self())  # 最终类型是 "SubclassOfFoo"

  • 用法 2: 希望 current class 和 sub class 返回相同的类型
from typing import TypeVar

Self = TypeVar("Self", bound="Foo")

class Foo:
    def return_self(self: Self) -> Self:
        ...
        return self

class SubclassOfFoo(Foo):
    pass

reveal_type(Foo().return_self())  # 最终类型是 "Foo"
reveal_type(SubclassOfFoo().return_self())  # !!!!!最终类型是 "Foo"
  • 用法3: 希望 current class 和 sub class 返回相同的类型
# 用法 3 和 用法 2 使用场景稍微有一点不同
# 用法 2 中,返回的是 当前 class
# 用法 3 中,返回的一直是 Foo 的实例

class Foo:
    def return_self(self) -> 'Foo':
        ...
        return Foo()

class SubclassOfFoo(Foo):
    pass

reveal_type(Foo().return_self())  # 最终类型是 "Foo"
reveal_type(SubclassOfFoo().return_self())  # !!!!!最终类型是 "Foo"

字典类型(TypedDict)

在上一篇文章 Python 类型体操训练(一)-- 基础篇,介绍了 dict[key_type, value_type],定义一个字典,拥有特定的 key_typevalue_type,这个字典可以拥有无数的 keyTypedDict 是为了定义【拥有特定 key】的字典类型,key 的数量是确定的。

TypedDict-基础用法

基础定义,定义一个字典类型,叫做 Programer(程序员),有三个 key,分别是 name, age, github 分别是 str, int, str类型。

from typing import TypedDict

class Programer(TypedDict):
    name: str
    age: int
    github: str

# a 正确,所有类型匹配
a: Programer = {"name": "chaofa", "age": 28, "github": "github.com/bbruceyuan"}
# b 错误,因为 age 类型不匹配
b: Programer = {"name": "bbruceyuan", "age": 12.3, "github": "github.com/bbruceyuan"} 
# c 错误,因为缺少 key github
c: Programer = {"name": "chaofa", "age": 12}
# d 错误,因为缺少 key age
d: Programer = {"name": 'bruce', "github": "github.com/bbruceyuan"}

# 用法场景
# 一般用于 固定 key 的**字典**,而且明确是一个字典类型
# 现在有 dataclasses 之后,我个人觉得用一个 dataclass 会是更好的选择

TypedDict-NotRequired

还是上面这个例子,想定义一个程序员类(Programer),但是并不是每一个程序员都有自己的 Github 账号,这个时候 github 就不是一个必填的 key。这时候就可以使用 NotRequired 关键字。

from typing import TypedDict, NotRequired

class Programer(TypedDict):
    name: str
    age: int
    github: NotRequired[str]

# a 正确,所有类型匹配
a: Programer = {"name": "chaofa", "age": 28, "github": "github.com/bbruceyuan"}
# b 错误,因为 age 类型不匹配
b: Programer = {"name": "bbruceyuan", "age": 12.3, "github": "bbruceyuan.com"} 
# c **正确**,因为 key github 是可选 key
c: Programer = {"name": "chaofa", "age": 12}
# d 错误,因为缺少 key age
d: Programer = {"name": 'bruce', "github": "github.com/bbruceyuan"}

TypedDict-Required

还是上面这个例子,想定义一个程序员类(Programer),有 name, age, github, address, email 这些 Key, 但只有 name 是必须得,其他的都属于隐私不想公开,如果按照上面 NotRequired 的方式,就需要写很多 NotRequired。因此可以使用 Required 替代,具体见 case。

from typing import TypedDict, Required

class Programer(TypedDict, total=False):
    name: Required[str]
    age: int
    github: str
    address: str
    email: str

# a 正确,所有类型匹配
a: Programer = {"name": "chaofa", "age": 28, "github": "github.com/bbruceyuan", 'address': 'address', 'email': 'email'}
# b 错误,因为 age 类型不匹配
b: Programer = {"name": "bbruceyuan", "age": 12.3, "github": "bbruceyuan.com"} 
# c **正确**,因为 其他 key 都是可选的
c: Programer = {"name": "chaofa"}
# d **错误**,因为缺少 name key, 缺少了一定需要的 name key
d: Programer = {"age": 28, "github": "github.com/bbruceyuan", 'address': 'address', 'email': 'email'}

TypedDict-继承

TypeDict 定义的类可以和普通的 class 一样,通过继承来实现组合类型。

from typing import TypedDict

class Programer(TypedDict):
    name: str
    age: int

class GoogleProgramer(Programer):
    work_base: str   # 工作地 BASE

# a 正确,所有类型匹配
a: GoogleProgramer = {"name": "chaofa", "age": 28, "work_base": "china"} 
# b 错误,因为 缺少 work_base 这个 key
b: Programer = {"name": "bbruceyuan", "age": 25} 

通用类型(Generic)

写过 C++/Java 的同学可能知道 泛型的概念,一般会用一个 T 来表示这个变量可能是任意类型。C++语法结构为:template<class T> void func(T var) {...} Java 语法结构为 public class Hello<T> {...}

Python 的写法open in new windowScala 语言的泛型open in new window更为接近,语法是几乎是一样的,用 [T] 来表示泛型,方括号 [] 是用来接收泛型参数,T 是一个通用的参数标识符。

以下为 泛型参数的基本语法讲解,更高级用法见下一篇文章Python 类型体操训练(三)-- 高级篇

推荐写法 --方括号语法 (Python >= 3.12)

  • 例子 1,定义一个函数,输入和输出都是一个类型,不指定具体类型
# !!! good case (推荐)
def foo1[T](a: T, b: T) -> T:
    ...

# bad case (不推荐,实际上就是错的)
def foo2(a: Any, b: T) -> Any:
    ...

# 在这个例子看起来 T 有点像 Any 的用法
# foo1 表示 参数 和 返回值 的一样的类型
# foo2 参数 和 返回值 可能是不一样的类型
  • 例子 2, 如果需要输入和输出都是一个类型,这个类型就是 str 类型,那么可以写成
def foo1[T: str](a: T, b: T) -> T:
    ...
# 表示 T 可以是 str 
  • 例子 3,定义一个函数,输入和输出都是一个类型,这个类型只能是 int 或者 str
def foo1[T: (int, str)](a: T, b: T) -> T:
    ...
# 表示 T 可以是 int or str 
  • 例子 4,如果 T 需要是一个函数呢?
from collections.abc import Callable

def decorator[T: Callable](func: T) -> T:
    ...
# 表示 T 可以是 函数, 这也是定义装饰器的方法
  • 例子 5, 类中使用 泛型,比如 stack 可以接受任意类型
class Stack[T]:
    def __init__(self) -> None:
        self.items: list[T] = []

    def push(self, item: T) -> None:
        self.items.append(item)

    def pop(self):
        return self.items.pop()

Python 3.12 之前的写法

在 Python 3.12 之前,还不支持方括号 [T] 语法,因此需要使用一个叫做 TypeVar 的函数定义泛型,具体见链接open in new window

  • 例子 1
from typing import TypeVar

T = TypeVar("T")

def foo1[T](a: T, b: T) -> T:
    ...
  • 例子 2
from typing import TypeVar

# 表示可以是 str 的 subclass
T = TypeVar("T", bound=str)

def foo1[T](a: T, b: T) -> T:
    ...
  • 例子 3
from typing import TypeVar

# 表示 T 可以是 int or str 
T = TypeVar("T", str, int)

def foo1[T: (int, str)](a: T, b: T) -> T:
    ...
  • 例子 4
from collections.abc import Callable
from typing import TypeVar

# 表示 T 可以是 函数, 这也是定义装饰器的方法
T = TypeVar("T", bound=Callable)

def decorator[T: Callable](func: T) -> T:
    ...
  • 例子 5
from typing import TypeVar

T = TypeVar("T")

class Stack:
    def __init__(self) -> None:
        self.items: list[T] = []

    def push(self, item: T) -> None:
        self.items.append(item)

    def pop(self):
        return self.items.pop()

其他

Literal

Literal 是字面变量的意思,表示【只能是字面的取值】。

# 假设有一条路,只能往左或者往右,那么 direction 只能取值为:`left` 或者 `right`
from typing import Literal

def go(direction: Literal['left', 'right']):
    ...

# Literal 就是把所有可能得候选都写上,一般用于候选比较少的情况
# 比如 SQL JOIN 只能取值 inner, left, right, full 等

Callable

Callable 表示一个可调用对象,一般是一个函数。具体用法为: Callable[[函数参数1, 函数参数2, ..., 函数参数n], 函数返回值]

from collections.abc import Callable

def foo1(name: str) -> None:
    print(name)

def foo2(name: int) -> None
    print(name)

def accept_a_func(func: Callable[[str], None]) -> None:
    name = 'chaofa'
    func(name)

accept_a_func(foo1)   # 成功,符合 Callable 类型定义
accpet_a_func(foo2)   # 失败,因为 Callable 定义了,func 参数应该接受一个 str 类型

小结

通过阅读这一篇文章,可以知道 Python Class Variable 可以使用 ClassVar 定一个一个只能由 Class 修改的类变量,并且介绍了 Self 类型(这里已经看到了 前向注释的影子,具体可以参考下一篇);此外,着重了解 TypedDict 如何定义特定 Key 的字典类型,了解 RequiredNotRequired 的区别;然后介绍 Python Generic Type(泛型)的写法,明确了 Python 3.12 之后方括号 [T] 注释的写法,以及如何在 3.12 版本以前通过 TypeVar 定义通用类型;最后介绍了 LiteralCallable 两个重要且基础的 Python 类型。

Reference