python:鸭子类型使用场景

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):
        pass

    def __enter__(self):
        print("开始读取")
        return self

    def __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 Iterator

print(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 Iterator


class Data:
    def __init__(self, x):
        self.x = x

    def __iter__(self):
        return self

    def __next__(self):
        if self.x >= 10:
            raise StopIteration
        else:
            self.x += 1
            return self.x


data = 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 = x

    def __iter__(self):
        return self

    def __next__(self):
        if self.x >= 10:
            raise StopIteration
        else:
            self.x += 1
            return self.x


data = 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 Iterator

list_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 Iterator

list_a = [1, 2]
my_iterator = iter(list_a)

print(f"my_iterator is 'Iterator': {isinstance(my_iterator, Iterator)}")

# 输出如下:
# my_iterator is 'Iterator': True

print(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__()方法,而不关心它是否是一个具体的列表、元组或集合。

总之,鸭子模型让我们专注于对象的行为而不是其具体类型,在编写灵活、可重用的代码时非常有用。

### Python中的鸭子类型详解 #### 什么是鸭子类型? 在Python中,“鸭子类型”是一种动态类型的实现方式。其核心理念来源于一句俗语:“如果它走起来像鸭子,叫起来也像鸭子,那么它就是一只鸭子。”这意味着,在判断对象的行为或属性时,不需要关心该对象的具体类型是什么,而只需关注它是否具有所需的方法或属性[^1]。 #### 鸭子类型的原理 Python是一门动态语言,支持运行时绑定特性。因此,只要某个对象实现了特定方法(如`__len__()`, `__getitem__()`, 或者其他协议方法),就可以将其视为某种类型来处理,而不必显式声明它的具体类型。这种机制使得代码更加灵活且易于扩展[^2]。 #### 示例代码展示 下面通过一段简单的例子说明如何利用鸭子类型: ```python class Dog: def speak(self): return "Woof!" class Cat: def speak(self): return "Meow!" def make_animal_speak(animal): print(animal.speak()) dog = Dog() cat = Cat() make_animal_speak(dog) # 输出: Woof! make_animal_speak(cat) # 输出: Meow! ``` 在这个例子中,函数`make_animal_speak()`并不知道传入的对象到底是狗还是猫,但它只依赖于`speak()`方法的存在与否来进行操作。这就是典型的鸭子类型应用[^3]。 #### 使用场景分析 - **数据结构抽象**:当设计通用的数据容器或者迭代器接口时,可以通过定义统一的操作行为(比如`__iter__()`和`__next__()`)让不同种类的对象都能被一致对待。 - **插件系统开发**:允许第三方开发者自由创建符合预期API的新组件加入现有框架之中,无需修改原有逻辑即可完成功能扩充。 - **测试驱动开发(TDD)**:编写模拟(mock)对象代替真实服务调用返回假定的结果集用于单元测试验证业务流程正确性。 #### 初学者注意事项 对于初学编程的人来说理解并掌握好这一概念非常重要但也存在一定难度。建议从以下几个方面入手逐步深入学习: 1. 学习常见内置类型所遵循的标准协议; 2. 尝试自己动手实践自定义类模仿这些标准协议表现形式; 3. 多阅读高质量开源项目源码观察实际生产环境中是如何运用此类技巧解决问题的; #### 总结 综上所述,鸭子类型作为Python语言的一大特色赋予了程序员极大的灵活性与创造力空间的同时也需要我们保持谨慎态度合理规划架构避免滥用造成维护成本上升等问题发生。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值