流畅的Python(第二版)伴读之 1.1 Python数据模型[1/3]

由于Python的历史很长(诞生于1991年),一直有版本的迭代和优化,因此Python语言是一种自洽性很强的计算机语言,语法自成体系。对于一直以来学习和使用Python的人来说,会形成一种编程习惯,俗称Pythonic——Python风。

但是熟悉C++、Java或其他OOP语言的人会对Python的一些用法不习惯,例如一个集合元素的计数,Python用内置的len()函数:len(collection),而不是调用集合对象的len方法collection.len()。这种区别,是Python语言体系的特点之一,这里的语言体系就是Python数据模型,即日常编程用到的API集合。

如何理解Python数据模型呢?可以把Python语言看成一个完整的产品,而数据模型就是这个产品的组成部件,这些部件包括序列、函数、迭代器、协程(功能类似多线程,但更轻量)、类、上下文管理器(如闭包)等。

维基百科上,对象模型的概念是:某种计算机语言中对象的属性集合。Python数据模型的概念,其实就是Python的数据和语法,也即Python对外(对程序员)的所有API的统称。

在Python中,构建类的时候,编程人员会花相当多的时间编写一些特殊方法,这些方法被Python自动调用,而不是在其他代码中手动显式调用。这些被自动调用的方法的方法名,通常由双下划线开始和结束。例如:当代码中读取obj[key]这个变量时,会自动调用obj对象内的 __getitem__ 这个特殊方法,即Python解释器根据程序员写的代码,自动调用相应的方法。

这类特殊方法,称为魔术方法(Magic method,也有人把__getitem__读作dunder getitem,dunder是双下划线的简洁说法)。

当我们想实现以下功能时,通常会用到魔术方法:

  • 集合的操作
  • 获取或设置对象的属性
  • 元素的迭代与遍历(包括异步迭代)
  • 运算符重载
  • 某些函数或方法的调用
  • 字符串形式(即print一个对象时,显示的字符串)
  • 用await进行异步编程
  • 实现对象的构造函数和析构函数
  • 使用with管理上下文(或异步的async with)
用Python编写Card Deck纸牌游戏

例1-1是一段简单代码,其中包含了两个魔术方法:__getitem__ 和 __len__。

例1-1 用集合表示的扑克牌

import collections
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()

    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]

这个FrenchDeck类描述了一副扑克牌(不含大小王),ranks是点数,包括2~10、JQKA;suits是花色,包括黑桃、方片、梅花、红桃。

代码中,首先使用collections.namedtuple创建了一个card命名元组(namedtuple,是轻量级的类,详情请参考官方文档)。命名元组对象和类对象相似,包含了一些属性字段,它还类似数据库里的一条记录。

个人认为,命名元组namedtuple与类相比,优势仅限于轻量,代码可以少写一点点,但对于初学者,理解和用法上不太容易。通常情况对性能要求并不非常严格,因此如果不想了解命名元组,直接用类就可以。待以后用到数据库编程,返回大量数据集的时候,再用命名元组可以大幅降低资源使用,提高效率(但多数情况下还是可以不用,因为有ORM)。

命名元组创建的card对象的使用如下:

>>> beer_card = Card('7', 'diamonds')
>>> beer_card
Card(rank='7', suit='diamonds')

示例中的核心类是FrenchDeck,它封装了一些重要信息。这个类可以像普通的集合那样,使用len()函数来返回其内部封装的card对象的数量:

>>> deck = FrenchDeck()
>>> len(deck)
52

使用序号下标写法可以返回card元素,这是因为它调用了__getitem__方法。例如获取第一个和最后一个card对象:

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

如果需要一个随机card,并不需要单独写一个方法。Python有个内置函数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__魔术方法,所以若不实现这个魔术方法,choice函数根本不知道如何选择元素)。

但是魔术方法的优势可不止于此。

因为__getitem__相当于重载了“[ ]”运算符,deck对象就自然而然的支持切片操作。下例演示了获取牌垛的前三张牌,从第12张牌开始隔13张取一张牌以获取所有Ace:

>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]
>>> deck[12::13]
[Card(rank='A', suit='spades'), Card(rank='A', suit='diamonds'), Card(rank='A', suit='clubs'), Card(rank='A', suit='hearts')]

__getitem__还让deck对象具有了可迭代性:

>>> for card in deck: # doctest: +ELLIPSIS
...        print(card)
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')

还可先翻转deck,再迭代:

>>> for card in reversed(deck): # doctest: +ELLIPSIS
...        print(card)
Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')

有时候,迭代行为是默默进行的:使用in操作符判断一个序列是否包含某个元素时,如果没有定义__contains__方法,就会遍历整个序列,直至找到这个元素。本例中,FrenchDeck类是可迭代的,下面的操作就运用这个规则:

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

纸牌的排序规则,通常是先按照点数来排序(注意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]

此处把点数和花色进行换算,得出权重值。其实更简单的方法是使用元组比较大小,元组排序时,先按第一个元素排序,第一个元素相同时,按照第二个元素排序。例如(2,50)、(3,20)、(3,30)就是正序排列。

把spades_high函数名作为参数传给sorted,就可以实现deck的升序排序:

>>> for card in sorted(deck, key=spades_high): #spades_high后面一定不要加()
...        print(card)
Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
... (此处省略46 cards )
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')

虽然FrenchDeck隐式继承了object类,但是其绝大多数功能是数据模型和语法体系实现,而不是因为继承于object。通过实现__len__和 __getitem__,让FrenchDeck具有了传统序列的特征,可用于迭代、切片、标准库函数(诸如random.choice、reversed和sorted等)。Python通过__len__和__getItem__将对deck的一系列操作传递给了self._card这个序列型的属性字段。

如何洗牌?

目前为止,FrenchDeck还不能将牌打乱顺序,因为牌序还是固定的。当然可以通过硬编码直接修改_card属性。在第13章,将通过__setitem__魔法方法搞定这个问题。

本文仅是第一章的三分之一,篇幅太长让人看了就想睡觉,所以分开发更好,校对起来也有耐心。致意! 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值