热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

《FluentPython》学习笔记:第1章Python数据模型

Python最好的品质之一是一致性。len(colleciton)而不是collection.len()写法所代表的庞大的设计思想,是形成我们通常说的“Python

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

len(colleciton)而不是collection.len()写法所代表的庞大的设计思想,是形成我们通常说的“Python风格”(Pythonic)的关键。


特殊方法

特殊方法的名字以两个下划线开头,以两个下划线结尾(例如__getitem__)。
比如obj[key]的背后就是__getitem__方法,为了能求得my_collection[key]的值,解释器实际上会调用my_collection.__getitem__(key)
魔术方法(magic method)是特殊方法的昵称。


1.1 一摞Python风格的纸牌

import collectionsCard = collections.namedtuple('Card', ['rank', 'suit']) # 等号前后的Card可不一致,一致是为避免混淆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]

__getitem__


  • 实现__getitem__方法后,该对象即可iterable,可以被当作可迭代的对象使用,具体的可迭代类型,看该方法的返回值
  • 可以使用位置索引 [n]来访问序列中的值,另外,因为 __getitem__方法把[]操作交给了 self._cards列表,所以自动支持切片操作。
  • 对象可迭代,如可以用 for i in obj迭代。当调用 for i in obj时,其实用的是 iter(obj),调用的 __iter__方法。但是如过没有实现 __iter__方法,那么它会令 position0开始递增,直到触发 IndexError结束,且只能是 IndexError类型的 Error,否则触发错误后会引发异常。
  • 可以用in运算符(即使没实现 __contains__,它会按顺序做一次迭代搜索)
  • 可以用random.choice()方法来随机获取一个元素(用这个方法还需要额外实现 __len__)

namedtuple

出处:Python标准库collections
生成了一个class,用于构建只有少数属性但是没有方法的对象,比如数据库条目。它相当于于下面的代码,但有且只能有属性rank和suit,无法再动态绑定任何新的属性和方法了(元组无法修改)。这使得原始数据的含义依然能被保留,增加可读性和便捷性。

class Card:def __init__(self, rank, suit):self.rank = rankself.suit = suit

语法:
collections.namedtuple(typename, field_names, *, verbose=False, rename=False, module=None)

typename:实际上就是你通过namedtuple创建的一个元组的子类的类名,通过这样的方式我们可以初始化各种各样的实例化元组对象。
field_names:类似于字典的key,在这里定义的元组可以通过这样的key去获取里面对应索引位置的元素值,这样的key可以是列表,也可以是用空格、/和逗号这样的分隔符隔开的字符串。
rename:如果rename指定为True,那么你的field_names里面不能包含有非Python标识符,Python中的关键字以及重复的name,如果有,它会默认给你重命名成‘_index’的样式,这个index表示该name在field_names中的索引,例:['abc', 'def', 'ghi', 'abc']将被转换成['abc', '_1', 'ghi', '_3']

# Basic example of namedtuple
Point = collections.namedtuple('Point', ['x', 'y'])
p = Point(11, y=22) # instantiate with positional or keyword arguments
print(Point.__doc__) # Point(x, y)
print(p.__doc__) # Point(x, y)
print(p) # Point(x=11, y=22)
print(p[0], p[1]) # 11 22 可以使用索引去获取namedtuple里面的元素x, y = p # 支持分包
print(x, y) # 11 22print(p.x + p.y) # 33 使用对应的字段名字也可以获取namedtuple里面的元素p.x = 33 # AttributeError: can't set attribute 属性值只读,不可修改

FrenchDeck 这个类跟任何标准Python 集合类型一样,可以用len() 函数来查看一叠牌有多少张:

deck = FrenchDeck()
print(len(deck)) #52

从一叠牌中抽取特定的一张纸牌,比如说第一张或最后一张,是很容易的:deck[0]deck[-1]。这都是由__getitem__方法提供的:

deck[0] #Card(rank='2', suit='spades')
deck[-1] #Card(rank='A', suit='hearts')
print(type(deck[0])) #
print(type(deck[:])) # 仅仅实现了__getitem__ 方法,这一摞牌就变成可迭代的了

我们需要单独写一个方法用来随机抽取一张纸牌吗?没必要,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') for v in deck: # 等同于 for v in deck[:]:print(v) # Card(rank='2', suit='spades') ...print(type(v)) # ...

现在已经可以体会到通过实现特殊方法来利用Python 数据模型的两个好处。


  • 复用你写的类时,无需去记忆标准操作的各式名称(“怎么得到元素的总数?是.size()还是.length()还是别的什么?”)。
  • 可以更加方便地利用Python 的标准库,比如random.choice函数,从而不用重新发明轮子。

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

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

按照黑桃最大、红桃次之、方块再次、梅花最小的规则来给扑克牌排序的函数,梅花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) # Card(rank='2', suit='clubs') ... Card(rank='A', suit='spades')

1.2 如何使用特殊方法


  • 特殊方法的存在是为了被Python 解释器调用的,你无需使用my_object.__len__()调用它们,应该使用len(my_object)
  • 使用len(my_object)时,如果my_object是自定义类的对象,会调用其__len__方法;如果是Python 内置的类型(如ist、str、bytearray等),CPython 会直接返回PyVarObject 里的ob_size属性,这比调用一个方法要快很多。
  • 很多时候,特殊方法的调用是隐式的,比如for i in x:这个语句,背后其实用的是iter(x),而这个函数的背后则是x.__iter__()方法。当然前提是这个方法在x 中被实现了。
  • 不要随意添加特殊方法,因为虽然现在这个名字没有被Python 内部使用,以后就不一定了。

Iterable: 有迭代能力的对象,一个类,实现了__iter__,那么就认为它有迭代能力,通常此函数必须返回一个实现了__next__的对象,如果自己实现了,你可以返回self,当然这个返回值不是必须的;
Iterator: 迭代器(当然也是Iterable),同时实现了__iter__和__next__的对象,缺少任何一个都不算是Iterator。

import collectionsCard = collections.namedtuple('Card', ['rank', 'suit'])class FrenchDeck:ranks = [str(n) for n in range(2, 11)] + list('JQKA')suits = 'spades diamonds clubs hearts'.split()start = 0def __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 __iter__(self):return selfdef __next__(self):if self.start>-len(self._cards): # 这里为了有所区别,进行反向遍历self.start -= 1return self._cards[self.start] else:raise StopIteration # 不想迭代时,抛出StopIteration异常(for语句会捕获此异常,并结束循环)doct = FrenchDeck()
for v in doct:print(v) # Card(rank='A', suit='hearts') ...

1.3 特殊方法一览

特殊方法一览


1.4 为什么len不是普通方法

如果x是一个内置类型的实例,那么len(x)的速度会非常快。背后的原因是CPython 会直接从一个C 结构体里读取对象的长度,完全不会调用任何方法。

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


1.5 本章小结

通过实现特殊方法,自定义数据类型可以表现得跟内置类型一样,从而让我们写出更具表达力的代码——或者说,更具Python 风格的代码。


1.6 延伸阅读


推荐阅读
author-avatar
Steven
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有