Python进阶_数据模型

[TOC]

Python数据模型

数据模型 ?:对Python框架的描述,规范了语言自身构建模块的接口。

一致性:任何获取对象长度的方法都是 len()Pythonic

如何达到一致性的呢?

对任何对象 obj,在使用 len(obj) 时,Python解释器会调用特殊方法,即 obj._len_()。只需要重写 __len__ 方法,就能够使用通用的 len() 来获取对象长度。

Magic Method

特殊方法。

__getitem & __len

  1. len(obj) and obj.__len__()
  2. obj[key] and obj.__getitem__(key)

Example 1:Card

1
2
3
4
5
6
7
import collections
# collections.namedtuple()用于构建只有少数属性而没有方法的对象。
Card = collections.namedtuple('Card', ['rank', 'suit'])

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

collections.namedtuple(, [])用于构建只有少数属性而没有方法的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 牌堆类
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]

>>> deck = FrenchDeck()
# 获取牌堆长度,直接使用len()
>>> len(deck)
52
# 获取特定位置的牌,直接使用[]
>>> deck[0]
Card(rank='2', suit='spades')
# 随机抽牌,直接使用random.choice()
>>> from random import choice
>>> choice(deck)
Card(rank='3', suit='spades')

''' French playing cards (jeu de cartes) are cards that use the French suits of trèfles (clovers or clubs♣), carreaux (tiles or diamonds♦), cœurs (hearts♥), and piques (pikes or spades♠). Each suit contains three face cards; the valet (knave or jack), the dame (lady or queen), and the roi (king). '''

可以看出,magic method有以下好处:

1、对于任意类,直接使用 magic method 就可以完成类的标准操作,不需要考虑获取长度是.length()还是.size()等形式。

2、可以使用像 random 这样的内置库达到定义的类的标准操作,不需要重写方法。

由于 class deck__getitem__ 方法将操作 [] 的对象定义为 self._cards,因此__getitem__方法对应的操作 [] 能够在 class deck 上实现纸牌的切片,实际上是 self._cards 的切片。

1
2
3
4
5
6
# deck的切片
>>> deck[:3]
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades'), Card(rank='4', suit='spades')]

>>> deck[11::13]
[Card(rank='K', suit='spades'), Card(rank='K', suit='diamonds'), Card(rank='K', suit='clubs'), Card(rank='K', suit='hearts')]

同样,__getitem__方法也导致 class deck 可迭代(__iter__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# deck的迭代和反向迭代
>>> for card in deck:
print(card)

Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
Card(rank='4', suit='spades')
''''''

>>> for card in reversed(deck):
print(card)

Card(rank='A', suit='hearts')
Card(rank='K', suit='hearts')
Card(rank='Q', suit='hearts')
''''''
# 实现`in`方法
>>> Card('Q', 'hearts') in deck
True
>>> Card('1', 'hearts') in deck
False

如何实现排序?

按照点数:min = 2, max = A,按照花色:spades > hearts > diamonds > clubs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义纸牌的位置权重
suit_values = dict(spades = 3, hearts = 2, diamonds = 1, clubs = 0)
def spades_high(card):
# FrenchDeck.ranks已按大小排好序,只需获取card.rank的下标即可
rank_value = FrenchDeck.ranks.index(card.rank)
return rank_value * len(suit_values) + suit_values[card.suit]

# sorted()函数调用位置权重计算函数,进行排序
>>> for card in sorted(deck, key = spades_high):
print(card)


Card(rank='2', suit='clubs')
Card(rank='2', suit='diamonds')
Card(rank='2', suit='hearts')
''''''
Card(rank='A', suit='diamonds')
Card(rank='A', suit='hearts')
Card(rank='A', suit='spades')

洗牌功能使用__setitem__实现。

通过实现__len____getitem__class FrenchDeck就几乎等同于Python数据类型:列表,能够实现列表的排序、切片等操作。


Attention!

实现如此简便的纸牌类的关键有两个:

1、唯一内置隐藏属性对应着 List 这一数据类型,使得具有形式:deck = FrenchDeck()deck[:3],简洁;

2、重载 magic method 使得类的操作具有普遍性,Pathonic。

magic method在除元编程以外的环境下很少用到,这是Python解释器调用的;

元编程:使用代码生成代码

对于__len__来说,CPython中直接读取 PyVarObject (可变内存对象)的C语言结构体的 ob_size属性,更快;


运算符重载

Example 2:Vector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)

def __abs__(self):
return (self.x**2 + self.y**2)**0.5

def __bool__(self):
return bool(self.x or self.y)

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)


if __name__ == "__main__":
try:
a, b = input("Please input the x, y of the vector: ").split()
c, d = input("Please input the x, y of the vector: ").split()
v1 = Vector(int(a), int(b))
v2 = Vector(int(c), int(d))
except:
print("Error!")
else:
print("=" * 80)
print("v1 + v2 = {}".format(v1 + v2))
print("v1 * 3 = {}".format(v1 * 3))
print("bool(v1) = {}".format(bool(v1)))
print("|v1| = {}".format(abs(v1)))

Vector类重定义了__add____mul____bool____abs____repr__内置方法,使得利用运算符+的向量的加法、利用*的向量的数乘、利用abs()的向量取模符合定义,并且可以利用bool()判断向量是否为非零向量。


Attention!

  1. 使用__repr__表达对象:
1
2
3
4
5
6
7
8
9
# 一般直接输出对象是这样的:
>>> v1
<__main__.Vector object at 0x00000246E3C96CC0>
# 使用repr()函数将对象用字符串表出:
>>> repr(v1)
'Vector(0, 0)'
# 直接输入对象名也调用`__repr__`方法:
>>> v1
Vector(0, 0)
repr与str

“Difference between str and repr in Python”(http://stackoverflow.com/questions/1436703/differencebetween-str-and-repr-in-python)是 Stack Overflow 上的一个问题,Python 程序员 Alex Martelli 和 Martijn Pieters 的回答很精彩。

  1. 使用__bool__进行真值判断:
1
2
return bool(self.x or self.y)
# 即为:若self.x对应布尔值为真,则返回self.x的布尔值,反之返回self.y的布尔值

比起使用以下语句更加高效

1
return bool(abs(self))

需要注意的是,我们定义的对象总被认为是 True,除非该对象重载了 __bool____len__

调用顺序:__bool__ -> __len__


Magic Method List

非运算符魔术方法:

类别 方法名
字符串/字节序列表示形式 __repr____str____format____byte__
数值转换 __abs____bool____complex____int____format____hash____index__
集合模拟 __len____getitem____setitem____delitem____comtains__
迭代枚举 __iter____reversed____next__
可调用模拟 __call__
上下文管理 __enter____exit__
实例创建和销毁 __new____init____del__
属性管理 __getattr____getattribute____setattr____delattr____dir__
属性描述符 __get____set____delete__
跟类相关的服务 __prepare____instancecheck____subcalsscheck__

运算符相关特殊方法:

类别 方法名及对应运算符
一元运算符 __neg__-、__pos__+、__abs__
比较运算符 __lt__<、__le__<=、__eq__==、__ne__!=、__gt__>、__ge__>=
算术运算符 __add__+、__sub__-、__mul__*、__truediv__/、__floordiv__//
__mod__%、__divmod__divmod()、__pow__**、__round__round()
反向算术运算符 __radd____rsub____rmul____rtruediv__
__rfloordiv____rmod____rdivmod____rpow__
增量赋值算术运算符 __iadd____isub____imul____itruediv____ifloordiv__<br 、__imod____ipow__
位运算符 __invert__~、__lshift__<<、__rshift__>>、__and__&、__or__
反向位运算符 __rlshift____rrshift____rand____rxor____ror__
增量赋值位运算符 __ilshift____irshift____iand____ixor____ior__

Advantages

为什么要使用魔术方法?

从FrenchDeck类的示例可以知道,Magic Method使得自定义的类(数据模型,以特有方式构建、储存数据)能够在各种操作方法上表现得像内置数据类型一般,这种共同性是Python简洁明了、表达力强的原因之一。

Python语言参考手册,“Data Model”:https://docs.python.org/3/reference/datamodel.html

Martelli's Stack Overflow:http://stackoverflow.com/users/95810/alexmartelli

平衡的艺术

为什么将 len() 作为一个特殊方法?如果 x 是内置类型,那么 len(x) 会直接从C结构体中提取属性,不调用方法,速度非常快。如果不是内置类型,则使用 __len__。因此 len() 作为特殊方法在内置类型的效率和语言的一致性上达成平衡。