python从低级到高级的技巧(更新中)

一、Python数据模型

1.特殊方法

深入理解特殊方法,下面的例子虽然使用了len以及[]来获取长度与索引值,但是因为我们定义特殊方法时已经将其更改,故最后返回值是我们自己定义的。

class Testlen():
    # 这是一个测试特殊方法的类
    # def __init__(self):

    def __len__(self):
        return 99

    def __getitem__(self, pos):
        return '233'


if __name__ == '__main__':
    test_1 = Testlen()
    length = len(test_1)
    item = test_1[0]
    print(length, item)

out: 99 233

那么这么做的意义在哪里呢?

事实上,如果是Python内置的类型,比如列表(list)、字符串(str)、字节序列(bytearray)等,那么CPython会抄个近路,__len__实际上会直接返回PyVarObject里的ob_size属性。PyVarObject是表示内存中长度可变的内置对象的C语言结构体。直接读取这个值比调用一个方法要快很多。

二、序列构成的数组

1.列表推导的可读性

低级:

symbols = 'abcdefg'
codes = []
for symbol in symbols:
    codes.append(ord(symbol))
print(codes)

out: [97, 98, 99, 100, 101, 102, 103]

高级:

symbols = 'abcdefg'
codes = [ord(symbol) for symbol in symbols]
print(codes)

out: [97, 98, 99, 100, 101, 102, 103]
2.笛卡尔积

列表推导还可以使用双循环来构建其他序列类型:

低级:

symbols = 'abc'
nums = '123'
codes = [(num, symbol) for num in nums for symbol in symbols]
print(codes)

[('1', 'a'), ('1', 'b'), ('1', 'c'), 
('2', 'a'), ('2', 'b'), ('2', 'c'), 
('3', 'a'), ('3', 'b'), ('3', 'c')]

高级:

symbols = 'abc'
nums = '123'
for mul in ((num, symbol) for num in nums for symbol in symbols):
    print(mul)

out:
('1', 'a')
('1', 'b')
('1', 'c')
('2', 'a')
('2', 'b')
('2', 'c')
('3', 'a')
('3', 'b')
('3', 'c')

用到生成器表达式之后,内存里不会留下一个有9个组合的列表,因为生成器表达式会在每次for循环运行时才生成一个组合。用生成器来初始化除列表之外的序列,可以避免额外的内存占用

3.元组不仅仅是不可变的列表

除了用作不可变的列表,元组还可以用于没有字段名的记录;for循环可以分别提取元组里的元素,也叫作拆包(unpacking)。拆包让元组可以完美地被当作记录来使用,元组拆包可以应用到任何可迭代对象上,唯一的硬性要求是,被可迭代对象中的元素数量必须要跟接受这些元素的元组的空档数一致。

低级:

a = 1
b = 2
temp = a
a = b
b = temp
print(a, b)

out: 2 1

高级:

a = 1
b = 2
a, b = b, a
print(a, b)

out: 2 1

这种比其他语言更加优雅的实现,就是基于元组实现的。

低级:

num = (20, 8)
output = divmod(num[0], num[1])
print(output)

out: (2, 4)

高级:

num = (20, 8)
output = divmod(*num)
print(output)

out: (2, 4)

这里利用了*运算符把一个可迭代对象拆开作为函数的参数的特点,还可以用*args来获取不确定数量的参数。

例如:

a, *b, c, d = range(7)
print(a, b, c, d)

out: 0 [1, 2, 3, 4] 5 6

collections.namedtuple是一个工厂函数,它可以用来构建一个带字段名的元组和一个有名字的类——这个带名字的类对调试程序有很大帮助。用namedtuple构建的类的实例所消耗的内存跟元组是一样的,因为字段名都被存在对应的类里面。这个实例跟普通的对象实例比起来也要小一些,因为Python不会用__dict__来存放这些实例的属性。这个就叫具名元组,他有很强大的功能,这里不展开叙述。

除了跟增减元素相关的方法之外,元组支持列表的其他所有方法。还有一个例外,元组没有__reversed__方法

4.切片

在Python里,像列表(list)、元组(tuple)和字符串(str)这类序列类型都支持切片操作。

在切片和区间操作里不包含区间范围的最后一个元素是Python的风格,这个习惯符合Python、C和其他语言里以0作为起始下标的传统。这样做带来的好处如下:

  • 当只有最后一个位置信息时,我们也可以快速看出切片和区间里有几个元素:range(3)和my_list[:3]都返回3个元素。
  • 当起止位置信息都可见时,我们可以快速计算出切片和区间的长度,用后一个数减去第一个下标(stop-start)即可。
  • 这样做也让我们可以利用任意一个下标来把序列分割成不重叠的两部分,只要写成my_list[:x]和my_list[x:]就可以了。

省略(ellipsis)的正确书写方法是三个英语句号(…),而不是Unicdoe码位U+2026表示的半个省略号(…)。省略在Python解析器眼里是一个符号,而实际上它是Ellipsis对象的别名,而Ellipsis对象又是ellipsis类的单一实例。它可以当作切片规范的一部分,也可以用在函数的参数清单中,比如f(a, …, z),或a[i:…]。
在NumPy中,…用作多维数组切片的快捷方式。如果x是四维数组,那么x[i, …]就是x[i, :, :, :]的缩写。

5.对序列使用+和*

错误:

# 初始化数组,方式正确
length = 3
array_1 = [] * length
array_2 = [0] * length
print(array_1)
print(array_2)
# 初始化矩阵,方式错误
mat_1 = [[]] * length
mat_2 = [[0]] * length
mat_3 = [[0] * length] * length
print(mat_1)
print(mat_2)
print(mat_3)

out:
[]
[0, 0, 0]
[[], [], []]
[[0], [0], [0]]
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]

如果在a * n这个语句中,序列a里的元素是对其他可变对象的引用的话,你就需要格外注意了,因为这个式子的结果可能会出乎意料。比如,你想用my_list=[[]] * 3来初始化一个由列表组成的列表,但是你得到的列表里包含的3个元素其实是3个引用,而且这3个引用指向的都是同一个列表。
这会导致无法修改一个特定元素。

*可以帮助我们快速建立列表:

正确:

# 初始化矩阵,不完全
mat_4 = [[] * length for n in range(length)]
# 初始化矩阵,方式正确
mat_5 = [[0] * length for n in range(length)]
print(mat_4)
print(mat_5)

out:
[[], [], []]
[[0, 0, 0], [0, 0, 0], [0, 0, 0]]
6.序列的增量赋值

例子:

tuple_1 = (1, 2, [3, 4])
tuple_1[2] += [5, 6]
print(tuple_1)

TypeError: 'tuple' object does not support item assignment

Out: (1, 2, [3, 4, 5, 6])

可以发现虽然报了错,但还是完成了赋值操作。
事实上是因为对列表赋值是可以完成的,但是将计算结果返回元组的过程失败了:

  • 不要把可变对象放在元组里面。增量赋值不是一个原子操作。
  • 我们刚才也看到了,它虽然抛出了异常,但还是完成了操作。
  • 查看Python的字节码并不难,而且它对我们了解代码背后的运行机制很有帮助。
7.list.sort方法和内置函数sorted
  1. list.sort方法会就地排序列表,也就是说不会把原列表复制一份。这也是这个方法的返回值是None的原因,提醒你本方法不会新建一个列表。

但是用返回None来表示就地改动这个惯例有个弊端,那就是调用者无法将其串联起来。而返回一个新对象的方法(比如说str里的所有方法)则正好相反,它们可以串联起来调用,从而形成连贯接口(fluent interface)。

  1. 与list.sort相反的是内置函数sorted,它会新建一个列表作为返回值。这个方法可以接受任何形式的可迭代对象作为参数,甚至包括不可变序列或生成器。而不管sorted接受的是怎样的参数,它最后都会返回一个列表。

key函数的使用:

# 按照每个元素的长度进行排序
def key_1(x):
    return len(x)

# 按照每个元素的第一个元素大小进行排序
def key_2(x):
    return x[0]

lst = [[9,8,7],
       [6,5,4,3],
       [2,1,0,0,0]]

lst.sort(key=key_1)
print(lst)

lst.sort(key=key_2)
print(lst)

out:
[[9, 8, 7], [6, 5, 4, 3], [2, 1, 0, 0, 0]]
[[2, 1, 0, 0, 0], [6, 5, 4, 3], [9, 8, 7]]
8.用bisect来管理已排序的序列

已排序的序列可以用来进行快速搜索,而标准库的bisect模块给我们提供了二分查找算法。bisect模块包含两个主要函数,bisectinsort

  1. 你可以先用bisect(haystack, needle)查找位置index,再用haystack.insert(index, needle)来插入新值。
  2. 你也可用insort来一步到位,并且速度更快一些。
9.当列表不是首选时

根据需求选择数据类型:

  • 要存放1000万个浮点数的话,数组(array)的效率要高得多,因为数组在背后存的并不是float对象,而是数字的机器翻译,也就是字节表述。这一点就跟C语言中的数组一样。
  • 如果需要频繁对序列做先进先出的操作,deque(双端队列)的速度应该会更快。
  • 如果在你的代码里,包含操作(比如检查一个元素是否出现在一个集合中)的频率很高,用set(集合)会更合适。set专为检查元素是否存在做过优化。但是它并不是序列,因为set是无序的。
  1. 数组

从Python 3.4开始,数组类型不再支持诸如 list.sort() 这种就地排序方法。要给数组排序的话,得用sorted函数新建一个数组:

a = array.array(a.typecode, sorted(a))
  1. 内存视图

内存视图其实是泛化和去数学化的NumPy数组。它让你在不需要复制内容的前提下,在数据结构之间共享内存。其中数据结构可以是任何形式,比如PIL图片、SQLite数据库和NumPy的数组,等等。

  1. NumPy和SciPy

  1. 双向队列和其他形式的队列

双向队列实现了大部分列表所拥有的方法,也有一些额外的符合自身设计的方法,比如说popleft和rotate。但是为了实现这些方法,双向队列也付出了一些代价,从队列中间删除元素的操作会慢一些,因为它只对在头尾的操作进行了优化

10.其他
  1. 有些对象里包含对其他对象的引用;这些对象称为容器

因此,我特别使用了“容器序列”这个词,因为Python里有是容器但并非序列的类型,比如dict和set。容器序列可以嵌套着使用,因为容器里的引用可以针对包括自身类型在内的任何类型。

  1. 与此相反,扁平序列因为只能包含原子数据类型,比如整数、浮点数或字符,所以不能嵌套使用。

称其为“扁平序列”是因为我希望有个名词能够跟“容器序列”形成对比。这个词是我自己发明的,专门用来指代Python中“不是容器序列”的序列,在其他地方你可能找不到这样的用法。如果这个词出现在维基百科上面的话,我们需要给它加上“原创研究”标签。我更倾向于把这类词称作“自创名词”,希望它能对你有所帮助并为你所用。

三、字典和集合

1.泛映射类型

标准库里的所有映射类型都是利用dict来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用作这些映射里的键(只有键有这个要求,值并不需要是可散列的数据类型)。

  • 可散列

一个可散列的对象必须满足以下要求:
(1)支持hash( )函数,并且通过__hash__( )方法所得到的散列值是不变的。
(2)支持通过__eq__( )方法来检测相等性。
(3)若a==b为真,则hash(a)==hash(b)也为真。

  • 不可散列

原子不可变数据类型(str、bytes和数值类型)都是可散列类型,frozenset也是可散列的,因为根据其定义,frozenset里只能容纳可散列类型。元组的话,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的。
可散列类型使用hash方法就会返回其对应的散列值。

创建字典的方式:

a = dict(one=1, two=2, three=3)
b = {'one': 1, 'two': 2, 'three': 3}
c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
d = dict([('two', 2), ('one', 1), ('three', 3)])
e = dict({'three': 3, 'one': 1, 'two': 2})
print(a == b == c == d == e)

out: True
2.字典推导

可以从列表(由元组组成,每个元组包含2个元素)使用推导式来构建字典,与列表推导相似。

3.常见的映射方法

Python里大多数映射类型的构造方法都采用了类似的逻辑,因此你既可以用一个映射对象来新建一个映射对象,也可以用包含(key, value)元素的可迭代对象来初始化一个映射对象。

setdefault处理找不到的键:

低级:

my_dict = {'one': [1], 'two': [2], 'three': [3]}
key = 'four'
new_value = 4
if key not in my_dict:
    my_dict[key] = []
my_dict[key].append(new_value)
print(my_dict)

out: {'one': [1], 'two': [2], 'three': [3], 'four': [4]}

高级:

my_dict = {'one': [1], 'two': [2], 'three': [3]}
key = 'four'
new_value = 4
my_dict.setdefault(key, new_value).append(new_value)
print(my_dict)

out: {'one': [1], 'two': [2], 'three': [3], 'four': [4]}
4.映射的弹性键查询
  1. defaultdict:处理找不到的键的一个选择

我们新建了这样一个字典:dd=defaultdict(list),如果键’new-key’在dd中还不存在的话,表达式dd[‘new-key’]会按照以下的步骤来行事:

  • 调用 list() 来建立一个新列表。
  • 把这个新列表作为值,'new-key’作为它的键,放到dd中。
  • 返回这个列表的引用。
  1. 特殊方法__missing__

在__getitem__碰到找不到的键的时候,Python就会自动调用它,而不是抛出一个KeyError异常。这使得我们可以在找不到键的时候,可以处理此类情况。

__missing__方法只会被__getitem__调用(比如在表达式d[k]中)

像 k in my_dict.keys() 这种操作在Python 3中是很快的,而且即便映射类型对象很庞大也没关系。这是因为 dict.keys() 的返回值是一个“视图”。视图就像一个集合,而且跟字典类似的是,在视图里查找一个元素的速度很快。而不是在列表中逐个对比。

5.字典的变种
  1. collections.OrderedDict
    这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。

  2. collections.ChainMap
    该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。

  3. collections.Counter
    这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。

  4. collections.UserDict
    这个类其实就是把标准dict用纯Python又实现了一遍。跟OrderedDict、ChainMap和Counter这些开箱即用的类型不同,UserDict是让用户继承写子类的。

6.子类化UserDict

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

7.不可变映射类型

types模块中引入了一个封装类名叫MappingProxyType。如果给这个类一个映射,它会返回一个只读的映射视图。虽然是个只读视图,但是它是动态的。

8.集合论

集合的本质是许多唯一对象的聚集。因此,集合可以用于去重。

  1. 集合字面量

像{1, 2, 3}这种字面量句法相比于构造方法(set([1, 2, 3]))要更快且更易读。后者的速度要慢一些,因为Python必须先从set这个名字来查询构造方法,然后新建一个列表,最后再把这个列表传入到构造方法里。但是如果是像{1, 2, 3}这样的字面量,Python会利用一个专门的叫作BUILD_SET的字节码来创建集合。

由于Python里没有针对frozenset的特殊字面量句法,我们只能采用构造方法。

  1. 集合推导

  1. 集合的操作

9.dict和set的背后

对 in 操作的实验中,最快的时间来自“集合交集花费时间”这一列,不出所料的是,最糟糕的表现来自“列表花费时间”这一列。由于列表的背后没有散列表来支持in运算符,每次搜索都需要扫描一次完整的列表,导致所需的时间跟据haystack的大小呈线性增长。

散列表其实是一个稀疏数组(总是有空白元素的数组称为稀疏数组)。

从字典中取值的算法流程图:

  1. 字典在内存上的开销巨大。
    由于字典使用了散列表,而散列表又必须是稀疏的,这导致它在空间上的效率低下。举例而言,如果你需要存放数量巨大的记录,那么放在由元组或是具名元组构成的列表中会是比较好的选择;最好不要根据JSON的风格,用由字典组成的列表来存放这些记录。用元组取代字典就能节省空间的原因有两个:其一是避免了散列表所耗费的空间,其二是无需把记录中字段的名字在每个元素里都存一遍。

  2. 键查询很快
    dict的实现是典型的空间换时间。

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

  4. 往字典里添加新键可能会改变已有键的顺序
    无论何时往字典里添加新的键,Python解释器都可能做出为字典扩容的决定。扩容导致的结果就是要新建一个更大的散列表,并把字典里已有的元素添加到新表里。这个过程中可能会发生新的散列冲突,导致新散列表中键的次序变化。
    由此可知,不要对字典同时进行迭代和修改。如果想扫描并修改一个字典,最好分成两步来进行:首先对字典迭代,以得出需要添加的内容,把这些内容放在一个新字典里;迭代结束之后再对原有字典进行更新。

set和frozenset的实现也依赖散列表,但在它们的散列表里存放的只有元素的引用(就像在字典里只存放键而没有相应的值)。在set加入到Python之前,我们都是把字典加上无意义的值当作集合来用的。

10.其他

“优雅是简约之父”

四、文本和字节序列

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值