流畅的python笔记(一)Python数据模型

目录

一、python最好的品质之一是一致性

二、python风格纸牌类实现

三、如何使用特殊方法

四、模拟数值类型

五、小结


一、python最好的品质之一是一致性

没有仔细去查看一致性的具体解释,仅以自己理解,以书上的一个例子来说吧,在python中用len(collection)来得到序列行对象collection的序列长度。len是python的标准库内置函数。对于python中的所有序列数据对象(无论是内置或者自定义类对象),求其长度的方法都是用内置的len函数,而不是有些用collection.size(),有些用collection.length()等。所以当我们看到代码中用了len(collection)时,即便collection是我们没见过的对象,但我们也能马上猜测出其是序列型对象,且我们要求其长度。这便是python代码一致性的表现。

        而python是怎么实现一致性的呢?答案是通过python对象中的特殊方法(也叫魔术方法)。特殊方法是python对象中以双下划线“__”开头和结尾的方法,比如“__getitem__”,“__init__”等。每个特殊方法几乎都跟一个python标准库内置函数或操作符相关联,这里相关联的意思是当我们调用某个内置函数或者使用操作符的时候,python解释器会自动调用对象内的特殊方法。比如特殊方法“__len__”,当我们调用python中的内置函数len(collection)的时候,python解释器会自动调用collection对象内的特殊方法“__len__“。因此,当我们定义自己的类的时候,只要在内部实现“__len__”,那么就可以用标准库函数len来求取该对象的长度,因此也就实现了python代码的一致性。从上边分析也可以看出,特殊方法一般不能直接由对象调用,而是通过标准库函数或操作符来间接调用。

        接下来看几个例子,怎么用魔术方法来写出一致性高的pythonic代码。

二、python风格纸牌类实现

本例子的主要目的是展示__getitem__和__len__这两个魔术方法的实现和作用。

        纸牌类定义如下:

import collections
Card = collections.namedtuple('Card', "rank, suit") # collections模块中是各种容器,这里用namedtuple定义了一个Card类,namedtuple定义得到的是
# 不能改变的字典,即兼具字典和元组的特性。这里我们的Card类有两个属性,rank和suit,其中rank有13中取值可能,代表大小,suit有四种可能,代表花色

class FrenchDeck:
    # 先定义两个类属性
    ranks = [str(n) for n in range(2, 11)] + list("JQKA") # 列表生成式,str()函数将数字类型转换成字符,ranks是一个字符列表,从2到A
    suits = "spades diamonds clubs hearts".split()
    
    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits for rank in self.ranks] # 对象的私有属性,是一个列表,列表中是13*4个Card对象
    def __len__(self):
        return len(self._cards)
    def __getitem__(self, positon):
        return self._cards[positon]

首先我们用collections模块中的namedtuple容器来定义了一个类Card,其本质就是一个不能改变的字典,有两个属性,rank和suit,分别代表大小和花色。

        然后我们定义了纸牌类FrenchDeck,其私有对象属性_card即是一个Card对象的列表,包含52个Card对象。

        可以看到我们实现了两个魔术方法,__len__和__getitem__:

        __len__方法保证我们可以直接用标准库len函数作用于一个FrenchDeck对象,当我们用len函数作用于FrenchDeck对象时,实际上解释器会调用FrenchDeck对象中的魔术方法__len__,因此我们实际计算的时其私有属性self._cards列表的长度。

__getitem__方法让我们可以对FrenchDeck对象进行索引操作。如下所示,deck是一个FrenckDeck对象,当我们对deck进行索引操作的时候,解释器实际上会调用魔术方法__getitem__,由我们类内__getitem__方法的实现可以看出,实际上返回的是self._cards[position],因此对deck做索引操作实际上是对self._cards列表做索引操作。

由上边__len__和__getitem__两个例子可以看出,这两个魔术方法的具体实现会影响到我们用标准库函数或者操作符作用于对象的时候的具体操作,上边我们求长度和索引操作都是针对self._cards列表操作的,如果我们的__len__和__getitem__函数中不是对self._cards操作,而是对另外的列表操作,那么对FrenchDeck对象进行求长度和索引操作就是针对另一个列表的了。

        此外,由于__getitem__方法把【】操作交给了self._cards列表,因此针对self._cards列表的随机选择,切片等功能都可以直接实现。random是python标准库模块,其中有一个函数choice,可以实现从一个序列中随机选取一个元素。

  

最重要的是,如果一个类实现了__getitem__方法,那么这个类就变成可迭代的了。对于可迭代对象,就可以用for in 的方式进行遍历。

 

迭代通常是隐式进行的,也就是有时候表面上看并没有表现出循环迭代的现象,但实际上是进行了迭代的。比如一个集合类型,如果没有实现__contains__方法,那么in运算符就会做一次迭代搜索。因为FrenchDeck类是可迭代的,因此in运算符可以用在FrenchDeck对象上。

还可以对一个FrenchDeck对象进行排序。python中的sorted函数与C++中类似,也可以传入一个函数作为参数来将一个序列排序。如下图所示,对牌的排序要同时考虑点数大小与花色。字典suit_values中即是记录了不同花色的情况,图中是按黑桃,红桃,方块,梅花排序的。下边index是列表对象的方法,用来根据元素值来定位其在列表中的位置,这里是用位置索引来作为不同点数的情况,比如2,其索引就是0,A的索引就是12。然后综合考虑索引和点数,spades_high的参数是要排序的列表中的元素,返回值是根据这个元素计算出来的排序需要考虑因素的量化结果,某个元素对应的结果越大,其在排序后就越靠前。这里是升序排列。

 

由这个纸牌例子可以看出,通过实现__len__和__getitem__这两个方法,我们的自定义类就能像python自带的序列型数据类型一样,可以使用标准库中的诸多函数操作。其实本质就是我们在实现__len__和__getitem__方法的时候,把对自定义类的操作直接交给self._cards这个列表了,所以操作自定义列就像是在操作列表。

三、如何使用特殊方法

对于自定义类,需要通过内置的标准库函数(例如len,iter,str)去处理类对象,这个时候python会自动去调用其对应的特殊方法(比如__len__),不要用对象直接调用特殊方法(比如deck.__len__)。、

        对于python内置类型,比如列表,字符串等,CPython会走捷径,自定义类中的__len__方法会直接返回PyVarObject中的ob_size属性。PyVarObject是表示内存中长度可变的内置对象的C结构体,直接读取这个值要比调用一个方法快的多。

        特殊方法设计出来就是给python解释器调用的,其名字都是固定的,不要自己添加新的特殊方法。

四、模拟数值类型

利用特殊方法,让自定义对象可以通过python自带的运算符进行运算。本质应该还是在特殊方法的实现中把具体的操作委托给python内置对象。

        本例中实现一个二维向量。

向量的运算主要有加法(+)、求模(abs)、数乘(*)等,而这些方法的实现需要借助四个特殊方法:__repr__、__abs__、__add__、__mul__。

from math import hypot # 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):
        return hypot(self.x, self.y)
    
    def __bool__(self):
        return bool(abs(self)) # 因为对象可能是多维的,因此认为只有其模为0的时候才是false

    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)

        

python中有一个内置函数repr,它的作用是把一个对象用字符串的形式表达出来,这就是对象的字符串表示形式。当我们调用repr的时候,解释器就会自动帮我们调用特殊方法__repr__。比如下边的语句:

当我们直接打印一个Vector对象时候,如果我们实现了特殊方法__repr__,那么终端就会输出:

如果我们没有实现特殊方法__repr__,那么终端就会输出:

即只会告诉我们在内存地址的某处有个Vector对象,但是不会告诉我们关于这个对象的更多信息。因此如果我们在一个类中实现了__repr__方法,就可以直接用print打印这个对象的信息了。说到__repr__,就不得不提__str__,这两个特殊方法分别对应两个内置函数repr和str,repr和str函数的用处都是得到一个对象的字符串表示形式,即参数是一个对象,返回值是字符串。只是repr是面向程序员的,开发时候用的,str是面向用户的,更注重可读性。具体区别可参考__str__ 和 __repr__ 的那些事 - 知乎 (zhihu.com)

        通过实现__add__和__mul__,我们的自定义类就可以使用“+”和“*”这两个运算符了,当我们使用这两个运算符时,解释器会自动调用__add__和__mul__方法。这两个运算符都是中缀运算符,从我们的实现中也可以看到,其不改变两个操作对象,然后返回一个新构造的对象。

        默认情况下,我们自定义类的实例用bool判断时总是真的,除非我们自己实现了__bool__或__len__特殊方法。bool(x)的背后时解释器自动调用x.__bool__()的结果,如果没有实现__bool__方法,那么bool(x)会尝试调用x.__len__()。本例中我们的逻辑是当对象的“模”为0时返回False,否则返回True。__bool__也可以用以下方式实现:

但是不能直接用 return self.x or self.y,因为python中的 or 其返回值并不一定时布尔值,比如"4 or 5" , 其返回值是4而不是True。

五、小结

通过实现特殊方法,我们就可以让自定义数据类型表现得像内置类型,可以用同样的内置函数来处理自定义类型对象和内置类型对象。尤其是对序列型数据类型的模拟,是特殊方法用的最多的地方。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值