python中的数据结构

python数据模型

特殊方法示例
from math import hypot

class Vector:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        
    """不管输出还是通过print打印,都按__repr__方法计算
    __str__只有在使用str()函数或者print()函数才被调用"""
    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)
特殊方法一览

常用跟运算符无关的特殊方法

  • 数值转换: __abs__, __bool__, __complex__, __int__, __float__, __hash__, __index__
  • 属性管理: __getattr__, __getattribute__, __setattr__, __delattr__, __dir__
  • 属性描述符:__get__, __set__, __delete__

常用跟运算符相关的特殊方法

  • 一元运算符: __neg__, __pos__, __abs__
  • 比较运算符:__lt__, __le__, __eq__, __ne__
  • 算术运算符:__add__, __sub__, __mul__
  • 反向算术运算符:__radd__,__rsub__,__rmul__

序列构成的数组

  • 可变序列:list, bytearray, array.array, collections.deque
  • 不可变序列: tuple, str, bytes

元祖拆包

a, b = (3, 4)
>>> a    # 3
>>> b    # 4

可以使用*运算符把一个可迭代对象拆开作为函数的参数

>>> divmod(20, 8)  # (2,4)
>>> t = (20, 8)
>>> divmod(*t)     # (2,4)

用*来处理剩下的元素

a,b,*rest = range(5)
>>> a, b, rest                 # (0, 1, [2,3,4])
>>> a, *body, c, d = range(5)  
>>> a, body, c, d             # (0, [1,2], 3, 4)
>>> *head, b, c ,d = range(5) 
>>> head, b ,c ,d           # ([0,1],2,3,4)

切片slices
使用序列a*n这个语句中,序列a里的元素是对其他可变对象引用的话,比如my_list = [[]] * 3来初始化一个列表的列表,得到列表里包含的三个元素其实是三个指向同一列表的引用。应该使用

my_list = [['_'] for i in range(3)]

序列的增量赋值
+=背后的特殊方法是__iadd__(就地加法), *=背后的特殊方法是__imul__

>>> l = [1,2,3]
>>> id(l)   # 430123123
>>> l *= 2 
>>> l   # [1,2,3,1,2,3]
# 运用增量乘法后,列表的ID没变,新元素追加到列表上
>>> id(l)   # 430123123
>>> t = (1,2,3)
>>> id(t)    # 430123431
>>> t *= 2
# 运用增量乘法后,新元祖会被创建,但是效率很低,每次都要创建新对象
>>> id(t)   # 4301233421   

用bisect管理已排序的序列
bisect模块包含两个主要函数, bisect和insort, 两个函数都利用二分查找算法在有序序列中查找或插入元素

import bisect

hystack = [1,4,5,6,8,12,15,20]
needle = 5
print(bisect.bisect(hystack, needle))

数组
python数组和C语言数组一样精简,创建数组需要一个类型码,用来表示在底层C语言应该存放怎样的数据类型

from array import array
from random import random
# 用可迭代对象家里双精度浮点数组(类型码是'd')
floats = array('d', random() for i in range(100))

# 把数组存入二进制文件中
fp = open('floats.bin', 'wb')
floats.tofile(fp)
fp.close()

# 新建一个双精度浮点空数组
floats2 = array('d')
# 从二进制文件中读取100个浮点数
floats2.fromfile(fp, 100)
fp.close()

列表和元组

  • 高效搜索必需的两大要素是排序算法和搜索算法。Python列表有一个内建的排序算法使用了Tim排序。Tim排序可以在最佳情况下以O(n)(最差情况下则是O(nlog n))的复杂度排序。
    它运用了多种排序算法。对于给定的数据,它使用探索法猜测哪个算法的性能最优(更确切地说,它混用了插入排序和合并排序算法)来达到这样的性能。
  • bisect模块提供了一个简便的函数让你可以在保持排序的同时往列表中添加元素,以及一个高度优化过的二分搜索算法函数来查找元素
  • 列表是动态数组, 它们可以改变且可以重设长度
  • 元组是静态数组, 它们不可变且内部数据一旦创建便无法改变; 元组缓存与python运行时的环境, 这意味着我们每次使用元组无须访问内核去分配内存
动态数组: 列表
  • 这是因为动态数组支持resize操作,可以增加数组的容量。当一个大小为N的列表第一次需要添加数据时,Python会创建一个新的列表,足够存放原来的N个元素以及额外需要添加的元素。不过,实际分配的并不是N+1个元素,而是M个,M >N,这是为了给未来的添加预留空间。然后旧列表的数据被复制到新列表中,旧列表则被销毁。从设计理念上来说,第一次的添加可能会是后续多次添加的开始,通过预留空间的做法,我们就可以减少这一分配空间的操作的次数以及内存复制的次数
  • 当我们需要添加数据时,我们可以直接利用额外的空间并增加列表的有效容量,N。我们继续添加数据,N会继续增长直到N == M。此时,没有额外的空间给我们插入,我们必须创建一个拥有更多额外空间的新列表
静态数组: 元组
  • 元组固定且不可变. 这意味着一旦元组被创建, 它的内容无法被修改,大小也无法改变.
  • 可以将两个元组合并成一个新元组. 但要为新生成的元组分配额外的空间. 举个例子:一个使用过append操作大小为1000000000的列表实际上占用112500007的内存, 而保存同样数据的元组始终占用1000000000的内存,
    这使得元组对于静态诗句是一个轻量级且更好的选择
  • 创建列表和创建时间对比. 元组可以被轻松的创建, 因为它可以避免和操作系统打交道(Python是一门垃圾收集语言,这意味着当一个变量不再被使用时,Python会将该变量使用的内存释放回操作系统,以供其他程序(或变量)使用。然而,对于长度为1~20的元组,即使它们不再被使用,它们的空间也不会立刻被还给系统,而是留待未来使用。这意味着当未来需要一个同样大小的新元组时,我们不再需要向操作系统申请一块内存来存放数据,因为我们已经有了预留的内存。)
>>> %timeit l = [0,1,2,3,4,5,6,7,8,9]
1000000 loops, best of 3: 285 ns per loop

>>> %timeit t = (0,1,2,3,4,5,6,7,8,9)
10000000 loops, best of 3: 55.7 ns per loop

字典和集合

泛映射类型

collections.abc模块中有MappingMutableMapping这两个抽象基类,他们的作用是为dict和其他类似的类型定义形式接口。可以使用isinstance一起来判定某个数据是不是广义上的映射类型

>>> mydict = {}
>>> isinstance(my_dict, abc.Mapping)
True

标准库里所有映射类型都是利用dict来实现的,它们有个共同的限制,只有可散列的数据类型才能作用这些映射里的键
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现__hash__方法。不可变数据类型都是散列的

>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> tf = (1, 2, frozenset([30, 40]))
>>> hash(tf)
-4118419923444501110

字典推导

>>> DIAL_CODES = [
... (86, 'China'),
... (91, 'India'),
... (1, 'United States'),
... ]
>>> country_code = {country: code for code, country in DIAL_CODES} 
>>> country_code
{'China': 86, 'India': 91, 'United States': 1}

常见映射方法

  • dict
  • collections.defaultdict: 在实例化一个defaultdict 的时候,需要给构造方法提供一个可调用对象,这个可调用对象会在__getitem__ 碰到找不到的键的时候被调用,让__getitem__ 返回某种默认值。
  • collections.Orderdict:添加键的时候保证顺序
  • collections.Counter: 计数器

用setdefault处理找不到的键
每个Python 程序员都知道可以用d.get(k, default) 来代替d[k],
给找不到的键一个默认的返回值(这比处理KeyError 要方便不少)。但是要更新某个键对应的值的时候,不管使用__getitem__ 还是get 都会不自然,而且效率低。

# 效率低
mydict.get(key, []).append(new_value)

# 多了一次查询
if key not in my_dict:
    my_dict[key] = []
my_dict[key].append(value)

mydict.setdefault(key, []).append(new_value)

>>> country_code.setdefault('CHINA',0) + 1
>>> country_code['CHINA'] = [1,2]
>>> country_code.setdefault('CHINA',[]).append(1)

特殊方法__missing__

所有的映射类型在处理找不到的键的时候,都会牵扯到__missing__ 方法。这也是这个方法称作“missing”的原因。

  • 虽然基类dict 并没有定义这个方法,但是dict 是知道有这么个
    东西存在的。也就是说,如果有一个类继承了dict,然后这个继承类提供了__missing__ 方法,那么在__getitem__ 碰到找不到的键的时候,Python 就会自动调用它,而不是抛出一个KeyError 异常。__missing__ 方法只会被__getitem__ 调用
# 演示__missing__如何被__getitem__调用的
class Mydict(dict):
    def __missing__(self, key):
        return "missed key"
        
test = Mydict({'a':2,'b':1})
>>> test['a']   # 2
>>> test['b']   # 1
>>> test['c']   # 'missed key'

子类化UserDict
就创造自定义映射来说,以UserDict为基类,比普通的dict为基类方便

而更倾向于从UserDict而不是从dict继承的主要原因是,后者有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方法,但是UserDict 就不会带来这些问题

import collections
# 无论是添加、更新还是查询操作,StrKeyDict 都会把非字符串的键转换为字符串
class StrKeyDict(collections.UserDict): 
    def __missing__(self, key): 
        if isinstance(key, str):
            raise KeyError(key)
        return self[str(key)]
    def __contains__(self, key):
        return str(key) in self.data 
    def __setitem__(self, key, item):
        self.data[str(key)] = item
        
test = StrKeyDict({'a': 2, 'b': 3})
>>> test    # {'a': 2, 'b': 3}
>>> 'b' in test    # True
>>> test[2] = 5
>>> test    # {'a': 2, 'b': 3, '2': 5}
>>> test[2], test['2']    # (5,5)

不可变映射类型
types模块中引入了一个封装类名叫做MappingProxyType。如果给这个类
一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。这意味着如果对原映射做出了改动,我们通过这个视图可以观察到,但是无法通过这个视图对原映射做出修改

>>> from types import MappingProxyType
>>> d = {1:'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[1] 
'A'
# 无法通过这个视图对原映射做出修改
>>> d_proxy[2] = 'x' 
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
# 对原映射做出改动,可以通过视图观察到

>>> d[2] = 'B'
>>> d_proxy 
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'
>>>

dict和set背后的长处和弱点

  • dict的实现及其导致的结果

    • 键必须是可散列的,支持hash()函数,并且通过_hash_()方法所得到的散列值是不变的
    • 字典在内存上的开销巨大, 由于字典使用了散列表,而散列表又是稀疏的,导致空间上的效率低下
    • 键查询很快。dict的实现是电信的空间换时间,字典类型有着巨大的内存开销,但它们提供了无视数据量大小的快速访问
    • 键的次序取决于添加顺序
    • 往字典里添加新键可能会改变键已有顺序
  • set的实现以及导致的结果

    • 集合里的元素必须是可散列的
    • 集合很消耗内存
    • 可以很高效的判断元素是否存在于集合
    • 元素的次序取决于被添加到集合里的次序
    • 往集合里添加元素,可能会改变集合里已有元素的次序

集合推导

# 集合推导
myest = {s for s in 'sdsadwqds'}
print(type(myest))  # <class 'set'>
print(myest)    # {'q', 's', 'w', 'd', 'a'}

字典和集合的性能分析

  • 字典和集合基于键的查询速度为O(1), 除此之外, 和列表/元组一样, 字典和集合的插入时间是O(1), 为了达到这一速度, 它们在底层使用的数据结构是一个开放地址散列表

  • 字典和集合使用散列表来获得O(1)的查询和插入. 原因是使用散列函数将任意一个键转变成了一个列表的索引

  • 不过,需要注意的是计算方案要能访问每一个可能的索引使得数据能够尽量均匀地分布在表中。数据分布的均匀程度被称为“负载因素”,它跟散列函数的熵有关

  • 当我们在查询某个键时,给出的键会被转化为索引进行检索. 如果该索引指向的位置中的键符合,那么就返回那个值. 如果不符合, 用同一方案继续创建新的索引,知道找到数据或者找到一个空桶. 找到空桶则认为表里不存在该数据

  • 当越来越多的项目被插入散列表时, 表本身必须改变大小来适应. 字典或集合默认的最小长度是8(也就是说,即使你只保存3个值,Python仍然
    会分配8个元素)。每次改变大小时,桶的个数增加到原来的4倍,直至达到50000个元素,之后每次增加到原来的2倍。

散列函数和熵
  • Python对象通常以散列表实现,因为它们已经有内建的__hash__和__cmp__函数。对于数字类型(int和float),散列值就是基于它们数字的比特值。元组和字符串的散列值基于它们的内容。而另一方面,列表不能被散列,因为它们的值可以改变。一旦列表的值发生改变,其散列值也会相应改变,也就改变了键在散列表中的相对位置。

  • 用户自定义类也有一个默认的散列和比较函数。默认的__hash__函数使用内建的id函数返回对象在内存中的位置。同样,__cmp__操作符比较的也是对象在内存中的位置的数字值。

  • 一个用户自定义的散列函数需要让散列值均匀分布以避免散列碰撞。碰撞太频繁会降低散列表的性能, 衡量散列函数分布的均匀程度的标准被称为散列
    函数的熵:
    S = − ∑ i p ( i ) l o g ( p ( i ) ) S = -\sum_i p(i)log(p(i)) S=ip(i)log(p(i))
    p(i)是散列函数给出散列值为i的概率. 当所有的散列值具有相同的概率被选择时,熵最大

字典的命名空间

每当python访问一个变量, 函数和模块, 都有一个体系来决定它去哪里查找这些对象.

  1. 首先,Python查找locals()数组,其内保存了所有本地变量的条目。Python花了很多精力优化本地变量查询的速度,而这也是整条链上唯一一个不需要字典查询的部分
  2. 如果它不在本地变量里,那么会搜索globals()字典
  3. 最后,如果对象也不在那里,则搜索__builtin__对象。
    要注意locals()和globals()是显的字典而__builtin__则是模块对象,在搜索__builtin__中的一个属性时,我们其实是在搜索它的locals()字典(对所有的模块对象和类对象都是如此!)。
    可以使用dis模块来理解命名查询怎么发生的
In [1]: import math

In [2]: from math import sin

In [3]: def test1(x):
   ...:     return math.sin(x)
   ...:

In [4]: import dis

In [5]: dis.dis(test1)
  2           0 LOAD_GLOBAL              0 (math)
              2 LOAD_METHOD              1 (sin)
              4 LOAD_FAST                0 (x)
              6 CALL_METHOD              1
              8 RETURN_VALUE

In [6]: def test2(x):
   ...:     return sin(x)
In [8]: dis.dis(test2)
  2           0 LOAD_GLOBAL              0 (sin)
              2 LOAD_FAST                0 (x)
              4 CALL_FUNCTION            1
              6 RETURN_VALUE

In [9]: def test3(x, sin=math.sin):
   ...:     return sin(x)
   ...:

In [10]: dis.dis(test3)
  2           0 LOAD_FAST                1 (sin)
              2 LOAD_FAST                0 (x)
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
  • test1()函数中, 一个math模块的引用必须被调入, 然后在模块上进行属性查询, 知道找到sin函数。 整个步骤经历了两次字典查询, 一次查找math模块, 一次在模块中查找sin函数
  • test2()函数中, 从math模块中显示的导入了sin函数, 因此该函数可在全局命名空间中直接被访问。 这意味着我们可以避免查询math模块以及后续的属性查询, 不过还是要在全局命名空间查找sin函数
  • test3()定义了sin函数作为参数关键字。 我们依然需要在模块中查找这一函数,但仅在test3函数第一次被定义时查找, 之后, 这一引用一默认关键字的形式作为一个本地变量被保存在函数的定义中

观察在速度上的区别

import time
from functools import wraps
from math import sin

def timewrapper(func):
  @wraps(func)
  # 用@wraps(func)将函数名和docstring暴露给func的调用者
  # 否则调用者看到的是秀时期自身的函数名和docstring
  def measure_time(*args, **kwargs):
    t1 = time.time()
    result = func(*args, **kwargs)
    t2 = time.time()
    print(f'{func.__name__} took {t2 - t1} seconds')
    return result
  return measure_time

@timewrapper
def tight_loop_slow(iterations):
  result = 0
  for i in range(iterations):
    result += sin(i)

@timewrapper
def tight_loop_fast(iterations):
  result = 0
  # 设置一个本地变量保存一个函数的全局引用,在调用函数时会进行一次全局查询
  # 但是在循环内对函数的每次调用会变快
  local_sin = sin
  for i in range(iterations):
    result += local_sin(i)
  return result

if __name__ == '__main__':
  iterations = 10000000
  result1 = tight_loop_slow(iterations) # tight_loop_slow took 1.1465401649475098 seconds
  result2 = tight_loop_fast(iterations) ## tight_loop_fast took 1.0387389659881592 seconds



字典和集合用法小结
  • 字典和集合适用于存储能够被键索引的数据。散列函数对键的使用方式极大地影响数据结构的最终性能
  • 字典和集合十分依赖它们的散列函数。如果它们的散列函数对某个数据类型不具有O(1)的计算时间,那么包含该数据类型的字典和集合都不具有O(1)保证
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值