Python数据模型
Python数据模型
一致性:
1、python语言具有良好的一致性,比如获取字符创长度可以使用len()方法,获取列表(数组)长度也可以使用len()方法。在其他 语言中,对于不同的数据类型可能有不同的获取长度方法,在java中获取字符串长度使用 字符串名.length(),而获取列表长度使用list.size()。
2、Python能这样做,背后其实是一些特殊方法提供的支撑。这些特殊方法的名字以两个下划线开头,并以两个下划线结尾(如__len__, __getitem__),这些特殊的方法能够使自己定义类能够支持迭代、集合、属性访问、运算符重载、函数和方法调用、对象的销毁和创建、字符串表示形式和格式化、管理上下文。
特殊方法初体验
下面用一个个简单的例子来展示__len__, __getitem__的用法,有一副扑克牌我们希望能够获取有多少张牌,以及能够通过下标拿到一张牌。
from itertools import product
#声明Card类,表示扑克中的一张牌
class Card(object):
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit
def __repr__(self):
return "Card(rank=%s, suit=%s)" % (self.rank, self.suit)
def __str__(self):
return "(%s, %s)" % (self.rank, self.suit)
#声明Poker类,表示一副扑克牌
class Poker(object):
suits = ["spade", "heart", "diamond", "club"]
ranks = [str(i) for i in range(2, 11)] + list("JQKA")
def __init__(self):
self._cards = [Card(*agrs) for agrs in product(self.ranks, self.suits)]
def __len__(self):
return len(self._cards)
def __getitem__(self, position):
return self._cards[position]
首先声明了一个简单的类Card来表示一张牌,这种只具有属性不具有方法的类,还可以用具名元组来构建
collections.namedtuple("Card", ["rank", "suit"])
实现了特殊方法__len__,那就可以用len()函数来查看一副扑克里有多少张牌:
>>> poker = Poker()
>>> len(poker)
52
实现了特殊方法__getitem__,那就通过下标从一副牌中抽取一张牌:
#这里Card实现了__repr__方法,所以打印的是Card的字符串表示形式
>>> poker[0]
Card(rank=2, suit=spade)
>>> poker[-1]
Card(rank=A, suit=club)
#将__repr__方法注释掉
>>> poker[-1]
<test.Card object at 0x000001E59568D438>
对于随机获取获取一张牌,我们不需要自己写一个方法来随机获取,python提供了一个从序列中随机获取元素的函数random.choice:
>>> from random import choice
>>> choice(poker)
Card(rank=Q, suit=heart)
从choice的源码中可以看出,return seq[i]使用了下标访问,最终还是调用了__getitem__这个特殊方法:
def choice(self, seq):
"""Choose a random element from a non-empty sequence."""
try:
i = self._randbelow(len(seq))
except ValueError:
raise IndexError('Cannot choose from an empty sequence') from None
return seq[i]
在重写的__getitem__方法中,把[]操作交给了self._cards列表,所以Poker类也是支持切片操作的:
>>> poker = Poker()
>>> poker[:6]
[Card(rank=2, suit=spade), Card(rank=2, suit=heart), Card(rank=2, suit=diamond), Card(rank=2, suit=club), Card(rank=3, suit=spade), Card(rank=3, suit=heart)]
在Pycharm中打断点可以看到,切片操作时,__getitem__实际是拿到了slice对象,然后交给了self._cards列表处理
另外实现了__getitem__方法,poker就变为可迭代(反向迭代)的了:
>>> for card in poker:
... print(card)
(2, spade)
(2, heart)
(2, diamond)
#反向迭代
>>> for card in reversed(poker):
... print(card)
(A, club)
(A, diamond)
(A, heart)
(A, spade)
虽然这里没有实现__contains__方法,但是也可以使用in运算符。是因为当没有实现__contains__方式时,in运算就会迭代搜索,而Poker是可迭代的,所以也自动的支持in运算。
#使用具名元组表示一张牌,在Poker实现__getitem__方法后可直接使用in运算,因为元组有判断相等的方法
>>> Card("2","spade") in poker
True
>>> Card("2","spades") in poker
False
#使用Card类表示一张牌,在Poker实现__getitem__方法后不可直接使用in运算,因为没有判断相等的方法
>>> Card("2","spade") in poker
False
#需要在Card类,重写__eq__方法
def __eq__(self, other):
return self.rank == other.rank and self.suit == other.suit
#再使用in运算
>>> Card("2","spade") in poker
True
>>> Card("2","spades") in poker
False
对于排序可以写一个排序方法,然后将这个方法传入sorted,即可实现升序排序:
def custom_sort(card):
suit_values = {"spade": 3, "heart": 2, "diamond": 1, "club": 0}
rank_value = Poker.ranks.index(card.rank)
return rank_value * len(suit_values) + suit_values[card.suit]
>>> sorted(poker, key=custom_sort)
[Card(rank=2, suit=club), Card(rank=2, suit=diamond), Card(rank=2, suit=heart), Card(rank=2, suit=spade), Card(rank=3, suit=club), Card(rank=3, suit=diamond), Card(rank=3, suit=heart), Card(rank=3, suit=spade), ....]
通过上面这个例子可以感受到python数据模型有两个好处:
1、各种数据模型的操作是统一的,如果需要特殊定制化处理,重写相应的特殊方法即可
2、可以更加方便的使用python标准库,不用重复造轮子
特殊方法的使用
对于自己编写的类,想要支持python标准操作就需要实现特殊方法,然而并不需要调用他们(如:obj.len()),而是通过统一的函数去使用len(obj)
模拟向量
下面通过实现一些特殊方法来模拟一个二维向量
class Vector(object):
def __init__(self, x=0, y=0):
self.x = x
self.y = y
#实现加法
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
#取模
def __abs__(self):
return hypot(self.x, self.y)
#数与向量的乘积
def __mul__(self, scalar):
return Vector(self.x * scalar, self.y * scalar)
def __repr__(self):
return "Vector(%s, %s)" % (self.x, self.y)
def __bool__(self):
return abs(self)
>>> vector = Vector(1, 2)
Vector(1, 2)
>>> vector1 = Vector(1, 3)
Vector(1, 3)
>>> print(vector)
Vector(1, 2)
>>> print(vector * 2)
Vector(2, 4)
>>> print(vector + vector1)
Vector(2, 5)
>>> print(bool(vector))
True
>>> print(abs(vector))
2.23606797749979
__mul__和__add__为Vector提供了*和+这两个算术运算
__abs__为Vector提供了模长计算
__bool__为Vector提供了自定义的真假判断,可以改写为如下所示,省去了模长运算,执行效率更高
def __bool__(self):
return bool(self.x or self.y)
__repr__把对象转化成字符串展示,且返回的字符串应该是准备的、无歧义的,能够尽可能的表达出如何用代码创建这个被打印的对象
__repr__和__str__区别: __str__在str()函数中被调用,或是在print函数打印时调用,如果一个对象没有__str__函数,而python又需要调用它的时候,解释器就会用__repr__代替
特殊方法列表
与运算符无关的特殊方法
类别 | 方法名 |
---|---|
字符串、字节序列表示形式 | __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__,__subclasschek__ |
与运算符相关的特殊方法
类别 | 方法名 |
---|---|
一元运算符 | __eng__ -,__pos__ +,__abs__ abs() |
比较运算符 | __lt__ <,__le__ <=,__eq__ ==,__ne__ !=,__gt__ >,__ge__ >= |
算术运算符 | __add__ +,__sub__ -,__mul__ *,__delitem__,__truediv__ /, __floordiv__ //,__mod__ %,__divmod__ divmod(),__pow__ **或pow(),__round__ round() |
反向算术运算符 | __radd__,__rsub__,__rmul__, __rtruediv__,__rfloordiv__,__rmod__, __rdivmod__,__rpow__ |
增量赋值运算符 | __iadd__,__isub__,__imul__,__itruediv__,__ifloordiv__,__imod__,__ipow__ |
位运算符 | __invert__ ~,__lshift__ <<,__rshift__ >>,__and__ &,__or__ |
反向位运算符 | __rlshift__,__rrshift__,__rand__,__ror__,__rxor__ |
增量赋值位运算符 | __irlshift__,__irrshift__,__irand__,__iror__,__irxor__ |
ps:当交换两个操作数位置时就会调用反向运算符(b * a而不是a * b);增量赋值运算符就是a *= b