《流畅的Python》Data model(数据/对象模型)

第一章 Data model

⚠️整本书都是讲解Data model,第一章是一个概述。

⚠️不适合初学者。因为special method和meta programming技巧只是Python代码的一部分。不是全部。

 

目标:学习第一章内容。之后是否深入还是先学习一些其他的知识待定。

 

纸牌的例子 

可以把data model是对Python的框架的描述。
 
无论在哪种框架下写程序,程序员都会花费大量时间操作implement很多的由框架自身调用的方法。
当你使用Python data model时也是一样的。
Python解释器调用特殊方法来运行基本的对象的操作,  这种特殊方法经常被special syntax激活。这种特殊方法的名字的写法:__xxx__。
例如:
object.__getitem__(self, key)这是一个特殊方法。a是一个dict,我们希望读取这个对象的key"a"对应的值。下例使用了a["a"],一个特殊的语法。解释器发现这个语法 就会调用特殊方法type(a).__getitem(a, "a"),来完成相应的读取操作。
>>> a = dict(a=1,b=2)
>>> a
{'a': 1, 'b': 2}
>>> a['a'] == type(a).__getitem__(a, 'a')
True

 

Data model中的special methods name让你的对象执行/实现,支持,交互basic language构架,例如:

  • Iteration
  • Collections 集合类
  • Attribute access
  • Operator overloading 重载操作符
  • 函数和方法的调用
  • Object创建和销毁
  • 字符串格式化
  • Managed contexts被管理的上下文代码(就是with语法块)
 
⚠️这些特殊方法,也被叫做magic method,或者dunder method。只是一种叫法。
 
 
import collections
# 扑克牌类,每张牌有2个属性:数字/字母rank,花色suit
Card = collections.namedtuple('Card', ['rank', 'suit'])

class FrenchDeck(object):
    # 2个类属性,储存牌的rank和suit。
    ranks = [str(n) for n in range(2, 11)] + list('JQKA') #13种rank
    suits = 'spades diamonds clubs hearts'.split()        #4种suit
    # 实例化一个对象,实例变量_cards(数据属性)是一个list,储存了52张Card(类型是namedtule)
    def __init__(self):
        self._cards = [Card(rank, suit) for suit in self.suits
                                        for rank in self.ranks]
        # self._cards = []
        # for suit in self.suits:
        #     for rank in self.ranks:
        #         self._cards.append(Card(rank, suit))

    # 因为内置函数len()的实参类型支持是内置的或序列或集合类型。(也包括collections中的类型)
    # 所以对自定义的类FrenckDeck,想查看实例对象的元素数量,需要重写__len__
    def __len__(self):
        print('The length of deck is %s' % len(self._cards))
        return len(self._cards)  #52
    # 重写__getitem__
    # 因为实例对象deck,使用特殊语法[],解释器就会去找__getitem__这个特殊方法。
    # 因为deck[参数],转化为__getitem__(self, 参数),最后调用self._cart[参数], 所以可以支持切片。
    def __getitem__(self, position):
        return self._cards[position]

deck = FrenchDeck()
print(len(deck))
print(deck[:2])

输出:

The length of deck is 52
52
[Card(rank='2', suit='spades'), Card(rank='3', suit='spades')]

 

因为__getitem__返回的是一个tuple子类的对象,它是iterable的所以可以使用for:

>>> for card in deck:
...     print(card)
...
Card(rank='2', suit='spades')
Card(rank='3', suit='spades')
...

解释:

for value in variable: pass语法的variable是一个iterator对象,它是iter(object)返回的。因此object有2类可能:

  • 是支持迭代协议(有 __iter__() 方法)的集合对象。
  • 或是支持序列协议(有 __getitem__() 方法,且数字参数从 0 开始。本例的deck实例对象有方法__getitem__()

 

 

问题:为每张扑克牌设定一个大小值。然后按照扑克大小排序?

    def spades_high(self,card):
        suit_values = dict(spades=3, hearts=2, diamonds=1, clubs=0)
        # 用ranks的索引表示一张卡牌的rank的大小。从0~12
        rank_value = FrenchDeck.ranks.index(card.rank)
        print(rank_value)
        return rank_value * len(suit_values) + suit_values[card.suit]

rank_value变量的取值范围0~12

len(suit_values): 得到4

所以:rank_value * len(suit_values)的值:0 4 8 12 16 20 24 28 32 36 40 44 48

得到的值,根据不同的牌的花色分别再加0,1,2,3。

所以:

               2 3 4 5   6   7   8   9   10   J   Q   k  A

club:        0  4  8 12 16 20 24 28 32 36 40 44 48

diamons:  1   5  9 13 17 21  25 29 33 37 41 45 49 

hearts:     2  6 10 14 18 22 26 30 34 38 42 46 50

spades:    3  7 11 15 19 23 27 31  35 39 43  47  51

 

#根据扑克大小排序:
for card in sorted(deck, key=deck.spades_high):
     print(card)

 

sorted(iterable, *, key=None)

使用sorted()返回一个按照大小排序的deck列表:

[Card(rank='2', suit='clubs'), Card(rank='2', suit='diamonds'), ...略]

 

 

in运算符

执行一次迭代搜索

>>> Card('Q', 'hearts') in deck True  #True

 

 

小结:

  • FrenchDeck 是继承object类。(implicitly)? 什么是隐式(即使不写FrenchDeck(object),也会继承object。这是Python3的特征)
  • 功能的实现,不是来自继承。而是使用data model和合成的方法来实现的。
  • 通过special methods,FrenchDeck的行为就像一个Python自带的序列数据类型。比如本例子的迭代和切片操作。
  • French类还可以使用标准库中的模块的方法。
  • 感谢合成composition,__len__, __getitem__的实现能够代理/传递所有的工作给一个list object: self._cards

 


 

 

How Special Methods Are Used?

首先,Special Methods是由解释器调用的。

  • 无需写my_object.__len__()这个格式❌,
  • 而是写len(my_object) ✅

如果实例对象是user-defined class的实例,解释器会调用你在类定义中实现的__len__这个实例方法。

 

但是,像list, str, bytearray等等内建类型。解释器为了提高运算速度,Cpython会抄近路,直接调用C语言写的对应的结构体内的值。

 

特殊方法的调用:不直接体现在代码上:implicit! 

例如for语句会调用iter()内置函数返回一个iterator用于迭代。如果参数对象是可迭代的,这个函数会是调用object.__iter__()。

 

除了元编程,一般很少直接使用special method。只有__init__方法用的较多:在子类中调用superclass。

 

尽量使用内置函数来代替special method:

  • 内置函数是一种扩展,除了调用对应的特殊方法,还有提供其他服务。
  • 对应内置类型数据,运行速度更快。可以看本书14.12节。

 

 

1.2.1 Emulating Numeric Types 模拟数值类型

自定义的对象,不能使用运算符,但通过特殊方法,我们能够让自定义对象使用运算符,进行运算。

本节展示了几个特殊对象,它们不会被直接的调用,而是被解释器调用。

 

例子:这是一个二维向量类:

from math import hypot

class Vector:
    def __init__(self, x=0, y=0):
        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):
        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)

 

解释器发现+号运输符会去调用__add__这个特殊方法:

>>> import linshi
>>> from linshi import Vector
>>> v1 = Vector(2, 4)
>>> v2 = Vector(2, 1)
>>> v1 + v2
Vector(4, 5)

 

解释器发现*号运输符会去调用__mul__这个特殊方法:

 

文档3.3.8给出了可以使用的模拟数字类型

 

1.2.2 String Representation

特殊方法__repr__被内建函数repr调用,它返回一个用字符串的形式表现的对象,以方便观察。这就是String Representation

上文举例的Vector类,如果没有定义__repr__方法,在控制台输出一个实例对象时,会输出<模块.类 object at 内存地址编号>的格式:

>>> from linshi import Vector
>>> v1 = Vector(2, 4)
>>> v1
<linshi.Vector object at 0x102a0bd60>

只会时👆这个表现形式。

 

repr就是Representation的前4个字母。repr()就是把一个自定义类的实例对象用自定义的方式来表现。

⚠️__repr__()主要用于调试。

⚠️在__repr__()的实现中,会使用了%r来获取对象的属性。

 

__repr__是__str__的替代。区别是__str__/str()没有返回值,需要配合print。可以用简单的指向操作:

__repr__ = __str__

 

 

1.2.3Boolean Value of a Custom Type

bool(object)会调用object.__boo__()方法

 

Overview of Special Methods

83个特殊方法,其中47个用于运算和比较操作。

 

 

 
 

 

Why len Is Not a Method

len()为什么是特殊方法?作者在论坛上https://www.python.org/doc/humor/#the-zen-of-python回答了这个问题。

本章1.2节也提到了。

when x is an instance of a built-in type. No method is called for the built-in objects of CPython: the length is simply read from a field in a C struct. Getting the number of items in a collection is a common operation and must work efficiently for such basic and diverse types as str, list, memoryview, and so on.

当x是一个内建类型(如list, str)的实例,那么len(x)并不会调用任何方法,即调用__len__,而是直接从一个C语言的结构体内读取对象的长度。

因为从集合中得到元素的数量是一个经常使用的功能,所以通过这种方法让len()变得非常高效,尤其是计算大的集合的时候,速度非常快。

 

另一方面,len()作为特殊方法,可以用于自定义数据类型。在保持内置类型的效率和保证语言的一致性上找到了平衡点。

 

小结

通过特殊方法,自定义数据类型就可以充分利用内置类型的方法。这是一种简化。

 

杂谈:

数据模型

也被称为对象模型。wiki定义对象模型是计算机编程的对象的属性。两者等同。

Magic Methods

等同于special methods。Ruby社区也有这个概念。目的都是利用这个概念来丰富metaobject protocol。但本质不是魔术,只是为了让用户能够使用核心开发者使用的工具。

Metaobjects

元对象协议。

The metaobject part refers to the objects that are the building blocks of the language itself. 

元对象涉及到构建语言本身的对象。

协议可以看成是interface接口。所以元对象协议完全等同于对象模型:一个用于构建核心语言的API。

 

 

 

本章是一个概述data model。后面的章节是:

  • Data Structures
  • Functions as Objects
  • Object-oriented idioms
  • Control Flow
  • 元编程

 

 

 

 

 

 

 

 
posted @ 2019-12-03 10:12  Mr-chen  阅读(778)  评论(0编辑  收藏  举报