流畅的Python读书笔记(一)


文章目录

  • 流畅的Python读书笔记(一)
  • Python数据模型
  • 一摞Python风格的纸牌
  • 准备
  • 开始看代码吧
  • 先来创建我们的类
  • 参考资料



以下叙述中有部分是笔者杜撰的,已有特别说明。

Python数据模型

Python最好的品质之一就是一致性

所谓一致性,笔者目前的理解是:对操作具有的统一表述。比如,在Python中,获取列表元素个数的语句为len(list),而如果你具有面向对象的编程经验,可能会疑惑,为什么不使用list.len()。当你对Python的这一性质有了更好的理解后,便会发现原因。

笔者自己也对这一性质没有太深的理解,感觉这两种写法都不是太奇怪。


  • 数据模型:数据模型其实是对 Python 框架的描述,它规范了这门语言自身构建模块的接口,这些模块包括但不限于序列、迭代器、函数、类和上下文管理器。理论性的东西,读起来就很抽象

上述定义中的Python框架在书中没有给出定义,那就自己凭当前的理解造一个吧。我们编写Python程序后,只是写好了一些文本,这些文本如何处理,以及需要运用哪些工具来处理,这都是需要考虑的问题。Python框架就是处理Python语言过程中需要使用的工具以及事先制定好的处理规则等的集合

上面提到了处理规则这一点,这也是本篇笔记的重点。书中给出的例子是这样的:

Python解释器碰到特殊的句法时,会使用特殊方法来激活一些基本的对象操作,这些特殊方法的名字一般是以两个下划线开头,两个下划线结尾(例如__getitem__)。比如obj[key]的背后实际上就是调用对应的__getitem__方法。为了能够求得my_collection[key]的值,解释器会调用my_collection.__getitem__(key)。需要强调的是,这是解释器自己去转换执行的。


  • 特殊方法(魔术方法):简单理解,特殊方法就是上面所讲的以两个下划线开头,两个下划线结尾的方法。特殊方法有一个昵称魔术方法。也可根据其方法命名的特点,称其为双下方法(dunder method)
  • 有了特殊方法,你便可以对python中的某些特殊语法,进行定制化,即按照自己的想法构建程序处理逻辑。
  • 魔术方法到底有哪些具体的用处呢?书中给出如下说明:
  • 这些特殊方法名能让你自己的对象实现和支持以下的语言构架,并与之交互:

• 迭代

• 集合类

• 属性访问

• 运算符重载

• 函数和方法的调用

• 对象的创建和销毁

• 字符串表示形式和格式化

• 管理上下文(即 with 块)

一摞Python风格的纸牌

书中通过这个例子来向我们展示如何构建一个自定义的类并在类中实现__getitem__和__len__这两个特殊方法。

准备

代码中调用了collections这个库,并主要使用了其中的collections.namedtuple类。这里先简单介绍一下collections.namedtuple类,对于这个库,很有必要熟练掌握,这里推荐一篇知乎博文【万字长文详解】Python库collections,让你击败99%的Pythoner - 知乎 (zhihu.com)

namedtuple直译过来可以称其为可命名元组。在Python内置的元组类型中,我们只能够通过下标索引的方式访问元组中的元素。而可命名元组,可以对元组中的每个元素指定一个字段名,并且通过字段名去访问对应的元组元素。

举个栗子,现在有一个Python元组tp = ('Tom', 'Cat', '华生的家')。这个元组中tp[0]表示动物昵称,tp[1]代表动物种类,tp[2]代表动物住所。显然,通过下标索引的方式访问数据不够直观,可以给每个元素一个字段名称。比如['name', 'species', 'address']。现在,我们便可以通过访问到动物的名字了,依次类推,tp.species,tp.address会访问到动物的什么属性呢?

开始看代码吧

由于阅读代码是一件很有意义的事,特别是通过自己的耐心阅读能够感受到作者编码的逻辑,这很重要,后面的代码就仅仅只是粘贴吧。主要是代码写注释太麻烦了,读懂一遍之后再看,基本不需要注释。

先来创建我们的类

import collections

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

class FrenchDeck:
    ranks = [str(rank) for rank in range(2, 11)] + list('JQKA')
    suits = 'spades diamonds clubs hearts'.split()

    def __init__(self):
        self._cards = [Card(rank, suit) for rank in self.ranks for suit in self.suits]

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

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

对于上述代码中的__len__和__getitem__可以先不看,主要是理解其他代码。

理解好后,先创建一个实例对象,然后打印看看会是什么吧。

deck = FrenchDeck()
print(deck)
#结果为:
#<__main__.FrenchDeck object at 0x0000017F5DBD4370>

显然,上述结果目前看来,对我们没啥太大帮助。

接着重点来介绍那两个魔术方法吧。

  • __len__

可以获取纸牌的总张数:

len(deck)
# 上述函数调用结果为:52

这个调用的处理逻辑是怎样的呢?实际上,我们前面提到的Python框架就是将len(deck)转换为deck.__len__()的调用。查看FrenchDeck.__len__的函数体return len(self._cards),可以发现,实际上就是转换成了求卡牌列表的元素数量,而卡牌列表的数据类型是Python内置的列表类型。

  • __getitem__
print(deck[0])
#结果为:
#Card(rank='2', suit='spades')

__getitem__的命名可以退出,该魔术方法的作用是:取出元素。就是对obj[key]这样的特殊语法进行转换。比如这里就是转换成了deck.__getitem__(0)。实际处理过程,看函数体就能明白。


__getitem__还需要说明其他三点:

  • 因为__getitem__方法把 [] 操作交给了 self._cards 列表,所以我们的 FrenchDeck 类自动支持切片(slicing)操作
deck[:3] #只取前三张牌
deck[48:] #只取牌面为A的4张牌
#这里需要理解类中的那个列表推导式即self._cards的生成过程
  • 另外,仅仅实现了__getitem__方法,这一摞牌就变成可迭代的了(包括正向和反向迭代)。
for card in deck: #正向迭代
    print(card)

for card in reversed(deck): #反向迭代
    print(card)

#输出结果就不在展示
  • 迭代通常是隐式的,譬如说一个集合类型没有实现__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):
    rank_value = FrenchDeck.ranks.index(card.rank)
    return rank_value * len(suit_values) + suit_values[card.suit] 

for card in sorted(deck, key=spades_high):
    print(card)

参考资料

  • Fluent Python by Luciano Ramalho (O’Reilly). Copyright
    2015 Luciano Ramalho, 978-1-491-94600-8.(流畅的Python)