Python小技巧

Python编程技巧.[环境:Python3.6.0]

Part 1 基本数据结构与collections

过滤

过滤操作是一个很常用的操作,Python作为函数式编程语言,提供了非常好的解析支持.

比如过滤负数,常规操作如下:

1
2
3
4
5
data = [-4,2,4,9,10,-2]
res = []
for n in data:
if n >= 0:
res.append(n)

但事实上,仅仅需要一行就可以解决问题

1
res = [x for x in data if x >= 0]

如果使用filter函数,也仅仅需要一行:

1
res = filter(lambda x : x >= 0, data)

经过测试,列表解析的速度更快,因此应该更多的使用解析的方式.

同样,对于字典.集合等基本数据结构,解析方式都可以快速的进行过滤.

请看:

1
2
3
from random import randint
data = {k: randint(60,100) for k in range(22)} # 随机生产一个字典
res = {k:v for k,v in data.items() if v >= 90} # 筛选出所有v >= 90 的键值对.

对于集合:

1
2
data = {randint(-10,10) for x in range(10)}
res = {x for x in data if x >= 0}

其实只要理解了解析式,过滤操作就会变得很简单了.

命名.统计.字典排序

命名

当我们用一个列表来存储一条类似数据库记录的对象时,会遇到这样的问题.

1
2
student = ('Justin', 19, 'male', 'justin13wyx@gmail.com')
print('Name:',studnet[0]) # Is there anyone who can figure out what student[0] refer to ???

此时,如果我们想要取得student的姓名,就要通过数组下标的方式来查找,这样会大大降低程序的可读性.

首先,我们想到了C#DEFINE,即:常量声明.

那么我们的程序就会变成这样:

1
2
3
4
5
6
7
8
NAME = 0
AGE = 1
SEX = 2
MAIL = 3
student = ('Justin', 19, 'male', 'justin13wyx@gmail.com')
print("Name:",student[NAME]) # Now it is clear to see. Buuuut...,it is a little complicated,isn't it ??
# ---------------------------
NAME, AGE, SEX, MAIL = range(4) # This is a better way.

为了给元组中的每个元素命名,我们引入collections中的namedtuple.

这个namedtuple构造器的方法签名是这样的:namedtuple(typename, field_names, *, verbose=False, rename=False, module=None)

其中,typename决定类型的名字(注意:这个名字决定返回的tuple子类的名字)field_names是一个列表.

1
2
3
4
from collections import namedtuple
Student = namedtuple('Student', ['name', 'age', 'sex', 'mail'])
s = Student('name', 19, 'male', 'mailAddr')
print(s['name'])

此时使用isinstance来测试一下 isinstance(s, tuple), 会返回True
同样使用isinstance(s,Student)会返回True.
使用type(s)来看看, 会返回 <class '__main__.Student'>.

所以,namedtuple是可以动态创建类的,这个创建的类是tuple的子类.

统计

常规的统计方法是这样的:

1
2
3
4
5
from random import randint
data = [randint(0,10) for _ in range(20)]
counter = dict.fromkeys(data, 0) # 生成
for x in data:
counter[x] += 1

这样处理后的counter就是一个data的频度字典了.

接着可以使用sorted 函数来进行从大到小的排序从而得到结果.

一种更便利的方式是引入collectionsCounter

Counter类继承于dict,适用方法如下:

1
2
c = Counter(data)
c.most_common(3) # 选出频度最高的三个元素

most_common方法返回的是一个元素为tuplelist.

如果要进行词频统计,我们需要正则模块re.

1
2
3
4
5
6
7
import re
from collections import Counter
f = open('./text', 'r')
txt = f.read()
# 现在就可以开始统计了
c = Counter(re.split('\W+', txt))
c.most_common(10) # 选出最高的十个单词

字典排序

如果曾经尝试过字典排序,你就会发现:字典中的可迭代对象是key

可以通过下面这段代码来验证

1
2
3
from random import randint
d = { k:randint(0,20) for k in range(10) }
list(iter(d)) # 返回的是0,1,2,3...8,9.

但是更多的场景下,我们需要对字典的值进行排序.

有以下几个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
2
3
from random import randint, sample
# 在本例中可以这样设计测试数据
s1 = { k:randint(1,5) for k in sample('abcdef', randint(3,6)) }

得到了测试数据s1,s2,s3,我们就可以开始编写代码了.

首先,我们想到了使用map函数,为了遍历多个字典,那么如何将他们两两做处理呢?

这就需要Python函数式编程另一个很常用的成员reduce了.

reduce是来源于functools包的,需要引入.

1
from functools import reduce

reduce接受一个function作为处理函数,后面是一个sequence.

其处理流程就有点像递归(我觉得…),前一个的处理结果就是下一个的参数,

那么有了这些前导函数的铺垫,我们就可以来看一下这个功能的实现啦.

1
2
3
4
5
6
7
8
9
10
fromN random import randint, sample
from functools import reduce
def getData():
return { k: randint(0,4) for k in sample('abcdef', randint(3,6)) }
s1 = getData()
s2 = getData()
s3 = getData()
# 至此我们完成了准备工作
reduce(lambda a,b : a&b, map(dict.keys, [s1,s2,s3]))
# Awesome! 只需要一行代码就可以解决问题 !

使用Deque来实现历史记录

Deque是一个双端队列(环形缓冲区),使用这么一个数据结构可以实现一个历史记录的功能.

根据Deque的长度来决定历史记录的条数,多出来的记录会被删除,仅保留最新的N条.

1
2
from collections import deque
q = deque([], 5) # 一定要声明!否则这个deque的最大长度是默认的NULL

这样就可以得到一个deque对象了,接着我们存入数据:

1
2
3
4
5
6
7
8
9
10
q.append(12)
q.append(23)
q.append(34)
q.append(45)
q.append(56)
q
# Out: [12,23,34,45,56]
q.append(67)
q
# Out: [23,34,45,56,67]

可以看出,第一个值已经被挤出去了.

迭代

这之中有两个极易混淆的东西,一个叫迭代器(iterator),一个叫可迭代对象(iterable)

涉及到的核心方法和接口有__iter____next__.一个最最最简单的迭代实现就像是这样:

1
2
3
4
5
6
class T:
def __iter__(self):
return Ter()
class Ter:
def __next__(self):
return "0"

调用时就像是这样:

1
2
3
4
t = T()
ter = iter(t) # 因为t实现了iter接口
next(ter) # ter实现了next接口
# Out:0

在这个例子中,ter是迭代器,t是一个可迭代对象.也就是说,实现了iter接口的就是一个可迭代对象.

对于字符串,我们都知道是一个可迭代的对象,如果你使用Python2,在检测时会发现:

1
2
3
hasattr("str", '__iter__') # 返回的是False
# 但是:
hasattr("str", '__getitem__') # 返回True

在Python3中你是看不到这个现象的,因为str对象已经实现了__iter__接口

所以说,如果一个对象实现了__getitem__接口或者__iter__接口,它就是可迭代的.就可以使用iter()方法获得他的迭代器.

接着,就可以使用next()方法对一个迭代器进行迭代.

用迭代器实现一个小栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class SmallLiZi:
def __init__(self, start, end):
self.start = start
self.end = end
def __iter__(self):
return SmallLiZier(self.start, self.end)
class SmallLiZier:
def __init__(self, start, end):
self.start = start
self.end = end
self.index = 0
def __next__(self):
self.index += 1
# print(self.index, self.end)
if self.index >= self.end:
raise StopIteration
while not self.isPrime(self.index):
self.index += 1
print(self.index)
return self.index
def isPrime(self, num):
for n in range(2,num):
if num % n == 0:
return False
return True

这就是一个完全没有考虑算法的简单小程序,每次next得到一个素数.

这个例子也可以通过**生成器(generator)**来实现, 一个简单的生成器的小栗子就像是这样:

1
2
3
4
5
6
7
8
def f():
yield 1
yield 2
yield 3
g = f()
next(g) # 1
next(g) # 2
next(g) # 3

如果使用type()查看g对象, 就会发现他是一个生成器对象(<class 'generator'>), 这个对象同时实现了__iter__.__next__接口.

1
2
"__iter__" in dir(g) # True
"__next__" in dir(g) # True

简单的迭代大家都会, 在这里说说反向迭代.

反向迭代的意思就是倒序迭代, 譬如,给定一个列表我们可以反向(迭代)输出.

实现的方法很多, 大家一定都已经想到了很多办法: 比如,使用reverse函数反向列表, 使用倒序切片[::-1]得到一个新的反向列表.

但这样的方法都不是很好, 因为reverse改变了原来的列表,切片操作会得到新的列表, 太占用资源.

这个时候就要使用reversed()来得到一个反向迭代器, 也就是<class 'list_reverseiterator'>.

我们已经知道, iter()方法调用的是__iter__接口, 那么reversed调用的是什么神奇的小东西呢?

再次查看一个列表, 我们发现果然里面有一个__reversed__!

那么, 我们就可以是用这个特性来写一个灵活的发生器了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FloatRange:          
def __init__(self, start,end,step):
self.start = start
self.end = end
self.step = step

def __iter__(self):
n = self.start
while n < self.end:
yield n
n += self.step

def __reversed__(self):
d = self.end
while d > self.start:
yield d
d -= self.step

Python中的一个强大特性就是切片操作, 然而,并不是每一个对象都可以进行切片的.

比如, 我们以一个文件句柄来做举例:

1
2
3
4
5
f = open("./somefile")
f[10:20]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '_io.TextIOWrapper' object is not subscriptable.

很好, 他报错了.看样子文件句柄是不能直接迭代的.

难道没有还是什么好的办法能够直接迭代读取文件呢?

方法自然是有的. Python标准库itertools中提供了islice接口.

来尝试一下:

1
2
3
4
5
from itertools import islice
f = open("./somefile")
for line in islice(f, 10, 20):
print(line)
# 哈哈有效果啦!

但是当继续迭代时:

1
2
3
for n in f.readlines():
print(n)
# woc, 怎么不从头开始 ?!

注意, islice的操作并不能像原生的切片操作一样,事实上,这样的切片事实上还是迭代,只是将不合要求的部分舍去了.

可我就是想要原生切片的效果!

好…那么先来看一下数组下标的实现.

先来写一个小列表:

1
2
3
4
l = [1,2,3]
l[0] # 1
l.__getitem__(0) # 1
l.__getitem__(1) # 2

哦!原来是这样, 所谓下标就是调用了__getitem__方法.

那么实现下标查找和切片就变得可能了.

我们写一个这样的类:

1
2
3
4
5
6
7
8
9
class Test:                 
def __init__(self, file):
self.file = file
self.index = 0
def __getitem__(self, n):
while self.index < n-1:
f.readline()
self.index += 1
return f.readline()

好了,现在它可以使用下标来读取文件了.

接下来实现切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Test:
def __init__(self, file):
self.file = file
self.index = 0
def __getitem__(self, n):
if isinstance(n, int):
while self.index < n - 1:
self.file.readline()
self.index += 1
return f.readline()
if isinstance(n, slice):
start = n.start
end = n.stop
print(end)
res = []
if start is None:
start = 0
for line in range(end):
if line >= start:
res.append(self.file.readline())
else:
self.file.readline()
return res

好了,现在就可以对文件进行真正的切片了.

8.30的更新: 上述的两个文件读取的代码有误, 待重新开发.

顺便提一个问题, 重新获取文件描述符和移动文件指针哪一个更节省资源呢 ?

我们知道操作系统对文件描述符的数量是有限制的. 而这个所谓描述符其实就是对应的就是一个整数来映射打开的目标文件. 而所谓文件指针其实就是结构体的一个属性而已. 那么就这样来看, 似乎是直接移动文件指针更好. 不知道这样想是否正确?


之前在谈字典的迭代的时候, 我们提到了zip函数, 现在再来更细致的说一下它.

zip的基本功能就是返回一个迭代的zip对象, 一开始我都没有注意到, 以为只是拼接这么简单, 没想到返回的对象竟然是迭代对象.(后来看了一下, 真的呢..”next“ in dir(z)..True…)

所以说在使用list(z)进行转列表时, z就会失去了作用.

如果传入zip的列表长度不一致会怎么样呢 ? 自己做实验 ! (哈哈哈.难道你能把我的头放到键pan.cdsvfbgnrtjhe..

如果传入zip的列表长度不一致,zip会选择最小长度的并且抛弃其他列表所有超出的元素..

使用zip来进行一次并行迭代(在一个for循环进行多个对象的同时迭代):

1
2
3
4
5
6
7
8
from random import randint
chi = [randint(60,100) for _ in range(44)]
math = [randint(60,100) for _ in range(44)]
eng = [randint(60,100) for _ in range(44)]
# 得到测试数据
total = []
for c,m,e in zip(chi):
total.append(c+m+e)

说完了并行迭代, 我们再来看一下串行迭代.

所谓串行迭代就是指按照顺序迭代多个可迭代对象.

比如这样的一种情况:

有四个班的成绩, 每个班的人数不一样, 现在想要统计英语大于90的人的总数, 怎么做呢?

现在不能并行迭代了, 因为人数不一样的, 如果我们能够把他们拼到一起就好了,所以现在就来使用itertools.chain吧, 他能把多个iterable拼接到一起

用法就像是这样:

1
2
from itertools import chain
total = chain(p1,p2,p3)

那么, 使用chain这个问题的一种可能的实施方案是这样的:

1
2
3
4
5
6
7
8
9
10
11
from random import randint
from itertools import chain
c1 = [randint(60,100) for _ in range(44)]
c2 = [randint(60,100) for _ in range(41)]
c3 = [randint(60,100) for _ in range(39)]
c4 = [randint(60,100) for _ in range(43)]
count = 0
for stu in chain(c1,c2,c3,c4):
if stu > 90:
count += 1
count # 36(样例输出)