Python编程技巧.[环境:Python3.6.0]
Part 1 基本数据结构与collections
过滤
过滤操作是一个很常用的操作,Python作为函数式编程语言,提供了非常好的解析支持.
比如过滤负数,常规操作如下:
1 | data = [-4,2,4,9,10,-2] |
但事实上,仅仅需要一行就可以解决问题
1 | res = [x for x in data if x >= 0] |
如果使用filter
函数,也仅仅需要一行:
1 | res = filter(lambda x : x >= 0, data) |
经过测试,列表解析的速度更快,因此应该更多的使用解析的方式.
同样,对于字典.集合等基本数据结构,解析方式都可以快速的进行过滤.
请看:
1 | from random import randint |
对于集合:
1 | data = {randint(-10,10) for x in range(10)} |
其实只要理解了解析式,过滤操作就会变得很简单了.
命名.统计.字典排序
命名
当我们用一个列表来存储一条类似数据库记录的对象时,会遇到这样的问题.
1 | student = ('Justin', 19, 'male', 'justin13wyx@gmail.com') |
此时,如果我们想要取得student的姓名,就要通过数组下标的方式来查找,这样会大大降低程序的可读性.
首先,我们想到了C
的#DEFINE
,即:常量声明.
那么我们的程序就会变成这样:
1 | NAME = 0 |
为了给元组中的每个元素命名,我们引入collections
中的namedtuple
.
这个namedtuple构造器的方法签名是这样的:namedtuple(typename, field_names, *, verbose=False, rename=False, module=None)
其中,typename决定类型的名字(注意:这个名字决定返回的tuple子类的名字)field_names
是一个列表.
1 | from collections import namedtuple |
此时使用isinstance
来测试一下 isinstance(s, tuple)
, 会返回True
同样使用isinstance(s,Student)
会返回True
.
使用type(s)
来看看, 会返回 <class '__main__.Student'>
.
所以,namedtuple
是可以动态创建类的,这个创建的类是tuple
的子类.
统计
常规的统计方法是这样的:
1 | from random import randint |
这样处理后的counter
就是一个data
的频度字典了.
接着可以使用sorted
函数来进行从大到小的排序从而得到结果.
一种更便利的方式是引入collections
的Counter
类
Counter类继承于dict
,适用方法如下:
1 | c = Counter(data) |
most_common
方法返回的是一个元素为tuple
的list
.
如果要进行词频统计,我们需要正则模块re
.
1 | import re |
字典排序
如果曾经尝试过字典排序,你就会发现:字典中的可迭代对象是key
可以通过下面这段代码来验证
1 | from random import randint |
但是更多的场景下,我们需要对字典的值进行排序.
有以下几个point:
- 使用sorted的key参数
- 使用zip函数进行拼接
- 可以变出来V->k这样的
- 使用dict.items()变元组
- 这样变出来的只能是K->V
直接说一下sorted
函数的使用吧,我们继续使用上面的字典d
1 | sorted(d.items(), key=lambda x: x[1], reverse=True) |
也可以使用zip进行元组列表的拼接.
字典公共键
如何获取多个字典的公共键?
说到这个问题,想想这么一种场景,有多个字典记载了不同场比赛球员的进球情况,现在需要得知谁在这三场比赛中都进了球.
这样就需要获取复数个字典的公共键.
获取键的方式是dict.keys()
函数,他会返回一个<class 'dict_keys'>
对象.
这个对象是可以像集合一样进行交并集操作的,这就为我们提供了一个思路.
再在这里插一下快速获取测试数据的一种方法,使用random提供的randint和sample可以快速构建测试数据.
请看下面的例子:
1 | from random import randint, sample |
得到了测试数据s1
,s2
,s3
,我们就可以开始编写代码了.
首先,我们想到了使用map
函数,为了遍历多个字典,那么如何将他们两两做处理呢?
这就需要Python函数式编程另一个很常用的成员reduce
了.
reduce
是来源于functools
包的,需要引入.
1 | from functools import reduce |
reduce
接受一个function
作为处理函数,后面是一个sequence
.
其处理流程就有点像递归(我觉得…),前一个的处理结果就是下一个的参数,
那么有了这些前导函数的铺垫,我们就可以来看一下这个功能的实现啦.
1 | fromN random import randint, sample |
使用Deque来实现历史记录
Deque
是一个双端队列(环形缓冲区),使用这么一个数据结构可以实现一个历史记录的功能.
根据Deque的长度来决定历史记录的条数,多出来的记录会被删除,仅保留最新的N条.
1 | from collections import deque |
这样就可以得到一个deque对象了,接着我们存入数据:
1 | q.append(12) |
可以看出,第一个值已经被挤出去了.
迭代
这之中有两个极易混淆的东西,一个叫迭代器(iterator),一个叫可迭代对象(iterable)
涉及到的核心方法和接口有__iter__
和__next__
.一个最最最简单的迭代实现就像是这样:
1 | class T: |
调用时就像是这样:
1 | t = T() |
在这个例子中,ter
是迭代器,t
是一个可迭代对象.也就是说,实现了iter
接口的就是一个可迭代对象.
对于字符串,我们都知道是一个可迭代的对象,如果你使用Python2,在检测时会发现:
1 | hasattr("str", '__iter__') # 返回的是False |
在Python3中你是看不到这个现象的,因为str对象已经实现了__iter__
接口
所以说,如果一个对象实现了__getitem__
接口或者__iter__
接口,它就是可迭代的.就可以使用iter()
方法获得他的迭代器.
接着,就可以使用next()
方法对一个迭代器进行迭代.
用迭代器实现一个小栗子:
1 | class SmallLiZi: |
这就是一个完全没有考虑算法的简单小程序,每次next
得到一个素数.
这个例子也可以通过**生成器(generator)**来实现, 一个简单的生成器的小栗子就像是这样:
1 | def f(): |
如果使用type()
查看g
对象, 就会发现他是一个生成器对象(<class 'generator'>
), 这个对象同时实现了__iter__
.__next__
接口.
1 | "__iter__" in dir(g) # True |
简单的迭代大家都会, 在这里说说反向迭代.
反向迭代的意思就是倒序迭代, 譬如,给定一个列表我们可以反向(迭代)输出.
实现的方法很多, 大家一定都已经想到了很多办法: 比如,使用reverse
函数反向列表, 使用倒序切片[::-1]
得到一个新的反向列表.
但这样的方法都不是很好, 因为reverse
改变了原来的列表,切片操作会得到新的列表, 太占用资源.
这个时候就要使用reversed()
来得到一个反向迭代器
, 也就是<class 'list_reverseiterator'>
.
我们已经知道, iter()
方法调用的是__iter__
接口, 那么reversed
调用的是什么神奇的小东西呢?
再次查看一个列表, 我们发现果然里面有一个__reversed__
!
那么, 我们就可以是用这个特性来写一个灵活的发生器了!
1 | class FloatRange: |
Python中的一个强大特性就是切片操作, 然而,并不是每一个对象都可以进行切片的.
比如, 我们以一个文件句柄来做举例:
1 | f = open("./somefile") |
很好, 他报错了.看样子文件句柄是不能直接迭代的.
难道没有还是什么好的办法能够直接迭代读取文件呢?
方法自然是有的. Python标准库itertools
中提供了islice
接口.
来尝试一下:
1 | from itertools import islice |
但是当继续迭代时:
1 | for n in f.readlines(): |
注意, islice的操作并不能像原生的切片操作一样,事实上,这样的切片事实上还是迭代,只是将不合要求的部分舍去了.
可我就是想要原生切片的效果!
好…那么先来看一下数组下标的实现.
先来写一个小列表:
1 | l = [1,2,3] |
哦!原来是这样, 所谓下标就是调用了__getitem__
方法.
那么实现下标查找和切片就变得可能了.
我们写一个这样的类:
1 | class Test: |
好了,现在它可以使用下标来读取文件了.
接下来实现切片:
1 | class Test: |
好了,现在就可以对文件进行真正的切片了.
8.30的更新: 上述的两个文件读取的代码有误, 待重新开发.
顺便提一个问题, 重新获取文件描述符和移动文件指针哪一个更节省资源呢 ?
我们知道操作系统对文件描述符的数量是有限制的. 而这个所谓描述符其实就是对应的就是一个整数来映射打开的目标文件. 而所谓文件指针其实就是结构体的一个属性而已. 那么就这样来看, 似乎是直接移动文件指针更好. 不知道这样想是否正确?
之前在谈字典的迭代的时候, 我们提到了zip
函数, 现在再来更细致的说一下它.
zip
的基本功能就是返回一个迭代的zip对象, 一开始我都没有注意到, 以为只是拼接这么简单, 没想到返回的对象竟然是迭代对象.(后来看了一下, 真的呢..”next“ in dir(z)..True…)
所以说在使用list(z)
进行转列表时, z
就会失去了作用.
如果传入zip
的列表长度不一致会怎么样呢 ? 自己做实验 ! (哈哈哈.难道你能把我的头放到键pan.cdsvfbgnrtjhe..
如果传入zip
的列表长度不一致,zip
会选择最小长度的并且抛弃其他列表所有超出的元素..
使用zip
来进行一次并行迭代(在一个for循环进行多个对象的同时迭代):
1 | from random import randint |
说完了并行迭代, 我们再来看一下串行迭代.
所谓串行迭代就是指按照顺序迭代多个可迭代对象.
比如这样的一种情况:
有四个班的成绩, 每个班的人数不一样, 现在想要统计英语大于90的人的总数, 怎么做呢?
现在不能并行迭代了, 因为人数不一样的, 如果我们能够把他们拼到一起就好了,所以现在就来使用itertools.chain
吧, 他能把多个iterable
拼接到一起
用法就像是这样:
1 | from itertools import chain |
那么, 使用chain
这个问题的一种可能的实施方案是这样的:
1 | from random import randint |