第一章(提炼) Python数据模型

        Python风格的关键完全体现在Python的数据模型上,而数据模型所描述的API,为使用最地道的语言特性来构建开发者自己的对象提供了工具。当Python解析器遇到特殊句法时,会使用特殊方法去激活一些基本的对象操作。特殊方法以双下划线开头,以双下划线结尾(如:__getitem__)。如:obj[key]的背后就是__getitem__方法。魔术方法是特殊方法的昵称,特殊方法也叫双下方法。

一. 一摞Python风格的纸牌

使用__getitem__和__len__创建一摞有序的纸牌(使用collections.namedtuple构建了一个简单的类来表示一张纸牌,自python2.6开始,namedtuple就加入到python里,用以构建只有少数属性但没有方法的类):

import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])


class FrenchDeck:
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    # ♠, ♡, ♣, ♢,
    suits = ['\u2660', '\u2661', '\u2663', '\u2662']

    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, item):
        return self._cards[item]


# 扑克牌排序:2最小,A最大,花色由大到小:♠, ♡, ♢,♣
suit_values = {'\u2660': 3, '\u2661': 2, '\u2662': 1, '\u2663': 0}


def sort_rule(card):
    rank_value = FrenchDeck.ranks.index(card.rank)
    return rank_value * len(suit_values) + suit_values[card.suit]

演示1  我们自定义的FrenchDeck类可以像任何python标准集合类型一样,可以使用len()函数;查看一叠牌有多少张:

演示2  d[0]或d[-1]由__getitem__方法提供:

演示3  random.choice方法用于从一个序列中随机选出一个元素;随机取出一张纸牌:

想必,现在你已经体会到通过python特殊方法来利用Python数据模型的2个好处:

  1. 作为类的用户,无需去记住标准操作的各种名词,如获取长度是.size,还是.length,还是别的什么?
  2. 可以更加方便地利用python的标准库,如random.choice函数。

演示4  因为__getitem__方法把[]操作交给了self.cards列表,所有我们的FrenchDeck实例自动支持切片:

演示5  仅仅实现了__getitem__方法,这一摞牌即可迭代:

反向迭代也OK:

演示6  迭代通常是隐式的,比如一个集合类型没有实现__contains__方法,那么in运算符就会按顺序做一次迭代搜索。因此,in运算符可以用在我们的FrenchDeck实例上,因为它是可迭代的:

演示7 使用python标准库中的sorted函数:

总结:虽然FrenchDeck隐式地继承了object类,但功能却不是继承而来的。通过实现__len__和__getitem__这两个特殊方法,FrenchDeck类就跟一个Python自有的序列数据类型一样,可以体现出Python的核心语言特性(例如迭代和切片)。同时这个类还可用于标准库中诸如random.choice、reversed和sorted这些函数。

演示8  按照目前的设计,FrenchDeck还不支持洗牌,因为它是不可变的:

错误消息相当明确:对象不支持元素赋值。原因是,shuffle函数要调换集合中元素的位置,而FrenchDeck只实现了不可变的序列协议。可变的序列还必须提供__setitem__方法。

演示9  为FrenchDeck打猴子补丁,把它变成可变的,让random.shuffle函数能处理:

备注:Python是动态语言,因此我们可以在运行时添加__setitem__方法,甚至是在交互式控制台中添加。演示9中把set_card函数赋值给特殊方法__setitem__,从而把它依附到FrenchDeck类上。这种技术叫猴子补丁 :在运行时修改类或模块,而不改动源码。猴子补丁很强大,但猴子补丁的代码与要打补丁的程序耦合十分紧密。

        定义set_card函数时使用的参数为deck,position,card;而在类中创建__setitem__方法默认使用的参数是self,key和value。此处这样定义,只是为了说明每个Python方法说到底都是普通函数,把第一个参数命名为self只是一种约定。在控制台会话中使用那几个参数没问题,不过在Python源码文件中最好按照文档那样使用self、key和value。

二. 如何使用特殊方法

        首先明确一点,特殊方法的存在是为了被Python解析器调用的。即没有obj.__len__()这种写法,而是len(obj)。在执行len(obj)时,如果obj是一个自定义类的对象,那么python会自己去调用我们实现的__len__方法。

        然而如果是python内置类型,比如列表、字符串。字节序列等,那么CPython会抄个近路,__len__实际上会返回PyVarObject里的ob_size属性;直接读取这个值比调用一个方法要快得多。很多时候,特殊方法的调用是隐式的,比如for i in x:这个语句其实是调用iter(x),而这个函数的背后是x.__iter__()方法。

        通过内置函数(如:len、iter、str等等)来使用特殊方法是最好的选择。这些内置函数不仅会调用这些方法,通常还提供额外的好处,而且对于内置类型来说,它们的速度更快。

2.1 模拟数值类型

一个简单的二维向量类

from math import hypot


class Vector:
    """自定义二维向量"""
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __repr__(self):
        return f'Vector({self.x},{self.y})'

    def __abs__(self):
        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, scaler):
        return Vector(self.x * scaler, self.y * scaler)

演示1  自定义二维向量的使用

备注:python内置的repr函数能把一个对象用字符串的形式表达出来以便辨认,这就是字符串表示形式。repr是通过调用__repr__这个特殊方法来得到一个对象的字符串表示形式的。交互式控制台和调试程序用repr函数来获取字符串表示形式。__repr__和__str__的区别在于,后者是在str()函数被调用或是在用print函数打印一个对象的时候才调用的,并且它返回的字符串对终端用户更友好。如果你只想实现这2个特殊方法中的一个,__repr__是更好的选择,因为如果一个对象没有__str__函数,而python又需要调用它的时候,解析器会调用__repr__作为替代。

        通过__add__和__mul__为向量类带来了+和*两个运算符。此外还可以使用__radd__和__rmul__实现交换律。

        默认情况下,我们自己定义的类的实例总被认为是真的,除非这个类对__bool__或者__len__函数有自己的实现。bool(x)的背后是调用x.__bool__()的结果;如果不存在__bool__方法,那么bool(x)会调用x.__len__()。若返回0,则bool会返回False;否则返回True。

2.2 特殊方法一览

跟运算符无关的特殊方法
类别 方法名
字符串/字节序列表示形式 __repr__、__str__、__format__、__bytes__
数值转换 __abs__、__bool__、__complex__、__int__、__float__、__hash__、__index__
集合模拟 __len__、__getitem__、__setitem__、__delitem__、__contains__
迭代枚举 __iter__、__reversed__、__next__
可调用模式 __call__
上下文管理 __enter__、__exit__
实例的创建和销毁 __nex__、__init__、__del__
属性管理 __getattr__、__getattribute__、__setattr__、__delattr__、__dir__
属性描述符 __get__、__set__、__delete__
跟类相关的服务 __prepare__、___instancecheck__、__subclasscheck__
跟运算符相关的特殊方法
类别 方法名和对应的运算符
一元运算符 __neg__ -、__pos__ +、__abs__ abs()
比较运算符 __lt__ <、__le__ <=、__eq__ ==、__ne__ !=、__gt__ >、__ge__ >=
算数运算符 __add__ +、__sub__ -、__mul__ *、__truediv__ /、__floordiv__ //、__mod__ %、__divmod__ divmode()、__pow__ **/pow()、__round__ round()
反向算数运算符 __radd__、__rsub__、__rmul__、__rtruediv__、__rfloordiv__、__rmod__、__rdivmod__、
增量赋值算术运算符 __iadd__、__isub__、__imul__、__itruediv__、__ifloordiv、__imod__、__ipow__、、、
位运算符 __invert__ ~、__lshift__ <<、__rshift__ >>、__and__ &、__or__ |、__xor__ ^
反向位运算符 __rlshift__、__rrshift__、__rand__、__ror__、__rxor__
增量赋值位运算符 __ilshift__、__irshift__、__iand__、__ior__、__ixor__
发布了132 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Geroge_lmx/article/details/105273051