流畅的python-python 数据模型(魔法方法)

该博客系列是本人阅读Luciano Ramalho 的《流畅的python》的笔记或者思考,为了便于理解加入了部分自己的理解,由于水平有限,难免会有纰漏之处,欢迎指正。


文章目录

  • 第一章 python 数据模型
    • 1.python中的魔法方法
    • 2. 如何使用特殊方法
    • 3.其余常用特殊方法
    • 4.为什么len不是普通方法?


在开始本章内容之前,请先思考这样一个问题,为什么在获取序列长度时,python使用len(collection)而不是collection.len()?

第一章 python 数据模型

Python最好的品质就是一致性
数据模型事实上是对python框架的描述,规范了这门语言自身构建模块的接口。

1.python中的魔法方法

魔法方法(特殊方法)名字以两个下划线开头,以两个下划线结尾(例如__getitem__)。

特殊方法的存在是为了被Python解释器调用的,这些特殊方法提供了自己实现程序的接口。例如obj[key]的背后就是__getitem__方法,为了能求得my_collection[key]的值,解释器实际上会调用my_collection.__getitem__(key)。如果my_object是一个自定义类的对象,在执行len(my_object)的时候,解释器调用的是你实现的__len__方法,示例如下:

import collections
# 首先创建了一个Card类,nametuple可以方便创建只有属性的简单类
Card = collections.namedtuple('Card', ['rank', 'suit'])


# 构建了一个所有扑克牌的类
class FrenchDeck:
    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 suit in self.suits for rank in self.ranks
        ]

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

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


# 先看一个Card示例长啥样
print(Card('J', 'hearts'))

deck = FrenchDeck()  # 实例化类对象
# 当调用len()函数的时候,事实上自动对应的是__len__魔法方法
print('扑克牌个数为: ', len(deck))
# 当索引的时候,事实上自动对应的是__getitem__魔法方法
print('第一张扑克牌为:', deck[0])

输出为:

Card(rank='J', suit='hearts')
扑克牌个数为:  52
第一张扑克牌为: Card(rank='2', suit='spades')

__getitem__方法带来的不仅是直接索引,类似于切片可迭代等操作。

2. 如何使用特殊方法

  1. 必须明确的是,特殊方法的存在是为了被Python解释器调用的,你自己并不需要调用它们。也就是说没有my_object.__len__()这种写法,而应该使用len(my_object)。在执行len(my_object)的时候,如果my_object是一个自定义类的对象,那么Python会自己去调用其中由你实现的__len__方法。事实上,除非有大量的元编程存在,在自己代码中也尽可能减少直接调用特殊方法的频率,当然除了类的初始化方法__init__方法。

  2. 通过内置的函数(例如len、iter、str,等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好处,而且对于内置的类来说,它们的速度更快。以__len__为例,Python内置的类型,比如列表(list)、字符串(str)、字节序列(bytearray)等,那么CPython会抄个近路,__len__实际上会直接返回PyVarObject里的ob_size属性。PyVarObject是表示内存中长度可变的内置对象的C语言结构体。直接读取这个值比调用一个方法要快很多。

  3. 不要自己想当然地随意添加特殊方法,比如__foo__之类的,因为虽然现在这个名字没有被Python内部使用,以后就不一定了。

3.其余常用特殊方法

为了便于说明,我们自己定义一个向量类(当然,内置的complex对象已经实现了向量的功能):

from math import hypot


class Vector:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __repr__(self):
        return 'Vector(%r,%r)' % (self.x, self.y)

    def __abs__(self):
        # hypot返回欧几里德范数 sqrt(x*x + y*y),当然也可以自己定义
        return hypot(self.x, self.y)

    def __bool__(self):
        return bool(abs(self))

    def __add__(self, other):
        x = self.x + other.x
        y = self.y + other.y
        return Vector(x, y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)


v = Vector(3, 4)
print(abs(v))  # 调用__abs__魔法方法
print(v * 3)  # 在标量乘积的时候调用__mul__,而在打印新得到的向量时,调用__repr__
v2 = Vector(2, 1)
print(v + v2)  # 在+的时候调用__add__,而在打印新得到的向量时,调用__repr__

输出:

5.0
Vector(9,12)
Vector(5,5)

虽然代码里有6个特殊方法,但这些方法(除了__init__)并不会在这个类自身的代码中使用。即便其他方法要使用这个类的其余方法,也不会直接调用它们,就像bool(abs(self))中求向量的模长,应该使用abs而不是特殊方法__abs__,尽管bool(self.__abs__(self))是可行的。前面已经说过,一般只有Python的解释器会频繁地直接调用这些方法。接下来看一下出现的其他常用特殊方法:

  1. __repr__

    Python有一个内置的函数叫repr,它能把一个对象用字符串的形式表达出来以便辨认,这就是“字符串表示形式”。repr就是通过__repr__,这个特殊方法来得到一个对象的字符串表示形式的。

  • __repr__和__str__的区别

    __str__是在str函数被使用,或是在用print函数打印一个对象的时候才被调用的,它返回的字符串对终端用户更友好。更为笼统的说,__str__是面向用户的,而__repr__是面向程序员的。
    如果你只想实现__repr__和__str__中的一个,__repr__是更好的选择,因为如果一个对象没有__str__函数,而Python又需要调用它的时候,解释器会用__repr__作为替代,所以看起来__repr__可以实现的功能更多一点。

  1. __add__和__mul__

    通过__add__和__mul__,为向量类带来了+和*这两个算术运算符。

  2. __bool__

    bool(x)的背后是调用x.__bool__()的结果;如果不存在__bool__方法,那么bool(x)会尝试调用x.__len__()。若返回0,则bool会返回False;否则返回True。当然我们可以采用如下的形式更为高效:

    bool(self.x or self.y)
    
  3. item系列

    __getitem__(self, item)            对象通过 object[key] 触发
    __setitem__(self, key, value)    对象通过 object[key] = value 触发
    __delitem__(self, key)            对象通过 del object[key] 触发
    
  4. 非数值系类

类别 方法名
字符串/字节序列表示形式 __repr__、__str__、__format__、__bytes__
数值转换 __abs__、__bool__、__complex__、__int__、__float__、__hash____index__
集合模报 __len__、__getitem__、__setitem__、__delitem__、__contains__
迭代枚举 __iter__、__reversed__、__next__
可调用模拟 __call__
上下文管理 __enter__、__exit__
实例创建和销毁 __new__、__init__、__del__
属性管理 __getattr__、__getattribute__、__setattr__、__delartr____dir__
属性描述符 __get__、__set__、__delete__
跟类相关的服务 __ prepare__、__instancecheck__、__subclasscheck__

大多数的都可以根据特殊方法名知道与具体函数或方法的联系,此处针对几个重要的进行说明:

  • __contains__ 在使用 in 运算符时被调用

  • __new__、__init__的区别

    __new__是用来构造实例的,__init__对返回的实例进行一些属性的初始化,我们在写一个类的时候首先都会写一个__init__方法去初始化变量,却很少使用__new__,因此就容易忽略__new__,其实在我们继承基类object时同时从基类中继承了__new__方法,所以就不需要重新在子类中实现,而仅需要对变量进行初始化:

FooParent.__init__(self) # 或者使用super
super(Child,self).__init__("data from Child")
  • ​ __enter__、__exit__

    __enter__与__exit__是实现with的类特殊方法,主要用作文件打开中:

    with open("file.txt", "r") as f:
        f.readlines()
    

    __enter__:初始化后返回实例
    __exit__:退出时做处理,例如清理内存,关闭文件,删除冗余等

  • __setattr__、__getattr__、__getattribute__与__delattr__
    在类定义中,可以通过传入参数,赋值给self来定义类的属性,当实例化之后就不能更改它的属性了,如果想获取、添加、删除属性怎么办?这就用到这里要讲的4个特殊方法,__setattr__、__getattr__、__getattribute__与__delattr__,它们的功能分别是:

    方法 功能
    __setattr__ 设置属性
    __getattribute__ 该方法可以拦截对对象属性的所有访问企图,不管该属性存在不存在,当属性被访问时,自动调用该方法(只适用于新式类)。因此常用于实现一些访问某属性时执行一段代码的特性。如果访问属性不存在的时候随后会调用
    __getattr__ 调用不存在的属性首先调用__getattribute__方法(如果该方法未定义,会调用基类的__getattribute__方法),触发AttributeError异常并自动捕获,然后才调用__getattr__方法。
    __delattr__ 删除属性

    更加详细的内容可以参加我的这篇博客:python中的__setattr__、getattr、__getattribute__与__delattr__方法

  1. 数值型特殊方法

    类别 方法名和对应的运算符
    一元运算符 _neg_ -、_ pos_ +、_abs_ abs ()
    众多比较运算符 _It_ <、_le_ <=、_eq_ ==、_ne_ ! =、_gt_ >、_ge_ >=
    算术运算符 _add_ +、_sub_-、_mul_ *、_truediv_/、_floordiv_//、_mod_%、_divmod_divmod ()、_pow__**或pow()、_round_ round ()
    反向算术运算符 _radd_、_rsub_、_rmul_、_rt rue div_、_rfloordiv_、_rmod_、_rdivmod_、_rpow_
    增量赋值算术运算符 _iadd_、_isub 、_imul_、_itruediv_、_ifloordiv_、_imod_、_ipow_
    位运算符 _invert_ ~、_Ishift_ <<、_rshift_ >>、_and_ &、_or_ |、_xor_ ^
    反向位运算符 _rlshift_、_rrshirt_、_rand_、_rxor_、_ror_
    增量赋值位运算符 _ilshift_、_irshift_、_iand_、_ixor_、_ior_
  2. 其余特殊方法

    运算 代码 特殊方法
    类析构函数 del instant _del_
    格式化字符串 format(x, format_spec) _format_
    遍历迭代器 iter(list) _iter_
    取迭代器下一个值 next(list) _next_
    列出类的所有属性和方法 dir(instance) _dir_
    自定义散列值 hash(instance) _hash_
    自定义拷贝 copy.copy(instance) -copy_
    自定义深层拷贝 copy.deepcopy(instance) _deepcopy_

4.为什么len不是普通方法?

len之所以不是一个普通方法,是为了让Python自带的数据结构可以走后门,abs也是同理。但是多亏了它是特殊方法,我们也可以把len用于自定义数据类型。这种处理方式在保持内置类型的效率和保证语言的一致性之间找到了一个平衡点,也印证了“Python之禅”中的另外一句话:“不能让特例特殊到开始破坏既定规则。”

事实上,只需要认为abs和len都是一元运算符。这样也就解释了为什么在获取序列长度时,python使用len(collection)而不是collection.len()

你可能感兴趣的:(流畅的python,python技巧)