第1章:Python 数据模型

流畅的Python

目录

流畅的Python

1、一摞python风格的纸牌

1.2、如何调用 特殊方法

示例:1-2一个简单的二维向量

1.2.1 字符串表示形式

1.3 特殊方法一览

为什么len不是普通方法


1、一摞python风格的纸牌

"""
数据模型其实是对 Python 框架的描述,它规范了这门语言自身构建模块
的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理
器。
Python 解释器碰到特殊的句法时,会使
用特殊方法去激活一些基本的对象操作,这些特殊方法的名字以两个下
划线开头,以两个下划线结尾(例如 __getitem__)。比如
obj[key] 的背后就是 __getitem__ 方法,
这些特殊方法名能让你自己的对象实现和支持以下的语言构架,并与之交互:
    迭代
    集合类
    属性访问
    运算符重载
    函数和方法的调用
    对象的创建和销毁
    字符串表示形式和格式化
    管理上下文(即 with 块)

"""
import collections
from random import choice

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()
    suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)

    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]

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


if __name__ == '__main__':
    deck = FrenchDeck()
    print(len(deck))  # 调用__len__()
    print(choice(deck))  # 随机选择一个元素
    print("deck[0]{}".format(deck[0]))  # deck[0] 或 deck[-1]。这都是由 __getitem__ ()提供的
    """
    现在已经可以体会到通过实现特殊方法来利用 Python 数据模型的两个好处。
        1、作为你的类的用户,他们不必去记住标准操作的各式名称(“怎么得到元素的总数?是 .size() 还是 .length() 还是别的什么?”)。
        2、可以更加方便地利用 Python 的标准库,比如 random.choice 函数,从而不用重新发明轮子。
    
    因为 __getitem__ 方法把 [] 操作交给了 self._cards 列表,所以我们的 deck 类自动支持切片


    迭代通常是隐式的,譬如说一个集合类型没有实现 __contains__ 方
法,那么 in 运算符就会按顺序做一次迭代搜索。于是,in 运算符可以
用在我们的 FrenchDeck 类上,因为它是可迭代的
    """
    # print(deck[:3])
    # print(deck[12::13])
    # # 实现了__getitem__()就变成了可迭代的了
    # for card in deck:
    #     print(card)
    # # 反向迭代
    # for card in reversed(deck):
    #     print(card)
    print(Card('Q', 'hearts') in deck)
    for card in sorted(deck, key=deck.spades_high):
        print(card)

1.2、如何调用 特殊方法

"""
        特殊方法的存在是为了被 Python 解释器调用的,你自己
并不需要调用它们。也就是说没有 my_object.__len__() 这种写
法,而应该使用 len(my_object)。在执行 len(my_object) 的时
候,如果 my_object 是一个自定义类的对象,那么 Python 会自己去调
用其中由你实现的 __len__ 方法
    如果是 Python 内置的类型,比如列表(list)、字符串
(str)、字节序列(bytearray)等,那么 CPython 会抄个近
路,__len__ 实际上会直接返回 PyVarObject 里的 ob_size 属
性。PyVarObject 是表示内存中长度可变的内置对象的 C 语言结构
体。直接读取这个值比调用一个方法要快很多。
    很多时候,特殊方法的调用是隐式的,比如 for i in x: 这个语句,
背后其实用的是 iter(x),而这个函数的背后则是 x.__iter__() 方
法。当然前提是这个方法在 x 中被实现了
    通常你的代码无需直接使用特殊方法。除非有大量的元编程存在,直接
调用特殊方法的频率应该远远低于你去实现它们的次数。唯一的例外可
能是 __init__ 方法,你的代码里可能经常会用到它,目的是在你自
己的子类的 __init__ 方法中调用超类的构造器。
通过内置的函数(例如 len、iter、str,等等)来使用特殊方法是
最好的选择。这些内置函数不仅会调用特殊方法,通常还提供额外的好
处,而且对于内置的类来说,它们的速度更快

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

示例:1-2一个简单的二维向量

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):
        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)
"""

虽然代码里有 6 个特殊方法,但这些方法(除了 __init__)并不会在
这个类自身的代码中使用。即便其他程序要使用这个类的这些方法,也
不会直接调用它们,一般只有 Python 的解释器会频繁地直接调用这些方法。接下一起来看
看每个特殊方法的实现。


"""

1.2.1 字符串表示形式

    

"""

Python 有一个内置的函数叫 repr,它能把一个对象用字符串的形式表
达出来以便辨认,这就是“字符串表示形式”。repr 就是通过
38
__repr__ 这个特殊方法来得到一个对象的字符串表示形式的。如果没
有实现 __repr__,当我们在控制台里打印一个向量的实例时,得到的
字符串可能会是 。

在 __repr__ 的实现中,我们用到了 %r 来获取对象各个属性的标准字
符串表示形式——这是个好习惯,它暗示了一个关键:Vector(1, 2)
和 Vector('1', '2') 是不一样的

__repr__ 和 __str__ 的区别在于,后者是在 str() 函数被使用,
或是在用 print 函数打印一个对象的时候才被调用的,并且它返回的
字符串对终端用户更友好
如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择,
因为如果一个对象没有 __str__ 函数,而 Python 又需要调用它的时
候,解释器会用 __repr__ 作为替代。
"""

v1=Vector(2,2)
v2=Vector(2,1)
print(type(1))
v3=v1 + v2
print(v3)
    """"
    通过 __add__ 和 __mul__,示例 1-2 为向量类带来了 + 和 * 这两个算
    术运算符。值得注意的是,这两个方法的返回值都是新创建的向量对
    象,被操作的两个向量(self 或 other)还是原封不动,代码里只是
    读取了它们的值而已。中缀运算符的基本原则就是不改变操作对象,而
    是产出一个新的值。
    """


自定义的布尔值:

    尽管 Python 里有 bool 类型,但实际上任何对象都可以用于需要布尔值
的上下文中(比如 if 或 while 语句,或者 and、or 和 not 运算
符)。为了判定一个值 x 为真还是为假,Python 会调用 bool(x),这
个函数只能返回 True 或者 False。
默认情况下,我们自己定义的类的实例默为True,除非这个类对
__bool__ 或者 __len__ 函数有自己的实现。bool(x) 的背后是调用
x.__bool__() 的结果;如果不存在 __bool__ 方法,那么 bool(x)
会尝试调用 x.__len__()。若返回 0,则 bool 会返回 False;否则
返回 True。
我们对 __bool__ 的实现很简单,如果一个向量的模是 0,那么就返回
False,其他情况则返回 True。因为 __bool__ 函数的返回类型应该
是布尔型,所以我们通过 bool(abs(self)) 把模值变成了布尔值。
在 Python 标准库的文档中,有一节叫作“Built-in
Types”(https://docs.python.org/3/library/stdtypes.html#truth),其中规定
了真值检验的标准。通过实现 __bool__,你定义的对象就可以与这个
标准保持一致。
如果想让 Vector.__bool__ 更高效,可以采用这种实现:
def __bool__(self):
 return bool(self.x or self.y)
它不那么易读,却能省掉从 abs 到 __abs__ 到平方再到平方根这
些中间步骤。通过 bool 把返回类型显式转换为布尔值是为了符合
__bool__ 对返回值的规定,因为 or 运算符可能会返回 x 或者 y
本身的值:若 x 的值等价于真,则 or 返回 x 的值;否则返回 y 的
值。

 v = Vector([1, False, {"key": "val"}, (1, 2, 3)], ["2", "3", "4"])
 print(bool(v1))
 print(bool(v))  # __bool__如果使用abs方法 则必须为数字否则会报错
 v0 = Vector(0, 1)
 print(bool(v0))  # 只要有一个属性为true 则返回true

 

1.3 特殊方法一览

表1-1:跟运算符无关的特殊方法

类别

方法名

字符串 / 字节序列表示形式

__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____delattr____dir__

属性描述符

__get____set____delete__

跟类相关的服务

__prepare____instancecheck____subclasscheck__

表1-2:跟运算符相关的特殊方法

类别

方法名和对应的运算符

一元运算符

__neg__ -__pos__ +__abs__ abs()

众多比较运算符

__lt__ <__le__ <=__eq__ ==__ne__ !=__gt__ >__ge__ >=

算术运算符

__add__ +__sub__ -__mul__ *__truediv__ /__floordiv__ //__mod__ %__divmod__ divmod()__pow__ ** 或pow()__round__ round()

反向算术运算符

__radd____rsub____rmul____rtruediv____rfloordiv____rmod____rdivmod____rpow__

增量赋值算术运算符

__iadd____isub____imul____itruediv____ifloordiv____imod____ipow__

位运算符

__invert__ ~__lshift__ <<__rshift__ >>__and__ &__or__ |__xor__ ^

反向位运算符

__rlshift____rrshift____rand____rxor____ror__

增量赋值位运算符

__ilshift____irshift____iand____ixor____ior__

⚠️ 当交换两个操作数的位置时,就会调用反向运算符(b * a 而不是 a * b)。增量赋值运算符则是一种把中缀运算符变成赋值运算的捷径(a = a * b 就变成了 a *= b)。

 

为什么len不是普通方法

如果 x 是一个内置类
型的实例,那么 len(x) 的速度会非常快。背后的原因是 CPython 会直
接从一个 C 结构体里读取对象的长度,完全不会调用任何方法。获取一
个集合中元素的数量是一个很常见的操作,在
str list memoryview 等类型上,这个操作必须高效
换句话说, len 之所以不是一个普通方法,是为了让 Python 自带的数据
结构可以走后门, abs 也是同理。但是多亏了它是特殊方法,我们也可
以把 len 用于自定义数据类型。这种处理方式在保持内置类型的效率
和保证语言的一致性之间找到了一个平衡点,也印证了 “Python 之禅
的另外一句话: 不能让特例特殊到开始破坏既定规则。
如果把 abs len 都看作一元运算符的话,你也许更能接受
它们 —— 虽然看起来像面向对象语言中的函数,但实际上又不是函
数。有一门叫作 ABC 的语言是 Python 的直系祖先,它内置了一个
# 运算符,当你写出 #s 的时候,它的作用跟 len 一样。如果写成
x#s 这样的中缀运算符的话,那么它的作用是计算 s x 出现的
次数。在 Python 里对应的写法是 s.count(x) 。注意这里的 s
一个序列类型。

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