流畅的Python(十一)-从协议到抽象基类

本文详细探讨了Python中两种接口实现方式:鸭子类型和协议,以及抽象基类的使用。通过实例展示了鸭子模型、序列协议的运用,包括如何通过猴子补丁扩展功能,以及如何定义和使用抽象基类和虚拟子类。
摘要由CSDN通过智能技术生成

一、核心要义

主要讨论Python中的`接口`,所谓接口就是类实现或继承的一套公开(按照定义,受保护的属性和私有属性不在接口中)属性和方法,包括特殊方法,如__getitem__或__add__等。Python有两套`规范`接口的方式:

1. 鸭子类型和协议,这种方式对接口的`规范`更多是一种约定俗成,并没有代码层面的强制性。

2. 抽象基类,这种方式对接口的`规范`更明确,也能够更显式地验证子类是否符合设计规范

二、代码示例

1、鸭子模型

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/22 21:52
# @Author  : Maple
# @File    : 00-鸭子模型.py
# @Software: PyCharm


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

在鸭子类型中,关注点在于对象的行为能做什么;而不是关注对象所属的类型。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"游泳"和"散步"方法。
在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的"游泳"和"散步"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的"游泳"和"散步"
方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名
鸭子类型通常得益于"不"测试方法和函数中参数的类型,而是依赖文档、清晰的代码和测试来确保正确使用
"""

class Duck:
    def __init__(self,name):
        self.name = name

    def swim(self):
        print('{}在游泳'.format(self.name))

    def walk(self):
        print('{}在散步'.format(self.name))



class Chicken:
    def __init__(self, name):
        self.name = name

    def swim(self):
        print('{}在游泳'.format(self.name))

    def walk(self):
        print('{}在散步'.format(self.name))


class Person:
    def __init__(self,name):
        self.name = name

    def watch(self,pet):
        pet.swim()
        pet.walk()

if __name__ == '__main__':

    duck = Duck('唐老鸭')
    chicken = Chicken('烧鸡')
    person = Person('Max')

    # watch中的对象,不用关心是什么类型,只要它具有swim和walk方法即可
    person.watch(duck)
    print('****************')
    person.watch(chicken)

2、序列协议

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/22 19:52
# @Author  : Maple
# @File    : 01-序列协议.py
# @Software: PyCharm

class Person:
    # 实现序列协议的__getitem__方法
    # 该对象就具备可迭代(本来应该实现了__iter__方法后才具备)和判断元素是否在对象中(本来应该实现了__contains__方法后才具备)的能力
    # 这就是协议的强大之处,只要实现了某个协议(此例是序列)的部分方法,该对象就自动具备该类协议对象的功能
    def __getitem__(self, pos):
        return list(['Maple','kelly','Max'])[pos]

    # 实现序列协议的__len__方法
    # 该对象就能够通过len统计其长度
    def __len__(self):
        return 3


if __name__ == '__main__':

    person = Person()
    # 1. person可迭代
    for p in person:
        """输出
           Maple
           kelly
           Max
        """
        print(p)

    #2. 可判断某个元素是否在person对象中
    print('Maple' in person) #True

    #3.对象长度统计
    print(len(person)) # 3

3、猴子补丁

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/22 20:05
# @Author  : Maple
# @File    : 02-猴子补丁.py
# @Software: PyCharm

"""
如果某个类在最初定义的时候,没有实现协议的部分方法,导致某些功能无法使用
可以在后续 通过“猴子补丁”的方式,添加能够实现该功能的协议方法集合
"""
from random import shuffle


class Cars:
    # 注意第一个参数self,只是约定俗称的命名,并非必然如此
    # 唯一明确的只是它代表的含义:类的实例化对象
    def __init__(self,items):
        self.items = list(items)

    # 实现__getitem__方法,cars对象可迭代,但cars对象集不能被更改
    def __getitem__(self, pos):
        return self.items[pos]

    def __len__(self):
        return len(self.items)


if __name__ == '__main__':
    # 1. cars对象迭代
    cars = Cars(['奔驰','宝马','奥迪'])
    print([car for car in cars]) # ['奔驰', '宝马', '奥迪']
    # 尝试随机化cars,报错原因是Cars类没有实现序列协议中的__setitem__方法,所以Cars目前是不可变的序列
    #shuffle(cars) #'Cars' object does not support item assignment

    # 2.为Cars动态添加__setitem__方法
    def set_car(self,key,value):
        self.items[key] = value

    Cars.__setitem__ = set_car
    # 此时就可以对cars进行shuffle
    shuffle(cars)
    print(list(cars)) #['奥迪', '奔驰', '宝马']

4、定义抽象基类的子类

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/23 7:01
# @Author  : Maple
# @File    : 03-定义抽象基类的子类.py
# @Software: PyCharm

import collections
from collections.abc import MutableSequence


Card = collections.namedtuple('Card',['rank','suit'])
class FrenchDeck(MutableSequence):
    """MutableSequence抽象基类有三个抽象方法,子类必须要实现
    1. __setitem__
    2. __delitem__
    3. insert
    """

    ranks = [str(n) for n  in range(2,11)] +  list('JQKA')
    suits = 'spades diamonds clubs hearts'.split(' ')

    def __init__(self):
        self._cards = [Card(rank,suit) for rank in self.ranks
                      for suit in self.suits]

    def __len__(self):
        return len(self._cards)

    def __getitem__(self, position):
        return self._cards[position]


    def __setitem__(self, key, value):
        self._cards[key] = value

    def __delitem__(self, position):
        del self._cards[position]

    def insert(self,position,value):
        self._cards.insert(position,value)


if __name__ == '__main__':
    # 1.通过切片获取元素
    deck = FrenchDeck()
    print(deck[0])

    #2.插入元素
    deck.insert(0,Card('0','spades'))
    print(deck[0])   # Card(rank='0', suit='spades')

    #3.删除元素
    #  删除第二步添加的元素
    del deck[0]
    print(deck[0]) # Card(rank='2', suit='spades')

5、自定义抽象基类

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/23 20:00
# @Author  : Maple
# @File    : 04-自定义抽象基类.py
# @Software: PyCharm
import abc

# 定义抽象基类
import random


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self,iterable):
        """从可迭代对象中加载元素"""

    @abc.abstractmethod
    def pick(self):
        """随机删除元素,然后将其返回

        如果实例为空,这个方法应该抛出LookupError
        """
    def loaded(self):
        """如果至少有一个元素,则返回True,否则返回False"""
        return bool(self.inspect())


    def inspect(self):
        """返回一个有序元组,由当前元素构成"""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))

# 定义一个Tombola基类的测试子类:证明如果子类未实现基类的全部抽象方法,将无法实例化(虽然issubclass仍然为True)
class Fake(Tombola):
    """该类继承抽象基类Tombola,同时并没有实现全部的抽象方法,该类实例化会报错"""
    def load(self,iterable):
        self.items = list(iterable)

    def pick(self):
        pass

# Tombola的第一个子类
class BingCage(Tombola):

    def __init__(self,items):
        self._randomizer = random.SystemRandom()
        self._items = []
        self.load(items)

    def load(self,items):
        self._items.extend(items)
        self._randomizer.shuffle(self._items)

    def pick(self):
        try:
            return self._items.pop()
        except IndexError:
            raise LookupError('pick from empty BingCage')

    # 实现对象可直接调用
    def __call__(self):
        self.pick()


class LotteryBlower(Tombola):

    def __init__(self,iterable):
        # 通过list创建一个iterable的拷贝,从而对balls的操作,不会影响到传递进来的对象
        self._balls = list(iterable)

    def load(self,iterable):
        self._balls.extend(iterable)

    def pick(self):
        try:
            position = random.randrange(len(self._balls))
            return self._balls[position]
        except ValueError:
            raise LookupError('pick from empty LotteryBlower')

    def loaded(self):
        return bool(self._balls)

    def inspect(self):
        return tuple(sorted(self._balls))




if __name__ == '__main__':

    print(issubclass(Fake,Tombola)) # True

    #1.如果Fake没有实现全部抽象方法, 实例化会报错
    print('*************1.如果Fake没有实现全部抽象方法, 实例化会报错**************')
    fake = Fake() #  Can't instantiate abstract class Fake with abstract methods pick

    # 2.如果Fake实现了全部的抽象方法, 就能够正常实例化
    print('*************2.如果Fake实现了全部的抽象方法, 就能够正常实例化**************')
    fake = Fake()

    # 3.BingCage测试
    print('*************3.BingCage测试**************')
    bingcage = BingCage([1,2,3])
    # 返回元素
    print(bingcage.pick()) # 2
    print(bingcage.pick()) # 1
    print(bingcage.pick()) # 3
    #print(bingcage.pick())  # LookupError: pick from empty BingCage

    # 4.LotteryBlower测试
    print('*************4.LotteryBlower测试**************')
    lotter = LotteryBlower(['Maple','Kelly'])
    print(lotter.pick()) # Kelly

6、虚拟子类

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 2024/2/23 20:52
# @Author  : Maple
# @File    : 05-虚拟子类.py
# @Software: PyCharm
import abc
from random import randrange


class Tombola(abc.ABC):
    @abc.abstractmethod
    def load(self,iterable):
        """从可迭代对象中加载元素"""

    @abc.abstractmethod
    def pick(self):
        """随机删除元素,然后将其返回

        如果实例为空,这个方法应该抛出LookupError
        """
    def loaded(self):
        """如果至少有一个元素,则返回True,否则返回False"""
        return bool(self.inspect())


    def inspect(self):
        """返回一个有序元组,由当前元素构成"""
        items = []
        while True:
            try:
                items.append(self.pick())
            except LookupError:
                break
        self.load(items)
        return tuple(sorted(items))

@Tombola.register
class TomboList(list):# 继承list
    """虚拟子类在代码层面,可以不用重写 基类的任何方法,但issubclass和isinstance仍然会判断为真
    --所以全靠自觉 去实现基类的方法"""

    def pick(self):
        if self:
            position = randrange(len(self))
            # pop方法继承自list
            return self.pop(position)

    # load方法
    load = list.extend

    def loaded(self):
        return bool(self)

    def insert(self):
        return tuple(sorted(self))

# 注册虚拟类 除了像上面那样使用装饰器,还可以使用如下方式(更通用):
# Tombola.register(TomboList)



if __name__ == '__main__':

    # 1.虚拟子类测试
    print(issubclass(TomboList,Tombola)) # True
    t_list = TomboList([1,2,3])

    # 2.虚拟子类实例测试
    print(isinstance(t_list,Tombola)) # True

    # 3.虚拟子类继承关系列表只会列出`真实`的超类(如下Tombola并不在列表里)
    #  TomboList继承list,然后list继承object
    print(TomboList.__mro__) # (<class '__main__.TomboList'>, <class 'list'>, <class 'object'>)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值