专栏首页AI 算法笔记python 数据模型

python 数据模型

本文的代码例子:

https://github.com/ccc013/CodesNotes/blob/master/FluentPython/1_Python%E6%95%B0%E6%8D%AE%E6%A8%A1%E5%9E%8B.ipynb

前言

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

通常在不同框架下写程序,都需要花时间来实现那些会被框架调用的方法,python 当然也包含这些方法,当 python 解释器碰到特殊的句法的时候,会使用特殊方法来激活一些基本的对象操作,这种特殊方法,也叫做魔术方法(magic method),通常以两个下划线开头和结尾,比如最常见的 __init__, __len__ 以及 __getitem__ 等,而 obj[key] 这样的操作背后的特殊方法是 __getitem__,初始化一个类示例的时候,如 obj= Obj() 的操作背后,特殊方法就是 __init__

通过实现 python 的这些特殊方法,可以让自定义的对象实现和支持下面的操作:

  • 迭代
  • 集合类
  • 属性访问
  • 运算符重载
  • 函数和方法的调用
  • 对象的创建和销毁
  • 字符串表示形式和格式化
  • 管理上下文(也就是 with 块)

一摞 Python 风格的纸牌

接下来尝试自定义一个类,并实现两个特殊方法:__getitem____len__ ,看看实现它们后,可以对自定义的类示例实现哪些操作。

这里自定义一个纸牌类,并定义了数字和花色,代码如下所示:

import collections
# 用 nametuple 构建一个类来表示纸牌
Card = collections.namedtuple('Card', ['rank', 'suit'])

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]

其中辅助用到 collections 库的 nametuple ,用来表示一张纸牌,其属性包括数字 rank 和 花色 suit ,下面是对这个 Card 的简单测试:

# 测试 Card
beer_card = Card('7', 'diamonds')
beer_card

接着就是测试自定义的 FrenchDeck 类,这里会调用 len() 方法看看一摞纸牌有多少张:

# 测试 FrenchDeck
deck = FrenchDeck()
len(deck)

然后是进行索引访问的操作,这里测试从正序访问第一张,以及最后一张纸牌的操作:

print(deck[0], deck[-1])

如果想进行随机抽取卡牌,可以结合 random.choice 来实现:

# 随机抽取,结合 random.choice
from random import choice

choice(deck)

由于我们实现 __getitem__ 方法是获取纸牌,所以也可以支持切片(slicing)的操作,例子如下所示:

# 切片
print(deck[:3])
print(deck[12::13])

另外,实现 __getitem__ 方法就可以支持迭代操作:

# 可迭代的读取
for card in deck:
    print(card)

反向迭代也自然可以做到:

# 反向迭代
for card in reversed(deck):
    print(card)
    break

另外,当然也可以自定义排序规则,如下所示:

# 制定排序的规则
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)

总结一下,实现 python 的特殊方法的好处包括:

  • 统一方面的名称,如果有别人采用你自定义的类,不用花更多精力记住不同的名称,比如获取数量都是 len() 方法,而不会是 size 或者 length
  • 可以更加方便利用 python 的各种标准库,比如 random.choicereversedsorted ,不需要自己重新发明轮子

如何使用特殊方法

这里分两种情况来说明对于特殊方法的调用:

  1. python 内置的类型:比如列表(list)、字典(dict)等,那么 CPython 会抄近路,即 __len__ 实际上会直接返回 PyVarObject 里的 ob_size 属性。PyVarObject 是表示内存中长度可变的内置对象的 C 语言结构体,直接读取这个值比调用一个方法要快很多
  2. 自定义的类:通过内置函数(如 len, iter, str 等)调用特殊方法是最好的选择。

对于特殊方法的调用,这里还要补充说明几点:

  • 特殊方法的存在是为了被 Python 解释器调用的。我们不需要调用它们,即不需要这么写 my_object.__len__(),而应该是 len(my_object),这里的 my_object 表示一个自定义类的对象。
  • 通常对于特殊方法的调用都是隐式的。比如 for i in x 循环语句是用 iter(x) ,也就是调用 x.__iter__() 方法。
  • 除非有大量元编程存在,否则都不需要直接使用特殊方法;

接下来是实现一个自定义的二维向量类,然后自定义加号的特殊方法,实现运算符重载。

代码例子如下所示:

# 一个简单的二维向量类
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 __bool__(self):
        return bool(abs(self))
        
    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)

这里除了必须实现的 __init__外,还实现了几个特殊方法:

  • __add__: 加法运算符;
  • __bool__ :用于判断是否真假,也就是在调用bool() 方法;默认情况下是自定义类的实例总是被认为是真的,但如果实现了 __bool__或者 __len__ ,则会返回它们的结果,bool()首先尝试返回 __bool__ 方法,如果没有实现,则会尝试调用 __len__ 方法
  • __mul__ :实现的是标量乘法,即向量和数的乘法;
  • __abs__ :如果输入是整数或者浮点数,返回输入值的绝对值;如果输入的是复数,返回这个复数的模;如果是输入向量,返回的是它的模;
  • __repr__ : 可以将对象用字符串的形式表达出来;

这里要简单介绍下 __repr____str__ 两个方法的区别:

  • __repr__ :交互式控制台、调试程序(debugger)、%str.format 方法都会调用这个方法来获取字符串形式;
  • __str__ :主要是在 str()print() 方法中会调用该方法,它返回的字符串会对终端用户更加友好;
  • 如果只想实现其中一个方法,__repr__ 是更好的选择,因为默认会调用 __repr__ 方法。

接下来就是简单测试这个类,测试结果如下所示:

特殊方法一览

下面分别根据是否和运算符相关分为两类的特殊方法:

和运算符无关的特殊方法

__repr__, __str__,__format__,__bytes__

和运算符相关的特殊方法

__neg__ -, __pos__ +,__abs__ abs()

这里有两类运算符要解释一下:

  • 反向运算符:交换两个操作数的位置的时候会调用反向运算符,比如 b * a 而不是 a * b
  • 增量赋值运算符:把一种中缀运算符变成赋值运算的捷径,即是 a *= b 的操作

为什么 len 不是普通方法

len 之所以不是普通方法,是为了让 Python 自带的数据结构变得高效,前面也提到内置类型在使用 len 方法的时候,CPython 会直接从一个 C 结构体里读取对象的长度,完全不会调用任何方法,因此速度会非常快。而在 python 的内置类型,比如列表 list、字符串 str、字典 dict 等查询数量是非常常见的操作。

这种处理方式实际上是在保持内置类型的效率和保证语言的一致性之间找到一个平衡点。

小结

本文介绍了两个代码例子,说明了在自定义类的时候,实现特殊方法,可以实现和内置类型(比如列表、字典、字符串等)一样的操作,包括实现迭代、运算符重载、打印类实例对象等,然后还根据是否和运算符相关将特殊方法分为两类,并列举出来了,最后也介绍了 len 方法的例子来说明 python 团队是如何保持内置类型的效率和保证语言一致性的。

本文分享自微信公众号 - 算法猿的成长(AI_Developer),作者:kbsc13

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 如何用栈实现浏览器的前进和后退?

    这里先介绍一下栈的定义和实现,并介绍它的一些常用的应用,最后再简单实现一个简单的浏览器前进和后退的操作。

    kbsc13
  • Python-100例(5-6) 排序&斐波那契数列

    这次是分享 Python-100 例的第五和第六题,分别是排序和斐波那契数列问题,这两道题目其实都是非常常见的问题,特别是后者,一般会在数据结构的教程中,讲述到...

    kbsc13
  • [GAN学习系列3]采用深度学习和 TensorFlow 实现图片修复(下)

    http://bamos.github.io/2016/08/09/deep-completion/

    kbsc13
  • TF-IDF

    简单来说,向量空间模型就是希望把查询关键字和文档都表达成向量,然后利用向量之间的运算来进一步表达向量间的关系。比如,一个比较常用的运算就是计算查询关键字所对应的...

    easyAI
  • java基础-抽象类抽象方法

    你好! 这篇文章将讲述java中的抽象类和抽象方法的知识点,这个是最简单的,也是最容易被遗忘的。

    编程大道
  • PHP 面向对象 抽象类

    使用 abstract 关键字申明抽象类和抽象方法。抽象类不能被实例化,只能被其他类继承。 abstract class A { // 抽象方法没有函数...

    康怀帅
  • “抽象类”到底抽不抽象?实例对比一看便知!

    最近在学习C#和Java的抽象类和接口时搞得头疼,今天在这里和大家分享一下Java和C#中的抽象类到底是怎么样的存在,是否真的像名称那样“抽象”?

    灰小猿
  • python pexpect

    py3study
  • 继续电商网站的购物车

    这一阶段的内容会比较枯燥,没办法啊。纯业务逻辑这块,写不出情趣来。有兴趣的就耐着性子看看,没耐性的可以等等看,改天写个有趣的内容之类的 我这个简单的购物车从功能...

    web前端教室
  • 原创:R语言18讲 (二)

    在学习R语言数据分析之前,我们对于R这个软件需要做一些简单的了解,这样对于我们以后编程和数据分析有很大的帮助,简单是说,就是大致了解一下R软件的界面的和操作特性...

    小莹莹

扫码关注云+社区

领取腾讯云代金券