Fluent Python(一)序幕 内置方法

1. python数据模型

1.1 一摞Python风格的纸牌

import collections

# namedtuple方法 用以构建只有少数属性但没有方法的对象
# (比如 数据库条目)
Card = collections.namedtuple('Card', ['rank', 'suit'])


class FrenchDeck:
    # 遍历2-10 将n变成字符串后 放入列表
    # 两个列表相加
    ranks = [str(n) for n in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()

    # 依次遍历suit 和rank中的两个参数,以两个参数suit和rank
    # 生成实例_cards并赋值给_cards
    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                         for rank in self.ranks]

    # 计算self的属性_cards实例的长度
    def __len__(self):
        return len(self._cards)

    # 调用内置函数__getitem__() 获取对应位置的卡片
    def __getitem__(self, position):
        return self._cards[position]

与命令行交互

当前目录模块
# 启动python console
# import 模块名(不带后缀.py) // import card1
# 通过card1.函数名 调用模块中的功能 // 

>import card1
>beer_card = Card('7', 'diamonds')
Traceback (most recent call last):
  File "<input>", line 1, in <module>
NameError: name 'Card' is not defined
>beer_card = card1.Card('7', 'diamonds')
>beer_card
Card(rank='7', suit='diamonds')

Card = collections.nametimple('Card', ['rank', 'suit'])
self._cards = [Card(suit, rank) for suit in self.suits
               	   for rank in self.ranks]

所以这里nametumple生成了一个叫Card的类

导入非当前目录模块

告诉解释器除了从自己的默认目录中寻找外,还从我当前的目录寻找

>>> sys.path.append("E:\project\fluentpython\1_1card.py")


从一叠牌中抽取特定的一张纸牌,比如说第一张或最后一张,是很容易

的:deck[0] 或 deck[-1]。这都是由 getitem 方法提供的:

>>> deck[0] Card(rank='2', suit='spades') 
>>> deck[-1] Card(rank='A', suit='hearts')

随机选出一张牌: 使用random.choice

>>> from random import choice 
>>> choice(deck) Card(rank='3', suit='hearts') 
>>> choice(deck) Card(rank='K', suit='spades') 
>>> choice(deck) Card(rank='2', suit='clubs')

使用特殊方法的好处:

  • 作为你的类的用户,他们不必去记住标准操作的各式名称(“怎么

    得到元素的总数?是 .size() 还是 .length() 还是别的什

    么?”)。

  • 更加方便的利用python标准库, 比如random.choice函数,从而不必重复发明轮子

__getitem__方法把[]操作交给了self._cards列表
# 查看最上面三张
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]
# 这里先遍历 花色,再遍历大小 ,但是元组是rank靠前

# 只看A面
# 遍历12:0中 步长为13那项
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'), Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

# 反向迭代
>>> for card in reversed(deck): # doctest: +ELLIPSIS ... print(card) 
    
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
Card(rank='J', suit='hearts')
Card(rank='10', suit='hearts')
Card(rank='9', suit='hearts')
Card(rank='8', suit='hearts')
Card(rank='7', suit='hearts')
Card(rank='6', suit='hearts')
Card(rank='5', suit='hearts')
Card(rank='4', suit='hearts')
Card(rank='3', suit='hearts')
Card(rank='2', suit='hearts')
......

如果想要保证显示所有的结果 而不是...缩略,需要在注释中加上

# doctest: +ELLIPSIS
迭代通常是隐式的,,譬如说一个集合类型没有实现 __contains__ 方 法,那么 in 运算符就会按顺序做一次迭代搜索。于是,in 运算符可以 用在我们的 FrenchDeck 类上,因为它是可迭代的:

>>> Card('Q', 'hearts') in deck 
True 
>>> Card('7', 'beasts') in deck 
False

那么排序呢?我们按照常规,用点数来判定扑克牌的大小,2 最小、A

最大;同时还要加上对花色的判定,黑桃最大、红桃次之、方块再次、

梅花最小。下面就是按照这个规则来给扑克牌排序的函数,梅花 2 的大

小是 0,黑桃 A 是 51:

# 花色=对应的值
suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)

# 升序排列
def spades_high(card):
    # 先对card进行排序
    # 在FrenchDeck排序后的索引中查找card.rank的值
    # 赋值给rank_value
    rank_value = FrenchDeck.ranks.index(card.rank)
    # 返回 排名值 * 花色的值 + 花色值的字典 对应的value值
    return rank_value * len(suit_values) + suit_values[card.suit]

有了 spades_high 函数,就能对这摞牌进行升序排序了:

# deck实例中每个card实例 都用key执行一遍 后按照升序(sirted)返回
>>> for card in sorted(deck, key=spades_high): # doctest:+ELLIPSIS ... print(card)
Card(rank='2', suit='clubs') 
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts') 
... (46 cards ommitted) 
Card(rank='A', suit='diamonds') 
Card(rank='A', suit='hearts') 
Card(rank='A', suit='spades')

虽然 FrenchDeck 隐式地继承了 object 类, 但功能却不是继承而来

的。我们通过数据模型和一些合成来实现这些功能。通过实现 len

getitem 这两个特殊方法,FrenchDeck 就跟一个 Python 自有

的序列数据类型一样,可以体现出 Python 的核心语言特性(例如迭代和

切片)。同时这个类还可以用于标准库中诸如

random.choice、reversed 和 sorted 这些函数。另外,对合成的运

用使得 lengetitem 的具体实现可以代理给 self._cards

这个 Python 列表(即 list 对象)。

在 Python 2 中,对 object 的继承需要显式地写为 FrenchDeck(object);而在 Python 3 中, 这个继承关系是默认的。

如何洗牌

按照目前的设计, FrenchDeck是不能洗牌的, 因为这摞牌是不可变的(immutable) :

卡牌和他们的位置都是固定的, 除非我们破坏这个类的封装性, 直接对_cards进行操作.

第11章会讲到, 其实只需要一行代码来实现__setitem__方法, 洗牌功能就不是问题了.

1.2 如何使用特殊方法

尽量不要使用 __len__()方法,要使用len(sth)的写法
# 如果使用的是list,str,btyearray  CPythopn会抄近道,使用ob_size属性
for i in x:  #背后 iter(x)  # 背后 x.__iter__()

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

1.2.1 模拟数值类型(Vector)

利用特殊方法,可以让自定义对象通过加号“+”(或是别的运算符)进

行运算。第 13 章对此有详细的介绍,现在只是借用这个例子来展示特殊方法的使用。

二维向量

image-20200729234227189

v1 = Vector(2, 4)

v2 = Vector(2, 1)

v1 + v2

1.2.2 字符串表现形式(repr)

from math import hypot

class Vector:

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

    # 返回类名
    def __repr__(self):
        return 'Vector({}, {})'.format(self.x, self.y)
		# return 'Vector(%r %r)' % (self.x, self.y)
    # abs方法
    def __abs__(self):
        return hypot(self.x, self.y)

    # 返回abs实例的布尔值
    def __bool__(self):
        return bool(abs(self))

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

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

__repr__ 可以得到字符串的表现形式, 如果不设置的话,默认打印实例,会指向实例的地址
比如: <Vector object at 0x10e100070>

%r 暗示了向量的构造函数只允许接收数值,不接受字符串

__repr__ 所犯回的字符串应该准确,无歧义,并且尽可能表达出如何用代码创建出这个被打印的对象.

_repr__ 和 __str__ 的区别在于,后者是在 str() 函数被使用,或 是在用 print 函数打印一个对象的时候才被调用的,并且它返回的字 符串对终端用户更友好。

如果你只想实现这两个特殊方法中的一个,__repr__ 是更好的选择, 因为如果一个对象没有 __str__ 函数,而 Python 又需要调用它的时 候,解释器会用 __repr__ 作为替代。

1.2.3 算术运算符(add, mul)

__add__和__mul__ 计算所产生的的结果是一个新的对象,原本的参数self和other对象并不会改变. (中缀运算符的基本原则: 不改变操作对象)

image-20200730232136320

1.2.4 自定义的布尔值(bool)

if, while, and, or等的背后都是调用bool(x),bool(x)的背后是__bool__()

__bool__()本质上是判断参数的模值是否为0
# 如果想让 Vector.__bool__ 更高效,可以采用这种实现:
def __bool__(self): 
    return bool(self.x or self.y)
    
# 能省去从原先的 abs--> __abs__ --> __bool__()

#它不那么易读,却能省掉从 abs 到 __abs__ 到平方再到平方根这 些中间步骤。通过 bool 把返回类型显式转换为布尔值是为了符合 __bool__ 对返回值的规定,因为 or 运算符可能会返回 x 或者 y 本身的值:若 x 的值等价于真,则 or 返回 x 的值;否则返回 y 的值

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:跟运算符相关的特殊方法

image-20200730234441683

image-20200730234448110

Tips

当交换两个操作数的位置时,就会调用反向运算符(b * a 而不是 a * b)。增量赋值运算符则是一种把中缀运算符变成赋值 运算的捷径(a = a * b 就变成了 a *= b)。第 13 章会对这两 者作出详细解释。

1.4 为什么len不是普通方法

如果被len()操作的对象是一个类型的实例,操作速度会非常快
背后的原因是 CPython 会直 接从一个 C 结构体里读取对象的长度,完全不会调用任何方法。获取一个集合中元素的个数是一个很常见的操作,str,list,memoryview等类型上,这个操作必须高效

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

Tips:

如果把 abs 和 len 都看作一元运算符的话,你也许更能接受 它们——虽然看起来像面向对象语言中的函数,但实际上又不是函 数。有一门叫作 ABC 的语言是 Python 的直系祖先,它内置了一个 # 运算符,当你写出 #s 的时候,它的作用跟 len 一样。如果写成 x#s 这样的中缀运算符的话,那么它的作用是计算 s 中 x 出现的次 数。在 Python 里对应的写法是 s.count(x)。注意这里的 s 是一个 序列类型。

1.5 本章小结

通过实现特殊方法,自定义数据类型可以表现的跟内置类型一样,从而让我们写出更具表达力的代码,或者说,更Pythonic的代码.
python对象的一个基本要求是,他要有合理的表现形式.这一点__repr__和__str__可以满足这个要求.
前者方便我们调试和记录日志.
后者是给终端用户看的.
这就是数据模型中存在特殊方法__repr__和__str__的原因


对序列数据类型的模拟是特殊方法用得最多的地方,这一点在 FrenchDeck 类的示例中有所展现。在第 2 章中,我们会着重介绍序列 数据类型,然后在第 10 章中,我们会把 Vector 类扩展成一个多维的 数据类型,通过这个练习你将有机会实现自定义的序列。

Python 通过运算符重载这一模式提供了丰富的数值类型,除了内置的那 些之外,还有 decimal.Decimal 和 fractions.Fraction。这些数据 类型都支持中缀算术运算符。在第 13 章中,我们还会通过对 Vector 类的扩展来学习如何实现这些运算符,当然还会提到如何让运算符满足 交换律和增强赋值。 

Python 数据模型的特殊方法还有很多,本书会涵盖其中的绝大部分,探 讨如何使用和实现它们。

1.6 延伸阅读

一本是《Python 参考手册(第 4 版)》8,另一本是与 Brian K.

Jones 合著的《Python Cookbook(第 3 版)中文版》。

数据模型还是对象模型

这正好是“Python 数据模型”所要描述的概念。我在本

书中一直都会用“数据模型”这个词,首先是因为在 Python 文档里对

这个词有偏爱,另外一个原因是 Python 语言参考手册中与这里讨论

的内容最相关的一章的标题就是“数据模

魔术方法

考虑一下 JavaScript,情况就正好反过来了。JavaScript 中的对象有

不透明的魔术般的特性,而你无法在自定义的对象中模拟这些行

为。比如在 JavaScript 1.8.5 中,用户的自定义对象不能有只读属

性,然而不少 JavaScript 的内置对象却可以有。因此在 JavaScript

中,只读属性是“魔术”般的存在,对于普通的 JavaScript 用户而

言,它就像超能力一样。2009 年推出的 ECMAScript 5.1 才让用户

可以定义只读属性。JavaScript 中跟元对象协议有关的部分一直在

进化,但由于历史原因,这方面它还是赶不上 Python 和 Ruby。

元对象

The Art of the Metaobject ProtocalAMOP)是我最喜欢的计算机图

书的标题。客观来说,元对象协议这个词对我们学习Python 数据模

型是有帮助的。元对象所指的是那些对建构语言本身来讲很重要的

对象,以此为前提,协议也可以看作接口。也就是说,元对象协

议是对象模型的同义词,它们的意思都是构建核心语言的 API。

一套丰富的元对象协议能让我们对语言进行扩展,让它支持新的编

程范式。AMOP 的第一作者 Gregor Kiczales 后来成为面向方面编程

的先驱,他写出了一个 Java 扩展叫 AspectJ,用来实现他对面向方

面编程的理念。其实在 Python 这样的动态语言里,更容易实现面向

方面编程。现在已经有几个 Python 框架在做这件事情了,其中最重

要的是 zope.interface(http://docs.zope.org/zope.interface/)。第11 章的延伸阅读里会谈到它。

posted on 2020-08-17 08:06  sunnywillow  阅读(207)  评论(0编辑  收藏  举报