【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, Tuple

def 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 List

CustomerID = str
OrderID = str

def process_orders(customer_ids: List[CustomerID], order_ids: List[OrderID]) -> None:
    # 处理订单逻辑
    pass

CustomerIDOrderID 是类型别名,可以提高代码的可读性。尽管类型提示不会强制执行类型检查,但它们为开发者和工具提供了更多信息,使得代码更易于理解、维护和分析。类型提示通常由 IDE、静态分析工具和类型检查器(如 mypy)用于提供更好的开发体验和更早地发现潜在的错误。

typing 模块介绍

typing 模块的主要作用是为 Python 引入类型提示,它允许开发者在代码中提供关于变量、函数参数、函数返回值等的类型信息。这种类型提示并不会影响程序的实际执行,但它为开发者和工具提供了更多的上下文信息,而主要作用有以下几个:

  1. 代码可读性: 类型提示可以让代码更加清晰和易读。通过阅读代码,开发者可以更容易地理解变量的类型、函数的参数和返回值,从而更好地理解代码的意图。

  2. 静态类型检查: 使用类型提示后,可以使用静态类型检查工具(例如 mypy)在开发阶段进行类型检查,捕获一些潜在的类型错误。这有助于减少在运行时由于类型问题导致的错误。

  3. 文档生成: 类型提示可以被用来生成文档,自动文档生成工具(如 Sphinx)能够根据类型提示自动生成文档,进一步提高文档的准确性和可维护性。

  4. 提高开发工具的智能提示: 集成开发环境(IDE)可以利用类型提示提供更准确的代码补全和智能提示,使开发者更加高效。

  5. **功能增强:**用于增强 Type Hints 的功能,使得类型提示更加灵活和表达力更强。typing 模块中的类型工具可以用于构建更复杂的类型结构

from typing import List, Tuple, Optional

def 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之后,listdict均支持泛型类型: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'] = 300
    return test_dict

if __name__ == "__main__":
    dict_one = {'one': 100, 'two': 200}
    temp_dict = get_test_dict_info(test_dict=dict_one)
    print(temp_dict)

在编写过程中,你会发现有以下情况:

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第1张图片

它会提示我们参数类型为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'] = 300
    return test_dict

if __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 ClassOne

class 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第一个类,在第二个类的方法中使用其对象,我们在编写时会发现以下问题:

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第2张图片

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第3张图片

它有时候无法获取到我们的方法或者查找到许多部分名称相同的方法,尤其是在调用复杂时,此时我们可以像为变量注解一样来添加类的注解:

object: ClassName

我们以修改第二个类的代码为例:

from class_one import ClassOne

class 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对此将会终止脚本运行:

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第4张图片

首先先讲一个细节,注解内容是字符串时,注解语法同样也是生效的,原因见后文的annotations

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第5张图片

明确这点后,在导入时使用以下语句进行处理,并且依次简要介绍其功能:

from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from class_two import ClassTwo

__future__实现向后兼容语言特性

A future statement, from __future__ import , 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:
    pass

print(example.__annotations__)
# 输出: {'a': , 'b': , 'return': }

在这个例子中,__annotations__ 字典包含了参数 ab 以及返回值的类型信息。这对于在运行时访问这些信息或者通过代码分析工具来检查和分析代码非常有用。然而,如果在使用 from __future__ import annotations 语句的情况下,__annotations__ 的行为会有所不同。在这种情况下,__annotations__ 不再包含字符串形式的类型注解,而是直接包含解释后的类型信息。这使得在代码中不再需要使用字符串引号来表示类型,而直接使用类型符号。

from __future__ import annotations

def example(a: int, b: str) -> float:
    pass

print(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': }

TYPE_CHECKING解决循环引入问题

TYPE_CHECKINGtyping 模块中的一个特殊变量,用于解决在类型注解中避免循环导入问题的情况。

在 Python 中,当两个模块相互导入时,可能会导致循环导入的问题,其中一个模块引用了另一个模块,而另一个模块又引用了第一个模块,从而形成了循环。这可能会导致在运行时出现 ImportError。

为了解决这个问题,typing 模块中引入了一个特殊的变量 TYPE_CHECKING。这个变量在运行时始终为 False,但在类型检查工具(如 mypy)运行时为 True。因此,可以使用 TYPE_CHECKING 变量在类型注解中创建条件导入,从而避免循环导入问题。

示例:

# module_a.py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from module_b import MyClassB

class MyClassA:
    def __init__(self, instance_b: 'MyClassB') -> None:
        self.instance_b = instance_b
# module_b.py
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from module_a import MyClassA

class MyClassB:
    def __init__(self, instance_a: 'MyClassA') -> None:
        self.instance_a = instance_a

在这个例子中,TYPE_CHECKING 被用于创建条件导入。当运行时 TYPE_CHECKINGFalse,因此实际的导入语句不会执行,而在类型检查时,TYPE_CHECKINGTrue,导入语句会被执行,从而避免了循环导入问题。这种技术特别在涉及复杂类之间的相互引用和类型注解的情况下有用。

常量注解Final

Fianl_value_name: Final[type]

示例如下,记得导入Final

from typing import Final

FLAG: Final[int] = 233

但是,并不会阻止我们修改,改了也能够正常运行:

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第6张图片

阻止继承装饰器final

使用@final装饰器来阻止继承注解:

class BaseClass:
    def __init__(self) -> None:
        self.greeting_str = 'Hello World'

    @final
    def show_greeting_str(self) -> None:
        print(self.greeting_str)

与上面一致,仅仅只是声明,并不会妨碍运行:

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第7张图片

完整代码如下:

from typing import final


class BaseClass:
    def __init__(self) -> None:
        self.greeting_str = 'Hello World'

    @final
    def 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 overload

class Shape:
    def draw(self):
        print("Drawing a shape")

class Circle(Shape):
    @overload
    def draw(self, color: str) -> None:
        pass

    def draw(self, color: str = "red"):
        print(f"Drawing a {color} circle")

if __name__ == "__main__":
    circle = Circle()
    circle.draw()             # 输出: Drawing a red circle
    circle.draw("blue")       # 输出: Drawing a blue circle

重写装饰器override

区分两者:

  • overload 装饰器用于标记方法的多个版本,但真正的重载需要在实现中手动处理。
  • override 注解用于确保子类中的方法正确地覆盖了父类中的方法。
from typing import override


class BaseClass:
    def occupy_function(self) -> None:
        pass

class SubClass(BaseClass):
    @override
    def 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 cast

def example_function() -> str:
    flag = None
    flag = cast(str, flag)
    return flag

print(example_function())

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第8张图片

下面是一些使用场景:

  1. 类型推断不准确的情况:

    from typing import List, cast
    
    def process_data(data: List[str]) -> None:
        # 在某些情况下,类型检查器无法正确推断 data 的类型
        # 使用 cast 进行强制类型转换,告诉类型检查器 data 确实是 List[str]
        processed_data = cast(List[str], data)
        # 这样可以避免类型检查器的警告
        for item in processed_data:
            print(item)
    
  2. 处理动态属性或方法:

    from typing import cast
    
    class CustomObject:
        def __getattr__(self, name: str) -> int:
            # 在这里,类型检查器无法确定 getattr 返回的是什么类型
            result = cast(int, getattr(self, name))
            return result + 10
    
  3. 处理 JSON 数据:

    from typing import Any, Dict, cast
    
    def 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 Optional

class InfoClass:
    pass

class 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 Union


class InfoClass:
    pass

class TestClass:
    def __init__(self, object: Union[InfoClass, None]) -> None:
        self.content: Union[InfoClass, None] = None

使用 | 进行可选注解(Python3.10+)

还有一种新版本的书写方法(Python3.10+):

class InfoClass:
    pass

class 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 the TypeVar-based approach specified in PEP 484, but is more concise and easier to follow.

Common use cases include alternative constructors provided as classmethods, and __enter__() methods that return self:

class MyLock:
 def __enter__(self) -> Self:
     self.lock()
     return self
 ...

class MyInt:
 @classmethod
 def 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 = value

    def add(self, x: float) -> 'Calculator':
        self.value += x
        return self  # 返回方法本身,以支持链式调用

    def subtract(self, x: float) -> 'Calculator':
        self.value -= x
        return self  # 返回方法本身,以支持链式调用

    def multiply(self, x: float) -> 'Calculator':
        self.value *= x
        return self  # 返回方法本身,以支持链式调用

    def divide(self, x: float) -> 'Calculator':
        if x != 0:
            self.value /= x
        else:
            print("Error: Division by zero.")
        return self  # 返回方法本身,以支持链式调用


if __name__ == "__main__":
    # 使用链式调用
    result = Calculator(10).add(5).subtract(3).multiply(2).divide(4).value
    print(result)  # 输出: 6.0

    # 打印类型和检查实例
    print(type(Calculator(10).add(5)))  # 输出: 
    print(isinstance(Calculator(10).add(5), Calculator))  # 输出: True 

在上述示例中,Calculator 类有四个方法:addsubtractmultiplydivide。每个方法在完成相应的操作后都返回 self,这样就可以在方法调用之后继续调用其他方法。

通过创建 Calculator 对象并进行链式调用,可以一步步地执行一系列数学操作,并且最终通过访问 value 属性获取结果。这种模式使代码看起来更加流畅和易于理解。

要注意的是,返回方法本身并进行链式调用的做法是一种设计选择,并不是所有类都需要这样做。在某些情况下,这可能使代码更简洁,但在其他情况下,可能会降低代码的可读性

使用Self注解方式示例

from typing import Self

class Calculator:
    def __init__(self, value: float = 0) -> None:
        self.value = value

    def add(self, x: float) -> Self:
        self.value += x
        return self  

    def subtract(self, x: float) -> Self:
        self.value -= x
        return self  

    def multiply(self, x: float) -> Self:
        self.value *= x
        return self  

    def divide(self, x: float) -> Self:
        if x != 0:
            self.value /= x
        else:
            print("Error: Division by zero.")
        return self  

为何使用Self注解

试想上述代码中,我们修改了类的签名,而注解是字符串,就会引起连续报错,不过现在的编辑器较为智能,比如PyCharm就会自动为你修改,所以我们推荐使用Self注解,不过还有其他应对方法,例如from __future__ import annotations

from __future__ import annotations

class Calculator:
    def __init__(self, value: float = 0) -> None:
        self.value = value

    def add(self, x: float) -> Calculator:
        self.value += x
        return self  # 返回方法本身,以支持链式调用

    def subtract(self, x: float) -> Calculator:
        self.value -= x
        return self  # 返回方法本身,以支持链式调用

    def multiply(self, x: float) -> Calculator:
        self.value *= x
        return self  # 返回方法本身,以支持链式调用

    def divide(self, x: float) -> Calculator:
        if x != 0:
            self.value /= x
        else:
            print("Error: Division by zero.")
        return self  # 返回方法本身,以支持链式调用

或者官方文档中的:

from typing import TypeVar

TShape = TypeVar("TShape", bound="Shape")

class Shape:
 def set_scale(self: TShape, scale: float) -> TShape:
     self.scale = scale
     return self


class Circle(Shape):
 def set_radius(self, radius: float) -> Circle:
     self.radius = radius
     return self

Circle().set_scale(0.5).set_radius(2.7)  # => Circle

现在来看用泛型语法注解还是较为复杂,不如Self直观,不太推荐使用

ClassVar类属性注解

在 Python 类型注解中,ClassVar 是一个用于表示类属性的注解。类属性是属于类而不是实例的属性。使用 ClassVar 注解可以更明确地指定一个变量是类属性,并且该注解只能接收一个类型。

下面是一个使用 ClassVar 注解的示例:

from typing import ClassVar

class MyClass:
    class_attribute: ClassVar[int] = 42

    def __init__(self, instance_attribute: str) -> None:
        self.instance_attribute: str = instance_attribute


if __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 Literal

def colorize(is_color: Literal['red', 'green', 'blue']) -> str:
    return is_color


if __name__ == "__main__":
    color_type: str = colorize("red")

需要注意的是,Literal 是在 Python 3.8 及以后版本的 typing 模块中引入的,如果你使用的是更早版本的 Python,可能需要考虑其他的方式来达到类似的效果。

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第9张图片

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, List

T = TypeVar('T')

def repeat_item(item: T, n: int) -> List[T]:
    return [item] * n

TAny类型,接收任意类型参数

协变泛型注解

所谓协变泛型实质上是一种划定了上界的泛型,TypeVar 中的 bound 参数用于指定类型变量的上界(bound),表示类型变量可以是哪些类型的子类型。上界提供了对类型变量进行限制的机制,使得在使用该类型变量时,其实际类型必须符合指定的上界条件。

from typing import TypeVar

T = TypeVar('T', bound=SomeType)
  • T:类型变量的名称。

  • bound=SomeType:指定类型变量 T 的上界为 SomeTypeSomeType 可以是一个具体的类型,也可以是一个泛型类型。

  • 限制类型范围: 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, Union

T = 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)

  • 当一个类型的子类型的顺序与原始类型的子类型的顺序相同时,我们称之为协变。
  • 在协变中,如果 AB 的子类型,那么 List[A] 就是 List[B] 的子类型。

示例:

class Animal:
    pass

class Dog(Animal):
    pass

animals: List[Animal] = [Dog(), Animal()]  # 协变

在这个例子中,List[Dog]List[Animal] 的子类型,因为DogAnimal的子类型。

逆变(Contravariance)

  • 当一个类型的子类型的顺序与原始类型的子类型的顺序相反时,我们称之为逆变。
  • 在逆变中,如果 AB 的子类型,那么 Callable[B] 就是 Callable[A] 的子类型。

示例:

class Animal:
    pass

class Dog(Animal):
    pass

from typing import Callable

def 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, " + name

def farewell(name):
    return "Goodbye, " + name

def 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, TypeVar

T = TypeVar('T')

class Box(Generic[T]):
    def __init__(self, content: T) -> None:
        self.content = content

    def get_content(self) -> T:
        return self.content


if __name__ == "__main__":

    int_box: Box[int] = Box(42)
    str_box: Box[str] = Box("Hello, Generics!")

    # 获取并打印内容
    print("Integer Box Content:", int_box.get_content())  # 输出:42
    print("String Box Content:", str_box.get_content())   # 输出:Hello, Generics!

多泛型参数实现

from typing import Generic, TypeVar

T = TypeVar('T')
U = TypeVar('U')

class Pair(Generic[T, U]):
    def __init__(self, first: T, second: U) -> None:
        self.first: T = first
        self.second: U = second


if __name__ == "__main__":

    int_str_pair: Pair[int, str] = Pair(42, "Hello, Generics!")

    # 获取并打印内容
    print("First Element:", int_str_pair.first)   # 输出:42
    print("Second Element:", int_str_pair.second)  # 输出:Hello, Generics!

多继承泛型实现

from typing import Generic, TypeVar, List

T = 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] = items
        self.metadata: U = metadata

class 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 = content


if __name__ == "__main__":

    int_str_box: Box[int, str] = Box("233hhh", [1, 2, 3, 4, 5], "Box Metadata")

    # 获取并打印内容
    print("Box Content:", int_str_box.content)          # 输出:233hhh
    print("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, ClassVar

class 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 = name

    def 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 = name
        self.breed = breed

    def 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 property
    print(f"{cat.name} is a cat")

    # Dog-specific property
    print(f"{dog.name} is a {dog.breed} dog")

    # Common behavior
    cat.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 TypedDict

class Person(TypedDict):
    name: str
    age: int
    email: str

在这个例子中,Person 是一个 TypedDict,指定了三个键:nameageemail,以及它们对应的值类型。这允许你创建一个具有这些键的字典并进行类型检查:

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第10张图片

如果尝试为键分配错误类型的值,类型检查器(如 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=True
 title: str
 year: NotRequired[int]

It is an error to use Required[] or NotRequired[] 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, NotRequired

class 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, Unpack

class Person(TypedDict):
    name: str
    age: int
    email: str

def occupy_function(**kwargs: Unpack[Person]) -> None:
    pass

if __name__ == "__main__":
    occupy_function(name="233", age=18, email="[email protected]")

在编辑时我们会得到以下提示:

【Python编程-二万字长文浅析-使用Type Hints与Typing模块提高代码可维护性_第11张图片

你可能感兴趣的:(Python,python,开发语言)