python:鸭子类型使用场景
1 前言
“一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟可以被称为鸭子。“----鸭子模型
鸭子模型是Python中的一种编程哲学,也被称为“鸭子类型”。它来源于一句话:“如果它走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子。”这个哲学思想强调对象的行为比其具体类型更重要。与C++、Java等编译型语言不一样的是,Python作为解释器语言,其语言层面的设计理念有独特之处,鸭子模型便是其中之一。在面向对象的世界中,编译型语言判断一个对象是否隶属于某个类,依靠的是类的继承机制,换句话说,即使一个对象实现了某个类的所有方法也不行;而在Python中,只要实例对象实现了某个类的所有必要的方法,即使不存在继承关系,也可以看作是这个类。
2 使用
举个简单的栗子:
class Duck:def walk(self):print(f'I am duck, walk with {self}')def drink(self):print(f'I am duck, drink with {self}')class SmallBird:def walk(self):print(f'I am also duck, walk with {self}')def drink(self):print(f'I am also duck, drink with {self}')def duck_action_run(obj):obj.walk()obj.drink()duck = Duck()
smallBird = SmallBird()
duck_action_run(duck)print('*' * 10)
duck_action_run(smallBird)
结果:
I am duck, walk with <__main__.Duck object at 0x0000017442006FA0>
I am duck, drink with <__main__.Duck object at 0x0000017442006FA0>
**********
I am also duck, walk with <__main__.SmallBird object at 0x0000017442006FD0>
I am also duck, drink with <__main__.SmallBird object at 0x0000017442006FD0>
我们来分析下这个简单示例,首先定义了Duck类和SmallBird类,分别具有walk方法和drink方法,两者并不具备任何继承关系;然后定义一个duck_action_run函数,该函数接收一个对象,在函数内调用该对象的walk方法和drink方法;最后实例化一个Duck和SmallBird对象,先后传递给duck_action_run函数来执行,并都可以执行成功。在这个例子里,我们可以引入一个"协议"的概念,"协议"代表一系列特征,譬如鸭子协议是walk和drink,任何实现了鸭子协议的对象都可以当作鸭子,这个就是鸭子模型和协议。
(1)再举个Python中更加常见的案例,with读取以及关闭文件资源:
“with”,上下文管理器。众所周知,在任何编程语言里面,文件处理都是基本的IO操作,都包含open和close两个操作,因为这两个操作是操作系统要求的。在操作系统里,每当打开一个文件,获取一个文件句柄后就会占用操作系统的资源,所以操作系统要求文件处理完后需要应用去close,释放掉文件句柄资源。然而现实情况是,开发者往往忘记close,最终导致资源泄漏问题频发。为了解决这个问题,Python使用with来进行上下文管理,在with的语句块结束后自动close,不再需要手动操作(with关键字会自动处理完后关闭文件资源,释放资源占用)。上下文管理器示例如下:
with open("xiaoxu.txt", 'r', encoding='utf-8') as f:f.read()
在上面这个例子中,在with语句中进行open操作,然后调用read方法,最后并没有显示调用close,但不存在资源释放问题,因此with结束后会自动调用了close。那么with的底层原理是什么?类比鸭子模型介绍中关于协议的概念:
协议(Protocol)则是一种约定或契约,描述了对象应该具有的方法和属性。在Python中,协议是一种非正式的接口定义方式,它没有严格的语法要求,只需确保对象实现了协议中定义的方法和属性即可。协议允许我们根据对象的行为来定义接口,而不依赖于具体的类或类型。使用协议,我们可以通过定义一个适当的接口来描述对象的行为,而不仅仅依赖于继承关系。这样,不同的对象可以来自不同的类,但只要它们实现了相同的协议,我们就可以在代码中使用它们。
Python中的一些常见协议包括可迭代协议(Iterable Protocol)、可调用协议(Callable Protocol)和上下文管理器协议(Context Manager Protocol)等。这些协议定义了一组方法或属性,用于描述对象应该具备的行为。
我们回到刚才的示例中,在with中对应的是实现上下文管理器协议__enter__方法和__exit__方法,换句话说,只要一个类具备__enter__方法和__exit__方法,就可以使用with管理,with开始时调用__enter__方法,结束时自动调用__exit__方法,示例代码如下:
class OwnFile:def run(self):passdef __enter__(self):print("开始读取")return selfdef __exit__(self, exc_type, exc_val, exc_tb):print(f'exc_type:{exc_type}, exc_val:{exc_val}, exc_tb:{exc_tb}')print('结束读取')with OwnFile() as f:print(f)
结果:
开始读取
<__main__.OwnFile object at 0x000001E49570B2B0>
exc_type:None, exc_val:None, exc_tb:None
结束读取
在这个例子中,我们可以看到没有显示调用OwnFile的__enter__方法和__exit__方法,由with自动调用了,因此对于资源释放类,资源释放的操作可以放到__exit__方法中,这样配合with语句使用会方便很多,也降低出错的概率。
(2)另外以可迭代对象和迭代器为例:
可迭代 (iterable):如果一个对象具备有__iter__() 或者 __getitem__()其中任何一个魔术方法的话,这个对象就可以称为是可迭代的。其中,__iter__()的作用是可以让for循环遍历,而__getitem__()方法可以让实例对象通过[index]索引的方式去访问实例中的元素。所以,列表List、元组Tuple、字典Dictionary、字符串String等数据类型都是可迭代的。
迭代器 (iterator): 如果一个对象同时有__iter__()和__next__()魔术方法的话,这个对象就可以称为是迭代器。__iter__()的作用前面我们也提到过,是可以让for循环遍历。而__next__()方法是让对象可以通过 next(实例对象) 的方式访问下一个元素。列表List、元组Tuple、字典Dictionary、字符串String等数据类型虽然是可迭代的,但都不是迭代器,因为他们都没有next( )方法。
如何判断可迭代(iterable) & 迭代器(iterator)
我们可以借助Python中的**isinstance(object, classinfo)**函数来判断一个对象是否是一个已知类型。如下例子中,通过isinstance( )函数分别判断列表、元组、字典、字符串是不是可迭代或迭代器。
from collections import Iterable
from collections import Iteratorprint(f"List is 'Iterable': {isinstance([], Iterable)}")
print(f"Tuple is 'Iterable': {isinstance((), Iterable)}")
print(f"Dict is 'Iterable': {isinstance({}, Iterable)}")
print(f"String is 'Iterable': {isinstance('', Iterable)}")print("=" * 25)print(f"List is 'Iterator': {isinstance([], Iterator)}")
print(f"Tuple is 'Iterator': {isinstance((), Iterator)}")
print(f"Dict is 'Iterator': {isinstance({}, Iterator)}")
print(f"String is 'Iterator': {isinstance('', Iterator)}")# 输出如下:
# List is 'Iterable': True
# Tuple is 'Iterable': True
# Dict is 'Iterable': True
# String is 'Iterable': True
# =========================
# List is 'Iterator': False
# Tuple is 'Iterator': False
# Dict is 'Iterator': False
# String is 'Iterator': False
通过对定义的分析和比较我们得知:迭代器都是可迭代的(因为迭代器都包含__iter__()函数),但可迭代的不一定是迭代器(因为未必每个可迭代就包含__next__()方法)。
创建一个迭代器
得益于Python的鸭子类型特性,只要我们实现类似具备某一特征的方法,就可以认为它就是什么。所以我们定义了一个类并在类中实现__iter__()和__next__()方法,那么这个类就可以当做是一个迭代器了。
from collections import Iteratorclass Data:def __init__(self, x):self.x = xdef __iter__(self):return selfdef __next__(self):if self.x >= 10:raise StopIterationelse:self.x += 1return self.xdata = Data(0)print(f"data is 'Iterator': {isinstance(data, Iterator)}")# 输出如下:
# data is 'Iterator': True
如上例子中我们可以看到,最后我们用isinstance()函数判断得到结果为True,证明我们定义的实例对象是一个真正的迭代器了。因为是迭代器,我们就可以用for循环来验证试试。
class Data:def __init__(self, x):self.x = xdef __iter__(self):return selfdef __next__(self):if self.x >= 10:raise StopIterationelse:self.x += 1return self.xdata = Data(0)for d in data:print(d)# 输出如下:
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9
# 10
执行结果:
上述例子中,我们定义的类对象内部,把x的值显示在大于等于10以内,否则就会抛出StopIteration异常错误(当然实际使用时并不会出错,只是中断继续执行。)我们先创建了一个初始值为0的实例对象,最后顺利的用for循环遍历了从1到10的数字,因为内部对大于等于10的限制,所以输出到10的时候就停止了。特别要注意的是,如果你再次单独去执行for循环的话不会有任何输出,因为迭代器默认只运行一次。
除了自己定义__iter__()和__next__()魔术方法的外,我们还可以使用Python内置的iter()函数来返回一个迭代器,像下面这样。iter()方法,可以传入可迭代对象(可迭代对象有__iter__()和__getitem__()魔术方法)参数,返回迭代器对象(迭代器对象有__iter__()和__next__()魔术方法)。
from collections.abc import Iteratorlist_a = [1, 2, 3, 4, 5, 6]
my_iterator = iter(list_a)print(f"my_iterator is 'Iterator': {isinstance(my_iterator, Iterator)}")# 输出如下:
# my_iterator is 'Iterator': True
我们知道,迭代器必须具备两个基本方法__iter__()和__next__(),而__next__()方法是让对象可以通过 next(实例对象) 的方式访问下一个元素。所以让我们验证下用next()的方式去访问这个我们转换过的迭代器是否能正常运行。
from collections.abc import Iteratorlist_a = [1, 2]
my_iterator = iter(list_a)print(f"my_iterator is 'Iterator': {isinstance(my_iterator, Iterator)}")# 输出如下:
# my_iterator is 'Iterator': Trueprint(next(my_iterator))
print(next(my_iterator))
print(next(my_iterator)) # error:StopIteration
# 输出如下:
# 1
# 2
# StopIteration
最后,我们还可以使用Python内置的dir()函数来看看传入参数的属性,方法等信息,比如我们用它来看看之前从list转换成的my_iterator迭代器。
print(dir(my_iterator))
结果:
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__',
'__iter__', '__le__', '__length_hint__', '__lt__', '__ne__', '__new__', '__next__',
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__',
'__str__', '__subclasshook__']
可以发现,也是意料之中的,my_iterator迭代器包含了两个基本方法__iter__()和__next__()方法。
(3)总结一下
在Python中,鸭子模型指的是我们关注对象的行为(方法和属性)而不是其具体的类型。如果一个对象具有我们所期望的行为,我们就可以将其视为满足我们的需求,而无需关注其实际的类型。这种灵活性使得在Python中编写可重用和灵活的代码变得更加容易。例如,如果我们编写了一个需要迭代对象的函数,我们只关心对象是否具有__iter__()方法,而不关心它是否是一个具体的列表、元组或集合。
总之,鸭子模型让我们专注于对象的行为而不是其具体类型,在编写灵活、可重用的代码时非常有用。