类和对象的进阶

本篇文章探讨一下python中的关于类和对象的几个进阶概念:鸭子类型 、 抽象基类 、协议。了解这些概念对后面语言的深入学习大有裨益。

这一篇内容比较难以理解,需要多次反复的看和学习。

鸭子类型

鸭子类型的起源

鸭子类型起源于美国印第安纳州的诗人詹姆斯·惠特科姆·莱利(James Whitcomb Riley,1849-1916)的诗句,原话翻译过来是这样讲的:

『当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。』

一位名叫Alex Martelli的意大利软件工程师(同时也是Python软件基金会研究员),在2000年左右最早将这个概念引入到程序设计范畴中,之后这个概念在现代编程语言中被广泛推广。

鸭子类型的定义

鸭子类型(英语:duck typing)在程序设计中是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由“当前方法和属性的集合”决定

在鸭子类型中,关注点在于对象的行为能做什么;而不是关注对象所属的类型。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的"走"和"叫"方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名

鸭子类型通常得益于"不"测试方法和函数中参数的类型,而是依赖文档、清晰的代码和测试来确保正确使用

在常规类型中,我们能否在一个特定场景中使用某个对象取决于这个对象的类型,而在鸭子类型中,则取决于这个对象是否具有某种属性或者方法——即只要具备特定的属性或方法,能通过鸭子测试,就可以使用。

多态

具体体现在编程语言上,就是多态的实现方式。

多态性指的是可以在不用考虑对象具体类型的情况下而直接使用对象,这就需要在设计时,把对象的使用方法统一成一种。

对于C++、Java等语言而言,如果要实现多态,必须要通过继承关系,如:

#严格的基于继承的多态
class Duck():
    def say(self):
        pass

    def run(self):
        pass

class Drake(Duck):
    def say(self):
        print('Drake say: Quack!Quack!')

    def run(self):
        print('Drake run:Swing!')

class Swan(Duck):
    def say(self):
        print('Swan say: Quack!Quack!')

    def run(self):
        print('Swan run:Swing!')

def duckDo(duck):
    duck.say()
    duck.run()

ducks = [Drake(),Swan()]

duckDo(ducks[0])
duckDo(ducks[1])

Drake和Swan必须继承Duck,duckDo的输入参数是Duck,Duck的子类型才能传入。

但是Python中支持的鸭子类型不需要这种继承关系,只需要Drake和Swan有say()和run()方法即可。

#基于协议的鸭子类型
class Drake():
    def say(self):
        print('Drake say: Quack!Quack!')

    def run(self):
        print('Drake run:Swing!')

class Swan():
    def say(self):
        print('Swan say: Quack!Quack!')

    def run(self):
        print('Swan run:Swing!')

def duckDo(duck):
    duck.say()
    duck.run()

ducks = [Drake(),Swan()]

duckDo(ducks[0])
duckDo(ducks[1])

 鸭子类型( duck typing )语言使用鸭子测试来评估对象是否可以被解析为特定的类型。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。

在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。

鸭子类型无需使用 type() 或 isinstance() 进行检查(注意,它通常使用 hasattr() 来检查。

# 定义一个计算函数,接收3个入参
def calculate(a, b, c):
    return (a + b) * c


# 分别计算3种情况的结果
result1 = calculate(1, 2, 3)
result2 = calculate([1, 2, 3], [4, 5, 6], 2)
result3 = calculate("打工人", "打工魂", 3)

# 打印3种结果
print(result1, result2, result3, sep='\n’)

‘’’
9
[1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6]
打工人打工魂打工人打工魂打工人打工魂
’’’


# 鸭子类
class Duck:

    def quack(self):
        print("这鸭子正在嘎嘎叫")

    def feathers(self):
        print("这鸭子拥有白色和灰色的羽毛")

# 人类
class Person:

    def quack(self):
        print("这人正在模仿鸭子")

    def feathers(self):
        print("这人在地上拿起1根羽毛然后给其他人看")


# 函数/接口
def in_the_forest(duck):
    duck.quack()
    duck.feathers()


if __name__ == '__main__':

    donald = Duck()  # 创建一个Duck类的实例
    john = Person()  # 创建一个Person类的实例

    in_the_forest(donald)  # 调用函数,传入Duck的实例
    in_the_forest(john)    # 调用函数,传入Person的实例

具体在python中鸭子类型的体现如下面的代码所示:

class CollectionClass():
  lists = [1,2,3,4]
  def __getitem__(self, index):
    return self.lists[index]

iter_able_object = CollectionClass()

class Another_iterAbleClass():
  lists=[1,2,3,4]
  list_position = -1

  def __iter__(self):
    return self

  def next(self): #还有更简单的实现,使用生成器或迭代器什么的:)
    self.list_position += 1
    if self.list_position >3:
      raise StopIteration
    return self.lists[self.list_position]

another_iterable_object=Another_iterAbleClass()

print(iter_able_object[1])
print(iter_able_object[1:3])
output>>
2
[2, 3]

another_iterable_object[2]
output>>
Traceback (most recent call last):
 File "/Users/steinliber/a.py", line 32, in 
  another_iterable_object[2]
TypeError: 'Another_iterAbleClass' object does not support indexing

print(next(another_iterable_object))
output>>
1
print(next(another_iterable_object))
output>>
2

print(next(iter_able_object))
output>>
Traceback (most recent call last):
 File "/Users/steinliber/a.py", line 29, in 
  print(next(iter_able_object))
TypeError: IterAbleClass object is not an iterator

从上面两个类来说,第一个类实现了__getitem__方法,那python的解释器就会把它当做一个collection,就可以在这个类的对象上使用切片,获取子项等方法,第二个类实现了__iter__next方法,python就会认为它是一个iterator,就可以在这个类的对象上通过循环来获取各个子项。一个类可以实现它有能力实现的方法,并只能被用于在它有意义的情况下。

这两个类和上面的鸭子类相比较,其实用于切边的[](它其实调用的是python的slice函数)和用于循环的iter()就相当于watch_duck函数,这些函数都接收任意类的对象,并调用实现功能所需要的对象中的方法来实现功能,若该函数中调用的方法对象里面不存在,就报错。

从上面可以看出,python鸭子类型的灵活性在于它关注的是这个所调用的对象是如何被使用的,而没有关注对象类型的本身是什么。所以在python中使用isinstance来判断传入参数的类型是不提倡的,更pythonic的方法是直接使用传入的参数,通过try,except来处理传入参数不符合要求的情况。我们应该通过传入对象的能力而不是传入对象的类型来使用该对象。

collections里面有一个类叫Iterable,类中有一个叫__iter__的方法。

from typing import Iterable

a = [1, 2, 3]
print(type(a))
print(isinstance(a, Iterable))
print(type(a).mro())

# 输出:
# 
# True
# [, ]

可以看到我们定义了一个变量a,这个a是一个list类型,但是我们在执行isinstance(a, Iterable)的时候发现结果为True,表明aIterable类的一个实例,但是当我们去看list类的定义的时候(如下图),就可以发现,list类并没有继承Iterable类,那么是什么让结果为True的呢?

我现在定义一个Family类,如下:

from typing import Iterable
class Family(object):
    def __init__(self, family_list):
        self.family_list = family_list

    def __iter__(self):
        return iter(self.family_list)

family = Family(["father", "mother", "son"])
print(isinstance(family, Iterable))
print(type(family).mro())

‘’'
True
[, ]
‘''

我们发现一点,这个isinstance(family, Iterable)执行的结果也是True,那么代表着family也是Iterable类的一个实例。

通过观察,我们可以发现list类和Family类都实现了Iterable类中的__iter__方法,这个正是结果为True的关键。

判断是否为Iterable类似下面的代码:

def isiterable(obj):
    try:
        iter(obj)
        return True
    except TypeError: # not iterable
        return False

hasattr() 

hasattr() 函数可以检测一个对象有没有指定的属性,在鸭子类型中经常用来检查对象是否实现了响应的方法。

语法如下:

hasattr(obj, name, /)

参数:

  • obj:要操作的对象,类或者实例
  • name:属性名,是一个字符串

该实参是一个对象和一个字符串。如果字符串是对象的属性之一的名称,则返回 True,否则返回 False。

此功能是通过调用 getattr(object, name) 看是否有 AttributeError 异常来实现的。

def parse(text):
    if hasattr(text, 'read'):
        text = text.read()

    # Parse the text here...

 

def func(something):
    if hasattr(something, '__getitem__'):	
        print(something[0])
    else:
        print("Error")

抽象基类

鸭子类型给了编程充分的灵活性,但在一些场景也容易造成混乱,有时我们需要严格要求子类遵循一定的接口规范,Python中也给出了解决方案。 

Python中也有如C++、Java一样的抽象基类的概念。

抽象基类(Abstract Base Class,简称ABC)是一种特殊的类,它不能被实例化,只能被其他类继承。抽象基类可以定义一些抽象方法,这些方法在基类中没有实现(或者只有部分实现),必须由子类提供实现。

抽象基类的定义

要定义一个抽象基类,我们需要使用 abc 模块中的 ABC 类和 abstractmethod 装饰器。以下是一个简单的示例:

from abc import ABC, abstractmethod

class AbstractAnimal(ABC):
    @abstractmethod
    def make_sound(self):
        pass

class Dog(AbstractAnimal):
    def make_sound(self):
        return "Woof!"

在这个例子中,AbstractAnimal 是一个抽象基类,它定义了一个抽象方法 make_soundDog 是一个具体的类,它继承自 AbstractAnimal 并提供了 make_sound 方法的实现。

如果我们尝试创建 AbstractAnimal 的实例,Python将抛出一个错误,因为抽象基类不能被实例化:

animal = AbstractAnimal()  # Raises TypeError: Can't instantiate abstract class AbstractAnimal with abstract methods make_sound

如果我们定义一个类继承自抽象基类,但没有提供所有抽象方法的实现,Python也会抛出一个错误,因为所有抽象方法必须由子类提供实现:

class Cat(AbstractAnimal):
    pass

cat = Cat()  # Raises TypeError: Can't instantiate abstract class Cat with abstract methods make_sound

抽象基类是一种强大的工具,可以帮助我们定义接口并确保子类遵循这些接口。这使得我们的代码更加稳健,更容易理解和维护。

抽象基类的要点:

  • 继承abc.ABC
  • 使用@abc.abstractmethod装饰器标记抽象方法
  • 抽象基类也可以包含普通方法
  • 抽象基类的子类必须覆盖抽象方法(普通方法可以不覆盖),可以使用super()函数调用抽象方法,为它添加功能,而不是从头开始实现

class abc.ABC

一个使用 ABCMeta 作为元类的工具类。抽象基类可以通过从 ABC 派生来简单地创建,这就避免了在某些情况下会令人混淆的元类用法,例如:

from abc import ABC

class MyABC(ABC):
    pass

注意 ABC 的类型仍然是 ABCMeta,因此继承 ABC 仍然需要关注元类使用中的注意事项,比如可能会导致元类冲突的多重继承。当然你也可以直接使用 ABCMeta 作为元类来定义抽象基类,例如:

from abc import ABCMeta

class MyABC(metaclass=ABCMeta):
    pass

@abc.abstractmethod

用于声明抽象方法的装饰器

使用此装饰器要求类的元类是 ABCMeta 或是从该类派生。一个具有派生自 ABCMeta 的元类的类不可以被实例化,除非它全部的抽象方法和特征属性均已被重载。抽象方法可通过任何普通的“super”调用机制来调用。 abstractmethod() 可被用于声明特性属性和描述器的抽象方法。

class C(ABC):
    @abstractmethod
    def my_abstract_method(self, arg1):
        ...
    @classmethod
    @abstractmethod
    def my_abstract_classmethod(cls, arg2):
        ...
    @staticmethod
    @abstractmethod
    def my_abstract_staticmethod(arg3):
        ...

    @property
    @abstractmethod
    def my_abstract_property(self):
        ...
    @my_abstract_property.setter
    @abstractmethod
    def my_abstract_property(self, val):
        ...

    @abstractmethod
    def _get_x(self):
        ...
    @abstractmethod
    def _set_x(self, val):
        ...
    x = property(_get_x, _set_x)

class abc.ABCMeta

用于定义抽象基类(ABC)的元类。

使用该元类以创建抽象基类。抽象基类可以像 mix-in 类一样直接被子类继承。你也可以将不相关的具体类(包括内建类)和抽象基类注册为“抽象子类” —— 这些类以及它们的子类会被内建函数 issubclass() 识别为对应的抽象基类的子类,但是该抽象基类不会出现在其 MRO(Method Resolution Order,方法解析顺序)中,抽象基类中实现的方法也不可调用(即使通过 super() 调用也不行)。

使用 ABCMeta 作为元类创建的类含有如下方法:

ABCMeta.register(subclass)

将“子类”注册为该抽象基类的“抽象子类”,或者说可以将“抽象基类”注册为指定类型的“父类”。

比如collections.abc模块的源码中,把内置类型tuplestrrangememoryview注册为Sequence的虚拟子类:

Sequence.register(tuple) 
Sequence.register(str) 
Sequence.register(range) 
Sequence.register(memoryview) 

例如:

import abc

class MyABC(abc.ABC):
    @abc.abstractmethod
    def foo(self):
        pass

print('is tupple MyABC:', isinstance(tuple, MyABC))
print("is tupple MyABC's subclass:", issubclass(tuple, MyABC))
MyABC.register(tuple)
print('is tupple MyABC:', isinstance(tuple, MyABC))
print("is tupple MyABC's subclass:", issubclass(tuple, MyABC))

‘’'
is tupple MyABC: False
is tupple MyABC's subclass: False
is tupple MyABC: False
is tupple MyABC's subclass: True
‘''

也可以使用register装饰器来进行注册:

import abc

class MyABC(abc.ABC):
    @abc.abstractmethod
    def foo(self):
        pass

@MyABC.register
class MyFoo():
    def foo(self):
        print('foo.print()')

def checktype(o):
    if isinstance(o, MyABC) :
        o.foo()
    else:
        print('o is not MyABC!')

o = MyFoo()
checktype(o)

‘’'
foo.print()
‘''

__subclasshook__(subclass)

实现的目的与rigister一样,但更加灵活。

检查 subclass 是否是该抽象基类的子类。也就是说对于那些你希望定义为该抽象基类的子类的类,你不用对每个类都调用 register() 方法了,而是可以直接自定义 issubclass 的行为。(这个类方法是在抽象基类的 __subclasscheck__() 方法中调用的。)

该方法必须返回 TrueFalse 或是 NotImplemented。如果返回 Truesubclass 就会被认为是这个抽象基类的子类。如果返回 False,无论正常情况是否应该认为是其子类,统一视为不是。如果返回 NotImplemented,子类检查会按照正常机制继续执行。

为了对这些概念做一演示,请看以下定义 ABC 的示例:

class Foo:
    def __getitem__(self, index):
        ...
    def __len__(self):
        ...
    def get_iterator(self):
        return iter(self)

class MyIterable(ABC):

    @abstractmethod
    def __iter__(self):
        while False:
            yield None

    def get_iterator(self):
        return self.__iter__()

    @classmethod
    def __subclasshook__(cls, C):
        if cls is MyIterable:
            if any("__iter__" in B.__dict__ for B in C.__mro__):
                return True
        return NotImplemented

MyIterable.register(Foo)

ABC MyIterable 定义了标准的迭代方法 __iter__() 作为一个抽象方法。这里给出的实现仍可在子类中被调用。get_iterator() 方法也是 MyIterable 抽象基类的一部分,但它并非必须被非抽象派生类所重载。

这里定义的 __subclasshook__() 类方法指明了任何在其 __dict__ (或在其通过 __mro__ 列表访问的基类) 中具有 __iter__() 方法的类也都会被视为 MyIterable。

最后,末尾行使得 Foo 成为 MyIterable 的一个虚子类,即便它并没有定义 __iter__() 方法(它使用了以 __len__() 和 __getitem__() 术语定义的旧式可迭代对象协议)。注意这将不会使 get_iterator 成为对 Foo 来说可用的方法,所以也另外提供了一个它。

标准库中的基类

Python的collections模块提供了一些抽象基类。

Iterable提供了__iter__方法以支持迭代,Container提供了__contains__方法以支持in运算符,Sized提供了__len__方法以支持len()方法。

  • Collection

Collection抽象基类继承自Iterable、Container和Sized,整合了三个基类需要实现的协议,实际上都是继承自Collection,而不是直接继承自Iterable、Container和Sized。

  • Sequence、Mapping和Set

这三个类是不可变集合。分别实现了三类常用类型,并且这三类均有可变的子类 —— MutableSequence、MutableMapping以及MutableSet。

  • MappingView

视图类主要是创建一个动态随源数据改变而改变的数据结构。MappingView有三个重要的子类,其分别是ItemsView、KeysView以及ValuesView。映射方法.items()、.keys()以及.values()分别返回这三个类的实例。其中,ItemsView和KeysView还从set类继承了大量方法,ValuesView则仅另外继承自Collection。

  • Callable和Hashable

这两个抽象基类相当的“孤立”,一方面既不继承自其他类,另一方面也很少被其他类继承。Callable类提供了__call__方法,Hashable类提供了__hash__方法。在使用中通常不会主动声明继承自这两个类以支持相应的方法,只要实现相应的方法,Python就能够将自定义的类识别为对应抽象基类的子类。例如对于Callable,一个自定义类只要实现了__call__方法,就能够通过isinstance的类型检查。这一特点源于__subclasshook__方法,__subclasscheck__方法会调用__subclasshook__方法进行子类检查。

class TestCLS:

    def __len__(self):
        return 1

    def __call__(self):
        return self.__len__()


from collections.abc import Sized, Callable

print("TestCLS类是否是Sized的子类?", isinstance(TestCLS(), Sized))
print("TestCLS类是否是Callable的子类?", isinstance(TestCLS(), Callable))

  • Iterator

Iterator继承自Iterable,在Iterable的基础上,Iterator添加了__next__方法。

类型注解和提示

Python是动态类型的语言,在行时不强制要求函数与变量类型,但Python提供了类型注解,可以用于类型检查器、IDE、错误检查器等第三方工具进行类型检查和错误提示。

为什么要做类型注解

随着项目越来越大,代码也就会越来越多,在这种情况下,如果没有类型注解,我们很容易不记得某一个方法的入参类型是什么,一旦传入了错误类型的参数,再加上python是解释性语言,只有运行时候才能发现问题, 这对大型项目来说是一个巨大的灾难。

类型注解的作用

自python3.5开始,PEP484为python引入了类型注解(type hints) 机制。 主要作用如下:

  • 类型检查,防止运行时出现参数和返回值类型、变量类型不符合。
  • 作为开发文档附加说明,方便使用者调用时传入和返回参数类型。
  • 该模块加入后并不会影响程序的运行,不会报正式的错误,只有提醒。pycharm目前支持typing检查,参数类型错误会黄色提示

因为类型注释的检查是IDE来完成的,所以不同的IDE有不同的方式。PyCharm完全支持类型注解。

类型注解的用法

变量定义的注解

在定义一个变量时,直接在后面 : 类型 就可以给这个变量指定一个类型,后面可以继续写赋值语句 = 1,不写赋值语句也不会出错

# 声明变量时使用
num: int = 1
str1: str
    

# 函数参数也可以使用
def test(num: int = 1):
    return num

函数返回值的类型注解

在函数的冒号之前, -> 可以指定函数的返回值类型,注意,None 是一种类型提示特例

def test(num: int = 1) -> int:  # -> 可以指明函数的返回值类型
    return num


def test(num: int = 1) -> None:  # -> 可以指明函数的返回值类型
    return None

查看函数注解

通过 __annotations__ 查看一个函数的注解

def test(a: str, b: int = 2) -> None:
    pass

print(test.__annotations__)


#打印结果: {'a': , 'b': , 'return': None}

list、tuple类型的注解

基本注解

与简单类型相似:

def test_list(a:list, b:tuple):
    print(int(a)) #提示类型错误
    return {b:a}

test_list([1,2,3], [4,5,6]) #提示类型错误

在一些代码中会见到List、Tuple、Dict,大写的ListTupleDict与原来的有什么区别的?

from typing import List, Tuple, Dict
def test_list(a:List, b:Tuple) ->Dict:
    print(a,b)
    return {b: a}

l1 = list([1,2,3])
t1 = tuple((4,5,6))
d1 = test_list(l1, t1)  #并不提示类型错误

print(type(List), type(list)) #两个类型并不相同
print(List.mro()) #父类是list

print(type(Tuple), type(tuple)) #两个类型并不相同
print(Tuple.mro()) #父类是tuple

print(type(Dict), type(dict)) #两个类型并不相同
print(Dict.mro()) #父类是dict

‘’'
[1, 2, 3] (4, 5, 6)
 
[, ]
 
[, ]
 
[, ]
‘''

typing.List是class 'list'的子类

元素注解

很多时候,我们需要指定的参数不仅仅是list与tuple,还要包括他们的元素类型。

def test_list(a:list[int], b:tuple[str,int]):
    print(a,b)
    return {b:a}

test_list([1,2,3], ('a',4)) 
test_list([1,2,3], (4,'a')) #提示类型错误

注意,元素的类型只能使用中括号括起来,不能使用其他的。

list与tuple互用

有时候希望接受list和tuple参数都行,这样就要用到它们共同的基类Sequence,但也有个问题,就是dict类型也可以传入,并且没有类型错误的提示。

from typing import Sequence
def test_list(lo:Sequence[int]):
    return sum(lo)

test_list([1,2,3,4])
test_list((1,2,3,4))
test_list({1:'a', 2:'b'})
# test_list(['1','2','3']) #提示类型错误

dict类型的注解

dict的元素类型注解,好像在pycharm里没有作用,可能是pycharm的bug:

def test_dict(d1: dict[str:int]) -> int:
    total = 0
    for value in d1.values():
        total += value

    return total


test_dict({'a': 1, 'b': 2})
test_dict({1: 'a', 2: 'b'})

自定义类型

基本注解

class Dog:
    def speak(self) -> str:
        return "Woof!"

class Cat:
    def speak(self) -> str:
        return "Meow!"

def make_sound(d: Dog) -> str:
    s = d.speak()
    print(s)
    return s

make_sound(Dog())
make_sound(Cat()) #提示类型错误,可以运行
make_sound(Dog) #提示类型错误,运行会出错,它的类型是Type[Dog]

方法的类型为自己

class Dog:
    def speak(self) -> str:
        return "Woof!"

    def change(self, other:Dog): #提示类型错误
        return "Change"

这个例子中类初始化的时候传入的参数类型为自己,这就是一个“鸡生蛋,蛋生鸡”的问题,所以IDE的判断就会出问题。这时候,需要将类型转为字符串的方式传入。

class Dog:
    def speak(self) -> str:
        return "Woof!"

    def change(self, other:'Dog'):
        return "Change:" + other.speak()

d1 = Dog()
d2 = Dog()
print(d2.change(d1))

#Change:Woof!

其他一些特殊用法

使用Union

在很多函数中,我们都有多个入参, 对于这些入参我们都会想要设置一个或多个默认值, 默认值的一种常见情况就是None,但是None是一个独立的类型,需要一个参数有两种类型,Union可以做这种选择。

Union[X, Y] 等价于 X | Y ,意味着满足 X 或 Y 之一。

def make_sound(d: Dog) -> str:
    if d is None:
        return ''
    return d.speak()



d1 = None
make_sound(d1) #提示类型错误

使用Union后:

def make_sound(d: Union[Dog, None] = None) -> str:
    if d is None:
        return ''
    return d.speak()

d1 = None
make_sound(d1) 

其他需要多类型支持的也都可以使用Union。

要定义一个联合类型,可以使用类似 Union[int, str] 或简写 int | str,参数必须是某种类型,且至少有一个。

Union的嵌套会被扁平化:

Union[Union[int, str], float] == Union[int, str, float]

单参数之联合类型就是该参数自身:

Union[int] == int  # The constructor actually returns int

冗余的参数会被跳过,例如:

Union[int, str, int] == Union[int, str] == int | str

比较联合类型,不涉及参数顺序,例如:

Union[int, str] == Union[str, int]

不可创建 Union 的子类或实例,没有 Union[X][Y] 这种写法。

使用Optional

使用Union是可以正常工作的,但是如果仅仅是表示str类型或None类型,Union并不是特别的优雅,这种时候可以使用Optional。

Optional[X] 等价于 X | None (或 Union[X, None] ) 。

from typing import Optional


class Dog:
    def speak(self) -> str:
        return "Woof!"


def make_sound(d: Optional[Dog]) -> str:
    if d is None:
        return ''
    return d.speak()


d1 = None
make_sound(d1)

 为什么这样更优雅呢? 因为这样写不仅仅是变得简单了,更主要是因为很直观,看到这个Option你就知道这个参数可以不传入,或者传入你指定的非None类型。

使用Literal

Literal字面量类型,一种新的类型,不仅限制类型,还能限制取值,比如性别参数只能是男性和女性:

from typing import Literal

class Student:
    def __init__(self, name:str, gender:Literal['male', 'female']):
        self.name = name
        self.gender = gender

a = Student('John', 'male')
b = Student('Rose', 'woman') #提示类型错误

修改为下面的代码:

from typing import Literal

class Student:
    def __init__(self, name:str, gender:Literal['male', 'female']):
        self.name = name
        self.gender = gender

g = 'female'
a = Student('John', 'male')
b = Student('Rose', g) #提示类型错误

还是类型错误,因为g看上去是字符串类型,不是Literal类型,需要改成:

from typing import Literal

GenderType = Literal['male', 'female']


class Student:
    def __init__(self, name: str, gender: GenderType):
        self.name = name
        self.gender = gender


g: GenderType = 'female'
a = Student('John', 'male')
b = Student('Rose', g)  

这样写是清晰了,但也有些复杂!也可以写成这样:

from typing import Literal

class Student:
    def __init__(self, name: str, gender: Literal['male', 'female']):
        self.name = name
        self.gender = gender


g: Literal['male', 'female'] = 'female'
a = Student('John', 'male')
b = Student('Rose', g)  

使用NoReturn

函数不返回任何内容也是极其常见的,针对这种情况,我们使用NoReturn就比None更加的直观。

from typing import NoReturn


def error(errcode: int) -> NoReturn:
    if errcode == 0:
        pass
    else:
        raise ValueError

从程序的运行结果可以看到, 不管是None还是NoReturn,其实本质都一样。但是使用NoReturn就比None更容易理解,便于后期快速分析代码。

ClassVar

标记类变量的特殊类型构造器。

打包在 ClassVar 内的变量注解是指,给定属性应当用作类变量,而不应设置在类实例上。用法如下:

class Starship:
    stats: ClassVar[dict[str, int]] = {} # class variable
    damage: int = 10                     # instance variable

ClassVar 仅接受类型,也不能使用下标。

ClassVar 本身不是类,不应用于 isinstance() 或 issubclass()。ClassVar 不改变 Python 运行时行为,但可以用于第三方类型检查器。例如,类型检查器会认为以下代码有错:

enterprise_d = Starship(3000)
enterprise_d.stats = {} # Error, setting class variable on instance
Starship.stats = {}     # This is OK

Final

标记子类不能覆盖(overridden)

MAX_SIZE: Final = 9000
MAX_SIZE += 1  # Error reported by type checker

class Connection:
    TIMEOUT: Final[int] = 10

class FastConnector(Connection):
    TIMEOUT = 1  # Error reported by type checker

复杂返回对象

有时候返回的是一个tuple,也可以自定义返回类型:

from typing import Tuple, Optional

ReturnType = Tuple[int, Optional[str]]

def foo(a:int) -> ReturnType:
    if a == 0:
        return 0, None
    else:
        return 1, 'input a <> 0'


ret_code, errmsg = foo(1)
ret_code, errmsg = foo(0)

元组(tuple)的特殊类型注解

对于 Python 中的大多数容器,类型系统假定容器中的所有元素都是相同类型的。例如:

from collections.abc import Mapping

# Type checker will infer that all elements in ``x`` are meant to be ints
x: list[int] = []

# Type checker error: ``list`` only accepts a single type argument:
y: list[int, str] = [1, 'foo']

# Type checker will infer that all keys in ``z`` are meant to be strings,
# and that all values in ``z`` are meant to be either strings or ints
z: Mapping[str, str | int] = {}

list 只接受一个类型参数,因此类型检查程序会对上述对 y 的赋值报错。同样, Mapping 只接受两个类型参数:第一个参数表示键的类型,第二个参数表示值的类型。

然而,与大多数其它 Python 容器不同的是,在 Python 惯用代码中,元组中的元素并不都是相同类型的,这种情况很常见。因此,在 Python 的类型系统中,元组被特殊化了。tuple 接受*任意数量*的类型参数:

# OK: ``x`` is assigned to a tuple of length 1 where the sole element is an int
x: tuple[int] = (5,)

# OK: ``y`` is assigned to a tuple of length 2;
# element 1 is an int, element 2 is a str
y: tuple[int, str] = (5, "foo")

# Error: the type annotation indicates a tuple of length 1,
# but ``z`` has been assigned to a tuple of length 3
z: tuple[int] = (1, 2, 3)

要表示一个 任意 长度的元组,并使其中所有元素的类型都为``T`` ,请使用``tuple[T, ...]`` 。要表示空元组,请使用``tuple[()]`` 。使用``tuple`` 作为注释等同于使用``tuple[Any, ...]``:

x: tuple[int, ...] = (1, 2)
# These reassignments are OK: ``tuple[int, ...]`` indicates x can be of any length
x = (1, 2, 3)
x = ()
# This reassignment is an error: all elements in ``x`` must be ints
x = ("foo", "bar")

# ``y`` can only ever be assigned to an empty tuple
y: tuple[()] = ()

z: tuple = ("foo", "bar")
# These reassignments are OK: plain ``tuple`` is equivalent to ``tuple[Any, ...]``
z = (1, 2, 3)
z = ()

 

 

协议

从Python3.8开始,也有一个类似于强类型中接口的概念:协议(Protocol ),它也叫static duck typing(静态的鸭子类型)。

在 typing 模块中,Protocol 是一个用于定义协议(Protocol)的类。
协议是一种形式化的接口,定义了一组方法或属性的规范,而不关心具体的实现。Protocol 类提供了一种方式来定义这些协议。

使用 Protocol 类时,可以定义一个类,继承自 Protocol 并在类中定义需要的方法或属性。
这样,通过继承 Protocol,可以告诉静态类型检查器,该类遵循了特定的协议。

from typing import Protocol

class Animal(Protocol):
    def speak(self) -> str:
        pass

class Dog:
    def speak(self) -> str:
        return "Woof!"

class Cat:
    def speak(self) -> str:
        return "Meow!"

class Car:
    def run(self) -> str:
        return 'Wu'

def make_sound(animal: Animal) -> str:
    return animal.speak()

dog = Dog()
cat = Cat()

print(make_sound(dog))  # Output: Woof!
print(make_sound(cat))  # Output: Meow!

car = Car()
print(make_sound(car)) #IDE会提示语法错误

Protocol 类可以是泛型,例如:

T = TypeVar("T")

class GenProto(Protocol[T]):
    def meth(self) -> T:
        ...

类型的别名

可以直接给类型起别名,类型别名是通过为类型赋值为指定的别名来定义的。 

Vector = list[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

# passes type checking; a list of floats qualifies as a Vector.
new_vector = scale(2.0, [1.0, -4.2, 5.4])

类型别名适用于简化复杂的类型签名。例如:

from collections.abc import Sequence

ConnectionOptions = dict[str, str]
Address = tuple[str, int]
Server = tuple[Address, ConnectionOptions]

def broadcast_message(message: str, servers: Sequence[Server]) -> None:
    ...

# The static type checker will treat the previous type signature as
# being exactly equivalent to this one.
def broadcast_message(
        message: str,
        servers: Sequence[tuple[tuple[str, int], dict[str, str]]]) -> None:

类型别名可以用 TypeAlias 来标记,以显式指明该语句是类型别名声明,而不是普通的变量赋值:

from typing import TypeAlias


Vector: TypeAlias = list[float] # 相当于 Vector = list[float]

也可以使用NewType来自定义类型:

from typing import  NewType
UserId = NewType('UserId', int)  # 创建一个新类型(类型名字,类型)

def foo(user:UserId):
    print(f'{user=}')

u1: UserId = UserId(1)
print(u1, type(u1))  # 1 
foo(1) #提示类型错误
foo(u1)

u2: UserId = UserId('1') #提示类型错误
print(u2, type(u2))  # 1 
foo(u2)

‘’'
1 
user=1
user=1
1 
user='1'
‘’'


def get_user_name(user_id: UserId) -> str:
    ...

# passes type checking
user_a = get_user_name(UserId(42351))

# fails type checking; an int is not a UserId
user_b = get_user_name(-1)

UserId 类型的变量可执行所有 int 操作,但返回结果都是 int 类型。这种方式允许在预期 int 时传入 UserId,还能防止意外创建无效的 UserId

# 'output' is of type 'int', not 'UserId'
output = UserId(23413) + UserId(54341)

注意,这些检查只由IDE(如pycharm)静态类型检查器强制执行。在运行时,语句

 Derived = NewType('Derived', Base) 

将产生一个 Derived 可调用对象,该对象立即返回你传递给它的任何参数。 这意味着语句 Derived(some_value) 不会创建一个新的类,也不会引入超出常规函数调用的很多开销。

更确切地说,在运行时,some_value is Derived(some_value) 表达式总为 True。

创建 Derived 的子类型是无效的:

from typing import NewType

UserId = NewType('UserId', int)

# Fails at runtime and does not pass type checking
class AdminUserId(UserId): pass

然而,我们可以在 "派生的" NewType 的基础上创建一个 NewType。

from typing import NewType

UserId = NewType('UserId', int)

ProUserId = NewType('ProUserId', UserId)

同时,ProUserId 的类型检查也可以按预期执行。

请记住使用类型别名将声明两个类型是相互 等价 的。 使用 Alias = Original 将使静态类型检查器在任何情况下都把 Alias 视为与 Original 完全等价。 这在你想要简化复杂的类型签名时会很有用处。

反之,NewType 声明把一种类型当作另一种类型的 子类型Derived = NewType('Derived', Original) 时,静态类型检查器把 Derived 当作 Original 的 子类 ,即,Original 类型的值不能用在预期 Derived 类型的位置。这种方式适用于以最小运行时成本防止逻辑错误。

高阶用法

函数对象作为参数的类型注解

基本注解

函数注解需要使用typing.Callable,参数和返回值的类型定义在随后的方括号中,形式如下:

func:Callable[[参数1类型,参数2类型,...],返回值类型]

from typing import Callable
import functools


def mydec(func: Callable[[int, int], int]):
    @functools.wraps(func)
    def wrapper(a: int, b: int) -> int:
        print(f"begin call function '{func.__name__}' params, a={a}, b={b}")
        ret = func(a, b)
        print(f"end call function '{func.__name__}'.")
        return ret

    return wrapper


def myadd(a: int, b: int) -> int:
    return a + b


def myabs(a: int):
    return abs(a)


f1 = mydec(myadd)
f2 = mydec(myabs)

f1(1,2)
f2(1) #提示少了参数
from typing import Callable
import functools


def mydec(func: Callable[[int, int], int]):
    @functools.wraps(func)
    def wrapper(a: int, b: int) -> int:
        print(f"begin call function '{func.__name__}' params, a={a}, b={b}")
        ret = func(a, b)
        print(f"end call function '{func.__name__}'.")
        return ret

    return wrapper

@mydec
def myadd(a: int, b: int) -> int:
    return a + b

@mydec
def myabs(a: int):
    return abs(a)


myadd(1,2)
myabs(1)  #提示少了参数

from collections.abc import Callable, Awaitable

def feeder(get_next_item: Callable[[], str]) -> None:
    ...  # Body

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    ...  # Body

async def on_update(value: str) -> None:
    ...  # Body

callback: Callable[[str], Awaitable[None]] = on_update #函数变量声明时的类型注解

下标语法总是要刚好使用两个值:参数列表和返回类型。 参数列表必须是一个由类型组成的列表或省略号。 返回类型必须是单一类型。

变参

如果将一个省略号字面值 ... 作为参数列表,则表示可以接受包含任意形参列表的可调用对象:

def concat(x: str, y: str) -> str:
    return x + y

x: Callable[..., str]
x = str     # OK
x = concat  # Also OK

Callable 无法表达复杂的签名如接受可变数量参数的函数、重载的函数 或具有仅限关键字形参的函数。 但是,这些签名可通过定义具有 __call__() 方法的 Protocol 类来表达:

from collections.abc import Iterable
from typing import Protocol

class Combiner(Protocol):
    def __call__(self, *vals: bytes, maxlen: int | None = None) -> list[bytes]: ...

def batch_proc(data: Iterable[bytes], cb_results: Combiner) -> bytes:
    for item in data:
        ...

def good_cb(*vals: bytes, maxlen: int | None = None) -> list[bytes]:
    ...
def bad_cb(*vals: bytes, maxitems: int | None) -> list[bytes]:
    ...

batch_proc([], good_cb)  # OK
batch_proc([], bad_cb)   # Error! Argument 2 has incompatible type because of
                         # different name and kind in the callback

泛型(Generic)

基本型

由于无法以通用方式静态地推断容器中保存的对象的类型信息,标准库中的许多容器类都支持下标操作来以表示容器元素的预期类型。

from collections.abc import Mapping, Sequence

class Employee: ...

# Sequence[Employee] indicates that all elements in the sequence
# must be instances of "Employee".
# Mapping[str, str] indicates that all keys and all values in the mapping
# must be strings.
def notify_by_email(employees: Sequence[Employee],
                    overrides: Mapping[str, str]) -> None: ...

容器内无法明确的类型可以通过泛型了表示,使用TypeVar来定义一个泛型:

from collections.abc import Sequence
from typing import TypeVar

T = TypeVar('T')                  # Declare type variable "T"

def first(l: Sequence[T]) -> T:   # Function is generic over the TypeVar "T"
    return l[0]

上面的例子中就定义了一个泛型类型T

完整的语法如下:

class typing.TypeVar(name*constraintsbound=Nonecovariant=Falsecontravariant=False)

用法:

T = TypeVar('T')  # T理论上可以匹配任意类型
S = TypeVar('S', bound=str)  # T可以作为str的任意子类型
A = TypeVar('A', str, bytes)  # 必须是  str 或 bytes 类型

Python是动态类型语言,泛型的类型只作为IDE等工具的类型检查使用,对程序的执行没有影响。

def repeat(x: T, n: int) -> Sequence[T]:
    """Return a list containing n references to x."""
    return [x]*n


def print_capitalized(x: S) -> S:
    """Print x capitalized, and return x."""
    print(x.capitalize())
    return x


def concatenate(x: A, y: A) -> A:
    """Add two strings or bytes objects together."""
    return x + y

请注意,类型变量可以是 被绑定的 , 被约束的 ,或者两者都不是,但不能既是被绑定的 又是 被约束的。

类型变量可以被绑定到具体类型、抽象类型( ABC 或 protocol ),甚至是类型的联合:

U = TypeVar('U', bound=str|bytes)  # Can be any subtype of the union str|bytes
V = TypeVar('V', bound=SupportsAbs)  # Can be anything with an __abs__ method

但是,如果使用 约束 类型变量,则意味着 TypeVar 只能被解析为恰好是给定的约束之一:

a = concatenate('one', 'two')
reveal_type(a)  # revealed type is str

b = concatenate(StringSubclass('one'), StringSubclass('two'))
reveal_type(b)  # revealed type is str, despite StringSubclass being passed in

c = concatenate('one', b'two')  # error: type variable 'A' can be either str or bytes in a function call, but not both

元组元素泛型

元组中有多个元素,可能每个元素的类型都不一样,为了表示元组元素的类型:

class typing.TypeVarTuple(name)

类型变量元组

T = TypeVar("T")
Ts = TypeVarTuple("Ts")

def move_first_element_to_last(tup: tuple[T, *Ts]) -> tuple[*Ts, T]:
    return (*tup[1:], tup[0])

一个普通类型变量将启用单个类型的形参化。 作为对比,一个类型变量元组通过将 任意 数量的类型变量封包在一个元组中来允许 任意 数量类型的形参化。 例如:

# T is bound to int, Ts is bound to ()
# Return value is (1,), which has type tuple[int]
move_first_element_to_last(tup=(1,))

# T is bound to int, Ts is bound to (str,)
# Return value is ('spam', 1), which has type tuple[str, int]
move_first_element_to_last(tup=(1, 'spam'))

# T is bound to int, Ts is bound to (str, float)
# Return value is ('spam', 3.0, 1), which has type tuple[str, float, int]
move_first_element_to_last(tup=(1, 'spam', 3.0))

# This fails to type check (and fails at runtime)
# because tuple[()] is not compatible with tuple[T, *Ts]
# (at least one element is required)
move_first_element_to_last(tup=())

请注意解包运算符 * 在 tuple[T, *Ts] 中的使用。 在概念上,你可以将 Ts 当作一个由类型变量组成的元组 (T1, T2, ...)。 那么 tuple[T, *Ts] 就将变为 tuple[T, *(T1, T2, ...)],这等价于 tuple[T, T1, T2, ...]。 

x: Ts          # Not valid
x: tuple[Ts]   # Not valid
x: tuple[*Ts]  # The correct way to do it

类型变量元组可被用在与普通类型变量相同的上下文中。 例如,在类定义、参数和返回类型中:

Shape = TypeVarTuple("Shape")
class Array(Generic[*Shape]):
    def __getitem__(self, key: tuple[*Shape]) -> float: ...
    def __abs__(self) -> "Array[*Shape]": ...
    def get_shape(self) -> tuple[*Shape]: ...

元组泛型能和基本泛型进行组合使用: 

DType = TypeVar('DType')
Shape = TypeVarTuple('Shape')

class Array(Generic[DType, *Shape]):  # This is fine
    pass

class Array2(Generic[*Shape, DType]):  # This would also be fine
    pass

class Height: ...
class Width: ...

float_array_1d: Array[float, Height] = Array()     # Totally fine
int_array_2d: Array[int, Height, Width] = Array()  # Yup, fine too

但是,请注意在一个类型参数或类型形参列表中最多只能有一个类型变量元组:

x: tuple[*Ts, *Ts]                     # Not valid
class Array(Generic[*Shape, *Shape]):  # Not valid
    pass

最后,一个已解包的类型变量元组可以被用作 *args 的类型标注:

def call_soon(
         callback: Callable[[*Ts], None],
         *args: *Ts
) -> None:
    ...
    callback(*args)

相比非解包的 *args 标注 —— 例如 *args: int,它将指明 所有 参数均为 int —— *args: *Ts 启用了对 *args 中 单个 参数的类型的引用。 在此,这允许我们确保传入 call_soon 的 *args 的类型与 callback 的(位置)参数的类型相匹配。

参数泛型

针对可调用对象的传入参数,设置了一个新的泛型:

class typing.ParamSpec(name*bound=Nonecovariant=Falsecontravariant=False)

用法:

P = ParamSpec('P')

参数泛型与其他泛型一样,主要是给静态类型检查器使用。它们被用来将一个可调用对象的参数类型转发给另一个可调用对象的参数类型——这种模式通常出现在高阶函数和装饰器中。它们只有在 Concatenate 中使用时才有效,或者作为 Callable 的第一个参数,或者作为用户定义的泛型的参数。

例如,为了给一个函数添加基本的日志记录,我们可以创建一个装饰器 add_logging 来记录函数调用。 参数规范变量告诉类型检查器,传入装饰器的可调用对象和由其返回的新可调用对象有相互依赖的类型参数:

from collections.abc import Callable
from typing import TypeVar, ParamSpec
import logging

T = TypeVar('T')
P = ParamSpec('P')

def add_logging(f: Callable[P, T]) -> Callable[P, T]:
    '''A type-safe decorator to add logging to a function.'''
    def inner(*args: P.args, **kwargs: P.kwargs) -> T:
        logging.info(f'{f.__name__} was called')
        return f(*args, **kwargs)
    return inner

@add_logging
def add_two(x: float, y: float) -> float:
    '''Add two numbers together.'''
    return x + y

如果没有 ParamSpec,以前注释这个的最简单的方法是使用一个 TypeVar 与绑定 Callable[..., Any]。

  • 类型检查器不能对 inner 函数进行类型检查,因为 *args 和 **kwargs 的类型必须是 Any。
  • cast() 在返回 inner 函数时,可能需要在 add_logging 装饰器的主体中进行,或者必须告诉静态类型检查器忽略 return inner。

由于 ParamSpec 同时捕获了位置参数和关键字参数,P.args 和 P.kwargs 可以用来将 ParamSpec 分割成其组成部分。 P.args 代表给定调用中的位置参数的元组,只能用于注释 *args。 P.kwargs 代表给定调用中的关键字参数到其值的映射,只能用于注释 **kwargs

用户定义的泛型类型

用户定义的类可以定义为泛型类。通过继承Generic[T]作为基类,可以自己定义泛型类型,T为单个泛型类型。

from typing import TypeVar, Generic
from logging import Logger

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('%s: %s', self.name, message)

Generic[T]基类定义了__class_getitem__() 方法,可以在自定义的类型上使用迭代:

from collections.abc import Iterable

def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
    for var in vars:
        var.set(0)

一个泛型可以有任何数量的类型变量。所有种类的 TypeVar 都可以作为泛型的参数:

from typing import TypeVar, Generic, Sequence

T = TypeVar('T', contravariant=True)
B = TypeVar('B', bound=Sequence[bytes], covariant=True)
S = TypeVar('S', int, str)

class WeirdTrio(Generic[T, B, S]):
    ...

Generic 类型变量的参数应各不相同。下列代码就是无效的:

from typing import TypeVar, Generic
...

T = TypeVar('T')

class Pair(Generic[T, T]):   # INVALID
    ...

您可以通过 Generic 来使用多重继承:

from collections.abc import Sized
from typing import TypeVar, Generic

T = TypeVar('T')

class LinkedList(Sized, Generic[T]):
    ...

从泛型继承的时候,也可以固定部分类型:

from collections.abc import Mapping
from typing import TypeVar

T = TypeVar('T')

class MyDict(Mapping[str, T]):
    ...

如果泛型的类型是Any,不用显示的指定

from collections.abc import Iterable

class MyIterable(Iterable): # Same as Iterable[Any]
    ...

用户定义的通用类型别名也同样被支持。示例:

from collections.abc import Iterable
from typing import TypeVar
S = TypeVar('S')
Response = Iterable[S] | int

# Return type here is same as Iterable[str] | int
def response(query: str) -> Response[str]:
    ...

T = TypeVar('T', int, float, complex)
Vec = Iterable[tuple[T, T]]

def inproduct(v: Vec[T]) -> T: # Same as Iterable[tuple[T, T]]
    return sum(x*y for x, y in v)

其他特殊类型

命名元组

collections.namedtuple的类型版本

collections.namedtuple(typenamefield_names*rename=Falsedefaults=Nonemodule=None)

  • 返回一个新的元组子类,名为 typename 。这个新的子类用于创建类元组的对象,可以通过字段名来获取属性值,同样也可以通过索引和迭代获取值。子类实例同样有文档字符串(类名和字段名)另外一个有用的 __repr__() 方法,以 name=value 格式列明了元组内容。
  • field_names 是一个像 [‘x’, ‘y’] 一样的字符串序列。另外 field_names 可以是一个纯字符串,用空白或逗号分隔开元素名,比如 'x y' 或者 'x, y' 。
  • 任何有效的Python 标识符都可以作为字段名,除了下划线开头的那些。有效标识符由字母,数字,下划线组成,但首字母不能是数字或下划线,另外不能是关键词 keyword 比如 classforreturnglobalpass, 或 raise 。
  • 如果 rename 为真, 无效字段名会自动转换成位置名。比如 ['abc', 'def', 'ghi', 'abc'] 转换成 ['abc', '_1', 'ghi', '_3'] , 消除关键词 def 和重复字段名 abc 。
  • defaults 可以为 None 或者是一个默认值的 iterable 。如果一个默认值域必须跟其他没有默认值的域在一起出现,defaults 就应用到最右边的参数。比如如果域名 ['x', 'y', 'z'] 和默认值 (1, 2) ,那么 x 就必须指定一个参数值 ,y 默认值 1 , z 默认值 2 。
  • 如果 module 值有定义,命名元组的 __module__ 属性值就被设置。
  • 具名元组实例毋需字典来保存每个实例的不同属性,所以它们轻量,占用的内存和普通元组一样。
  • 要支持封存操作,应当将命名元组类赋值给一个匹配 typename 的变量。
# Basic example
>>>Point = namedtuple('Point', ['x', 'y'])
>>>p = Point(11, y=22)     # instantiate with positional or keyword arguments
>>>p[0] + p[1]             # indexable like the plain tuple (11, 22)
33
>>>x, y = p                # unpack like a regular tuple
>>>x, y
(11, 22)
>>>p.x + p.y               # fields also accessible by name
33
>>>p                       # readable __repr__ with a name=value style
Point(x=11, y=22)

命名元组尤其有用于赋值

EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade')

import csv
for emp in map(EmployeeRecord._make, csv.reader(open("employees.csv", "rb"))):
    print(emp.name, emp.title)

import sqlite3
conn = sqlite3.connect('/companydata')
cursor = conn.cursor()
cursor.execute('SELECT name, age, title, department, paygrade FROM employees')
for emp in map(EmployeeRecord._make, cursor.fetchall()):
    print(emp.name, emp.title)

除了继承元组的方法,命名元组还支持三个额外的方法和两个属性。为了防止字段名冲突,方法和属性以下划线开始。

classmethod somenamedtuple._make(iterable)

类方法从存在的序列或迭代实例创建一个新实例。

>>>t = [11, 22]
>>>Point._make(t)
Point(x=11, y=22)

somenamedtuple._asdict()

返回一个新的 dict ,它将字段名称映射到它们对应的值:

>>>p = Point(x=11, y=22)
>>>p._asdict()
{'x': 11, 'y': 22}

somenamedtuple._replace(**kwargs)

返回一个新的命名元组实例,并将指定域替换为新的值

>>>p = Point(x=11, y=22)
>>>p._replace(x=33)
Point(x=33, y=22)

>>>for partnum, record in inventory.items():
    inventory[partnum] = record._replace(price=newprices[partnum], timestamp=time.now())

somenamedtuple._fields

字符串元组列出了字段名。用于提醒和从现有元组创建一个新的命名元组类型。

>>>p._fields            # view the field names
('x', 'y')

>>>Color = namedtuple('Color', 'red green blue')
>>>Pixel = namedtuple('Pixel', Point._fields + Color._fields)
>>>Pixel(11, 22, 128, 255, 0)
Pixel(x=11, y=22, red=128, green=255, blue=0)

somenamedtuple._field_defaults

>>>Account = namedtuple('Account', ['type', 'balance'], defaults=[0])
>>>Account._field_defaults
{'balance': 0}
>>>Account('premium')
Account(type='premium', balance=0)

要获取这个名字域的值,使用 getattr() 函数 :

>>>getattr(p, 'x')
11

转换一个字典到命名元组,使用 ** 两星操作符

>>>d = {'x': 11, 'y': 22}
>>>Point(**d)
Point(x=11, y=22)

 因为一个命名元组是一个正常的Python类,它可以很容易的通过子类更改功能。这里是如何添加一个计算域和定宽输出打印格式:

>>>class Point(namedtuple('Point', ['x', 'y'])):
    __slots__ = ()
    @property
    def hypot(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __str__(self):
        return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

>>>for p in Point(3, 4), Point(14, 5/7):
    print(p)
Point: x= 3.000  y= 4.000  hypot= 5.000
Point: x=14.000  y= 0.714  hypot=14.018

上面的子类设置 __slots__ 为一个空元组。通过阻止创建实例字典保持了较低的内存开销。

子类化对于添加和存储新的名字域是无效的。应当通过 _fields 创建一个新的命名元组来实现它:

Point3D = namedtuple('Point3D', Point._fields + ('z',))

文档字符串可以自定义,通过直接赋值给 __doc__ 属性:

 

Book = namedtuple('Book', ['id', 'title', 'authors'])
Book.__doc__ += ': Hardcover book in active collection'
Book.id.__doc__ = '13-digit ISBN'
Book.title.__doc__ = 'Title of first printing'
Book.authors.__doc__ = 'List of authors sorted by last name'

class typing.NamedTuple

collections.namedtuple() 的类型版本

不使用赋值语句,而是直接使用类继承的方式:

class Employee(NamedTuple):
    name: str
    id: int

相当于:

Employee = collections.namedtuple('Employee', ['name', 'id'])

为字段提供默认值,要在类体内赋值:

class Employee(NamedTuple):
    name: str
    id: int = 3

employee = Employee('Guido')
assert employee.id == 3

带默认值的字段必须在不带默认值的字段后面。

由此产生的类有一个额外的属性 __annotations__ ,给出一个 dict ,将字段名映射到字段类型。(字段名在 _fields 属性中,默认值在 _field_defaults 属性中,这两者都是 namedtuple() API 的一部分。)

NamedTuple 子类也支持文档字符串与方法:

class Employee(NamedTuple):
    """Represents an employee."""
    name: str
    id: int = 3

    def __repr__(self) -> str:
        return f''

NamedTuple 子类也可以为泛型:

class Group(NamedTuple, Generic[T]):
    key: T
    group: list[T]

 反向兼容用法:

Employee = NamedTuple('Employee', [('name', str), ('id', int)])
class Component(NamedTuple):
    part_number: int
    weight: float
    description: Optional[str] = None

类对象的类型

Python中一切都是对象,每个类型如int、str本身也是对象,称为类对象,类对象的类型是typing.Type,通过type()函数可以获取到变量、类型的类对象。

a = 3         # Has type ``int``
b = int       # Has type ``type[int]``
c = type(a)   # Also has type ``type[int]``

 注意的是,类对象和类的实例对象是不同的两类对象,类的继承关系也是类对象类型的继承关系:

class User: ...
class ProUser(User): ...
class TeamUser(User): ...

def make_new_user(user_class: type[User]) -> User:
    # ...
    return user_class()

make_new_user(User)      # OK
make_new_user(ProUser)   # Also OK: ``type[ProUser]`` is a subtype of ``type[User]``
make_new_user(TeamUser)  # Still fine
make_new_user(User())    # Error: expected ``type[User]`` but got ``User``
make_new_user(int)       # Error: ``type[int]`` is not a subtype of ``type[User]``

Any类型

Any 是一种特殊的类型。静态类型检查器认为所有类型均与 Any 兼容,同样,Any 也与所有类型兼容。

也就是说,可对 Any 类型的值执行任何操作或方法调用,并赋值给任意变量:

from typing import Any

a: Any = None
a = []          # OK
a = 2           # OK

s: str = ''
s = a           # OK

def foo(item: Any) -> int:
    # Passes type checking; 'item' could be any type,
    # and that type might have a 'bar' method
    item.bar()
    ...

注意,Any 类型的值赋给更精确的类型时,不执行类型检查。例如,把 a 赋给 s,在运行时,即便 s 已声明为 str 类型,但接收 int 值时,静态类型检查器也不会报错。

此外,未指定返回值与参数类型的函数,都隐式地默认使用 Any:

def legacy_parser(text):
    ...
    return data

# A static type checker will treat the above
# as having the same signature as:
def legacy_parser(text: Any) -> Any:
    ...
    return data

需要混用动态与静态类型代码时,此操作把 Any 当作 应急出口。

Any 和 object 的区别。与 Any 相似,所有类型都是 object 的子类型。然而,与 Any 不同,object 不可逆:object 不是 其它类型的子类型。

就是说,值的类型是 object 时,类型检查器几乎会拒绝所有对它的操作,并且,把它赋给更精确的类型变量(或返回值)属于类型错误。例如:

def hash_a(item: object) -> int:
    # Fails type checking; an object does not have a 'magic' method.
    item.magic()
    ...

def hash_b(item: Any) -> int:
    # Passes type checking
    item.magic()
    ...

# Passes type checking, since ints and strs are subclasses of object
hash_a(42)
hash_a("foo")

# Passes type checking, since Any is compatible with all types
hash_b(42)
hash_b("foo")

使用 object,说明值能以类型安全的方式转为任何类型。使用 Any,说明值是动态类型。

AnyStr

AnyStr实际是str或bytes类型:

AnyStr = TypeVar('AnyStr', str, bytes)
def concat(a: AnyStr, b: AnyStr) -> AnyStr:
    return a + b

concat("foo", "bar")    # OK, output has type 'str'
concat(b"foo", b"bar")  # OK, output has type 'bytes'
concat("foo", b"bar")   # Error, cannot mix str and bytes

Never类型

永远不会被调用的类型,一个没有成员的类型。

这可以用于定义一个永不应该被调用的函数,或一个永不返回的函数:

from typing import Never

def never_call_me(arg: Never) -> None:
    pass

def int_or_str(arg: int | str) -> None:
    never_call_me(arg)  # type checker error
    match arg:
        case int():
            print("It's an int")
        case str():
            print("It's a str")
        case _:
            never_call_me(arg)  # OK, arg is of type Never

Self类型

类型自己,在返回自身时有用:

from typing import Self, reveal_type

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

class SubclassOfFoo(Foo): pass

reveal_type(Foo().return_self())  # Revealed type is "Foo"
reveal_type(SubclassOfFoo().return_self())  # Revealed type is "SubclassOfFoo"

此标准在语法上等价于以下代码,但形式更为简洁:

from typing import TypeVar

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

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

如果在子类中也要返回父类的类型,可以直接声明返回父类的名称,但需要用引号标记: 

class Eggs:
    # Self would be an incorrect return annotation here,
    # as the object returned is always an instance of Eggs,
    # even in subclasses
    def returns_eggs(self) -> "Eggs":
        return Eggs()

类型检查相关的函数和装饰器

typing.cast(typeval)

把值强制转换为类型。

不变更返回值。对类型检查器而言,代表了返回值具有指定的类型,但运行时不做任何检查(以便让检查速度尽量快)。

typing.assert_type(valtype/)

让静态类型检查器确认 val 具有推断为 type 的类型。

在运行时这将不做任何事:它会原样返回第一个参数而没有任何检查或附带影响,无论参数的实际类型是什么。

当静态类型检查器遇到对 assert_type() 的调用时,如果该值不是指定的类型则会报错:

def greet(name: str) -> None:
    assert_type(name, str)  # OK, inferred type of `name` is `str`
    assert_type(name, int)  # type checker error

此函数适用于确保类型检查器对脚本的理解符合开发者的意图:

def complex_function(arg: object):
    # Do some complex type-narrowing logic,
    # after which we hope the inferred type will be `int`
    ...
    # Test whether the type checker correctly understands our function
    assert_type(arg, int)

typing.assert_never(arg/)

让静态类型检查器确认一行代码是不可达的。

def int_or_str(arg: int | str) -> None:
    match arg:
        case int():
            print("It's an int")
        case str():
            print("It's a str")
        case _ as unreachable:
            assert_never(unreachable)

 在运行时,如果调用此函数将抛出一个异常。

typing.reveal_type(obj/)

揭示一个表达式的推断静态类型。

当静态类型检查器遇到一个对此函数的调用时,它将发出包含参数类型的诊断信息。 例如:

x: int = 1
reveal_type(x)  # Revealed type is "builtins.int"

这在你想要调试你的类型检查器如何处理一段特定代码时很有用处。

该函数将不加修改地返回其参数,这将允许在表达式中使用它:

x = reveal_type(1)  # Revealed type is "builtins.int"

大多数类型检查器都能在任何地方支持 reveal_type(),即使并未从 typing 导入该名称。 从 typing 导入该名称能让你的代码运行时不会出现运行时错误并且更清晰地传递意图。

在运行时,该函数会将其参数的运行时类型打印到 stderr 并不加修改地返回它:

x = reveal_type(1)  # prints "Runtime type is int"
print(x)  # prints "1"

@typing.overload

标记是重载方法

@overload
def process(response: None) -> None:
    ...
@overload
def process(response: int) -> tuple[int, str]:
    ...
@overload
def process(response: bytes) -> str:
    ...
def process(response):
    ...  # actual implementation goes here

@typing.final

如果装饰方法,表示该方法不能被重载,如果是装饰类,表示类不能被继承

class Base:
    @final
    def done(self) -> None:
        ...
class Sub(Base):
    def done(self) -> None:  # Error reported by type checker
        ...

@final
class Leaf:
    ...
class Other(Leaf):  # Error reported by type checker
    ...

 结束

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