python小技巧-1

在阅读《流畅的python》以及《深入理解python特性》时发现这两本书都提及了python的一个库:collection.namedtuple,这个库可以快速的创建一个tuple对象并且可以为其命令,而且输出查看该对象值时,tuple中是以key=value的形式存储,方便了用户的使用,最主要的是:这个对象在内存中所消耗的字节数与普通tuple一样!

>>> from collections import namedtuple
>>> Card = namedtuple('Card',"rank suit")
>>> Card

>>> type(Card)

>>> ranks = [str(n) for n in range(2,11)] +list("JQKA")
>>> suits = 'spades diamonds clubs hearts'
>>> cards = [Card(rank,suit) for rank in ranks for suit in suits]
>>> cards
[Card(rank='2', suit='s'), Card(rank='2', suit='p'), Card(rank='2', suit='a'), Card(rank='2', suit='d'), Card(rank='2', suit='e'), Card(rank='2', suit='s'), Card(rank='2', suit=' ') 
 后面省略

可以发现使用了namedtupe后,每个tuple有了名称,而且存储格式为key=value的形式。

同时快速创建牌组的列表的推到式使用于笛卡尔积这样的形式,快速的构建list,无论是一个变量,还是变量;

关于tuple

都知道tuple中存储的是不能轻易修改的变量值,那么下面这段代码修改后会发现什么情况呢?

>>> t = (1,2,[30,40])
>>> t[2]
[30, 40]
>>> t[2]+=[50,60]

情况1:由于tuple存储的是不可修改的变量,修改会发生错误;

情况2:由于list是可变对象,可以修改成功;而且没有什么错误提示;

可惜的是,修改后既提示了错误,也让用户修改了其中可变对象的值

>>> t = (1,2,[30,40])
>>> t[2]
[30, 40]
>>> t[2]+=[50,60]
Traceback (most recent call last):
  File "", line 1, in 
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])

关于list

最近学习list的时候发现一个有趣的现象,那就是我切片的时候,切片范围比赋予的值多时,发现居然没有赋值的那一块部分的值居然消失了,留下代码记录一下:

>>> a = [1,2,3,4,5,6,7]
>>> a[2:5]=[33,44]
>>> a
[1, 2, 33, 44, 6, 7]

可以说切片功能十分强大,可以对序列进行嫁接、切除或就地修改操作。

关于dict

dict可以说是python中最强大的数据结构,因为他可以很方便的通过key值查到value;但是它花费的代价却是很大的,相比于list来说;

>>> import sys
>>> a = set()
>>> b = {}
>>> sys.getsizeof(a)
232
>>> sys.getsizeof(b)
248
>>> c = []
>>> sys.getsizeof(c)
72
>>> d = ()
>>> sys.getsizeof(d)
56

可以发现一个字典要消耗248字节,集合要消耗232字节,list只需要72字节,而tuple只需要花费56字节!

关于dict的底层实现,下边是书中的话语,已经解释的十分详细,无需我再添油加醋;

散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。 在一般的数据结构教材中,散列表里的单元通常叫作表元(bucket)。 在 dict 的散列表当中,每个键值对都占用一个表元,每个表元都有两 个部分,一个是对键的引用,另一个是对值的引用。因为所有表元的大 小一致,所以可以通过偏移量来读取某个表元。

因为 Python 会设法保证大概还有三分之一的表元是空的,所以在快要达 到这个阈值的时候,原有的散列表会被复制到一个更大的空间里面。

如果要把一个对象放入散列表,那么首先要计算这个元素键的散列值。 Python 中可以用 hash() 方法来做这件事情,接下来会介绍这一点。

  1. 散列值和相等性

    内置的 hash() 方法可以用于所有的内置类型对象。如果是自定义 对象调用 hash() 的话,实际上运行的是自定义的 __hash__。如 果两个对象在比较的时候是相等的,那它们的散列值必须相等,否 则散列表就不能正常运行了。例如,如果 1 == 1.0 为真,那么 hash(1) == hash(1.0) 也必须为真,但其实这两个数字(整型 和浮点)的内部结构是完全不一样的。

    为了让散列值能够胜任散列表索引这一角色,它们必须在索引空间 中尽量分散开来。这意味着在最理想的状况下,越是相似但不相等 的对象,它们散列值的差别应该越大。

  2. 散列表算法

    为了获取 my_dict[search_key] 背后的值,Python 首先会调用 hash(search_key) 来计算 search_key 的散列值,把这个值最低的几位数字当作偏移量,在散列表里查找表元(具体取几位,得看 当前散列表的大小)。若找到的表元是空的,则抛出 KeyError 异 常。若不是空的,则表元里会有一对 found_key:found_value。 这时候 Python 会检验 search_key == found_key 是否为真,如 果它们相等的话,就会返回 found_value。

    如果 search_key 和 found_key 不匹配的话,这种情况称为散列 冲突。发生这种情况是因为,散列表所做的其实是把随机的元素映射到只有几位的数字上,而散列表本身的索引又只依赖于这个数字 的一部分。为了解决散列冲突,算法会在散列值中另外再取几位, 然后用特殊的方法处理一下,把新得到的数字再当作索引来寻找表 元。若这次找到的表元是空的,则同样抛出 KeyError;若非 空,或者键匹配,则返回这个值;或者又发现了散列冲突,则重复 以上的步骤。

​ 图 3-3 展示了这个算法的示意图。

python小技巧-1_第1张图片

3-3:从字典中取值的算法流程图;给定一个键,这个算法要 么返回一个值,要么抛出 KeyError 异常

添加新元素和更新现有键值的操作几乎跟上面一样。只不过对于前者,在发现空表元的时候会放入一个新元素;对于后者,在找到相对应的表元后,原表里的值对象会被替换成新值。

另外在插入新值时,Python 可能会按照散列表的拥挤程度来决定是 否要重新分配内存为它扩容。如果增加了散列表的大小,那散列值所占的位数和用作索引的位数都会随之增加,这样做的目的是为了 减少发生散列冲突的概率。

表面上看,这个算法似乎很费事,而实际上就算 dict 里有数百万 个元素,多数的搜索过程中并不会有冲突发生,平均下来每次搜索 可能会有一到两次冲突。在正常情况下,就算是最不走运的键所遇 到的冲突的次数用一只手也能数过来。

了解 dict 的工作原理能让我们知道它的所长和所短,以及从它衍 生而来的数据类型的优缺点。下面就来看看 dict 这些特点背后的 原因。

dict 的实现及其导致的结果

  1. 键必须是可散列的 一个可散列的对象必须满足以下要求。

    (1) 支持 hash() 函数,并且通过 __hash__() 方法所得到的散列值是不变的。

    (2) 支持通过 __eq__() 方法来检测相等性。
    (3) 若 a == b 为真,则 hash(a) == hash(b) 也为真。

    所有由用户自定义的对象默认都是可散列的,因为它们的散列值由 id() 来获取,而且它们都是不相等的。

    如果你实现了一个类的 eq 方法,并且希望它是可 散列的,那么它一定要有个恰当的 hash 方法,保证在 a == b 为真的情况下 hash(a) == hash(b) 也必定为真。否则 就会破坏恒定的散列表算法,导致由这些对象所组成的字典和 集合完全失去可靠性,这个后果是非常可怕的。另一方面,如 果一个含有自定义的 eq 依赖的类处于可变的状态,那就不要在这个类中实现 hash 方法,因为它的实例是不可散列的。

  2. 字典在内存上的开销巨大

    由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空间上的效率低下。举例而言,如果你需要存放数量巨大的记录,那 么放在由元组或是具名元组构成的列表中会是比较好的选择;最好

​ 不要根据 JSON 的风格,用由字典组成的列表来存放这些记录。用元组取代字典就能节省空间的原因有两个:其一是避免了散列表所 耗费的空间,其二是无需把记录中字段的名字在每个元素里都存一 遍。

​ 在用户自定义的类型中,__slots__ 属性可以改变实例属性的存储 方式,由 dict 变成 tuple,相关细节在 9.8 节会谈到。

​ 记住我们现在讨论的是空间优化。如果你手头有几百万个对象,而 你的机器有几个 GB 的内存,那么空间的优化工作可以等到真正需 要的时候再开始计划,因为优化往往是可维护性的对立面。

3.键查询很快

dict 的实现是典型的空间换时间:字典类型有着巨大的内存开 销,但它们提供了无视数据量大小的快速访问——只要字典能被装在内存里。

4.键的次序取决于添加顺序

当往 dict 里添加新键而又发生散列冲突的时候,新键可能会被安 排存放到另一个位置。于是下面这种情况就会发生:由 dict([key1, value1), (key2, value2)] 和 dict([key2, value2], [key1, value1]) 得到的两个字典,在进行比较的时 候,它们是相等的;但是如果在 key1 和 key2 被添加到字典里的过程中有冲突发生的话,这两个键出现在字典里的顺序是不一样 的。

  1. 往字典里添加新键可能会改变已有键的顺序

    无论何时往字典里添加新的键,Python 解释器都可能做出为字典扩 容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字 典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲 突,导致新散列表中键的次序变化。要注意的是,上面提到的这些 变化是否会发生以及如何发生,都依赖于字典背后的具体实现,因 此你不能很自信地说自己知道背后发生了什么。如果你在迭代一个 字典的所有键的过程中同时对字典进行修改,那么这个循环很有可 能会跳过一些键——甚至是跳过那些字典中已经有的键。

    由此可知,不要对字典同时进行迭代和修改。如果想扫描并修改一 个字典,最好分成两步来进行:首先对字典迭代,以得出需要添加 的内容,把这些内容放在一个新字典里;迭代结束之后再对原有字 典进行更新。

这篇博客介绍python3.6以后的字典实现十分详细

https://zhuanlan.zhihu.com/p/...

Python3.6之后,往字典里添加新键是有序的,不存在改变已有键顺序的情况了

同理,对于集合来说,也是异常消耗内存的;集合有如下特点:

1:集合里的元素必须是可散列的。

2:集合很消耗内存。

3:可以很高效地判断元素是否存在于某个集合。

4:元素的次序取决于被添加到集合里的次序。

5:往集合里添加元素,可能会改变集合里已有元素的次序。

你可能感兴趣的:(python3.x)