本文主要是介绍【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Python编程-使用Type Hints与Typing模块提高代码可维护性
参考资料汇总
- Python-typing官方文档:【
typing
— Support for type hints】https://docs.python.org/3/library/typing.html - Python-官方文档Type Hints:【PEP 484 – Type Hints】https://peps.python.org/pep-0484/
- Python-官方文档Self Type:【PEP 673 – Self Type】https://peps.python.org/pep-0673/#abstract
Type Hints与typing介绍
Type Hints语法介绍
Type Hints 是 Python 3.5 引入的一项功能,它允许开发者在函数参数、返回值、变量等地方添加类型提示信息。这并不是强制性的类型检查,而是一种在代码中提供额外信息的机制,用于静态分析、文档生成和提高代码可读性
- 函数参数和返回值的类型提示:
def add(x: int, y: int) -> int:return x + y
在这个例子中,x: int
表示参数 x
的类型是整数,y: int
表示参数 y
的类型是整数,-> int
表示函数返回值的类型是整数。
- 变量的类型提示:
total: int = 0
这里的 total: int
表示变量 total
的类型是整数。
- 类型注解的灵活性:
def greet(name: str, age: Optional[int] = None) -> str:return f"Hello, {name}. You are {age} years old."# 使用类型提示
result: str = greet("Alice", 25)
在这个例子中,age: Optional[int]
表示 age
参数可以是整数或 None
。这里使用了 Optional
类型,它来自 typing
模块,用于表示可选的类型。
- 泛型(Generics):
from typing import List, Tupledef process_data(data: List[Tuple[str, int]]) -> List[str]:return [f"{name}: {age}" for name, age in data]
在这个例子中,List[Tuple[str, int]]
表示参数 data
是一个包含元组的列表,每个元组包含一个字符串和一个整数。
- 使用类型别名(Type Aliases):
from typing import ListCustomerID = str
OrderID = strdef process_orders(customer_ids: List[CustomerID], order_ids: List[OrderID]) -> None:# 处理订单逻辑pass
CustomerID
和 OrderID
是类型别名,可以提高代码的可读性。尽管类型提示不会强制执行类型检查,但它们为开发者和工具提供了更多信息,使得代码更易于理解、维护和分析。类型提示通常由 IDE、静态分析工具和类型检查器(如 mypy
)用于提供更好的开发体验和更早地发现潜在的错误。
typing 模块介绍
typing
模块的主要作用是为 Python 引入类型提示,它允许开发者在代码中提供关于变量、函数参数、函数返回值等的类型信息。这种类型提示并不会影响程序的实际执行,但它为开发者和工具提供了更多的上下文信息,而主要作用有以下几个:
-
代码可读性: 类型提示可以让代码更加清晰和易读。通过阅读代码,开发者可以更容易地理解变量的类型、函数的参数和返回值,从而更好地理解代码的意图。
-
静态类型检查: 使用类型提示后,可以使用静态类型检查工具(例如
mypy
)在开发阶段进行类型检查,捕获一些潜在的类型错误。这有助于减少在运行时由于类型问题导致的错误。 -
文档生成: 类型提示可以被用来生成文档,自动文档生成工具(如 Sphinx)能够根据类型提示自动生成文档,进一步提高文档的准确性和可维护性。
-
提高开发工具的智能提示: 集成开发环境(IDE)可以利用类型提示提供更准确的代码补全和智能提示,使开发者更加高效。
-
**功能增强:**用于增强 Type Hints 的功能,使得类型提示更加灵活和表达力更强。
typing
模块中的类型工具可以用于构建更复杂的类型结构
from typing import List, Tuple, Optionaldef process_data(data: List[Tuple[str, int]]) -> List[str]:return [f"{name}: {age}" for name, age in data]def greet(name: str, age: Optional[int] = None) -> str:return f"Hello, {name}. You are {age} years old."
注意:typing库的使用并不会像C++与Java一样,不匹配类型无法运行,它仅仅只是用于一种指明性的功能
部署typing与typing_extensions
pip install typing
pip install typing_extensions # 用于支持一些当前Python版本可能不支持的特性
内建类型与类型注解
内建类型:即Python默认拥有的几大基本数据类型
数据类型 | 描述 | 示例 |
---|---|---|
整数(int) | 用于表示整数。 | x = 5 |
浮点数(float) | 用于表示带有小数点的数字。 | y = 3.14 |
布尔值(bool) | 用于表示真(True)或假(False)的值,常用于条件判断。 | is_valid = True |
字符串(str) | 用于表示文本。 | message = "Hello, World!" |
列表(list) | 用于表示可变序列,可以包含不同类型的元素。 | numbers = [1, 2, 3] |
元组(tuple) | 用于表示不可变序列,类似于列表但不能被修改。 | coordinates = (x, y) |
集合(set) | 用于表示无序、唯一的元素集合。 | unique_numbers = {1, 2, 3} |
字典(dict) | 用于表示键值对映射。 | person = {'name': 'John', 'age': 30} |
None 类型 | 表示空值或缺失值。 | result = None |
注意:在python3.9之后,
list
与dict
均支持泛型类型:list[int]
即整数列表,dict[str, str]
表示以字符串为键值,字符串为值的字典
类型注解:通过 typing
模块引入内容
基本类型:
类型 | 描述 |
---|---|
int | 整数类型 |
float | 浮点数类型 |
bool | 布尔类型 |
str | 字符串类型 |
容器类型:
类型 | 描述 |
---|---|
List | 列表类型 |
Tuple | 元组类型 |
Dict | 字典类型 |
Set | 集合类型 |
特殊类型:
类型 | 描述 |
---|---|
Any | 表示任意类型,相当于取消类型检查 |
Union | 表示多种可能的类型 |
Callable 类型:
类型 | 描述 |
---|---|
Callable | 表示一个可调用对象的类型,可以指定函数的输入参数和返回值的类型 |
Generics:
类型 | 描述 |
---|---|
TypeVar | 定义一个类型变量,用于表示不确定的类型 |
Generic | 用于创建泛型类型,可以在类或函数中使用泛型 |
函数注解:
类型 | 描述 |
---|---|
Type | 表示一个类型 |
Optional | 表示一个可选的类型 |
AnyStr | 表示字符串的抽象类型,可以是 str 或 bytes |
类型别名:
类型 | 描述 |
---|---|
NewType | 创建一个新的类型,用于提高类型的可读性 |
注意:在python3.9之前的版本不能够使用dict与list内建类型,只能导入使用typing库的注解
变量,参数和函数添加注解
我们编写一个简单的程序:
def get_test_dict_info(test_dict):test_dict['three'] = 300return test_dictif __name__ == "__main__":dict_one = {'one': 100, 'two': 200}temp_dict = get_test_dict_info(test_dict=dict_one)print(temp_dict)
在编写过程中,你会发现有以下情况:
它会提示我们参数类型为Any,这是因为python是动态类型语言的缘故,这就会导致我们有时候不能够及时发现错误,例如我们直接传入字符串作为该函数的实参,在静态检查器运行前,python并不会提示你错误,并且在复杂调用情况下,甚至于某些静态检查器都不能很好地检查出问题,此时就需要我们的类型注解,以便于python检查类型:
对于变量的注解:
value: type # 例如字典类型写为 dict_one :dict
对于函数或方法应该指明返回类型,并且无参类型设为None
:
def function_example(value: type, ...) -> None # 对应返回值类型写对应类型
则对于上述代码我们应该修改为:
def get_test_dict_info(test_dict: dict) -> dict:test_dict['three'] = 300return test_dictif __name__ == "__main__":dict_one: dict[str, int] = {'one': 100, 'two': 200}temp_dict: dict = get_test_dict_info(test_dict=dict_one)print(temp_dict)
注意:在字典内容确定情况下,我们可以使用泛型表示,更加清晰明了
自定义类型添加类型注解
class ClassOne:def __init__(self) -> None:self.name = "OneClassName"def get_name(self) -> str:return self.name
from class_one import ClassOneclass ClassTwo:def get_class_one_object_name(self, object) -> str:return object.get_name()if __name__ == "__main__":object_one = ClassOne()object_two = ClassTwo()temp_obj: str = object_two.get_class_one_object_name(object=object_one)print(temp_obj)
如上所示,我们在不同文件创建了两个类,然后import
第一个类,在第二个类的方法中使用其对象,我们在编写时会发现以下问题:
它有时候无法获取到我们的方法或者查找到许多部分名称相同的方法,尤其是在调用复杂时,此时我们可以像为变量注解一样来添加类的注解:
object: ClassName
我们以修改第二个类的代码为例:
from class_one import ClassOneclass ClassTwo:def get_class_one_object_name(self, object: ClassOne) -> str:return object.get_name()if __name__ == "__main__":object_one: ClassOne = ClassOne()object_two: ClassTwo = ClassTwo()temp_obj: str = object_two.get_class_one_object_name(object=object_one)print(temp_obj)
循环引入类或模块问题与解决方法
试想一下,我们在上述的基础上,还想让第一个类访问第二个类的对象呢,为ClassTwo
加上与ClassOne
相同的构造方法与姓名获取方法后,我们进行注解后会发现,Python会提示:
未定义“ClassTwo”Pylance[reportUndefinedVariable]
此时你可能会想到在前面加上代码:
from class_two import ClassTwo
这样就会产生一个致命错误,循环引入,进而导致爆栈,python对此将会终止脚本运行:
首先先讲一个细节,注解内容是字符串时,注解语法同样也是生效的,原因见后文的annotations
:
明确这点后,在导入时使用以下语句进行处理,并且依次简要介绍其功能:
from __future__ import annotations
from typing import TYPE_CHECKINGif TYPE_CHECKING:from class_two import ClassTwo
__future__
实现向后兼容语言特性
A future statement,
from __future__ import <feature>
, directs the compiler to compile the current module using syntax or semantics that will become standard in a future release of Python. The__future__
module documents the possible values of feature. By importing this module and evaluating its variables, you can see when a new feature was first added to the language and when it will (or did) become the default:>>> import __future__ >>> __future__.division _Feature((2, 2, 0, 'alpha', 2), (3, 0, 0, 'alpha', 0), 8192)
from:【Python官方doc】https://docs.python.org/3/glossary.html#term-future
__future__
是一个特殊的模块,用于帮助实现向后兼容性和引入新的语言特性。通过在代码中导入__future__
模块并使用其中的特定功能,可以在较旧的Python版本中启用一些即将成为默认行为的特性。这样可以使原有的代码能在不同版本的Python中更加一致地运行。
例如,在Python 2.x中想要使用Python 3.x的print
语法,可以在代码的开头添加如下导入语句:
from __future__ import print_function
这将启用Python 3.x中的print
函数,而不是Python 2.x中的print
语句。
- 常见使用
特性 | 描述 |
---|---|
print_function | 启用使用print() 函数而不是print 语句。 |
division | 启用Python 3.x中的真正除法(即/ 操作符执行浮点除法),而不是Python 2.x中的整数除法。 |
absolute_import | 修改导入语句的语义,确保使用绝对导入,而不是相对导入。 |
unicode_literals | 启用字符串文字为Unicode字符串而不是字节字符串。 |
generators | 修改next() 函数的语法以适应Python 3.x中生成器的语法。 |
annotations改变类型注解行为
在 Python 中,from __future__ import annotations
是一个特殊的语法,用于调整类型注解的处理方式。在默认情况下,类型注解在函数签名中会被当作字符串处理,这导致了一些限制,特别是在涉及到递归类型注解时。annotations
的引入就是为了解决这个问题。
使用 from __future__ import annotations
可以改变类型注解的行为,使得类型注解在函数签名中不再被当作字符串处理(默认处理方式),而是被直接处理为类型。这使得在类型注解中引用同一模块中定义的类名等符号时,不再需要将类型注解放在字符串引号中。
这个改变的目的是为了简化类型注解的书写,特别是在定义复杂的数据结构或递归类型时,使得代码更加清晰和易读。这个特性从 Python 3.7 版本开始引入,但需要使用 from __future__ import annotations
才能启用。
__annotations__
属性
__annotations__
是 Python 中一个特殊的属性,用于访问包含函数或类成员中类型注解的字典。当在函数或类中使用类型注解时,解释器会将这些注解存储在 __annotations__
字典中。示例:
def example(a: int, b: str) -> float:passprint(example.__annotations__)
# 输出: {'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}
在这个例子中,__annotations__
字典包含了参数 a
、b
以及返回值的类型信息。这对于在运行时访问这些信息或者通过代码分析工具来检查和分析代码非常有用。然而,如果在使用 from __future__ import annotations
语句的情况下,__annotations__
的行为会有所不同。在这种情况下,__annotations__
不再包含字符串形式的类型注解,而是直接包含解释后的类型信息。这使得在代码中不再需要使用字符串引号来表示类型,而直接使用类型符号。
from __future__ import annotationsdef example(a: int, b: str) -> float:passprint(example.__annotations__)
# 输出: {'a': 'int', 'b': 'str', 'return': 'float'}
通过引入 from __future__ import annotations
,__annotations__
中存储的是直接的类型对象,而不是字符串。这在某些情况下可以提高代码的可读性,尤其是在处理递归类型注解等情况。
类的__annotations__
注意:直接访问一个类,无法获取到__annotations__
,将会获得一个空字典:在类级别定义的类型注解通常用于提供文档信息,而它们并不像函数级别的那样在运行时被自动存储在 __annotations__
属性中。在类中使用类型注解时,这些注解通常是用于文档生成工具(如 Sphinx)的目的,而不会被 Python 解释器直接存储。
可以使用 __annotations__
属性的方式是在类的 __init__
或其他方法中引入 __annotations__
属性,以确保类型信息被存储在类的 __annotations__
属性中。例如:
class MyClass:__annotations__ = {'attribute': int}print(MyClass.__annotations__) # 输出: {'attribute': <class 'int'>}
TYPE_CHECKING解决循环引入问题
TYPE_CHECKING
是 typing
模块中的一个特殊变量,用于解决在类型注解中避免循环导入问题的情况。
在 Python 中,当两个模块相互导入时,可能会导致循环导入的问题,其中一个模块引用了另一个模块,而另一个模块又引用了第一个模块,从而形成了循环。这可能会导致在运行时出现 ImportError。
为了解决这个问题,typing
模块中引入了一个特殊的变量 TYPE_CHECKING
。这个变量在运行时始终为 False
,但在类型检查工具(如 mypy
)运行时为 True
。因此,可以使用 TYPE_CHECKING
变量在类型注解中创建条件导入,从而避免循环导入问题。
示例:
# module_a.py
from typing import TYPE_CHECKINGif TYPE_CHECKING:from module_b import MyClassBclass MyClassA:def __init__(self, instance_b: 'MyClassB') -> None:self.instance_b = instance_b
# module_b.py
from typing import TYPE_CHECKINGif TYPE_CHECKING:from module_a import MyClassAclass MyClassB:def __init__(self, instance_a: 'MyClassA') -> None:self.instance_a = instance_a
在这个例子中,TYPE_CHECKING
被用于创建条件导入。当运行时 TYPE_CHECKING
为 False
,因此实际的导入语句不会执行,而在类型检查时,TYPE_CHECKING
为 True
,导入语句会被执行,从而避免了循环导入问题。这种技术特别在涉及复杂类之间的相互引用和类型注解的情况下有用。
常量注解Final
Fianl_value_name: Final[type]
示例如下,记得导入Final
from typing import FinalFLAG: Final[int] = 233
但是,并不会阻止我们修改,改了也能够正常运行:
阻止继承装饰器final
使用@final
装饰器来阻止继承注解:
class BaseClass:def __init__(self) -> None:self.greeting_str = 'Hello World'@finaldef show_greeting_str(self) -> None:print(self.greeting_str)
与上面一致,仅仅只是声明,并不会妨碍运行:
完整代码如下:
from typing import finalclass BaseClass:def __init__(self) -> None:self.greeting_str = 'Hello World'@finaldef show_greeting_str(self) -> None:print(self.greeting_str)class SubClass(BaseClass):def __init__(self) -> None:self.greeting_str = 'Hello Python'def show_greeting_str(self) -> None:print(self.greeting_str)if __name__ == "__main__":object_one: BaseClass = BaseClass()object_two: SubClass = SubClass()object_two.show_greeting_str()
重载装饰器overload
在 Python 中,方法签名覆盖(Method Signature Overriding)是一种技术,它允许子类中的方法具有与父类中的方法相同的名称,但具有不同的参数类型或个数。这可以通过使用 @overload
装饰器来实现。
需要注意的是,@overload
装饰器本身并不实际执行任何代码,它只是用于静态类型检查和文档生成。真正的实现位于装饰器下方的实际方法定义中。
from typing import overloadclass Shape:def draw(self):print("Drawing a shape")class Circle(Shape):@overloaddef draw(self, color: str) -> None:passdef draw(self, color: str = "red"):print(f"Drawing a {color} circle")if __name__ == "__main__":circle = Circle()circle.draw() # 输出: Drawing a red circlecircle.draw("blue") # 输出: Drawing a blue circle
重写装饰器override
区分两者:
overload
装饰器用于标记方法的多个版本,但真正的重载需要在实现中手动处理。override
注解用于确保子类中的方法正确地覆盖了父类中的方法。
from typing import overrideclass BaseClass:def occupy_function(self) -> None:passclass SubClass(BaseClass):@overridedef occupy_function(self) -> None:print("Hello World")if __name__ == "__main__":object: SubClass = SubClass()object.occupy_function() # 输出 Hello World
cast伪强制转换
typing.cast(typ, val)
Cast a value to a type.
This returns the value unchanged. To the type checker this signals that the return value has the designated type, but at runtime we intentionally don’t check anything (we want this to be as fast as possible).
from: 【Python官方doc】https://docs.python.org/3/library/typing.html?highlight=cast#typing.cast
typing.cast
的作用是将一个值强制转换为指定的类型,同时告诉类型检查器(例如 Mypy)不要对这次转换进行检查。这在一些特定的情况下很有用,例如当类型检查器无法推断正确的类型时,或者我们确切地知道某个值的类型但类型检查器无法确定,但是实际上它只是骗一下检查器,并不会真的进行转换。
举个例子:
from typing import castdef example_function() -> str:flag = Noneflag = cast(str, flag)return flagprint(example_function())
下面是一些使用场景:
-
类型推断不准确的情况:
from typing import List, castdef process_data(data: List[str]) -> None:# 在某些情况下,类型检查器无法正确推断 data 的类型# 使用 cast 进行强制类型转换,告诉类型检查器 data 确实是 List[str]processed_data = cast(List[str], data)# 这样可以避免类型检查器的警告for item in processed_data:print(item)
-
处理动态属性或方法:
from typing import castclass CustomObject:def __getattr__(self, name: str) -> int:# 在这里,类型检查器无法确定 getattr 返回的是什么类型result = cast(int, getattr(self, name))return result + 10
-
处理 JSON 数据:
from typing import Any, Dict, castdef process_json(data: Dict[str, Any]) -> None:# 假设我们知道 "count" 键对应的值是整数,但类型检查器无法确定count_value = cast(int, data.get("count", 0))print(f"Count: {count_value}")
可选类型注解
注意:可选类型注解并不能保证前后类型一致性,在一些特殊的情况下会出现报错现象
Optional可选类型注解
在建立某些对象时,我们可能会出于一定的考虑,将对象先设置为None
,部分检查器可能会因为注解与实际类型其不符合,从而给出警告,此时就需要我们的可选类型注解:
from typing import Optional对象名: Optional[可选的一个类名]
使用示例:
from typing import Optionalclass InfoClass:passclass TestClass:def __init__(self, object: Optional[InfoClass]) -> None:self.content: Optional[InfoClass] = None
Union可选类型注解
Union与Optional的区别在于,它可以使用多个类型的注解列表(注意:必须是两个及以上的注解),基本格式如下:
from typing import Union对象名: Union[可选类名1, 可选类名2 ...]
使用示例:
from typing import Unionclass InfoClass:passclass TestClass:def __init__(self, object: Union[InfoClass, None]) -> None:self.content: Union[InfoClass, None] = None
使用 |
进行可选注解(Python3.10+)
还有一种新版本的书写方法(Python3.10+):
class InfoClass:passclass TestClass:def __init__(self, object: InfoClass | None) -> None:self.content: InfoClass | None = None
链式调用Self
注解
The new
Self
annotation provides a simple and intuitive way to annotate methods that return an instance of their class. This behaves the same as theTypeVar
-based approach specified in PEP 484, but is more concise and easier to follow.Common use cases include alternative constructors provided as
classmethod
s, and__enter__()
methods that returnself
:class MyLock:def __enter__(self) -> Self:self.lock()return self...class MyInt:@classmethoddef fromhex(cls, s: str) -> Self:return cls(int(s, 16))...
Self
can also be used to annotate method parameters or attributes of the same type as their enclosing class.See PEP 673 for more details.
from : 【What’s New In Python 3.11】:https://docs.python.org/3/whatsnew/3.11.html?highlight=self#
什么是链式调用
在 Python 中,链式调用是指通过在方法调用的返回值上连续调用其他方法,从而形成一条方法调用的链。这种风格通常用于构建一系列相关的操作,使代码更加紧凑和可读。要实现链式调用,每个方法都需要返回一个对象,这个对象上有下一个方法可以被调用。
考虑以下示例,演示链式调用和返回方法本身的概念,并且进行类型验证:
class Calculator:def __init__(self, value: float = 0) -> None:self.value = valuedef add(self, x: float) -> 'Calculator':self.value += xreturn self # 返回方法本身,以支持链式调用def subtract(self, x: float) -> 'Calculator':self.value -= xreturn self # 返回方法本身,以支持链式调用def multiply(self, x: float) -> 'Calculator':self.value *= xreturn self # 返回方法本身,以支持链式调用def divide(self, x: float) -> 'Calculator':if x != 0:self.value /= xelse:print("Error: Division by zero.")return self # 返回方法本身,以支持链式调用if __name__ == "__main__":# 使用链式调用result = Calculator(10).add(5).subtract(3).multiply(2).divide(4).valueprint(result) # 输出: 6.0# 打印类型和检查实例print(type(Calculator(10).add(5))) # 输出: <class '__main__.Calculator'>print(isinstance(Calculator(10).add(5), Calculator)) # 输出: True
在上述示例中,Calculator
类有四个方法:add
、subtract
、multiply
和 divide
。每个方法在完成相应的操作后都返回 self
,这样就可以在方法调用之后继续调用其他方法。
通过创建 Calculator
对象并进行链式调用,可以一步步地执行一系列数学操作,并且最终通过访问 value
属性获取结果。这种模式使代码看起来更加流畅和易于理解。
要注意的是,返回方法本身并进行链式调用的做法是一种设计选择,并不是所有类都需要这样做。在某些情况下,这可能使代码更简洁,但在其他情况下,可能会降低代码的可读性。
使用Self
注解方式示例
from typing import Selfclass Calculator:def __init__(self, value: float = 0) -> None:self.value = valuedef add(self, x: float) -> Self:self.value += xreturn self def subtract(self, x: float) -> Self:self.value -= xreturn self def multiply(self, x: float) -> Self:self.value *= xreturn self def divide(self, x: float) -> Self:if x != 0:self.value /= xelse:print("Error: Division by zero.")return self
为何使用Self
注解
试想上述代码中,我们修改了类的签名,而注解是字符串,就会引起连续报错,不过现在的编辑器较为智能,比如PyCharm就会自动为你修改,所以我们推荐使用Self
注解,不过还有其他应对方法,例如from __future__ import annotations
from __future__ import annotationsclass Calculator:def __init__(self, value: float = 0) -> None:self.value = valuedef add(self, x: float) -> Calculator:self.value += xreturn self # 返回方法本身,以支持链式调用def subtract(self, x: float) -> Calculator:self.value -= xreturn self # 返回方法本身,以支持链式调用def multiply(self, x: float) -> Calculator:self.value *= xreturn self # 返回方法本身,以支持链式调用def divide(self, x: float) -> Calculator:if x != 0:self.value /= xelse:print("Error: Division by zero.")return self # 返回方法本身,以支持链式调用
或者官方文档中的:
from typing import TypeVarTShape = TypeVar("TShape", bound="Shape")class Shape:def set_scale(self: TShape, scale: float) -> TShape:self.scale = scalereturn selfclass Circle(Shape):def set_radius(self, radius: float) -> Circle:self.radius = radiusreturn selfCircle().set_scale(0.5).set_radius(2.7) # => Circle
现在来看用泛型语法注解还是较为复杂,不如Self直观,不太推荐使用
ClassVar类属性注解
在 Python 类型注解中,ClassVar
是一个用于表示类属性的注解。类属性是属于类而不是实例的属性。使用 ClassVar
注解可以更明确地指定一个变量是类属性,并且该注解只能接收一个类型。
下面是一个使用 ClassVar
注解的示例:
from typing import ClassVarclass MyClass:class_attribute: ClassVar[int] = 42def __init__(self, instance_attribute: str) -> None:self.instance_attribute: str = instance_attributeif __name__ == "__main__":# 访问类属性print(MyClass.class_attribute) # 输出: 42# 创建一个实例obj1 = MyClass("Object 1")# 访问实例属性print(obj1.instance_attribute) # 输出: Object 1# 修改类属性MyClass.class_attribute = 99# 类属性被所有实例共享print(obj1.class_attribute) # 输出: 99
在上述例子中,class_attribute
被注解为 ClassVar[int]
,表示这是一个整数类型的类属性。这样的注解提供了更明确的类型信息,有助于类型检查工具识别和捕获潜在的类型错误。
需要注意的是,在 Python 3.10 及以后的版本中,ClassVar
注解是可选的,因为 Python 可以根据上下文自动推断出类属性。在较早的版本中,可能需要使用 ClassVar
注解来提供类型信息。
Literal限制值注解
typing.Literal — Special typing form to define “literal types”.
Literal
can be used to indicate to type checkers that the annotated object has a value equivalent to one of the provided literals.from:【typing — Support for type hints】https://docs.python.org/3/library/typing.html?highlight=literal#typing.Literal
Literal
是 Python 中 typing
模块提供的一种注解,用于指定一个变量的值应该是特定字面值之一。它的作用是在类型提示中限制一个变量的取值范围。Literal
的常见用法是在定义函数参数或类属性时,用来明确表示该参数或属性应该是特定的几个值之一,特定值的类型为任何不可变的类型。
from typing import Literaldef colorize(is_color: Literal['red', 'green', 'blue']) -> str:return is_colorif __name__ == "__main__":color_type: str = colorize("red")
需要注意的是,Literal
是在 Python 3.8 及以后版本的 typing
模块中引入的,如果你使用的是更早版本的 Python,可能需要考虑其他的方式来达到类似的效果。
TypeVar泛型注解
class typing.TypeVar(name, *constraints, bound=None, covariant=False, contravariant=False, infer_variance=False)
Type variable.
The preferred way to construct a type variable is via the dedicated syntax for generic functions, generic classes, and generic type aliases:
class Sequence[T]: # T is a TypeVar...
This syntax can also be used to create bound and constrained type variables:
class StrSequence[S: str]: # S is a TypeVar bound to str...class StrOrBytesSequence[A: (str, bytes)]: # A is a TypeVar constrained to str or bytes...
However, if desired, reusable type variables can also be constructed manually, like so:
T = TypeVar('T') # Can be anything S = TypeVar('S', bound=str) # Can be any subtype of str A = TypeVar('A', str, bytes) # Must be exactly str or bytes
Type variables exist primarily for the benefit of static type checkers. They serve as the parameters for generic types as well as for generic function and type alias definitions.
TypeVar
是 Python 中的一个泛型类型的工具,它通常与泛型函数和类一起使用,以增强代码的类型提示和类型检查。TypeVar
允许定义一个类型变量,该变量可以在多个地方使用,并且在使用时可以根据需要绑定到不同的具体类型。
简单泛型注解
from typing import TypeVar, ListT = TypeVar('T')def repeat_item(item: T, n: int) -> List[T]:return [item] * n
即T
为Any
类型,接收任意类型参数
协变泛型注解
所谓协变泛型实质上是一种划定了上界的泛型,TypeVar
中的 bound
参数用于指定类型变量的上界(bound),表示类型变量可以是哪些类型的子类型。上界提供了对类型变量进行限制的机制,使得在使用该类型变量时,其实际类型必须符合指定的上界条件。
from typing import TypeVarT = TypeVar('T', bound=SomeType)
-
T
:类型变量的名称。 -
bound=SomeType
:指定类型变量T
的上界为SomeType
。SomeType
可以是一个具体的类型,也可以是一个泛型类型。 -
限制类型范围:
bound
参数允许你限制类型变量的范围,确保它只能是指定类型或指定类型的子类型。 -
提供类型信息: 指定上界后,类型系统可以更精确地推断类型变量的实际类型,提高类型检查的准确性。
-
支持多重上界: 可以指定多个上界,形成联合类型,表示类型变量必须是这些上界中任意一个的子类型。
from typing import TypeVar, Union# TypeVar with a single bound
T = TypeVar('T', bound=int)
def double_value(value: T) -> T:return value * 2# TypeVar with multiple bounds
U = TypeVar('U', bound=Union[int, float])
def add_values(a: U, b: U) -> U:return a + b
多类型泛型注解
from typing import TypeVar, UnionT = TypeVar('T', int, float, str)def display_content(content: T) -> None:print(content)# 使用
display_content(42) # 合法
display_content(3.14) # 合法
display_content("Hello") # 合法
display_content(True) # 不合法,因为bool不在允许的类型范围内
在上面的例子中,T
是一个多类型变量(使用此语法必须是两个以上的类型),可以是整数、浮点数或字符串。
协变与逆变
参考:【Python-协变与逆变】https://peps.python.org/pep-0484/#covariance-and-contravariance
在Python中,协变(covariance)和逆变(contravariance)通常用于描述类型系统中的关系。这两个概念与类型的子类型关系有关。
协变(Covariance)
- 当一个类型的子类型的顺序与原始类型的子类型的顺序相同时,我们称之为协变。
- 在协变中,如果
A
是B
的子类型,那么List[A]
就是List[B]
的子类型。
示例:
class Animal:passclass Dog(Animal):passanimals: List[Animal] = [Dog(), Animal()] # 协变
在这个例子中,List[Dog]
是 List[Animal]
的子类型,因为Dog
是Animal
的子类型。
逆变(Contravariance)
- 当一个类型的子类型的顺序与原始类型的子类型的顺序相反时,我们称之为逆变。
- 在逆变中,如果
A
是B
的子类型,那么Callable[B]
就是Callable[A]
的子类型。
示例:
class Animal:passclass Dog(Animal):passfrom typing import Callabledef handle_animal(animal_handler: Callable[[Animal], None], animal: Animal) -> None:animal_handler(animal)def handle_dog(dog: Dog) -> None:print("Handling a dog")# 逆变:将处理狗的函数传递给处理动物的函数
handle_animal(handle_dog, Dog())
在这个例子中,handle_animal
是处理任何动物的函数,它接受两个参数:animal_handler
是一个函数,用于处理动物,而 animal
是要处理的动物。我们有一个处理狗的函数 handle_dog
,它接受一个 Dog
类型的参数。接下来,我们调用 handle_animal
,将 handle_dog
作为参数传递给它。这正是逆变的体现,因为 handle_animal
的参数类型是 Callable[[Animal], None]
,而我们传递了一个 Callable[[Dog], None]
类型的函数作为参数,而不会引发类型错误。
动态函数调用
在Python中,将方法名传递给方法通常使用的是函数引用。在Python中,函数是第一类对象,这意味着它们可以被当作值传递、赋值给变量,并作为参数传递给其他函数。当你将方法名传递给另一个方法时,你实际上是将函数引用传递给它。
下面是一个简单的示例,说明如何将方法名传递给方法:
def greet(name):return "Hello, " + namedef farewell(name):return "Goodbye, " + namedef perform_greeting(greeting_function, name):result = greeting_function(name)print(result)# 将方法名 greet 传递给 perform_greeting 方法
perform_greeting(greet, "Alice")# 将方法名 farewell 传递给 perform_greeting 方法
perform_greeting(farewell, "Bob")
在这个例子中,perform_greeting
方法接受一个函数引用作为参数,并调用它来执行问候。这种方式允许动态地选择要执行的函数,从而增加程序灵活性。
Generic抽象基类
Generic
是 Python 中 typing
模块提供的一个抽象基类(Abstract Base Class),用于表示泛型类。在 typing
模块中,Generic
用于定义泛型类,即可以处理多种数据类型的类。通过在类定义中使用 Generic[T]
,其中 T
是类型变量,可以在实例化时指定具体的类型。
单泛型参数实现
from typing import Generic, TypeVarT = TypeVar('T')class Box(Generic[T]):def __init__(self, content: T) -> None:self.content = contentdef get_content(self) -> T:return self.contentif __name__ == "__main__":int_box: Box[int] = Box(42)str_box: Box[str] = Box("Hello, Generics!")# 获取并打印内容print("Integer Box Content:", int_box.get_content()) # 输出:42print("String Box Content:", str_box.get_content()) # 输出:Hello, Generics!
多泛型参数实现
from typing import Generic, TypeVarT = TypeVar('T')
U = TypeVar('U')class Pair(Generic[T, U]):def __init__(self, first: T, second: U) -> None:self.first: T = firstself.second: U = secondif __name__ == "__main__":int_str_pair: Pair[int, str] = Pair(42, "Hello, Generics!")# 获取并打印内容print("First Element:", int_str_pair.first) # 输出:42print("Second Element:", int_str_pair.second) # 输出:Hello, Generics!
多继承泛型实现
from typing import Generic, TypeVar, ListT = TypeVar('T', int, str)
U = TypeVar('U', int, str)class Container(Generic[T, U]):def __init__(self, items: List[T], metadata: U) -> None:self.items: List[T] = itemsself.metadata: U = metadataclass Box(Generic[T, U], Container[T, U]):def __init__(self, content: T, items: List[T], metadata: U) -> None:super().__init__(items, metadata)self.content: T = contentif __name__ == "__main__":int_str_box: Box[int, str] = Box("233hhh", [1, 2, 3, 4, 5], "Box Metadata")# 获取并打印内容print("Box Content:", int_str_box.content) # 输出:233hhhprint("Container Items:", int_str_box.items) # 输出:[1, 2, 3, 4, 5]print("Container Metadata:", int_str_box.metadata) # 输出:"Box Metadata"
Protocol抽象基类
在 Python 中,typing
模块提供了 Protocol
抽象基类,用于定义协议。Protocol
允许你明确地声明一个类应该具有哪些属性、方法或其他特征,从而实现协议。下面我们来实现一个协议:
from typing import Protocol, ClassVarclass AnimalProtocol(Protocol):sound: ClassVar[str]def make_sound(self) -> None:...def move(self, distance: float) -> None:...def eat(self, food: str) -> None:...class Cat:sound = "Meow"def __init__(self, name: str) -> None:self.name = namedef make_sound(self) -> None:print(f"{self.name} says {self.sound}")def move(self, distance: float) -> None:print(f"{self.name} moves {distance} meters gracefully")def eat(self, food: str) -> None:print(f"{self.name} enjoys eating {food}")class Dog:sound = "Woof"def __init__(self, name: str, breed: str) -> None:self.name = nameself.breed = breeddef make_sound(self) -> None:print(f"{self.name} barks {self.sound}")def move(self, distance: float) -> None:print(f"{self.name} runs {distance} meters energetically")def eat(self, food: str) -> None:print(f"{self.name} devours {food}")if __name__ == "__main__":cat = Cat(name="Whiskers")dog = Dog(name="Buddy", breed="Golden Retriever")# Cat-specific propertyprint(f"{cat.name} is a cat")# Dog-specific propertyprint(f"{dog.name} is a {dog.breed} dog")# Common behaviorcat.make_sound()cat.move(2.5)cat.eat("fish")dog.make_sound()dog.move(5.0)dog.eat("bones")
在 Python 中,
...
是一个特殊的语法,表示一个占位符,通常用于表示某个代码块未实现或者不需要实现。在协议中,...
的作用是指示方法的声明,而不需要提供具体的实现。协议的目的是定义一组规范,描述类应该具有的方法、属性或其他特征,而不是要求在协议中提供具体的实现。因此,使用
...
作为占位符是合理的,因为协议只是定义了接口,而不关心具体的实现逻辑
TypedDict字典类型
TypedDict
是 Python 3.8 中引入的一项特性,属于 PEP 589(TypeHints: Type Hints Customization)的一部分。它提供了一种指定字典中键和值类型的方式,使得能够更精确地进行类型注解。
以下是一个基本的 TypedDict
使用示例:
from typing import TypedDictclass Person(TypedDict):name: strage: intemail: str
在这个例子中,Person
是一个 TypedDict
,指定了三个键:name
、age
和 email
,以及它们对应的值类型。这允许你创建一个具有这些键的字典并进行类型检查:
如果尝试为键分配错误类型的值,类型检查器(如 MyPy)将捕获错误并提供反馈。
需要注意的是,TypedDict
主要用于静态类型检查,它不引入运行时检查。它在与工具(如 MyPy)一起使用时非常有用,这些工具可以分析代码并提供与类型相关的反馈。
Required 与 NotRequired(Python3.11+)
The
typing.Required
type qualifier is used to indicate that a variable declared in a TypedDict definition is a required key:class Movie(TypedDict, total=False):title: Required[str]year: int
Additionally the
typing.NotRequired
type qualifier is used to indicate that a variable declared in a TypedDict definition is a potentially-missing key:class Movie(TypedDict): # implicitly total=Truetitle: stryear: NotRequired[int]
It is an error to use
Required[]
orNotRequired[]
in any location that is not an item of a TypedDict. Type checkers must enforce this restriction.from:【PEP 655 – Marking individual TypedDict items as required or potentially-missing】https://peps.python.org/pep-0655/#specification
类型typing.Required
限定符用于指示 TypedDict 定义中声明的变量是必需的键(同时也是默认情况下的设置),typing.NotRequired
类型限定符用于指示 TypedDict 定义中声明的变量是非必须的键,下面给出示例:
from typing import TypedDict, Required, NotRequiredclass Person(TypedDict):name: Required[str]age: Required[int | None]email: NotRequired[str]if __name__ == "__main__":my_dict: Person = {'name':"233", 'age':None}
注意:在不是 TypedDict 项目的任何位置使用Required[]
或都是错误的。NotRequired[]
类型检查器必须强制执行此限制。
Unpack解包类型
Unpack
的类型主要用于解包参数,特别是在泛型类和函数中,以提供更灵活的类型提示。我们直接以传入位置关键字给函数来展示它的强大之处:
from typing import TypedDict, Unpackclass Person(TypedDict):name: strage: intemail: strdef occupy_function(**kwargs: Unpack[Person]) -> None:passif __name__ == "__main__":occupy_function(name="233", age=18, email="xxx@qq.com")
在编辑时我们会得到以下提示:
这篇关于【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!