fluent python笔记——数据模型

数据模型

python最好的品质之一就是一致性,当使用python工作一会儿之后,就可以正确的猜出语句的意思。而这都归功于python的数据模型。

数据模型其实是对python框架的描述,它规范了这门语言自身 构建模块 的接口,这些模块包括序列、迭代器、函数、类和上下文管理器。

python解释器遇到特殊句法时,会使用特殊方法激活对象的基本操作,这些特殊方法的格式为:__xxx__。比如obj[key]背后的方法其实是调用了obj.__getitem__(key)

这种特殊方法,可以使我们自己的对象,实现和支持以下的语言框架,并与之交互:迭代、集合类、属性访问、运算符重载、函数和方法调用、对象创建和销毁、字符串表示形式和格式化、管理上下文。


一、一副纸牌

接下来以实现一摞纸牌的例子,来展示如何实现__getitem____len__这两个特殊方法,见识到特殊方法的强大。

import collections
from random import choice

Card = namedTuple('Card',['rank', 'suit'])    # 定义类型Card

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

    def __init__(self):
        self._card = [Card(rank, suit) for suit in self.suits
                                       for rank in self.ranks]
    def __len__(self):
        return self.len(_card)
    def __getitem__(self,pos):
        return self._card[pos]

obj = CardGenerator()
print(len(obj))          # 获取长度
print(obj[0])            # 获取第一张牌
print(obj[-5:-3])        # 切片
print(choice(obj))       # 随机获取一张牌
print(obj[12::13])       # 先抽索引为12的那张牌,之后每13张抽一张牌

for card in reversed(obj):     # 反向遍历
    print(card)

首先,我们定义了一个有些像C结构体的类型Card,用来描述一张牌。然后,定义了CardGenerator类,用来描述一副纸牌。

我们都知道,自定义类型在没有重载运算符的情况下,不能够输入输出,不能够用标准库中的方法,更不可以迭代。而CardGenerator类,巧妙的运用了列表和定义特殊方法,只用了几行代码就使CardGenerator对象变成了一个,可获取长度的、可访问的、可迭代的、可以使用标准库方法的对象。

suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
def card_weight(card):
    rank_values = CardGenerator.ranks.index(card.rank)
    return rank_values * len(suit_values) + suit_values[card.suit]

for card in sorted(obj,key=card_weight):
    print card

在定义了每张牌的权值之后,也可以通过调用sorted函数对其进行排序。


二、如何使用特殊方法

首先我们需要明确,特殊方法的存在是为了python解释器调用的,而不是我们自己调用。就比如上述代码中的len(obj),实际上是解释器调用了由我们自己实现的 CardGenerator类中的__len__()特殊方法。

而对于python的内置类型如:list、str、bytearray等,len(list)实际上不会调用__len__(),而是会抄近路去调用,由C语言实现的PyVarObject结构体中的ob_size属性。直接调用这个属性会比调用函数快很多哦~

一般而言,许多特殊方法都是隐式调用的,比如for i in obj:这个语句,其实隐式的调用了obj.__iter__()特殊方法。并且大多数特殊方法是通过len、str这些内置函数调用的。

对于熟悉C++的各位,可以把python的特殊方法理解为虚函数

1. 模拟数值类型

我们通过实现一个二维向量类Vector来看一看数值类型是如何定义的。

from math import hypot   # 引入取模函数

class Vector2(object):

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return 'Vector(%r, %r)' % (self.x, self.y)

    def __abs__(self):      # 取模
        return hypot(self.x, self.y)

    def __add__(self, other):
        return Vector2(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar):
        return Vector2(self.x * scalar, self.y * scalar)

    def __bool__(self):
        return bool(abs(self))

首先,值得一提的是,我们定义了__repr__()方法,该方法返回一个对象的字符串以描述这个对象。在没有定义这个方法之前,我们在控制台输入print(Vector2(1, 2))会得到对象的地址,而不是输出对象的内容。在定义了这个特殊方法之后,输入同样内容,在没有定义__str__()的情况下,解释器会调用repr()内置函数,而repr内置函数则会调用对象的__repr__()方法。

之后,我们还定义了__bool__()方法,当我们调用bool()函数时,__bool__()特殊方法便会被调用。对于一个新的数值类型,难免会在条件语句中进行判断,因此__bool__()方法必不可少。当我们需要一个类型的bool值时,python解释器首先会看该类型内有没有__bool__()方法,如果没有则会根据__len__()方法的返回值确定bool值。


3. 总结

  • 特殊方法是为了使自定义类型表现的与内置类型一样而存在的。
  • 特殊方法是python解释器调用的,基本不需要自行调用
  • 特殊方法的调用栈一般为:某函数->解释器自动调用内置函数->内置函数调用特殊方法

关于str与repr

class test(object):
    def __init__(self, val):
        self.val = val
    def __str__(self):
        return 'test(%r)' % (self.val)

a = test(10)

a
>>> <__main__.a at 0x7fa91c314e50>
print(a)
>>> test(10)


class test(object):
    def __init__(self, val):
        self.val = val
    def __repr__(self):
        return 'test(%r)' % (self.val) 

a = test(10)

a
>>> test(10)
print(a)
>>> test(10)

可以看出,对于__str__()而言,输入对象名并不执行该函数,输出的依然是对象地址,输入print时才会执行。

当我们输入print(obj)时,python解释器首先调用str()内置函数,str()调用__str__()特殊方法。

如果当前类没有定义__str__()特殊方法,那么解释器会调用repr()内置函数,调用__repr__()特殊方法。

可以看出,str是处于外层的,repr是处于底层的,若是定义了__str__()__repr__()会被覆盖。因此,str一般面向用户,而repr面向程序员。

你可能感兴趣的:(python)