Loading... <div class="tip inlineBlock share simple"> ## 概要: 这片文章,是我阅读流畅的Python第二版 类和协议的 关于 **鸭子类型,大鹅类型,静态鸭子类型,静态类型**这几个相关概念的一个梳理,就当是我的读书笔记罢了。 这里当然也会引用书中的部分内容,毕竟,这种概念性的东西,我只能给出书上写的,之后我在稍微修饰一下,一般建议有做过相关了解的再来看这篇文章,就会觉得好懂一些。 本文,并不涉及抽象基类的核心内容,但是还是会有所提及,如果你闻所未闻,可以当没有看见。 </div> <div class="tip inlineBlock info simple small"> 先来听首我最近迷上的歌吧 <div class='handsome_aplayer player-content' data-preload="auto" data-autoplay="false" data-listMaxHeight="340px" data-order="list"> <div class="handsomePlayer-tip-loading"><span></span> <span></span> <span></span> <span></span><span></span></div><div class="handsome_aplayer_music" data-id="28151022" data-server="netease" data-type="song" data-auth="954ae355f925e23a015297c3744a2d27"></div> </div> </div> <div class="tip inlineBlock warning simple"> ## 什么是鸭子类型? 要想知道鸭子类型,你必须知道鸭子类型的一些特点,而书上是直接列举出了Python的序列协议: 1. 鸭子类型在运行时检查(区别于静态检查) 2. 第二点也就是最重要的那就是 鸭子类型注重 行为 当然,还有很多我并没有列举完... 这只是为了打个底 Python从一开始就默认使用的类型实现方式,鸭子类型不关心对象的具体类型。只要一个对象具有特定的方法或属性,它就可以被视为符合某个接口或类型,即使它并没有显式**地继承或实**现某个接口。只要你打上了猴子补丁,鸭子类型的灵活性使得代码更加灵活和适应性更强。 ### python的序列协议和可迭代协议 大白话:总而言之,翻译成大白话就是:如果你有具体的行为,比如会嘎嘎叫(一个方法),那么,因为你表现的像一个鸭子(实现了鸭子的 功能) 那么你 就是一个 鸭子。(你是不是也意识到了,这可能会出问题?) --- Python对序列的协议支持非常灵活,尤其是从方法的实现角度来看。举个例子,即使只实现了 `__getitem__`方法而没有实现 `__iter__`方法,该类的实例依旧可以迭代。这是因为Python解释器会发现双下划线 `getitem`方法的存在,并从0开始逐个调用以尝试迭代对象。 双下划线 `getitem`方法是一种后备机制,旨在尽可能减少需要编写大量特定方法的情况,而是更注重功能的实现。 《流畅的Python》一书从Python序列的例子入手,展示了Python对类似序列的对象的特殊处理。这种处理方式可能显得有些极端,因为为了迭代对象,Python会不遗余力地调用两个不同的方法。这实际上归因于Python对序列相关的抽象基类的定义。 所以,基本上对于鸭子类型,因为其动态的特性,我们通常会用到try except句法来防御。 简单来说就是,如果你能嘎嘎叫,那么 我就认为你是一只鸭子,然后,我对于你这只鸭子,我有专门的烹饪方法,来处理你。其次,说白了,使用鸭子类型,说明我就是一个懒狗用户,你对我输入了一只鸡还是一只鸭还是什么牛魔,我都不用管(指用isinstance做检查)只要你能嘎嘎叫(实现了一个功能),那么我就直接开始我的烹饪方式(或者下个蛋)。当然,报错了,关我什么事情?! 错了就是错了!!! </div> <div class="tip inlineBlock info simple"> ## 什么是大鹅类型? 书中提到:python没有interface关键字,所以就使用抽象基类作为接口,在运行时,或者静态时,都可以做检查。 大鹅类型,就是利用抽象基类实现的一种类型检查方式。对此,你需要定一个抽象基类的子类,然后,当你在运行检查的时候,使用isinstance等,来检查是否符合抽象基类的要求 当然,比起使用isinstance,我们更倾向于使用tryexcept,谁叫我们是懒狗用户,而不是勤快的用户呢? 所以,你可以理解为,大鹅类型,实际上是对鸭子类型的一个补充,**通过继承关系,大鹅类型可以帮助我们减少潜在的错误,提高代码的可靠性。** 一般来说,我们要玩起来大鹅类型,需要对现有的抽象基类,进行子类化。 这通常对我等(个人开发者)来说,学习成本是巨大的,并且通常得不到好处。 但是,我等(个人开发者),必须学习怎样看懂,阅读,抽象基类的相关代码,起码知道,这段代码在做什么。 </div> <div class="tip inlineBlock success simple"> ## 什么是静态类型?静态鸭子类型? 静态协议通常是通过抽象基类(Abstract Base Classes,简称 ABCs)来定义的。抽象基类是一种特殊的类,它不能直接实例化,但可以作为其他类的基类。通过继承抽象基类,子类可以继承抽象基类的方法和属性,并在子类中实现这些方法和属性。 在 Python 3.5 版本及以后的版本中,引入了类型提示(Type Hints)的概念,允许开发者在代码中添加类型提示注解,以指明变量的预期类型。这就不得不说说,python的typing模块和310之后的简写方式等内容了,如果你没有了解,也没有关系,毕竟,这才是这篇文章的目的,这里只是给你打个预防针而已。 很多人都知道,其实python的类型注解,基本上没有实际用处,因为它不会做出你想要做的事情,它只是给你看看,让你知道我接受什么什么,然后输出什么什么。 然而,当我们的注解+一个静态检查工具,这就是我们真正上发挥出静态类型检查。 现在的检查工具是mypy,没有听过,非常正常,因为这类相关内容,基本上团队所需的,在**非运行时检查**出问题,这是我们使用静态类型的目的。 我们来看一个具体的例子这里例子来自fastapi官方文档,这里是针对python 3.8以上的 ```python from typing import Union from fastapi import FastAPI from pydantic import BaseModel class Item(BaseModel): name: str description: Union[str, None] = None price: float tax: Union[float, None] = None app = FastAPI() @app.post("/items/") async def create_item(item: Item): return item ``` 在这里,我们使用BaseModel,然后我们使用typing,设置了我们某些字段是否可选。 来对比一下python3.10版本的 ```python from fastapi import FastAPI from pydantic import BaseModel class Item(BaseModel): name: str description: str | None = None price: float tax: float | None = None app = FastAPI() @app.post("/items/") async def create_item(item: Item): return item ``` 这就是静态类型和静态鸭子类型的特点。这种方式可以帮助提高代码的可读性和可维护性, 并保持 Python 语言的灵活性和动态特性。并且 > 通过同样的 Python 类型声明,**FastAPI** 提供了数据校验功能。 > > 所有的数据校验都由 [Pydantic](https://pydantic-docs.helpmanual.io/) 在幕后完成,所以你可以从它所有的优点中受益。并且你知道它在这方面非常胜任。 > > 你可以使用同样的类型声明来声明 `str`、`float`、`bool` 以及许多其他的复合数据类型 </div> 在来看用typing.Protocol是具体显式定义一个协议的例子 假设我们定义了这样一个函数top 他接受两个参数,一个是list,一个是int,可以返回前几的元素 (是一个列表) ```python def top(series: Iterable[T], length: int) -> list[T]: ordered = sorted(series, reverse=True) return ordered[:length] ``` 在上面的代码中,我们有一个没有定义的类型参数T, 如果是鸭子类型,那么我们可能会不太清楚,对于T,到底需要实现什么样的方法,我们的top函数才可以发挥作用。 这时候,我们需要显式的创建一个协议P,来约束我们的T 这里你需要知道,sorted函数接受任何Iterable[Any],然而当我们没有传递key,我们的sorted函数会在可迭代对象上调用<运算符。 只要支持<运算符,我们就可以正常的排序,而<运算符,就是__it__方法 所以,当我们的T实现了__it__方法,top函数就可以正常使用了 现在,我们来使用typing的protocol,来显式的定义一个协议P ```python from typing import Protocol, Any class SupportsLessThan(Protocol): # <1> 协议是Protocol的子类 def __lt__(self, other: Any) -> bool: ... # <2> 在协议主体中定义一个或者多个方法,主体是... ``` 现在,我们来使用协议P约束我们的类型T ```python from collections.abc import Iterable from typing import TypeVar from comparable import SupportsLessThan LT = TypeVar('LT', bound=SupportsLessThan) def top(series: Iterable[LT], length: int) -> list[LT]: ordered = sorted(series, reverse=True) return ordered[:length] ``` 与抽象基类相比,协议类型的关键优势在于,类型无需做任何特殊的声明,就可以与协议类型相容 这样,协议就可以利用现有的类型,或者不收我们控制的代码实现的 类型创建(勤快的我们与勤快的用户 对于我们要实现的:top方法,只要能实现lt方法,就可以相容, 而且,这样做,类型检查工具也可以发挥作用,因为我们的这个协议是显式定义的。 而鸭子类型隐含的协议对类型检查工具是不可见的。 这就是静态鸭子类型 top函数电费series参数采用的注解方式要表达的方式是,你的具体是什么,我不知道,不重要,你只需要实现__lt__方法,即可, python的鸭子类型并不阻止我们隐含的这层意思,只是静态检查工具无从知晓。 <div class="tip inlineBlock error simple"> ## 总结 本文简单地介绍了 Python 中的鸭子类型、大鹅类型、静态类型以及静态鸭子类型等概念。在 Python 中,鸭子类型注重对象的行为,而大鹅类型通过抽象基类的继承关系帮助我们减少潜在的错误,提高代码的可靠性。 同时,静态类型和静态鸭子类型通过类型提示等方式在代码可读性上和功能上都达到了很好的效果。 现如今,Go语言的静态鸭子类型是一种趋势,在所谓的接口的设计上,我等仍然任重而道远,Go语言中的接口,就是我们口中的静态协议。 相信,如果你使用python3.10(我现在就用310去搞fastapi),那么你会喜欢不导入typing这种的,实现非常可读,切具有相关限制功能的方法啥的啥的。 最后,一开始我推荐给你的歌听了么?感觉如何? </div> Last modification:November 12, 2023 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 2 如果觉得我的内容对你有用,请随意赞赏