解析不定长的iterable
:
_
表示占位符,但只能占一个位置,超过一个位置会抛出ValueError
*
表示不定长的占位,同时会将不定长的数据放入一个列表中,与切片
相比的优势在于可以解析不定长
的iterable
,当然,_和也能配合`_`使用,表示丢弃掉不定长的iterable1
2
3
4
5
61, 21, 3, 4, 5, 6) a = (
b, *c, _, _ = a
print(b)
1
print(c)
[21, 3, 4]
队列
与列表
的区别:
- 队列的两端插入和删除的时间复杂度为O(1),列表的两端插入和删除的时间复杂度为O(N)
- 列表为栈结构,为
先进后出
,队列为先进先出
,列表不限长度,队列默认也为不限长度,但是可以使用maxlen
来指定队列长度,超过长度时再append数据时,会先弹出队列第一个数据,再append进队列最后一个数据
队列的相关接口:
- append
- appendleft
- pop
- popleft
- extend
- insert
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15from collections import deque
3) a = deque(maxlen=
1) a.append(
2) a.append(
3) a.append(
4) a.append(
for i in a:
print(i)
...
2
3
4
0] = 10 a[
print(a)
deque([10, 3, 4], maxlen=3)
提取出iterable中的N个最大数或最小数
- 可用
heapq
中的nlargest
或nsmallest
- 也可用
sorted(iterable, key=key, reverse=True)[:n]
或sorted(iterable, key=key, reverse=True)[n:]
处理复杂字典类型
1 | d = {} |
相等
1 | from collections import defaultdict |
字典排序
由于字典是无序
的结构,想要控制字典中的顺序,可以使用collections
模块中的OrderedDict
类,OrderedDict
内部维护着一个根据键插入顺序排序的双向链表,这会在每次插入新元素的时候,会被放入链表尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。
ps:值得注意的是,OrderedDict
的空间消耗是普通字典类型的两倍
1 | from collections import OrderedDict |
OrderedDict
相关接口:
- popitem
- move_to_end
- copy
- keys
- values
- items
zip
zip
可与字典配合使用用于根据key
或者value
排序等等
1 | prices = { |
值得注意的是:zip()
函数创建的是一个只能访问一次的迭代器
1 | prices_and_names = zip(prices.values(), prices.keys()) |
字典的key,value和item
字典的 keys()
方法返回一个展现键集合的键视图对象。 键视图的一个很少被了解的特性就是它们也支持集合操作,比如集合并、交、差运算。 所以,如果你想对集合的键执行一些普通的集合操作,可以直接使用键视图对象而不用先将它们转换成一个 set。
字典的 items()
方法返回一个包含 (键,值) 对的元素视图对象。 这个对象同样也支持集合操作,并且可以被用来查找两个字典有哪些相同的键值对。
字典的 values()
方法也是类似,但是它并不支持这里介绍的集合操作。 某种程度上是因为值视图不能保证所有的值互不相同
对于重复数据的思考
set
虽然能很好且很快的取出重复数据,但是带来的问题是,由于set
中的元素是无序的,会导致set
去重之后的序列也会有被打乱的风险,在此提供的一个思路是,使用生成器函数
结合set
来去重,且当序列中的元素为unhashable
时,同样适用。此方法不仅仅适用于处理普通序列,且可以处理文件中消除重复行等操作。
1 | def dedupe(items, key=None): |
对于iterable中的计数问题
想要得到iterable中出现次数最多的元素的时候,可以采用sort
之后手动计数的方法,更为简单的方式是通过collections
模块中的Counter
函数。
1 | from collections import Counter |
Counter
提供的常见接口:
most_common
update
值得注意的是,Counter
支持常见的算术运算,包含+
,-
,and
,or
等
字典列表排序问题优化
在此引入operator
模块中的itemgetter
函数,f = itemgetter(2)
时,调用f(r)
返回r[2]
,当然,也可以传入多个参数,返回为一个包含多个下标的元祖
。对于排序的影响在于,sorted
函数排序时,可以指定按照指定的值排序。例如:
1 | rows_by_fname = sorted(rows, key=itemgetter('fname')) |
这表明会按照fname
来排序,当然也可以传入多个参数来排序,但是有时候也可以使用lambda
来代替
1 | rows_by_fname = sorted(rows, key=lambda r: r['fname']) |
相比而言,itemgetter
方式会更快一些
排序不支持原生比较的对象
如果需要排序的是一个实例序列(类似于[User(3), User(5), User(6)]
),想通过实例的属性来进行排序时,可使用operator
模块中的attrgetter
函数,例如:
1 | from operator import attrgetter |
当然也可以通过lambda
排序,但lambda
排序会慢一点
1 | sorted(users, key=lambda u: u.user_id) |
对于attrgetter
函数,提供了如下接口:
f = attrgetter('name')
,调用f(r)
时,返回f.name
f = attrgetter('name','date')
,调用f(r)
时,返回`(f.name,f.date)``f = attrgetter('name.first','name.last')
,调用f(r)
时,返回(f.name.first,f.name.last)
字典列表的分组排序
当需要对一个字典或实例的列表采用分组排序时,在此引入itertools
模块中的groupby
函数,作用类似于MYSQL中的group by
函数,使用此函数的前提在于,需要进行分组排序的列表,必须提前根据需要分组的元素进行了排序。以下为示例:
1 | from operator import itemgetter |
过滤列表中的元素
提供以下几种思路
- 使用
列表生成式
,优点在于代码量小,缺点在于过滤复杂条件麻烦且不宜读,而且会占用大量内存 - 使用
filter
函数,返回一个迭代器,而且可以将复杂的过滤条件封装在一个函数中 - 使用
itertools
模块中的compress
函数,也返回一个迭代器,而且采用的是一个Boolean
列表来过滤另外一个列表
列表生成式过滤数据:
1 | 1, 4, -5, 10, -7, 2, 3, -1] mylist = [ |
filter
函数:
1 | values = ['1', '2', '-3', '-', '4', 'N/A', '5'] |
compress
函数:
1 | addresses = [ |
1 | from itertools import compress |
过滤字典
可采用字典推导式
合并多个字典
可采用
dict
中的update
来讲两个字典合并为一个字典可使用
collections
模块中的ChainMap
函数,而且该函数只会创建一个临时的合并字典,以供数据采用,所以效率会更高一点
1 | a = {'x': 1, 'z': 3 } |
1 | from collections import ChainMap |
ChainMap
提供以下接口:
new_child
新加入一个字典进入一个ChainMapparents
返回父节点maps
返回字典列表形式
如果出现重复键,那么第一次出现的映射值会被返回
字符串分割问题
大多数情况可以使用
str.splite
满足要求更好的选择可以是使用
re
模块的splite
函数
1 | 'asdf fjdk; afed, fjek,asdf, foo' line = |
值得注意的是,需要选择是否使用括号来捕捉分组。如果又想使用括号,并且不想捕捉括号内分组,可使用?:...
模式
1 | r'(?:,|;|\s)\s*', line) re.split( |
re.splite
提供了maxsplite
参数,默认为0,表示将字符串中所有值都切分,如果不为0,表示将返回包含给定个数字符串的列表,剩余元素将作为最后一个元素输出
1 | '(?:\s|,|;)\s*', line, 3)) print(re.split( |
关于字符串开头或结尾的思考
str
提供了startwsitch
和endswitch
函数来检查字符串的开头或结尾,值得注意的是,传参只能为str
类型或tuple
类型。当然,参数也支持start
和end
,作为开始和结束位置的检查点
对于开头或结尾的处理,使用切片或者是正则表达式也是可以的,但是使用startwsitch
和endswitch
会更快且更方便
1 | 'http://www.python.org' url = |
关于字符串替换
- 对于一般情况,可使用
str.replace()
即可满足 - 更多情况,需要使用
re
模块的sub
函数,来个性化定制需要替换的值 str
提供了translate()
来替换和清理较为复杂的字符串
1 | 'Today is 11/27/2012. PyCon starts 3/13/2013.' text = |
反斜杠数字比如 \3
指向前面模式的捕获组号。如果需要使用相同模式来做多次替换,可以考虑先编译来提升性能
而对于更加复杂的替换,可以使用一个回调函数来替代
1 | from calendar import month_abbr |
如果想要查看发生了多少次替换,可用re.subn()
来代替
1 | r'\3-\1-\2', text) newtext, n = datepat.subn( |
ps:对于大多数正则表达式来说,都提供一个flags
参数,flags=re.IGNORECASE
是,表示忽略大小写
多行匹配模式
对于正则表达式来说,点(.
)并不支持换行符匹配,对于需要多行匹配的问题,提供两种思路:
- 在正则表达式中加入换行匹配规则,例:
re.compile(r'/\*((?:.|\n)*?)\*/')
re.compile()
函数接受一个参数叫re.DOTALL
,表示点(.
)接受包括换行符在内的任意字符
1 | r'/\*(.*?)\*/', re.DOTALL) comment = re.compile( |
字符串对齐
str
提供了ljust()
,rjust()
和center()
方法可以指定对齐方式- 使用
foemat()
函数对齐
1 | '>20') format(text, |
填充字符
1 | '=>20s') format(text, |
格式化多个值
1 | '{:>10s} {:>10s}'.format('Hello', 'World') |
关于字符串拼接的思考
- 最主流的还是使用
str
提供的join
函数 - 对于简单的拼接也可使用
+
和format()
函数
对于性能的考虑
使用+
连接符去操作大量字符串的效率非常低下,因为加号会带来内存复制和垃圾回收,应尽量避免写下面的函数
1 | s = '' |
更为聪明的做法是使用生成器表达式
1 | 'ACME', 50, 91.1] data = [ |
同时,注意不必要的字符连接
1 | print(a + ':' + b + ':' + c) # Ugly |
关于字节类型的字符串(byte类型)
字节类型的字符串支持大部分字符串的操作,例如:
replace
,splite
,切片
,正则表达式
等值得注意的是,字节类型的字符串通过索引返回的是整数而非操作数,例如
1
2
3b'hello' a =
0]) print(a[
104
ps:与python2对比,字节类型的字符串与普通字符串并无区别,例如:
1 | print type(bytes('hello')) |
字节类型与字符串类型的相互转换:
1 | b'hello' a = |
关于数字的处理
四舍五入
一般情况,可用python内置函数round(value, ndigits)
即可,ndigits
表示对于小数点后几位来四舍五入,当然,也可为负数,表示对整数后几位进行四舍五入
浮点数精度问题
1 | 4.2 a = |
这些错误是由底层CPU和IEEE 754标准通过自己的浮点单位去执行算术时的特征.所以没法自己去避免这些误差
想要无误差处理浮点数精度问题,可参考decimal
模块Decimal
函数
1 | from decimal import Decimal |
随机选择
random.choice()
表示随机选择一个字符
1 | import random |
random.sample()
表示随机选择n
个字符
1 | 2) random.sample(values, |
random.shuffle()
表示打乱排序
1 | random.shuffle(values) |
random.randint()
表示随机选择一个整数
1 | 0,10) random.randint( |
random.random()
表示随机生成一个从0到1的浮点数
1 | random.random() |
random.getrandbits(k)
表示随机生成k
为二进制随机数的整数
1 | 10) random.getrandbits( |
PS:值得注意的是,random
模块采用的是Mersenne Twister 算法来计算生成随机数。这是一个确定性算法, 但是你可以通过 random.seed()
函数修改初始化种子。所以,对于安全性要求高的应尽量避免random
模块的使用
时间与日期
时间段
1 | from datetime import timedelta |
值得注意的是,timedelta
函数并没有提供年
和月
的时间段函数,使用timedelta
的优势在于可是与datetime
的时间做运算
1 | from datetime import datetime |
迭代器与生成器
使用生成器实现深度优先算法
1 | class Node: |
反向迭代
对于一个iterable
可以使用reversed()
函数来实现反向迭代,例如
1 | print(reversed([1, 2, 3])) |
当然,列表的内存消耗过大,如果处理大文件形成的列表可以转换为元祖进行处理。
除此之外,可以使用自定义类中的魔法函数__reversed__()
来实现反向迭代,例如
1 | class Countdown: |
相比而言,反向迭代器运行非常高效,因为它不再需要将数据填充到一个列表中然后再去反向迭代这个列表。
生成器与迭代器切片
生成器与迭代器并不能像普通列表切片一样,可以通过itertools
模块中的islice()
函数实现切片,例
1 | from itertools import islice |
值得注意的是,islice()
是会消耗掉迭代器或生成器中的值,因为必须考虑到迭代器是一个不可逆的过程
1 | for i in a: |
处理文件中需要跳过的部分
顾名思义,跳过不需要的部分,可使用itertools
模块中的dropwhile()
函数,例如,可跳过注释部分,代码如下
1 | with open(path, 'r') as f: |
值得注意的是,dropwhile
函数只能跳过文章开头的部分,并不能跳过中间满足条件的部分
如果想要提取文件中的部分值,仍可通过islice
函数获取
1 | with open(path, 'r') as f: |
如果想要跳过所有满足条件的注释行,可改动下代码
1 | with open(path, 'r') as f: |
同时迭代多个序列
简言之,就是将多可序列+
起来,可使用内置函数zip()
,zip()
函数返回一个迭代器,并且迭代长度和参数中最短序列长度一致
1 | a = [1, 2, 3] |
当然,返回参数中最大长度也是可以的,可以使用itertools
模块中的zip_longest
函数
1 | from itertools import zip_longest |
关于多个可迭代对象求和
1 | a = [1, 2, 4] |
通过这种常规的方式是可以达到目的的,有两个问题:
- 列表过大时,消耗内存也会太大
- a,b不是同一种类型时,这种操作会抛异常
所以,在此引入itertools
模块中的chain
函数,上面的可改为:
1 | from itertools import chain |
chain
的优势在于,可处理两种不同类型的iterable
,且会省内存
关于嵌套序列的处理
关于嵌套序列的处理存在多种方法,在此使用生成器的方式,在于节省内存且代码优雅,例如
1 | from collections import Iterable |
文件与IO
关于文件读取
open()
函数打开文件时,对于换行符的识别在UNIX和Windows下的识别是不一样的(分别为\n
和\r\n
),默认情况下,python会统一处理换行符,并在输出时,将换行符替换为\n
,当然也可以手动指定换行符,使用newline
参数指定在文件编码时,可能出现编码和解码方式不一样而导致打开文件失败的情况,可指定
error
参数,来处理打开失败的情况
1 | with open(path, 'rt', encoding='ascii', errors='replace') as f: |
1 | with open(path, 'rt', encoding='ascii', errors='ignore') as f: |
关于print函数
1 | print(self, *args, sep=' ', end='\n', file=None) |
其中seq
是指定多个参数时的分隔符,默认为空格;end
是在输出的末尾加上需要的字符,使用end
参数在输出中禁止换行,默认为换行符;file
是指流文件,可以用作重定向字符到文件中
1 | print(2019, 7, 24, sep='-', end='!!!') |
值得注意的是,对于参数合并时,''.join
也支持字符串合并,但是仅支持字符串合并,而sep
参数能将不同类型的参数合并在一起
处理文件不存在时才能写入
言下之意是,在目录中不能存在这个文件名,如果存在就会抛异常,通常会存在以下两种方案:
- 通过
os.path.exists
判断该文件是否存在 open
函数提供了x
参数,表示会创建一个文件,并以写的方式打开,且文件如果已经存在会抛异常
1 | with open(path, 'x') as f: |
值得注意的是,x
参数是python3才引入的,之前版本并不支持
字符串I/O操作
在涉及到需要创建一个文件来存储数据时,通常存在以下两种方式:
- 在本地磁盘创建一个文件,以写的方式放入数据,用完之后再删了,这样的有点在于不会存在太大的内存限制,缺点在于可能会存在读写速度问题
- 使用
io.StringIO()
函数来处理,优势在于将数据存在内存中,读写速度会高于磁盘读写,缺点在于,数据量过大可能会带来内存问题
1 | # write a file |
同理,涉及到二进制数据时,需要用BytesIO
函数来代替
需要注意的是, StringIO
和 BytesIO
实例并没有正确的整数类型的文件描述符。 因此,它们不能在那些需要使用真实的系统级文件如文件,管道或者是套接字的程序中使用。
固定字符串长度迭代文件
1 | from functools import partial |
这样会不断产生一个固定大小的数据块,当然也可以自己根据需要产生的数据块进行逻辑处理。
需要注意的地方在于,普通文本的处理方式,默认迭代方法为一行一行的读取,这通常是更普遍的做法
关于os.path
处理文件路径
1 | import os |
1 | '/etc/passwd') os.path.isfile( |
不同类型文件格式处理
csv文件处理
读取
csv
文件包含两种读取方式,可通过字符串读取和字典读取
1
2
3
4
5import csv
with open('stocks.csv') as f:
f_csv = csv.reader(f)
headers = next(f_csv)
for row in f_csv:1
2
3
4import csv
with open('stocks.csv') as f:
f_csv = csv.DictReader(f)
for row in f_csv:写入
csv
文件也包含两种写入方式,可通过普通字符串写入和字典写入
1
2
3
4with open('stocks.csv','w') as f:
f_csv = csv.writer(f)
f_csv.writerow(headers)
f_csv.writerows(rows)
1
2
3
4
with open('stocks.csv','w') as f:
f_csv = csv.DictWriter(f, headers)
f_csv.writeheader()
f_csv.writerows(rows)
csv
文件中分割字符串csv.reader()
函数中带有delimiter
参数可指定分割方式1
2
3with open('stock.tsv') as f:
f_tsv = csv.reader(f, delimiter='\t')
for row in f_tsv:csv
文件字符转换由于csv中读取的数据都为字符串类型,只能手动操作数据类型转换
1
2
3
4
5
6col_types = [str, float, str, str, float, int]
with open('stocks.csv') as f:
f_csv = csv.reader(f)
headers = next(f_csv)
for row in f_csv:
row = tuple(convert(value) for convert, value in zip(col_types, row))当然这样存在风险性,是因为实际情况中csv文件或多或少存在缺失的数据,这样可能导致数据转换抛出异常
处理json数据
处理xml文件
python处理xml
文件通常存在多种方式,这里分别以处理简单文件和大文件为例
- 处理一般文件,通常使用
ElementTree
模块,python3.3之后会自动寻找可用的C库来加快速度
1 | try: |
解析根节点
1 | tree = ET.parse('111.xml') |
而获取root的原因在于方便后面解析使用, 通常情况,xml
结构标识为<tag attrib1=1>text</tag>tail
1 | #source data:<country name="Liechtenstein"> |
当然,root也是可以迭代的
1 | for i in root: |
也可以是根据某一个标签进行迭代
1 | for i in root.findall('country'): #遍历所有符合条件子节点 |
修改xml
需要注意的是,xml
中所有字符均为字符串类型,需要注意字符转换
1 | rank.text = str(new_rank) # 必须将int转为str |
修改之后的内容只是放在内存中,所以需要将内存里面的数据保存到磁盘中
处理大型xml文件
当然,处理大型文档,除了使用固有的函数模块之外,还可以使用普通文档解析方式,这样只不过会导致取值更麻烦而已
其实只要一想到处理大型数据,就应该第一时间想到迭代器或者生成器
1 | from xml.etree.ElementTree import iterparse |
iterparse()
方法允许对XML文档进行增量操作。 使用时,你需要提供文件名和一个包含下面一种或多种类型的事件列表: start
, end
, start-ns
和 end-ns
。由 iterparse()
创建的迭代器会产生形如 (event, elem)
的元组, 其中 event
是上述事件列表中的某一个,而 elem
是相应的XML元素。
start
事件在某个元素第一次被创建并且还没有被插入其他数据(如子元素)时被创建。 而 end
事件在某个元素已经完成时被创建。
在 yield
之后的下面这个语句才是使得程序占用极少内存的ElementTree的核心特性:
1 | elem_stack[-2].remove(elem) |
这个语句使得之前由 yield
产生的元素从它的父节点中删除掉。 假设已经没有其它的地方引用这个元素了,那么这个元素就被销毁并回收内存。
对节点的迭代式解析和删除的最终效果就是一个在文档上高效的增量式清扫过程。 文档树结构从始自终没被完整的创建过。尽管如此,还是能通过上述简单的方式来处理这个XML数据。
将字典类型数据转换为xml
存在两种解决方案:
- 手动构造,以字符串的
format
函数替代的方式来构造,不过这样显得有点蠢 - 使用
xml.etree.ElementTree
模块中的Element
函数
1 | from xml.etree.ElementTree import Element, tostringdef |
这样做的目的在于,可以通过查询数据库中的值放进字典中,利用字典生成xml
文件