Python抽象基类:ABC谢谢你,因为有你,温暖了四季!

博客 分享
0 250
优雅殿下
优雅殿下 2022-04-23 18:58:22
悬赏:0 积分 收藏

Python抽象基类:ABC谢谢你,因为有你,温暖了四季!

本文记录自己学习《Python Tricks: The Book》第四章“Classes & OOP”相关知识点的心得体会,重点就实例方法、类方法和静态方法的区别、抽象基类(Abstract Base Classes,ABC)以及具名元组等内容进行讨论。

Python抽象基类:ABC谢谢你,因为有你,温暖了四季!

  • Python抽象基类:ABC谢谢你,因为有你,温暖了四季!
    • 实例方法、类方法和静态方法
    • 抽象类
    • 具名元组
    • 参考资料

最近阅读了《Python Tricks: The Book》的第四章“Classes & OOP”,这一章节介绍了Python对面向对象编程的支持,内容包括“is”和“==”的区别、特殊方法“__str__”和“__repr__”的作用、自定义异常类、浅拷贝深拷贝、抽象基类以及实例方法和类方法的区别等。本文记录自己学习这些知识点的心得体会,重点讨论其中的实例方法、类方法和静态方法的应用场景、抽象类以及具名元组等内容。

实例方法、类方法和静态方法

实例方法、类方法和静态方法是三个不同的概念:

  • 实例方法用于访问和修改类实例的属性,它的第一个参数是“self”。当通过类名调用实例方法时,需要提供类的实例作为参数(传一个对象作为参数);当通过类的实例调用实例方法时,Python会自动把实例方法绑定到调用方,实例方法的“self”参数就是调用它的类实例(可以类比functools.partial?):

    class Pizza:    def __init__(self, size: int):        self.size = size    def get_size(self) -> int:        return self.sizePizza.get_size()  # TypeError, missing argument: 'self'Pizza.get_size(Pizza(25))  # 25Pizza(21).get_size()  # 21,不用显式指定实例方法的“self”参数啦
  • 类方法常被用作工厂方法,用于创建类的实例(可以类比Java中类的构造方法),它的第一个参数是“cls”。类的实例可能有多种构造方案,比如时间类,既可以通过指定年月日时分秒构造实例,也可以通过解析字符串“%Y-%m-%d %H:%M:%S”进行构造,而Python规定了一个类只能定义一个初始化方法“__init__”,这限制了根据类创建实例的灵活性。类方法是缓解该矛盾的有效方案:

    from typing import Listclass Pizza:    def __init__(self, ingredients: List[str]):        self.ingredients = ingredients    def __repr__(self):  # 定义把对象转化为字符串的方法,!r表示调用变量的__repr__方法        return f'Pizza({self.ingredients!r})'    @classmethod  # 类方法使用@classmethod装饰器修饰    def margherita(cls):        """"玛格丽塔,一种披萨的名称"""        return cls(['mozzarella', 'tomatoes'])    @classmethod    def prosciutto(cls):        """火腿披萨"""        return cls(['mozzarella', 'tomatoes', 'ham'])Pizza(['mozzarella', 'tomatoes'])Pizza.margherita()  # Pizza(['mozzarella', 'tomatoes'])Pizza.prosciutto()  # Pizza(['mozzarella', 'tomatoes', 'ham'])
  • 静态方法跟定义在相同模块中的函数没有明显的区别,它不依赖于类变量或实例对象的状态,需要使用“@staticmethod”装饰器修饰:

    from typing import Listimport mathclass Pizza:    def __init__(self, ingredients: List[str]):        self.ingredients = ingredients    @staticmethod    def circle_area(radius: float) -> float:        return radius ** 2 * math.pipizza = Pizza(['mozzarella', 'tomatoes'])pizza.circle_area(4)  # 50.27Pizza.circle_area(4)  # 50.27

抽象类

抽象类是指声明了抽象方法的类,它不能被实例化,但可以被继承。当抽象类被继承时,子类往往需要实现父类的所有抽象方法(如果只实现部分抽象方法,那子类也是抽象类)。抽象类常用于类型检查,即判断给定的类是否由某个基类派生(issubclass())或给定的对象是否为某个基类的实例(isinstance())。

import collectionsissubclass(list, collections.abc.Iterable)  # Trueissubclass(list, collections.abc.Hashable)  # Falsea = [1, 0, 2, 4]isinstance(a, collections.abc.Sequence)  # Trueisinstance(a, collections.abc.Mapping)  # False

此外,抽象类的使用能够让类的层次关系变得清晰,方便代码的开发和维护;抽象类声明接口,子类给出具体实现,子类实例的行为变得可以预期。举例来说,抽象类“collections.abc.Sized”声明了抽象方法“__len__”,Python的内置类型list、tuple、set、dict等都继承自该抽象类,因此可以通过“len()”函数获取这些类型的实例的大小。

早期Python通过在方法的定义体中抛出“NotImplementedError”异常的方式来声明抽象方法,抽象基类(Abstract Base Classes,ABC)出现以后,有了更好的方案。

  1. 通过抛出“NotImplementedError”异常定义抽象类

    class Base:"""基于NotImplementedError异常的抽象类"""def foo(self):    raise NotImplementedError()b = Base()  # 虽然名为抽象类,但还是可以被实例化
  2. 通过继承“abc.ABC”定义抽象类

    import abc  # 导入抽象基类模块class Base(abc.ABC):    """"通过继承abc.ABC定义抽象类"""        @abc.abstractmethod    def foo(self):        """"抽象方法使用@abc.abstractmethod装饰器标记"""        def bar(self):        pass  # 抽象类可以包含具体方法class Concrete(Base):    def foo(self):        print('听我说谢谢你,因为有你,温暖了四季~')b = Base()  # TypeError: Can't instantiate abstract class Base with abstract methods fooc = Concrete()c.foo()  # 听我说谢谢你,因为有你,温暖了四季~

具名元组

元组是不可变的列表,常被用于表示数据的记录(类比Java的Record类型?关系型数据库也把表中的一行数据称为元组)。元组中的元素只能通过索引进行访问,而整型的索引难以表示元素在数据记录中的语义,最终导致代码的可读性变差。为了解决该问题,Python提出了具名元组(Named Tuple),允许通过可读性强的标识符访问元组中的元素。有两种定义具名元组的方式:(1)利用“collections.namedtuple”工厂函数定义具名元组;(2)通过继承“typing.NamedTuple”定义具名元组。

  1. 利用“collections.namedtuple”工厂函数定义具名元组

    namedtuple工厂函数接收一个标识符和字段列表作为参数,返回以该标识符命名的类(是内置类型tuple的子类)。

    from collections import namedtupleCar = namedtuple('Car' , ['color', 'mileage'])tesla = Car('black', mileage=376.5)  # 创建实例时,提供位置参数、关键字参数均可tesla.color  # 'black',现在可以通过标识符访问元组的元素啦tesla._asdict()  # {'color': 'black', 'mileage': 376.5}tesla._replace(color='white')  # Car(color='white', mileage=376.5),通过替换元素构造新的元组Car._make(['white', 376.5])  # Car(color='white', mileage=376.5),通过类方法构造新的元组Car._fields  # ('color', 'mileage')
  2. 通过继承“typing.NamedTuple”定义具名元组

    import typingclass Car(typing.NamedTuple):  # 继承typing.NamedTuple    color: str    mileage: floattesla = Car('black', mileage=376.5)tesla.color  # 'black'tesla._asdict()  # {'color': 'black', 'mileage': 376.5}Car._fields  # ('color', 'mileage')Car._field_types  # {'color': str, 'mileage': float},相比于利用工厂函数定义的方式多了类型信息

具名元组占用的空间与内置的元组类型是相同的,这是我不能理解的地方。尽管字段名列表、类型信息可以绑定到类上面,但是诸如“_asdict()”、“_replace()”这样的实例方法需要绑定到具名元组的实例上,这为什么没有带来空间开销呢?还是说通过“sys.getsizeof()”获得的对象占用内存空间的大小跟想的是不太一样?

import sysa = ('black', 376.5)sys.getsizeof(a)  # 56,普通元组占用56个字节的空间b = Car('black', 376.5)sys.getsizeof(b)  # 56,具名元组同样占用56个字节的空间

除了具名元组,定义数据类的另一种方式是使用“dataclasses”模块,由于篇幅限制,这里就不再展开介绍了。

参考资料

  1. Python Tricks: The Book
  2. 《流畅的Python》,第十一章“接口:从协议到抽象基类”
  3. The definitive guide on how to use static, class or abstract methods in Python
  4. 正确理解Python中的@staticmethod@classmethod方法
  5. Python中的abc模块
posted @ 2022-04-23 18:16 Wan-deuk-i 阅读(0) 评论(0) 编辑 收藏 举报
回帖
    优雅殿下

    优雅殿下 (王者 段位)

    2018 积分 (2)粉丝 (47)源码

    小小码农,大大世界

     

    温馨提示

    亦奇源码

    最新会员