cook book(1:数据结构和算法 2:字符串和文本)
1:将序列分解为单独的变量
# 列表,元组,字符串,文件对象,迭代器和生成器都可以进行分解 data = ['ACME', 50, 91.1, (2012, 12, 21)] name, shares, price, (year, mon, day) = data
# 你可能只想解压一部分,丢弃其他的值。对于这种情况 Python 并没有提供特殊的语法。 但是你可以使用任意变量名去占位,到时候丢掉这些变量就行了。 _, aaa, bbb, _ = data
2:解压可迭代对象赋值给多个变量
def avg(minddle): """ 求平均值函数 :param minddle:一个可迭代的对象,对象里面都是数字类型 :return: """ if len(minddle) > 0: return sum(minddle) / len(minddle) def drop_first_last(grades): first, *middle, last = grades return avg(middle)
# 有一个公司前 8 个月销售数据的序列, 但是你想看下最近一个月数据和前面 7 个月的平均值的对比 *trailing_qtrs, current_qtr = sales_record # *trailing_qtrs取出前面七个月数据组成一个列表 trailing_avg = sum(trailing_qtrs) / len(trailing_qtrs) return avg_comparison(trailing_avg, current_qtr)
# 星号表达式在迭代元素为可变长元组的序列时是很有用的 records = [ ('foo', 1, 2), ('bar', 'hello'), ('foo', 3, 4), ] def do_foo(x, y): print('foo', x, y) def do_bar(s): print('bar', s) for tag, *args in records: # 这里把records里面初第一个元素后面的所有元素组成一个(x1,x2,x3....)的可迭代的元组args,args = (x1,x2,x3....) if tag == 'foo': do_foo(*args) # 这里把args这个元组解包成一个个元素x1,x2,x3.... elif tag == 'bar': do_bar(*args)
# 星号解压语法在字符串操作的时候也会很有用,比如字符串的分割。 line = 'nobody:*:-2:-2:Unprivileged User:/var/empty:/usr/bin/false' uname, *fields, homedir, sh = line.split(':')
# 有时候,你想解压一些元素后丢弃它们,你不能简单就使用 * , 但是你可以使用一个普通的废弃名称,比如 _ 或者 ign (ignore) record = ('ACME', 50, 123.45, (12, 18, 2012)) name, *_, (*_, year) = record print(year)
items = [1, 10, 7, 4, 5, 9] #在很多函数式语言中,星号解压语法跟列表处理有许多相似之处 head, *tail = items print(tail) print(*tail)
# 分割语法去巧妙的实现递归算法 import sys sys.setrecursionlimit(1000000) # 解除python解释器递归的限制 items = [i for i in range(1000)] def sum(items): # *拆分实现的递归 head, *tail = items return head + sum(tail) if tail else head print(sum(items)) len_s = len(items) def get_sum(items, long, index=0): # 避免使用过多内存直接使用列表下标的递归 if long == index + 1: return items[-1] return items[index] + get_sum(items, long, index + 1) print(get_sum(items, len_s))
3:保留最后 N 个元素
# deque(maxlen=N) 构造函数会新建一个固定大小的队列,当新的元素加入并且这个队列已满的时候, 最老的元素会自动被移除掉。 from collections import deque def search(lines, pattern, history=5): previous_lines = deque(maxlen=history) for line in lines: if pattern in line: yield line, previous_lines previous_lines.append(line) # Example use on a file if __name__ == '__main__': with open(r'../data/somefile.txt', encoding="utf8") as f: for line, prevlines in search(f, 'python', 5): print(prevlines) # for pline in prevlines: # print(pline, end='') # # print(prevlines) # print(line, end='') # # print('-' * 20)
# 搜索过程的代码和使用搜索结果的代码解耦,使用生成器一个个返回搜索结果
# deque(maxlen=N) 构造函数会新建一个固定大小的双端队列。当新的元素加入并且这个队列已满的时候, 最老的元素会自动被移除掉。 from collections import deque q = deque(maxlen=3) q.append(1) q.append(2) q.append(3) q.append(4) q.append(5) print(q) # deque([3, 4, 5], maxlen=3) q.appendleft(4) # 左边加 q.pop() # 后取值,弹出来 q.popleft() #左边取值也就是前面取值,弹出来
在队列两端插入或删除元素时间复杂度都是O(1)
,区别于列表,在列表的开头插入或删除元素的时间复杂度为O(N)
4:查找最大或最小的 N 个元素
# 堆heapq模块 heappush(heap, x) 将x压入堆中 heappop(heap) 从堆中弹出最小的元素 heapify(heap) 让列表具备堆特征 heapreplace(heap, x) 弹出最小的元素,并将x压入堆中 nlargest(n, iter) 返回iter中n个最大的元素 nsmallest(n, iter) 返回iter中n个最小的元素
# heapq 模块有两个函数:nlargest() 和 nsmallest() 可以完美解决这个问题。 # 集合中获得最大或者最小的 N 个元素列表:heapq 模块有两个函数:nlargest() 和 nsmallest from heapq import nlargest, nsmallest nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2] print(nlargest(5, nums)) # 求最大的五个数 print(nsmallest(4, nums)) # 求最小的四个数
# key接收一个关键字参数,更复杂的排序 portfolio = [ {'name': 'IBM', 'shares': 100, 'price': 91.1}, {'name': 'AAPL', 'shares': 50, 'price': 543.22}, {'name': 'FB', 'shares': 200, 'price': 21.09}, {'name': 'HPQ', 'shares': 35, 'price': 31.75}, {'name': 'YHOO', 'shares': 45, 'price': 16.35}, {'name': 'ACME', 'shares': 75, 'price': 115.65} ] cheap = heapq.nsmallest(3, portfolio, key=lambda s: s['price']) # 根据s元素里面的'price'这一项的值来排序 expensive = heapq.nlargest(3, portfolio, key=lambda s: s['price'])
# 数据集合中查找最大最小的N个元素,使用堆heapq,底层性能好,会先将集合数据进行堆排序后放入一个列表中 import heapq nums = [1, 8, 2, 23, 7, -4, 18, 23, 42, 37, 2] heap = list(nums) heapq.heapify(heap) # heapify构建一个堆特性(堆数据结构),元素1小于元素2和元素3,元素2小于元素4和元素5,元素3小于元素6和元素7---堆特性 print(heap) print(heapq.heappop(heap)) # heappop函数弹出第一个值,还会维护这个列表堆的特性,第一个值是最小的 print(heapq.heappop(heap)) print(heapq.heappop(heap))
堆数据结构最重要的特征是heap[0]
永远是最小的元素。并且剩余的元素可以很容易的通过调用heapq.heappop()
方法得到,
该方法会先将第一个元素弹出来,然后用下一个最小的元素来取代被弹出元素(这种操作时间复杂度仅仅是 O(log N),N 是堆大小)。
比如,如果想要查找最小的 3 个元素,你可以这样做:
当要查找的元素个数相对比较小的时候,函数nlargest()
和nsmallest()
是很合适的。
如果你仅仅想查找唯一的最小或最大(N=1)的元素的话,那么使用min()
和max()
函数会更快些。
类似的,如果 N 的大小和集合大小接近的时候,通常先排序这个集合然后再使用切片操作会更快点 (sorted(items)[:N]
或者是sorted(items)[-N:]
)。
需要在正确场合使用函数nlargest()
和nsmallest()
才能发挥它们的优势 (如果 N 快接近集合大小了,那么使用排序操作会更好些)。
5:实现一个优先级队列
# heapq实现一个优先级队列 # _queue里面放的是一个[(priority,index,item), (priority,index,item).....] # heappush每次往列表里面放元素遵循堆结构特性,1比2,3小,2比4,5小,3比6,7小..... # 往列表里放元素维持堆特性需要比较元素大小,先比较priority优先级,priority相同的话比较index下标 # heappop往遵循堆特性的列表取值会取第一个列表的元素并且继续维持列表的堆特性,1比2,3小,2比4,5小,3比6,7小..... import heapq class PriorityQueue: def __init__(self): self._queue = [] self._index = 0 def push(self, item, priority): heapq.heappush(self._queue, (-priority, self._index, item)) self._index += 1 def pop(self): return heapq.heappop(self._queue)[-1] class Item: def __init__(self, name): self.name = name def __repr__(self): return 'Item({!r})'.format(self.name) q = PriorityQueue() q.push(Item('foo'), 1) q.push(Item('bar'), 5) q.push(Item('spam'), 4) q.push(Item('grok'), 2) print(q.pop()) print(q.pop())
仔细观察可以发现,第一个pop()
操作返回优先级最高的元素。 另外注意到如果两个有着相同优先级的元素(foo
和grok
),pop 操作按照它们被插入到队列的顺序返回的。
函数heapq.heappush()
和heapq.heappop()
分别在队列_queue
上插入和删除第一个元素,
并且队列_queue
保证第一个元素拥有最高优先级( 1.4 节已经讨论过这个问题)。heappop()
函数总是返回”最小的”的元素,
这就是保证队列pop操作返回正确元素的关键。 另外,由于 push 和 pop 操作时间复杂度为 O(log N),
其中 N 是堆的大小,因此就算是 N 很大的时候它们运行速度也依旧很快
队列包含了一个(-priority, index, item)
的元组。 优先级为负数的目的是使得元素按照优先级从高到低排序。 这个跟普通的按优先级从低到高排序的堆排序恰巧相反index
变量的作用是保证同等优先级元素的正确排序。 通过保存一个不断增加的index
下标变量,
可以确保元素按照它们插入的顺序排序。 而且,index
变量也在相同优先级元素比较的时候起到重要作用。
通过引入另外的index
变量组成三元组(priority, index, item)
, 因为不可能有两个元素有相同的index
值。
Python 在做元组比较时候,如果前面的priority比较就已经可以确定结果了, 如果priority优先级相同比较index
# 元组和列表可以直接比较大写,先对比第一个值的大写,然后对比第二个值的大小 a = (1, Item('foo')) b = (5, Item('bar')) print(a > b)
6:字典中的键映射多个值 实现一个键对应多个值的字典也叫multidict
一个字典就是一个键对应一个单值的映射。
如果你想要一个键映射多个值,那么你就需要将这多个值放到另外的容器中, 比如列表或者集合里面。比如,你可以像下面这样构造这样的字典 d = { 'a' : [1, 2, 3], 'b' : [4, 5] } e = { 'a' : {1, 2, 3}, 'b' : {4, 5} }
选择使用列表还是集合取决于你的实际需求。如果你想保持元素的插入顺序就应该使用列表, 如果想去掉重复元素就使用集合(并且不关心元素的顺序问题)
# collections(带有默认值的字典) 模块中的 defaultdict 可以来构造这样一个键对应多个值的字典。
# defaultdict 的一个特征是它会自动初始化每个 key 刚开始对应的值,所以你只需要关注添加元素操作了。
# 不传值也可以
from collections import defaultdict d = defaultdict(list) d['a'].append(1) d['a'].append(2) d['b'].append(4) d = defaultdict(set) d['a'].add(1) d['a'].add(2) d['b'].add(4)
my_dict = defaultdict(list) #没有给这个字典添加任何元素的时候,所有的value都默认是list # 可以默认是各种类型的参数,但是参数必须是callable可以被调用的参数(名字) # list,set,dict都是可调用的 # 比如默认值是个5就不行,因为5不可以被调用 #使用dict时,如果引用的Key不存在,就会抛出KeyError。如果希望key不存在时,返回一个默认值,就可以用defaultdict: from collections import defaultdict dd = defaultdict(lambda: 'N/A') #lambda: 'N/A' 匿名函数可以调用,返回的值:N/A, # 字典任意的key都有一个默认的值 dd['key1'] = 'abc' print(dd['key1'] ) # 返回:abc key1存在 print(dd['key2'] ) # 返回:N/A key2不存在,返回默认值 from collections import defaultdict dd = defaultdict(lambda :5) #这个字典的默认值可以是任何数据:list,dict,set,值都可以 print(dd['a']) # 5 这就是默认字典:字典里面任意的key都有一个默认值5 print(dd) #defaultdict(<function <lambda> at 0x0000023308C12EA0>, {'a': 5})
# defaultdict 会自动为将要访问的键(就算目前字典中并不存在这样的键)创建映射实体。
# 如果你并不需要这样的特性,你可以在一个普通的字典上使用 setdefault() 方法来代替。 d = {} # 一个普通的字典 d.setdefault('a', []).append(1) # {'a': [1]} d.setdefault('a', []).append(2) d.setdefault('b', []).append(4)
7:字典排序 有序字典:collections
模块中的 OrderedDict
类
# OrderedDict,在迭代操作的时候它会保持元素被插入时的顺序 from collections import OrderedDict d = OrderedDict() d['foo'] = 1 d['bar'] = 2 d['spam'] = 3 d['grok'] = 4 # Outputs "foo 1", "bar 2", "spam 3", "grok 4" for key in d: print(key, d[key])
# 构建一个将来需要序列化或编码成其他格式的映射的时候, OrderedDict 是非常有用的。
比如,你想精确控制以 JSON 编码后字段的顺序,你可以先使用 OrderedDict 来构建这样的数据:
# OrderedDict 内部维护着一个根据键插入顺序排序的双向链表。每次当一个新的元素插入进来的时候,
它会被放到链表的尾部。对于一个已经存在的键的重复赋值不会改变键的顺序。 # 一个 OrderedDict 的大小是一个普通字典的两倍,因为它内部维护着另外一个链表。
所以如果你要构建一个需要大量 OrderedDict 实例的数据结构的时候(比如读取 100,000 行 CSV 数据到一个 OrderedDict 列表中去),
那么你就得仔细权衡一下是否使用 OrderedDict 带来的好处要大过额外内存消耗的影响。
8:字典的运算 字典中执行一些计算操作(比如求最小值、最大值、排序等等)?
prices = { 'ACME': 45.23, 'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.20, 'FB': 10.75 } # 为了对字典值执行计算操作,通常需要使用 zip() 函数先将键和值反转过来。
# zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。zip(iterable,iterable.......) min_price = min(zip(prices.values(), prices.keys())) # min_price is (10.75, 'FB') max_price = max(zip(prices.values(), prices.keys())) # max_price is (612.78, 'AAPL') # 类似的,可以使用 zip() 和 sorted() 函数来排列字典数据: prices_sorted = sorted(zip(prices.values(), prices.keys())) # prices_sorted is [(10.75, 'FB'), (37.2, 'HPQ'), # (45.23, 'ACME'), (205.55, 'IBM'), # (612.78, 'AAPL')] # 执行这些计算的时候,需要注意的是 zip() 函数创建的是一个只能访问一次的迭代器。 比如,下面的代码就会产生错误: prices_and_names = zip(prices.values(), prices.keys()) print(min(prices_and_names)) # OK print(max(prices_and_names)) # ValueError: max() arg is an empty sequence
# 字典上执行普通的数学运算,它们仅仅作用于键,而不是值 min(prices) # Returns 'AAPL' max(prices) # Returns 'IBM' # 想要在字典的值集合上执行这些计算,可以使用字典的 values() 方法来解决这个问题 min(prices.values()) # Returns 10.75 max(prices.values()) # Returns 612.78 # 如果可能还想要知道对应的键的信息(比如那种股票价格是最低的?) # 你可以在 min() 和 max() 函数中提供 key 函数参数来获取最小值或最大值对应的键的信息 min(prices, key=lambda k: prices[k]) # Returns 'FB' min只能得到键值 max(prices, key=lambda k: prices[k]) # Returns 'AAPL' #如果还想要得到最小值,你又得执行一次查找操作。比如: min_value = prices[min(prices, key=lambda k: prices[k])]
9:查找两字典的相同点
# 为了寻找两个字典的相同点,可以简单的在两字典的 keys() 或者 items() 方法返回结果上执行集合操作 a = { 'x' : 1, 'y' : 2, 'z' : 3 } b = { 'w' : 10, 'x' : 11, 'y' : 2 } # Find keys in common a.keys() & b.keys() # { 'x', 'y' } # & 求交集 # Find keys in a that are not in b a.keys() - b.keys() # { 'z' } # Find (key,value) pairs in common a.items() & b.items() # { ('y', 2)}
# 以现有字典构造一个排除几个指定键的新字典(字典推导) # Make a new dictionary with certain keys removed c = {key:a[key] for key in a.keys() - {'z', 'w'}} # c is {'x': 1, 'y': 2}
一个字典就是一个键集合与值集合的映射关系。 字典的 keys()方法返回一个展现键集合的键视图对象。
键视图的一个很少被了解的特性就是它们也支持集合操作,比如集合并、交、差运算。
所以,如果你想对集合的键执行一些普通的集合操作,可以直接使用键视图对象而不用先将它们转换成一个 set
字典的 items() 方法返回一个包含 (键,值) 对的元素视图对象。 这个对象同样也支持集合操作,并且可以被用来查找两个字典有哪些相同的键值对
尽管字典的 values() 方法也是类似,但是它并不支持这里介绍的集合操作。 某种程度上是因为值视图不能保证所有的值互不相同,
这样会导致某些集合操作会出现问题。 不过,如果你硬要在值上面执行这些集合操作的话,你可以先将值集合转换成 set,然后再执行集合运算就行了
10:删除序列相同元素并保持顺序
# 如果序列上的值都是 hashable 类型,那么可以很简单的利用集合或者生成器来解决这个问题 def dedupe(items): seen = set() for item in items: if item not in seen: yield item seen.add(item) # 不能省略,为if做判断item在不在集合准备的 a = [1, 5, 2, 1, 9, 1, 5, 10] list(dedupe(a)) # [1, 5, 2, 9, 10]
这个方法仅仅在序列中元素为 hashable
的时候才管用
# 如果你想消除元素不可哈希(比如 dict 类型)的序列中重复元素的话 def dedupe(items, key=None): seen = set() for item in items: val = item if key is None else key(item) if val not in seen: yield item seen.add(val) # 这里的key参数指定了一个函数,将序列元素转换成 hashable 类型 a = [ {'x':1, 'y':2}, {'x':1, 'y':3}, {'x':1, 'y':2}, {'x':2, 'y':4}] list(dedupe(a, key=lambda d: (d['x'],d['y']))) list(dedupe(a, key=lambda d: d['x']))
# key传递的是一个匿名函数,传递一个字段进去,返回的是d(['x'],d['y'])元组,然后判断每个元素匿名函数转化成元组后的值在不在seen里
# 在就不返回,不在就返回
# 如果仅仅就是想消除重复元素,通常可以简单的构造一个集 [1, 5, 2, 1, 9, 1, 5, 10] set(a) # 然而,这种方法不能维护元素的顺序,生成的结果中的元素位置被打乱 # 使用生成器+set可以去重还能维护元素顺序 # 使用生成器读取一个文件,消除重复行 with open(somefile,'r') as f: for line in dedupe(f): ...
11:命名切片 slice
# 记录(比如文件或其他类似格式)中的某些固定位置提取字段:可以使用slice命名切片 record = '....................100 .......513.25 ..........' cost_1 = int(record[20:23]) * float(record[31:37]) SHARES = slice(20, 23) PRICE = slice(31, 37) cost_2 = int(record[SHARES]) * float(record[PRICE]) print(cost_1) print(cost_2)
# 避免了使用大量难以理解的硬编码下标。这使得你的代码更加清晰可读。
# 一般来讲,代码中如果出现大量的硬编码下标会使得代码的可读性和可维护性大大降低。 比如,
# 如果你回过来看看一年前你写的代码,你会摸着脑袋想那时候自己到底想干嘛啊。 这是一个很简单的解决方案,它让你更加清晰的表达代码的目的。
# 内置的 slice() 函数创建了一个切片对象。所有使用切片的地方都可以使用切片对象 items = [0, 1, 2, 3, 4, 5, 6] a = slice(2, 4) print(items[2:4]) print(items[a]) items[a] = [10, 11] # a = slice(2, 4),就是2,3位置差入10,11 print(items) # [0, 1, 10, 11, 4, 5, 6] del items[a] # a = slice(2, 4),删除2,3位置的元素 print(items) # [0, 1, 4, 5, 6]
# 切片对象a,你可以分别调用它的 a.start , a.stop , a.step 属性来获取更多的信息 a = slice(5, 50, 2) a.start # 5 a.stop # 50 a.step # 2
# 通过调用切片的 indices(size) 方法将它映射到一个已知大小的序列上。
# 这个方法返回一个三元组 (start, stop, step) ,所有的值都会被缩小,
# 直到适合这个已知序列的边界为止。 这样,使用的时就不会出现 IndexError 异常 s = 'HelloWorld' a = slice(5, 50, 2) print(a.indices(len(s))) # (5, 10, 2) print(*a.indices(len(s))) # 5 , 10 , 2 for i in range(*a.indices(len(s))): # 等同range(5, 10, 2) print(s[i]) for i in range(5, 10 , 2): print(s[i])
12:序列中出现次数最多的元素 collections.Counter
words = [ 'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes', 'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the', 'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into', 'my', 'eyes', "you're", 'under' ] from collections import Counter word_counts = Counter(words) # 出现频率最高的3个单词 top_three = word_counts.most_common(3) print(top_three) # Outputs [('eyes', 8), ('the', 5), ('look', 4)]
# Counter 对象可以接受任意的由可哈希(hashable)元素构成的序列对象。
# 在底层实现上,一个 Counter 对象就是一个字典,将元素映射到它出现的次数上 word_counts['not'] # 1 word_counts['eyes'] # 8
# 手动增加计数,可以简单的用加法: from collections import Counter morewords = ['why', 'are', 'you', 'not', 'looking', 'in', 'my', 'eyes'] word_counts = Counter(morewords) print(word_counts) for word in morewords: word_counts[word] += 1 print(word_counts)
还可以使用update()
方法:
word_counts.update(morewords)
# Counter 实例一个鲜为人知的特性是它们可以很容易的跟数学运算操作相结合 from collections import Counter words = [ 'look', 'into', 'my', 'eyes', 'look', 'into', 'my', 'eyes', 'the', 'eyes', 'the', 'eyes', 'the', 'eyes', 'not', 'around', 'the', 'eyes', "don't", 'look', 'around', 'the', 'eyes', 'look', 'into', 'my', 'eyes', "you're", 'under' ] more_words = ['why', 'are', 'you', 'not', 'looking', 'in', 'my', 'eyes'] a = Counter(words) b = Counter(more_words) c = a + b d = a - b print(c) print(d)# Counter
对象在几乎所有需要制表或者计数数据的场合是非常有用的工具
13:通过某个关键字排序一个字典列表
# 一个字典列表,你想根据某个或某几个字典字段来排序这个列表 # 可以通过使用 operator 模块的 itemgetter 函数,可以非常容易的排序这样的数据结构。
假设你从数据库中检索出来网站会员信息列表,并且以下列的数据结构返回 rows = [ {'fname': 'Brian', 'lname': 'Jones', 'uid': 1003}, {'fname': 'David', 'lname': 'Beazley', 'uid': 1002}, {'fname': 'John', 'lname': 'Cleese', 'uid': 1001}, {'fname': 'Big', 'lname': 'Jones', 'uid': 1004} ] # 根据任意的字典字段来排序输入结果行是很容易实现的 from operator import itemgetter rows_by_fname = sorted(rows, key=itemgetter('fname')) rows_by_uid = sorted(rows, key=itemgetter('uid')) print(rows_by_fname) print(rows_by_uid)
# itemgetter() 函数也支持多个 keys rows_by_lfname = sorted(rows, key=itemgetter('lname','fname')) print(rows_by_lfname)
# rows 被传递给接受一个关键字参数的 sorted() 内置函数。 这个参数是 callable 类型,
并且从 rows 中接受一个单一元素,然后返回被用来排序的值。 itemgetter() 函数就是负责创建这个 callable 对象的
# operator.itemgetter() 函数有一个被 rows 中的记录用来查找值的索引参数。可以是一个字典键名称,
一个整形值或者任何能够传入一个对象的 __getitem__() 方法的值。 如果你传入多个索引参数给 itemgetter() ,
它生成的 callable 对象会返回一个包含所有元素值的元组, 并且 sorted() 函数会根据这个元组中元素顺序去排序。
但你想要同时在几个字段上面进行排序(比如通过姓和名来排序,也就是例子中的那样)的时候这种方法是很有用的
# itemgetter() 有时候也可以用 lambda 表达式代替 rows_by_fname = sorted(rows, key=lambda r: r['fname']) rows_by_lfname = sorted(rows, key=lambda r: (r['lname'],r['fname'])) 这种方案也不错。但是,使用 itemgetter() 方式会运行的稍微快点。因此,如果你对性能要求比较高的话就使用 itemgetter() 方式。
# itemgette也同样适用于 min() 和 max() 等函数 min(rows, key=itemgetter('uid')) # 按照uid选择最小的 max(rows, key=itemgetter('uid')) # 按照uid选择最大的
14:排序不支持原生比较的对象
# 内置的 sorted() 函数有一个关键字参数 key ,可以传入一个 callable 对象给它,
# 这个 callable 对象对每个传入的对象返回一个值,这个值会被 sorted 用来排序这些对象。
# 比如,如果你在应用程序里面有一个 User 实例序列,并且你希望通过他们的 user_id 属性进行排序,
# 你可以提供一个以 User 实例作为输入并输出对应 user_id 值的 callable 对象 class User: def __init__(self, user_id): self.user_id = user_id def __repr__(self): # user(23)实例化的时候本来是个对象,使用repr就返回repr里面的东西 return 'User({})'.format(self.user_id) def sort_notcompare(): users = [User(23), User(3), User(99)] print(users) print(sorted(users, key=lambda u: u.user_id))
# 另外一种方式使用 operator.attrgetter() 来代替 lambda 函数: from operator import attrgetter sorted(users, key=attrgetter('user_id')) # 类似遍历users这个里面的对象,毕竟对象里面user_id属性值来排序
选择使用 lambda 函数或者是 attrgetter() 可能取决于个人喜好。 但是, attrgetter() 函数通常会运行的快点,
并且还能同时允许多个字段进行比较。 这个跟 operator.itemgetter() 函数作用于字典类型很类似
# 如果 User 实例还有一个 first_name 和 last_name 属性 by_name = sorted(users, key=attrgetter('last_name', 'first_name')) 可以这样来排序
# from operator import attrgetter同样适用于像 min() 和 max() 之类的函数 min(users, key=attrgetter('user_id')) max(users, key=attrgetter('user_id'))
15:通过某个字段将记录分组
# 字典或者实例的序列,根据某个特定的字段比如 date 来分组迭代访问。 # itertools.groupby() 函数对于这样的数据分组操作非常实用 # 按 date 分组后的数据块上进行迭代,先需要按照指定的字段(这里就是 date )排序,然后调用 itertools.groupby() 函数 from operator import itemgetter from itertools import groupby rows = [ {'address': '5412 N CLARK', 'date': '07/01/2012'}, {'address': '5148 N CLARK', 'date': '07/04/2012'}, {'address': '5800 E 58TH', 'date': '07/02/2012'}, {'address': '2122 N CLARK', 'date': '07/03/2012'}, {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'}, {'address': '1060 W ADDISON', 'date': '07/02/2012'}, {'address': '4801 N BROADWAY', 'date': '07/01/2012'}, {'address': '1039 W GRANVILLE', 'date': '07/04/2012'}, ] # Sort by the desired field first rows.sort(key=itemgetter('date')) # Iterate in groups for date, items in groupby(rows, key=itemgetter('date')): print(date) for i in items: print(' ', i)groupby()
函数扫描整个序列并且查找连续相同值(或者根据指定 key 函数返回值相同)的元素序列。
在每次迭代的时候,它会返回一个值和一个迭代器对象, 这个迭代器对象可以生成元素值全部等于上面那个值的组中所有对象
一个非常重要的准备步骤是要根据指定的字段将数据排序。 因为groupby()
仅仅检查连续的元素,如果事先并没有排序完成的话,分组函数将得不到想要的结果。
# 想根据date 字段将数据分组到一个大的数据结构中去,并且允许随机访问, 那么你最好使用 defaultdict() 来构建一个多值字典 from collections import defaultdict rows = [ {'address': '5412 N CLARK', 'date': '07/01/2012'}, {'address': '5148 N CLARK', 'date': '07/04/2012'}, {'address': '5800 E 58TH', 'date': '07/02/2012'}, {'address': '2122 N CLARK', 'date': '07/03/2012'}, {'address': '5645 N RAVENSWOOD', 'date': '07/02/2012'}, {'address': '1060 W ADDISON', 'date': '07/02/2012'}, {'address': '4801 N BROADWAY', 'date': '07/01/2012'}, {'address': '1039 W GRANVILLE', 'date': '07/04/2012'}, ] rows_by_date = defaultdict(list) # 默认字典,里面默认是个list for i in rows: rows_by_date[i["date"]].append(i) # 字典没用i["data"]的时候就创建,里面默认是列表,列表里+元素 print(rows_by_date)
# 这里没有必要先将记录排序。因此,如果对内存占用不是很关心,
# 这种方式会比先排序然后再通过groupby()
函数迭代的方式运行得快一些
16:过滤序列元素
# 有一个数据序列,想利用一些规则从中提取出需要的值或者是缩短序列 # 最简单的过滤序列元素的方法就是使用列表推导 my_list = [1, 4, -5, 10, -7, 2, 3, -1] new_list = [i for i in my_list if i > 0] print(new_list) # 使用列表推导的一个潜在缺陷就是如果输入非常大的时候会产生一个非常大的结果集,占用大量内存 # 对内存比较敏感,那么你可以使用生成器表达式迭代产生过滤的元素 pos = (n for n in my_list if n > 0) # ()生成器表达式,[]列表推导式 for i in pos: print(i) # 如果过滤规则比较复杂,不能简单的在列表推导或者生成器表达式中表达出来 # 可以将过滤代码放到一个函数中, 然后使用内建的 filter() 函数 # filter() 函数创建了一个迭代器,因此如果你想得到一个列表的话,就得像示例那样使用 list()去转换 values = ['1', '2', '-3', '-', '4', 'N/A', '5'] def is_int(val): """ 判断val是否是int,如果是int就不会报错返回True 如果不是int就会报错走except里面的逻辑返回False :param val: :return: """ try: x = int(val) return True except ValueError: return False in_val = list(filter(is_int, values)) print(in_val)
# filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表。 # 两个参数,第一个为函数,第二个为序列,序列的每个元素作为参数传递给函数进行判断,
# 然后返回 True 或 False,最后将返回 True 的元素放到新列表中 filter(function, iterable) function -- 判断函数。 iterable -- 可迭代对象。 def is_odd(n): return n % 2 == 1 newlist = filter(is_odd, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) print(newlist) # 过滤出1~100中平方根是整数的数, 整数整除1的余数等于0 import math def is_sqr(x): return math.sqrt(x) % 1 == 0 newlist = filter(is_sqr, range(1, 101)) print(newlist)
# 列表推导和生成器表达式通常情况下是过滤数据最简单的方式,还能在过滤的时候转换数据 mylist = [1, 4, -5, 10, -7, 2, 3, -1] import math [math.sqrt(n) for n in mylist if n > 0] # 求大于0的数的平方根
# 将不符合条件的值用新的值代替,而不是丢弃它们 # 比如不仅想找到正数,而且还想将不是正数的数替换成指定的数 # 将过滤条件放到条件表达式中去解决 mylist = [1, 4, -5, 10, -7, 2, 3, -1] clip_neg = [n if n > 0 else 0 for n in mylist] print(clip_neg) # [1, 4, 0, 10, 0, 2, 3, 0]
# 过滤工具itertools.compress(), # 它以一个 iterable 对象和一个相对应的 Boolean 选择器序列作为输入参数,然后输出 iterable 对象中对应选择器为 True 的元素 # 需要用另外一个相关联的序列来过滤某个序列的时候,这个函数是非常有用的 from itertools import compress addresses = [ '5412 N CLARK', '5148 N CLARK', '5800 E 58TH', '2122 N CLARK', '5645 N RAVENSWOOD', '1060 W ADDISON', '4801 N BROADWAY', '1039 W GRANVILLE', ] counts = [0, 3, 10, 4, 1, 7, 6, 1] # 想将那些对应 count 值大于5的地址全部输出 more5 = [n > 5 for n in counts] # [False, False, True, False, False, True, True, False] more5_address = compress(addresses, more5) print(more5_address) # <itertools.compress object at 0x0000024BC66E7FA0> print(list(more5_address)) # ['5800 E 58TH', '1060 W ADDISON', '4801 N BROADWAY']
这里的关键点在于先创建一个Boolean
序列,指示哪些元素符合条件。
然后compress()
函数根据这个序列去选择输出对应位置为True
的元素,
和filter()
函数类似,compress()
也是返回的一个迭代器。
因此,如果你需要得到一个列表, 那么你需要使用list()
来将结果转换为列表类型
17:从字典中提取子集
# 最简单的方式是使用字典推导 prices = { 'ACME': 45.23, 'AAPL': 612.78, 'IBM': 205.55, 'HPQ': 37.20, 'FB': 10.75 } p1 = {key: value for key, value in prices.items() if value > 200} tech_names = {'AAPL', 'IBM', 'HPQ', 'MSFT'} p2 = {key: value for key, value in prices.items() if key in tech_names} print(p1) print(p2)
# 大多数情况下字典推导能做到的,通过创建一个元组序列然后把它传给 dict() 函数也能实现 p1 = dict((key, value) for key, value in prices.items() if value > 200)
字典推导方式表意更清晰,并且实际上也会运行的更快些 (在这个例子中,实际测试几乎比dict()
函数方式快整整一倍)
# 第二个例子程序也可以像这样重写: tech_names = { 'AAPL', 'IBM', 'HPQ', 'MSFT' } p2 = {key:prices[key] for key in prices.keys() & tech_names } # &交集 #运行时间测试结果显示这种方案大概比第一种方案慢 1.6 倍。 如果对程序运行性能要求比较高的话,
# 需要花点时间去做计时测试。 关于更多计时和性能测试
18:映射名称到序列元素, namedtuple():命名元组
# 有一段通过下标访问列表或者元组中元素的代码,但是这样有时候会使得你的代码难以阅读,
# 可以通过名称来访问元素 collections.namedtuple() # collections.namedtuple() 函数通过使用一个普通的元组对象来帮你解决这个问题。
# 这个函数实际上是一个返回 Python 中标准元组类型子类的一个工厂方法。 你需要传递一个类型名和你需要的字段给它,
# 然后它就会返回一个类,你可以初始化这个类,为你定义的字段传递值等。 from collections import namedtuple Subscriber = namedtuple('Subscriber', ['addr', 'joined']) sub = Subscriber('jonesy@example.com', '2012-10-19') print(sub) # Subscriber(addr='jonesy@example.com', joined='2012-10-19') print(sub.addr) # jonesy@example.com print(sub.joined) # 2012-10-19
# namedtuple的实例看起来像一个普通的类实例 # namedtuple跟元组类型是可交换的,支持所有的普通元组操作,比如索引和解压 len(sub) addr, joined = sub print(addr)
# 命名元组的一个主要用途是将你的代码从下标操作中解脱出来。 因此,如果你从数据库调用中返回了一个很大的元组列表,
# 通过下标去操作其中的元素, 当你在表中添加了新的列的时候你的代码可能就会出错了。但是如果你使用了命名元组,那么就不会有这样的顾虑 def compute_cost(records): total = 0.0 for rec in records: total += rec[1] * rec[2] return total # 下标操作通常会让代码表意不清晰,并且非常依赖记录的结构。 下面是使用命名元组的版本: from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price']) def compute_cost(records): total = 0.0 for rec in records: s = Stock(*rec) total += s.shares * s.price return total
# 命名元组另一个用途就是作为字典的替代,因为字典存储需要更多的内存空间。
# 如果你需要构建一个非常大的包含字典的数据结构,那么使用命名元组会更加高效。 但是需要注意的是,不像字典那样,命名元组是不可更改的 from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price']) s = Stock('ACME', 100, 123.45) print(s) # Stock(name='ACME', shares=100, price=123.45) s.shares = 75 # 报错,命名元组不可以修改值,但是比字典省内存
# 命名元组实例的replace() 方法也可以改变属性的值 # 会创建一个全新的命名元组并将对应的字段用新的值取代 from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price']) s = Stock('ACME', 100, 123.45) print(s, id(s)) # Stock(name='ACME', shares=100, price=123.45) 1254856218400 s = s._replace(shares=75) print(s, id(s)) # Stock(name='ACME', shares=75, price=123.45) 1254863925952
使用_replace前后的两个s对象的内存地址变化了,_replace创建了一个全新的命名元组
# _replace() 方法还有一个很有用的特性就是当你的命名元组拥有可选或者缺失字段时候,
# 它是一个非常方便的填充数据的方法。 你可以先创建一个包含缺省值的原型元组,然后使用 _replace() 方法创建新的值被更新过的实例 from collections import namedtuple Stock = namedtuple('Stock', ['name', 'shares', 'price', 'date', 'time']) stock_prototype = Stock('', 0, 0.0, None, None) def dict_to_stock(s): return stock_prototype._replace(**s) # **s解字典,解成 xxx= xxx的模式,刚好对应 xxx._replace(xxx=xxx) a = {'name': 'ACME', 'shares': 100, 'price': 123.45} print(dict_to_stock(a)) b = {'name': 'ACME', 'shares': 100, 'price': 123.45, 'date': '12/17/2012'} print(dict_to_stock(b)) # 你的目标是定义一个需要更新很多实例属性的高效数据结构,那么命名元组并不是你的最佳选择。
# 这时候你应该考虑定义一个包含 __slots__ 方法的类
19:转换并同时计算数据(使用生成器表达式)
# 在数据序列上执行聚集函数(比如 sum() , min() , max() ), 但是首先你需要先转换或者过滤数据 # 非常优雅的方式去结合数据计算与转换就是使用一个生成器表达式参数。 比如,如果你想计算平方和 nums = [1, 2, 3, 4, 5] s = sum(x * x for x in nums)
import os files = os.listdir('.') # 确定目录中是否存在任何.py文件 # any() 函数用于判断给定的可迭代参数 iterable 是否全部为 False,则返回 False, # 如果有一个为 True,则返回 True。 if any(name.endswith('.py') for name in files): # True表示有以.py结尾的文件 print('There be python!') else: print('Sorry, no python.') # 将元组输出为CSV s = ('ACME', 50, 123.45) print(','.join(str(x) for x in s)) # ACME,50,123.45 # 跨数据结构字段的数据缩减 portfolio = [ {'name': 'GOOG', 'shares': 50}, {'name': 'YHOO', 'shares': 75}, {'name': 'AOL', 'shares': 20}, {'name': 'SCOX', 'shares': 65} ] min_shares = min(s['shares'] for s in portfolio) print(min_shares)
# 当生成器表达式作为一个单独参数传递给函数时候的巧妙语法(你并不需要多加一个括号) s = sum((x * x for x in nums)) # 显式的传递一个生成器表达式对象 s = sum(x * x for x in nums) # 更加优雅的实现方式,省略了括号 # 上面两个表达式等效的
# 使用一个生成器表达式作为参数会比先创建一个临时列表更加高效和优雅 nums = [1, 2, 3, 4, 5] s = sum([x * x for x in nums]) # 这样有点low,这种方式同样可以达到想要的效果,但是它会多一个步骤,先创建一个额外的列表,
# 对于小型列表可能没什么关系,但是如果元素数量非常大的时候, 它会创建一个巨大的仅仅被使用一次就被丢弃的临时数据结构。
# 而生成器方案会以迭代的方式转换数据,因此更省内存
# 使用一些聚集函数比如 min() 和 max() 的时候你可能更加倾向于使用生成器版本, 它们接受的一个 key 关键字参数或许对你很有帮助 # Original: Returns 20 min_shares = min(s['shares'] for s in portfolio) # Alternative: Returns {'name': 'AOL', 'shares': 20} min_shares = min(portfolio, key=lambda s: s['shares'])
# 这两种方式都可以求最小值
20:合并多个字典或映射 ChainMap
# 在两个字典中执行查找操作(比如先从 a 中找,如果找不到再在 b 中找)collections 模块中的 ChainMap 类 from collections import ChainMap a = {'x': 1, 'z': 3} b = {'y': 2, 'z': 4} c = ChainMap(a, b) print(c) # ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4}) print(c['x']) # Outputs 1 (from a) print(c['y']) # Outputs 2 (from b) print(c['z']) # Outputs 3 (from a)
# 一个 ChainMap 接受多个字典并将它们在逻辑上变为一个字典。 然后,这些字典并不是真的合并在一起了,
# ChainMap 类只是在内部创建了一个容纳这些字典的列表 并重新定义了一些常见的字典操作来遍历这个列表。大部分字典操作都是可以正常使用的, from collections import ChainMap a = {'x': 1, 'z': 3} b = {'y': 2, 'z': 4} c = ChainMap(a, b) print(len(c)) # 3 print(list(c.keys())) # ['y', 'z', 'x']
如果出现重复键,那么第一次出现的映射值会被返回。
因此,例子程序中的c['z']
总是会返回字典a
中对应的值,而不是b
中对应的值。
# 对于字典的更新或删除操作总是影响的是列表中第一个字典 from collections import ChainMap a = {'x': 1, 'z': 3} b = {'y': 2, 'z': 4} c = ChainMap(a, b) c['z'] = 10 c['w'] = 40 del c['x'] print(a) # {'z': 10, 'w': 40} del c['y'] # 报错,只能操作a字典里的元素,不能操作b字典里的元素
# ChainMap 对于编程语言中的作用范围变量(比如 globals , locals 等)是非常有用的。 事实上,有一些方法可以使它变得简单 from collections import ChainMap values = ChainMap() values['x'] = 1 values = values.new_child() values['x'] = 2 values = values.new_child() values['x'] = 3 print(values) # ChainMap({'x': 3}, {'x': 2}, {'x': 1}) print(values['x']) # 3 values = values.parents print(values['x']) # 2 values = values.parents print(values["x"]) # 1 print(values) # ChainMap({'x': 1})
new_child()方法实质上是在列表的第一个元素前放入一个字典,默认是{},而parents是去掉了列表开头的元素
from collections import ChainMap m1 = {'Type': 'admin', 'codeID': '00001'} m2 = {'name': 'woodname','codeID': '00002'} m = ChainMap(m1, m2) print(m) # 输出: # ChainMap({'Type': 'admin', 'codeID': '00001'}, {'name': 'woodname', 'codeID': '00002'}) print(m.maps) # 输出:[{'Type': 'admin', 'codeID': '00001'}, {'name': 'woodname', 'codeID': '00002'}] for i in m.items(): print(i) # 输出: # ('name', 'woodname') # ('codeID', '00001') # ('Type', 'admin') print(m['name']) # 读取元素的值 print(m['codeID']) # 注意,当key重复时以最前一个为准 print(m.get('Type')) # 输出: # woodname # 00001 # admin # 新增map m3 = {'data': '888'} m=m.new_child(m3) # 将 m3 加入m print(m) # 输出: # ChainMap({'data': '888'}, {'Type': 'admin', 'codeID': '00001'}, {'name': 'woodname', 'codeID': '00002'}) print(m.parents) # m 的父亲 # 输出:ChainMap({'Type': 'admin', 'codeID': '00001'}, {'name': 'woodname', 'codeID': '00002'}) print(m.parents.parents) # 输出 : ChainMap({'name': 'woodname', 'codeID': '00002'})
# 作为 ChainMap 的替代,你可能会考虑使用 update() 方法将两个字典合并 a = {'x': 1, 'z': 3} b = {'y': 2, 'z': 4} merged = dict(b) merged.update(a) print(merged) # 这样也能行得通,但是它需要你创建一个完全不同的字典对象(或者是破坏现有字典结构)。 同时,如果原字典做了更新,这种改变不会反应到新的合并字典中去 a = {'x': 1, 'z': 3} b = {'y': 2, 'z': 4} merged = dict(b) merged.update(a) print(merged) # {'y': 2, 'z': 3, 'x': 1} a['x'] = 13 print(merged) # {'y': 2, 'z': 3, 'x': 1}
merged前后没有变化
# ChainMap使用原来的字典,它自己不创建新的字典。所以它并不会产生新的字典 from collections import ChainMap a = {'x': 1, 'z': 3} b = {'y': 2, 'z': 4} merged = ChainMap(a, b) print(merged) # ChainMap({'x': 1, 'z': 3}, {'y': 2, 'z': 4}) merged["x"] = 13 print(merged) # ChainMap({'x': 13, 'z': 3}, {'y': 2, 'z': 4})
# merged能更新
21:使用多个界定符分割字符串
# string 对象的 split() 方法只适应于非常简单的字符串分割情形,并不允许有多个分隔符或者是分隔符周围不确定的空格。 # 当你需要更加灵活的切割字符串的时候,最好使用 re.split() 方法 import re line = 'asdf fjdk; afed, fjek,asdf, foo' res = re.split(r"[;,\s]\s", line) # \s匹配空白,即 空格,tab键, []:匹配[ ]中列举的字符,只要这个模式被找到,
# 那么匹配的分隔符两边的实体都会被当成是结果中的元素返回,返回结果为一个字段列表,这个跟str.split()
返回值类型是一样的 print(res)
# 当你使用 re.split() 函数时候,需要特别注意的是正则表达式中是否包含一个括号捕获分组。 如果使用了捕获分组,那么被匹配的文本也将出现在结果列表中 import re line = 'asdf fjdk; afed, fjek,asdf, foo' fields = re.split(r'(;|,|\s)\s*', line) print(fields) # ['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo']
# 获取分割字符在某些情况下也是有用的,保留分割字符串,用来在后面重新构造一个新的输出字符串 import re line = 'asdf fjdk; afed, fjek,asdf, foo' fields = re.split(r'(;|,|\s)\s*', line) print(fields) # ['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo'] values = fields[::2] delimiters = fields[1::2] + [''] print(values) # ['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo'] print(delimiters) # [' ', ';', ',', ',', ',', ''] print(''.join(v+d for v, d in zip(values, delimiters))) # asdf fjdk;afed,fjek,asdf,foo
# 不想保留分割字符串到结果列表中去,但仍然需要使用到括号来分组正则表达式的话, 确保你的分组是非捕获分组,形如 (?:...) ?开头表示非捕获分组 re.split(r'(?:,|;|\s)\s*', line) ['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
22:字符串开头或结尾匹配
# 查字符串开头或结尾的一个简单方法是使用 str.startswith() 或者是 str.endswith() 方法 filename = 'spam.txt' filename.endswith('.txt') filename.startswith('file:')
# 如果你想检查多种匹配可能,只需要将所有的匹配项放入到一个元组中去, 然后传给 startswith() 或者 endswith() 方法 import os filenames = os.listdir(".") # 当前路径下的文件 # 查找当前路径下以.py和.idea结尾的文件 res = [name for name in filenames if name.endswith(('.py', '.idea'))] print(res) # ['.idea', 'test_001.py', 'test_002.py', 'test_003.py'] print(any(name.endswith('.py') for name in filenames)) # True:any里面的元素又一个true就为true
from urllib.request import urlopen def read_data(name): if name.startswith(('http:', 'https:', 'ftp:')): return urlopen(name).read() else: with open(name) as f: return f.read() # 如果文件以http和https和ftp开头那么就调用urlopen去访问url,否则使用文件打开方式访问 # startswith和endswith必须传入元组参数 choices = ['http:', 'ftp:'] url = 'http://www.python.org' # url.startswith(choices) # 报错,一定需要传元组 print(url.startswith(tuple(choices))) # True
# startswith() 和 endswith() 方法提供了一个非常方便的方式去做字符串开头和结尾的检查。
# 类似的操作也可以使用切片来实现,但是代码看起来没有那么优雅 filename = 'spam.txt' filename[-4:] == '.txt' url = 'http://www.python.org' url[:5] == 'http:' or url[:6] == 'https:' or url[:4] == 'ftp:'
# 正则表达式也能实现 import re url = "http://www.python.org" print(re.match('http:|https:|ftp:', url)) # 检查某个文件夹中是否存在指定的文件类型 if any(name.endswith(('.c', '.h')) for name in listdir(dirname)):
23:用Shell通配符匹配字符串 Unix Shell
# 使用 Unix Shell 中常用的通配符(比如 *.py , Dat[0-9]*.csv 等)去匹配文本字符串
# fnmatch 模块提供了两个函数—— fnmatch() 和 fnmatchcase() ,可以用来实现这样的匹配
from fnmatch import fnmatch, fnmatchcase
print(fnmatch('foo.txt', '*.txt')) # True
print(fnmatch('foo.txt', '?oo.txt')) # True
print(fnmatch('Dat45.csv', 'Dat[0-9]*')) # True
names = ['Dat1.csv', 'Dat2.csv', 'config.ini', 'foo.py']
res = [name for name in names if fnmatch(name, 'Dat*.csv')] # ['Dat1.csv', 'Dat2.csv'],匹配以Dat开头.csv结尾的数据
print(res)
# fnmatch() 函数使用底层操作系统的大小写敏感规则(不同的系统是不一样的)来匹配模式。 # On OS X (Mac) fnmatch('foo.txt', '*.TXT') # False # On Windows fnmatch('foo.txt', '*.TXT') # True
# 如果向不区分大小写进行匹配,可以使用 fnmatchcase() 来代替。它完全使用你的模式大小写匹配 fnmatchcase('foo.txt', '*.TXT') # False
from fnmatch import fnmatch, fnmatchcase addresses = [ '5412 N CLARK ST', '1060 W ADDISON ST', '1039 W GRANVILLE AVE', '2122 N CLARK ST', '4802 N BROADWAY', ] res = (addr for addr in addresses if fnmatchcase(addr, "* ST")) print(list(res)) # ['5412 N CLARK ST', '1060 W ADDISON ST', '2122 N CLARK ST'] ret = (addr for addr in addresses if fnmatchcase(addr, '54[0-9][0-9] *CLARK*')) # 寻找54xx CLARKxx print(list(ret)) # ['5412 N CLARK ST']
# fnmatch() 函数匹配能力介于简单的字符串方法和强大的正则表达式之间,只需要简单的通配符就能完成的时候可以使用这个
24:字符串匹配和搜索
# 匹配的是字面字符串,那么你通常只需要调用基本字符串方法就行,
# 比如 str.find() , str.endswith() , str.startswith() 或者类似的方法 text = 'yeah, but no, but yeah, but no, but yeah' print(text == 'yeah') # False print(text.startswith('yeah')) # True print(text.endswith('no')) # False print(text.find('no')) # 10(第十个元素出现no)
# 对于复杂的匹配需要使用正则表达式和 re 模块 # 匹配数字格式的日期字符串比如 11/27/2012 import re text1 = '11/27/2012' text2 = 'Nov 27, 2012' print(re.match(r'\d+/\d+/\d+', text1)) if re.match(r'\d+/\d+/\d+', text1): # \+:一个或者多个数字,这里匹配上了 print("yes") else: print("no") if re.match(r'\d+/\d+/\d+', text2): # \+:一个或者多个数字,这里匹配不上 print("yes") else: print("no")
# re正则如果你想使用同一个模式去做多次匹配,你应该先将模式字符串预编译为模式对象 import re text1 = '11/27/2012' date_pat = re.compile(r'\d+/\d+/\d+') if date_pat.match(text1): print('yes') else: print('no')
# match() 总是从字符串开始去匹配, # 如果你想查找字符串任意部分的模式出现位置, 使用findall() 方法去代替 import re text1 = '11/27/2012' date_pat = re.compile(r'\d+/\d+/\d+') text = 'Today is 11/27/2012. PyCon starts 3/13/2013.' res = date_pat.findall(text) print(res) # ['11/27/2012', '3/13/2013'] # 在定义正则式的时候,通常会利用括号去捕获分组 date_pat = re.compile(r'(\d+)/(\d+)/(\d+)') # 编译匹配规则 # 捕获分组后可以分别将每个组的内容提取出来 m = date_pat.match('11/27/2012') print(m) # <re.Match object; span=(0, 10), match='11/27/2012'> print(m.group(0)) # 11/27/2012 全部屁 print(m.group(1)) # 11 print(m.group(2)) # 27 print(m.group(3)) # 2012 print(m.groups()) # ('11', '27', '2012') month, day, year = m.groups() print(month, day, year) # 11 27 2012 print(text) # Today is 11/27/2012. PyCon starts 3/13/2013. ree = date_pat.findall(text) print(ree) # [('11', '27', '2012'), ('3', '13', '2013')] for month, day, year in ree: print(month, day, year)
# findall() 方法会搜索文本并以列表形式返回所有的匹配。 如果你想以迭代方式返回匹配,可以使用 finditer() 方法来代替 import re text = 'Today is 11/27/2012. PyCon starts 3/13/2013.' # 在定义正则式的时候,通常会利用括号去捕获分组 date_pat = re.compile(r'(\d+)/(\d+)/(\d+)') # 编译匹配规则 for m in date_pat.finditer(text): print(m.groups())
# re正则表达式核心步骤就是先使用 re.compile() 编译正则表达式字符串,
# 然后使用 match() , findall() 或者 finditer() 等方法 # 当写正则式字符串的时候,相对普遍的做法是使用原始字符串比如 r'(\d+)/(\d+)/(\d+)' 。
# 这种字符串将不去解析反斜杠,这在正则表达式中是很有用的。 如果不这样做的话,你必须使用两个反斜杠,类似 '(\\d+)/(\\d+)/(\\d+)' # 需要注意的是 match() 方法仅仅检查字符串的开始部分。它的匹配结果有可能并不是你期望的那样 m = datepat.match('11/27/2012abcdef') m.group() # '11/27/2012'
# 如果你想精确匹配,确保你的正则表达式以$结尾 import re date_pat = re.compile(r'(\d+)/(\d+)/(\d+)$') res = date_pat.match('11/27/2012abcdef') print(res) # None 匹配不到 ret = date_pat.match('11/27/2012') print(ret.group()) # 11/27/2012
# 如果仅仅是做一次简单的文本匹配/搜索操作的话,可以略过编译部分,直接使用 re 模块级别的函数 re.findall(r'(\d+)/(\d+)/(\d+)', text) # 如果打算做大量的匹配和搜索操作的话,最好先编译正则表达式,然后再重复使用它。
# 模块级别的函数会将最近编译过的模式缓存起来,因此并不会消耗太多的性能,
# 但是如果使用预编译模式的话,你将会减少查找和一些额外的处理损耗:compile预编译
25:字符串搜索和替换 str.replace()和re.sub()
text = 'yeah, but no, but yeah, but no, but yeah' print(text.replace('yeah', 'yep')) # str.replace()不会改变原始的str,返回一个新的对象
# 对于复杂的模式,使用 re 模块中的 sub() 函数 # 将形式是11/27/2012的这种日期字符串改成 2012-11-27(并不明确替换的字符是11/27/2012但是知道具体样式) import re text = 'Today is 11/27/2012. PyCon starts 3/13/2013.' res = re.sub(r'(\d+)/(\d+)/(\d+)', r'\3-\1-\2', text) print(res) # Today is 2012-11-27. PyCon starts 2013-3-13. \3和\2和\1表示前面匹配模式()括号内匹配上的内容sub()
函数中的第一个参数是被匹配的模式,第二个参数是替换模式。反斜杠数字比如\3
指向前面模式的捕获组号
# 相同的模式做多次替换,考虑先compile编译它来提升性能 import re text = 'Today is 11/27/2012. PyCon starts 3/13/2013.' patten = re.compile(r'(\d+)/(\d+)/(\d+)') res = patten.sub(r"\3-\1-\2", text) print(res) # Today is 2012-11-27. PyCon starts 2013-3-13.
# 使用命名分组,那么第二个参数使用 \g<group_name> import re text = 'Today is 11/27/2012. PyCon starts 3/13/2013.' res = re.sub(r'(?P<month>\d+)/(?P<day>\d+)/(?P<year>\d+)', r'\g<year>-\g<month>-\g<day>', text) print(res) # Today is 2012-11-27. PyCon starts 2013-3-13.
# 更加复杂的替换,可以传递一个替换回调函数来代替, from calendar import month_abbr import re def change_date(m): # 月份转成英文,并且组装年月日的回调函数 mon_name = month_abbr[int(m.group(1))] # 把月份转成英文, return f'{m.group(2)} {mon_name} {m.group(3)}' text = 'Today is 11/27/2012. PyCon starts 3/13/2013.' date_pat = re.compile(r'(\d+)/(\d+)/(\d+)') res = date_pat.sub(change_date, text) # 表达式匹配上的结果是m,m.groups()表示匹配到的全部元素 print(res) # Today is 27 Nov 2012. PyCon starts 13 Mar 2013.
# 一个替换回调函数的参数是一个 match 对象,也就是 match() 或者 find() 返回的对象。
# 使用 group() 方法来提取特定的匹配部分。回调函数最后返回替换字符串
# 如果除了替换后的结果外,你还想知道有多少替换发生了,可以使用 re.subn() 来代替 newtext, n = datepat.subn(r'\3-\1-\2', text) newtext # 'Today is 2012-11-27. PyCon starts 2013-3-13.' n # 2
26:字符串忽略大小写的搜索替换 re.IGNORECASE标志可以让re匹配替换忽略大小写
# 忽略大小写进行匹配 import re text = 'UPPER PYTHON, lower python, Mixed Python' res = re.findall('python', text) print(res) # ['python'] ret = re.findall('python', text, flags=re.IGNORECASE) print(ret) # ['PYTHON', 'python', 'Python'] ree = re.sub('python', 'snake', text, flags=re.IGNORECASE) print(ree) # UPPER snake, lower snake, Mixed snake
# 替换字符串自动跟被匹配字符串的大小写保持一致:需要一个辅助函数 import re def match_case(word): def replace(m): text = m.group() if text.isupper(): return word.upper() elif text.islower(): return word.lower() elif text[0].isupper(): return word.capitalize() else: return word return replace text = 'UPPER PYTHON, lower python, Mixed Python' res = re.sub('python', match_case('snake'), text, flags=re.IGNORECASE) print(res) # UPPER SNAKE, lower snake, Mixed Snake# matchcase('snake')
返回了一个回调函数(参数必须是match
对象),前面一节提到过,sub()
函数除了接受替换字符串外,还能接受一个回调函数
27:最短匹配模式
# 正则表达式之匹配最短模式和最长模式(贪婪模式和非贪婪模式) import re str_pat = re.compile(r'"(.*)"') # 匹配一对分隔符之间的文本的时候(比如引号包含的字符串) text = 'Computer says "no."' print(str_pat.findall(text)) # ['no.'] text2 = 'Computer says "no." Phone says "yes."' print(str_pat.findall(text2)) # ['no." Phone says "yes.'] 匹配到了一个最长的元素 # 模式 r'\"(.*)\"' 的意图是匹配被双引号包含的文本,在正则表达式中*操作符是贪婪的,因此匹配操作会查找最长的可能匹配。
# 正则默认贪婪模式匹配,可以在模式中的*操作符后面加上?修饰符就可以把匹配模式改成非贪婪模式 import re str_pat = re.compile(r'"(.*?)"') # 匹配一对分隔符之间的文本的时候(比如引号包含的字符串) text2 = 'Computer says "no." Phone says "yes."' print(str_pat.findall(text2)) # ['no.', 'yes.'] # 这样就使得匹配变成非贪婪模式,从而得到最短的匹配 # 在一个模式字符串中,点(.)匹配除了换行外的任何字符。 然而,如果你将点(.)号放在开始与结束符(比如引号)之间的时候,
# 那么匹配操作会查找符合模式的最长可能匹配。 这样通常会导致很多中间的被开始与结束符包含的文本被忽略掉,
# 并最终被包含在匹配结果字符串中返回。 通过在 * 或者 + 这样的操作符后面添加一个 ? 可以强制匹配算法改成寻找最短的可能匹配
28:多行匹配模式 re.DOTALL
# 用点(.)去匹配任意字符的时候,点(.)不能匹配换行符的事实 import re # 匹配c语言分割的注释 comment = re.compile(r'/\*(.*?)\*/') text1 = '/* this is a comment */' text2 = '''/* this is a multiline comment */ ''' print(comment.findall(text1)) # [' this is a comment '] print(comment.findall(text2)) # []
现在默认不支持换行匹配
# 修改模式字符串,增加对换行的支持 import re # 匹配c语言分割的注释 comment = re.compile(r'/\*((?:.|\n)*?)\*/') # ?取消分组优先 text2 = '''/* this is a multiline comment */ ''' print(comment.findall(text2)) # [' this is a \nmultiline comment ']
(?:.|\n)
指定了一个非捕获组 (也就是它定义了一个仅仅用来做匹配,而不能通过单独捕获或者编号的组), # ?取消分组优先
# 标志参数 re.DOTALL 也能进行分行匹配, 它可以让正则表达式中的点(.)匹配包括换行符在内的任意字符 import re # 匹配c语言分割的注释 comment = re.compile(r"/\*(.*?)\*/", flags=re.DOTALL) # 这里?表示取消贪婪匹配 text2 = '''/* this is a multiline comment */ ''' print(comment.findall(text2)) # [' this is a \nmultiline comment ']
对于简单的情况使用re.DOTALL
标记参数工作的很好, 但是如果模式非常复杂或者是为了构造字符串令牌而将多个模式合并起来(2.18节有详细描述),
这时候使用这个标记参数就可能出现一些问题。 如果让你选择的话,最好还是定义自己的正则表达式模式,这样它可以在不需要额外的标记参数下也能工作的很好
29:将Unicode文本标准化
# 在Unicode中,某些字符能够用多个合法的编码表示 >>> s1 = 'Spicy Jalape\u00f1o' >>> s2 = 'Spicy Jalapen\u0303o' >>> s1 'Spicy Jalapeño' >>> s2 'Spicy Jalapeño' >>> s1 == s2 False >>> len(s1) 14 >>> len(s2) 15 >>> # 这里的文本”Spicy Jalapeño”使用了两种形式来表示。 第一种使用整体字符”ñ”(U+00F1),第二种使用拉丁字母”n”后面跟一个”~”的组合字符(U+0303)
# 在需要比较字符串的程序中使用字符的多种表示会产生问题。 为了修正这个问题,你可以使用unicodedata模块先将文本标准化 import unicodedata s1 = 'Spicy Jalape\u00f1o' s2 = 'Spicy Jalapen\u0303o' t1 = unicodedata.normalize('NFC', s1) t2 = unicodedata.normalize('NFC', s2) print(t1 == t2) # True print(t1, ascii(t1)) # Spicy Jalapeño 'Spicy Jalape\xf1o' print(t2, ascii(t2)) # Spicy Jalapeño 'Spicy Jalape\xf1o' t3 = unicodedata.normalize('NFD', s1) t4 = unicodedata.normalize('NFD', s2) print(t3 == t4) # True print(t3, ascii(t3)) # Spicy Jalapeño 'Spicy Jalapen\u0303o' print(t4, ascii(t4)) # Spicy Jalapeño 'Spicy Jalapen\u0303o'normalize()
第一个参数指定字符串标准化的方式。 NFC表示字符应该是整体组成(比如可能的话就使用单一编码),而NFD表示字符应该分解为多个组合字符表示
# Python同样支持扩展的标准化形式NFKC和NFKD,它们在处理某些字符的时候增加了额外的兼容特性。 import unicodedata s = '\ufb01' # A single character print(s) # fi print(unicodedata.normalize('NFD', s)) # fi print(unicodedata.normalize('NFKD', s)) # fi print(unicodedata.normalize('NFKC', s)) # fi
# 标准化对于任何需要以一致的方式处理Unicode文本的程序都是非常重要的。 当处理来自用户输入的字符串而你很难去控制编码的时候尤其如此 # 在清理和过滤文本的时候字符的标准化也是很重要的。 比如,假设你想清除掉一些文本上面的变音符的时候(可能是为了搜索和匹配) import unicodedata s1 = 'Spicy Jalape\u00f1o' t1 = unicodedata.normalize('NFD', s1) # Spicy Jalapeño print(''.join(c for c in t1 if not unicodedata.combining(c))) # Spicy Jalapeno # combining() 函数可以测试一个字符是否为和音字符。 在这个模块中还有其他函数用于查找字符类别,测试是否为数字字符等等
30:在正则式中使用Unicode
# 默认情况下 re 模块已经对一些Unicode字符类有了基本的支持。 比如, \\d 已经匹配任意的unicode数字字符了
import re
num = re.compile('\d+')
num.match('123') # <_sre.SRE_Match object at 0x1007d9ed0>
num.match('\u0661\u0662\u0663') # <_sre.SRE_Match object at 0x101234030>
# 想在模式中包含指定的Unicode字符,你可以使用Unicode字符对应的转义序列(比如 \uFFF 或者 \UFFFFFFF ) arabic = re.compile('[\u0600-\u06ff\u0750-\u077f\u08a0-\u08ff]+') # 当执行匹配和搜索操作的时候,最好是先标准化并且清理所有文本为标准化格式(参考29小节)。
# 但是同样也应该注意一些特殊情况,比如在忽略大小写匹配和大小写转换时的行为 pat = re.compile('stra\u00dfe', re.IGNORECASE) s = 'straße' pat.match(s) # Matches pat.match(s.upper()) # Doesn't match s.upper() # 'STRASSE'
31:删除字符串中不需要的字符
# strip() 方法能用于删除开始或结尾的字符。 lstrip() 和 rstrip() 分别从左和从右执行删除操作,
# 默认情况下,这些方法会去除空白字符,但是你也可以指定其他字符 s = ' hello world \n' print(s.strip()) # 'hello world' print(s.lstrip()) # 'hello world \n' print(s.rstrip()) # ' hello world' t = '-----hello=====' print(t.lstrip('-')) # 'hello=====' print(t.strip('-=')) # 'hello' print(t.strip('=')) # '-----hello'
# strip() 方法去除操作不会对字符串的中间的文本产生任何影响 s = ' hello world \n' s = s.strip() print(s) # hello world # 如果你想处理中间的空格,那么你需要求助其他技术。比如使用 replace() 方法或者是用正则表达式替换 import re s = ' hello world \n' print(s.replace(" ", '')) # helloworld print(s) # hello world res = re.sub("\s+", " ", s) # \s空格替换成" " print(res) # hello world
# 通常情况下你想将字符串 strip 操作和其他迭代操作相结合,比如从文件中读取多行数据。 如果是这样的话,使用生成器 with open(filename) as f: lines = (line.strip() for line in f) for line in lines: print(line) # 在这里,表达式 lines = (line.strip() for line in f) 执行数据转换操作。
# 这种方式非常高效,因为它不需要预先读取所有数据放到一个临时的列表中去。 它仅仅只是创建一个生成器,并且每次返回行之前会先执行 strip 操作 # 对于更高阶的strip,你可能需要使用 translate() 方法
32:审查清理文本字符串
# 文本清理问题会涉及到包括文本解析与数据处理等一系列问题,简单的情形下,你可能会选择使用字符串函数(比如 str.upper() 和 str.lower() )将文本转为标准格式
# 使用 str.replace() 或者 re.sub() 的简单替换操作能删除或者改变指定的字符序列
# unicodedata.normalize() 函数将unicode文本标准化
# 想消除整个区间上的字符或者去除变音符,str.translate() 方法 >>> s = 'pýtĥöñ\fis\tawesome\r\n' >>> s 'pýtĥöñ\x0cis\tawesome\r\n' # 第一步是清理空白字符。先创建一个小的转换表格然后使用 translate() 方法: >>> remap = { ... ord('\t') : ' ', ... ord('\f') : ' ', ... ord('\r') : None # Deleted ... } >>> a = s.translate(remap) # 根据转换表来把对应的数据进行转换 >>> a 'pýtĥöñ is awesome\n' >>> 空白字符 \t 和 \f 已经被重新映射到一个空格。回车字符r直接被删除 # 可以以这个表格为基础进一步构建更大的表格。比如,删除所有的和音符 >>> import unicodedata >>> import sys >>> cmb_chrs = dict.fromkeys(c for c in range(sys.maxunicode) ... if unicodedata.combining(chr(c))) ... >>> b = unicodedata.normalize('NFD', a) >>> b 'pýtĥöñ is awesome\n' >>> b.translate(cmb_chrs) 'python is awesome\n' >>>
# 删除所有的和音符: import unicodedata import sys cmb_chrs = dict.fromkeys(c for c in range(sys.maxunicode) if unicodedata.combining(chr(c))) # combining() 函数可以测试一个字符是否为和音字符 # cmb_chrs:使用 dict.fromkeys() 方法构造一个字典,每个Unicode和音符作为键,对应的值全部为 None # 然后使用 unicodedata.normalize() 将原始输入标准化为分解形式字符。 # 然后再调用 translate 函数删除所有重音符。 同样的技术也可以被用来删除其他类型的字符(比如控制字符等) s = 'pýtĥöñ\fis\tawesome\r\n' remap = { ord('\t'): ' ', ord('\f'): ' ', ord('\r'): None # Deleted } a = s.translate(remap) print(a) # pýtĥöñ is awesome b = unicodedata.normalize('NFD', a) print(b) # pýtĥöñ is awesome b.translate(cmb_chrs) # 传入字典转化规则,将所有的重音符转换成None
# 将所有Unicode数字字符映射到对应的ASCII字符上的表格 import unicodedata import sys digitmap = {c: ord('0') + unicodedata.digit(chr(c)) for c in range(sys.maxunicode) if unicodedata.category(chr(c)) == 'Nd'} print(len(digitmap), digitmap) # 650个长度 x = '\u0661\u0662\u0663' for i, j in digitmap.items(): print(i, j) print(x.translate(digitmap)) # 123 print(ord('0')) # 字符串0的unidoce编码为48(十进制的48) print(chr(125269))
# 另一种清理文本的技术涉及到I/O解码与编码函数 # 先对文本做一些初步的清理, 然后再结合 encode() 或者 decode() 操作来清除或修改它 import unicodedata a = 'pýtĥöñ is awesome\n' b = unicodedata.normalize('NFD', a) print(b) # pýtĥöñ is awesome print(b.encode('ascii', 'ignore').decode('ascii'))
# 将原来的文本分解为单独的和音符。
# 接下来的ASCII编码/解码只是简单的一下子丢弃掉那些字符。这种方法仅仅只在最后的目标就是获取到文本对应ACSII表示的时候生效。
# 简单的替换操作, str.replace() 方法通常是最快的 # 清理空白字符串 def clean_spaces(s): s = s.replace('\r', '') s = s.replace('\t', ' ') s = s.replace('\f', ' ') return s
去测试的话,你就会发现这种方式会比使用translate()
或者正则表达式要快很多。
执行任何复杂字符对字符的重新映射或者删除操作的话,translate()
方法会非常的快。
33:字符串对齐
# 基本的字符串对齐操作,可以使用字符串的 ljust() , rjust() 和 center() 方法 text = 'Hello World' print(text.ljust(20)) # 右对其 print(text.rjust(20)) # 左对其 print(text.center(20)) # 中间对其 # 一共20个字符,往左靠,往右靠,往中间靠 # 所有这些方法都能接受一个可选的填充字符 print(text.rjust(20, "=")) # =========Hello World print(text.ljust(20, "=")) # Hello World========= print(text.center(20, "=")) # ====Hello World=====
# 函数 format() 同样可以用来很容易的对齐字符串,使用 <,> 或者 ^ 字符后面紧跟一个指定的宽度 text = 'Hello World' print(format(text, "=>20")) # =========Hello World 坐对其 print(format(text, '*^20s')) # ****Hello World***** 中间对其 # 格式化多个值的时候 print('{:>10s} {:>10s}'.format('Hello', 'World')) # format() 函数的它不仅适用于字符串,它可以用来格式化任何值 x = 1.2345 print(format(x, ">10")) # 1.2345 print(format(x, '^10.2f')) # 1.23
# 老的代码中,格式化文本的 % 操作符 text = 'Hello World' print('%-20s' % text) # Hello World print('%20s' % text) # Hello World # format() 要比 % 操作符的功能更为强大。
# 并且 format() 也比使用 ljust() rjust() 或 center() 方法更通用, 因为它可以用来格式化任意对象,而不仅仅是字符串
34:合并拼接字符串(将几个小的字符串合并为一个大的字符串)
# 想要合并的字符串是在一个序列或者 iterable 中,那么最快的方式就是使用 join() 方法 parts = ['Is', 'Chicago', 'Not', 'Chicago?'] print(" ".join(parts)) # Is Chicago Not Chicago? print(",".join(parts)) # Is,Chicago,Not,Chicago? print("".join(parts)) # IsChicagoNotChicago? # 连接的对象可能来自各种不同的数据序列(比如列表,元组,字典,文件,集合或生成器等),
# 如果在所有这些对象上都定义一个 join() 方法明显是冗余的。 因此你只需要指定你想要的分割字符串并调用他的 join() 方法去将文本片段组合起来
# 仅仅只是合并少数几个字符串,使用加号(+)通常已经足够了: a = 'Is Chicago' b = 'Not Chicago?' a + ' ' + b
# 源码中将两个字面字符串合并起来,你只需要简单的将它们放到一起 a = 'Hello' 'World'
# 使用加号(+)操作符去连接大量的字符串的时候是非常低效率的, 因为加号连接会引起内存复制以及垃圾回收操作 s = '' for p in parts: s += p # 这种写法会比使用 join() 方法运行的要慢,因为每一次执行+=操作的时候会创建一个新的字符串对象。 你最好是先收集所有的字符串片段然后再将它们连接起来 # 相对比较聪明的技巧是利用生成器表达式转换数据为字符串的同时合并字符串 data = ['ACME', 50, 91.1] print(','.join(str(d) for d in data)) # str(d) for d in data生成一个生成器然后再join # 注意不必要的字符串连接操作 print(a + ':' + b + ':' + c) # Ugly print(':'.join([a, b, c])) # Still ugly print(a, b, c, sep=':') # Better # 使用第三种打印方法
# 混合使用I/O操作和字符串连接操作的时候,仔细研究你的程序。 # Version 1 (string concatenation) f.write(chunk1 + chunk2) # Version 2 (separate I/O operations) f.write(chunk1) f.write(chunk2) # 如果两个字符串很小,那么第一个版本性能会更好些,因为I/O系统调用天生就慢 # 如果两个字符串很大,那么第二个版本可能会更加高效, 因为它避免了创建一个很大的临时结果并且要复制大量的内存块数据。 # 需要根据你的应用程序特点来决定应该使用哪种方案
# 编写构建大量小字符串的输出代码,使用生成器函数 def sample(): yield 'Is' yield 'Chicago' yield 'Not' yield 'Chicago?' # 可以简单的使用 join() 方法将这些片段合并起来 print(",".join(sample())) # Is,Chicago,Not,Chicago? # 也可以将字符串片段重定向到I/O: for part in sample(): f.write(part) # 还可以写出一些结合I/O操作的混合方案: def combine(source, maxsize): parts = [] size = 0 for part in source: parts.append(part) size += len(part) if size > maxsize: yield ''.join(parts) parts = [] size = 0 yield ''.join(parts) # 结合文件操作 with open('filename', 'w') as f: for part in combine(sample(), 32768): f.write(part) # 这里的关键点在于原始的生成器函数并不需要知道使用细节,它只负责生成字符串片段就行了。
35:字符串中插入变量 内嵌变量的字符串,变量被它的值所表示的字符串替换掉。
# format( ) 方法是一种所有情况都能使用的格式化方法 # str.format_map(mapping) 方法仅适用于字符串格式中可变数据参数来源于字典等映射关系数据时。mapping 会被直接使用而不是复制到一个 dict mapping —— 字典类型的数据。 a = '{who}爱{how_many}条{what}' dict_x = {'how_many': '一', 'who': '你', 'what': 'chai'} print(a.format_map(dict_x)) # 你爱一条chai dict_y = {'how_many': '一', 'who': '你', 'what': 'chai', 'when': '今天'} print(a.format_map(dict_y)) # '你爱一条chai'
# Python并没有对在字符串中简单替换变量值提供直接的支持。 但是通过使用字符串的 format() 方法来解决这个问题 s = '{name} has {n} messages.' res = s.format(name='Guido', n=37) print(res) # Guido has 37 messages. # 如果要被替换的变量能在变量域中找到, 那么你可以结合使用 format_map() 和 vars() s = '{name} has {n} messages.' name = 'Guido' n = 37 print(s.format_map(vars())) # Guido has 37 messages. vars() 函数返回对象object的属性和属性值的字典对象 # vars() 也适用于对象实例 class Info: def __init__(self, name, n): self.name = name self.n = n a = Info('Guido', 37) s = '{name} has {n} messages.' print(s.format_map(vars(a)))
# format 和 format_map() 的一个缺陷就是它们并不能很好的处理变量缺失的情况 s = '{name} has {n} messages.' s.format(name='Guido') # 少了一个参数报错 # 一种避免这种错误的方法是另外定义一个含有 __missing__() 方法的字典对象 class safe_sub(dict): """防止key找不到""" def __missing__(self, key): return '{' + key + '}'
# 如果key在dict里面找不到值就返回 {键}为值 # 利用这个类包装输入后传递给 format_map(),safe_sub接收一个字典 s = '{name} has {n} messages.' name = 'Guido' n = 37 del n print(s.format_map(safe_sub(vars()))) # 'Guido has {n} messages.'
# __missing__魔术方法,__getitem__来访问一个不存在的key的时候,会调用__miss__()方法获取默认值,并将该值添加到字典中去 class safesub(dict): def __missing__(self, key): return '{' + key + '}' print(safesub(vars()).__missing__("10")) # {10} 不存在"10"这个键,就返回{10} 如果存在"10"这个键就返回对应的值
# 代码中频繁的执行这些步骤,你可以将变量替换步骤用一个工具函数封装起来 import sys class safesub(dict): """防止key找不到""" def __missing__(self, key): return '{' + key + '}' def sub(text): return text.format_map(safesub(sys._getframe(1).f_locals)) name = 'Guido' n = 37 print(sub('Hello {name}')) # Hello Guido print(sub('You have {n} messages.')) # You have 37 messages. print(sub('Your favorite color is {color}')) # Your favorite color is {color}
# getframe()的用法:从调用堆栈返回帧对象。 print(sys._getframe().f_code.co_filename) # 当前文件名,可以通过__file__获得 print(__file__) # 这个也能获取当前文件名 print(sys._getframe(0).f_code.co_name) # <module>, 获取#当前函数名,调用该函数的函数的名字,如果没有被调用,则返回<module>,貌似call stack的栈低 print(sys._getframe().f_lineno) # 当前行号 23 def get_cur_info(): print(sys._getframe().f_code.co_filename) # 当前文件名,可以通过__file__获得 print(sys._getframe(0).f_code.co_name) # 当前函数名 print(sys._getframe(1).f_code.co_name) # 调用该函数的函数的名字,如果没有被调用,则返回<module>,貌似call stack的栈低 print(sys._getframe().f_lineno) # 当前行号 print(sys._getframe(1).f_locals) get_cur_info() print(sys._getframe().f_locals) # Python的locals()函数会以dict类型返回当前位置的全部局部变量,和vars使用类似 print(vars())sys._getframe(1)
返回调用者的栈帧。可以从中访问属性f_locals
来获得局部变量,
大部分情况下在代码中去直接操作栈帧应该是不推荐的,但是,对于像字符串替换工具函数而言它是非常有用的f_locals
是一个复制调用函数的本地变量的字典。 尽管你可以改变f_locals
的内容,
但是这个修改对于后面的变量访问没有任何影响。 所以,虽说访问一个栈帧看上去很邪恶,但是对它的任何操作不会覆盖和改变调用者本地变量的值
# sys._getframe(0) 从0阶返回数据,就是当前全局,函数内部可以返回 # sys._getframe(1) 从1阶返回数据,必须在函数内部调用才可以 # sys._getframe(2) 从2阶返回数据,必须在函数的函数内部调用才可以 import sys print(sys._getframe(0).f_locals) # sys._getframe(0)一阶的使用,不需要在函数内部 def func(): print(sys._getframe(1).f_locals) # sys._getframe(1)2阶的使用,需要在函数内部才不会报错 func() def func1(): def inner(): print(sys._getframe(2).f_locals) # sys._getframe(2)3阶的使用,需要在函数的函数内部才不会报错 inner() func1()
# % 字符串格式化代码 name = 'Guido' n = 37 '%(name) has %(n) messages.' % vars() # 运行报错,不能里面有两个% #字符串模板的使用: import string name = 'Guido' n = 37 s = string.Template('$name has $n messages.') print(s.substitute(vars())) # Guido has 37 messages. # format()和f方案而已更加先进,因此应该被优先选择
36:以指定列宽格式化字符串 textwrap.fill
# 一些长字符串,想以指定的列宽将它们重新格式化:textwrap 模块来格式化字符串的输出 import textwrap s = "Look into my eyes, look into my eyes, the eyes, the eyes, \ the eyes, not around the eyes, don't look around the eyes, \ look into my eyes, you're under." print(textwrap.fill(s, 70)) # 每行70个字符 print(textwrap.fill(s, 40)) # 每行40个字符 print(textwrap.fill(s, 40, initial_indent=' ')) # 每行40个字符,首行前面空格initial_indent print(textwrap.fill(s, 40, subsequent_indent=' ')) # 每行40个字符,除了首行其他行前面都空格subsequent_indent
# textwrap 模块对于字符串打印是非常有用的,当希望输出自动匹配终端大小的时候,使用os.get_terminal_size() 方法来获取终端的大小尺寸 import os print(os.get_terminal_size().columns) fill() 方法接受一些其他可选参数来控制tab,语句结尾等
37:在字符串中处理html和xml
# 想将HTML或者XML实体如 &entity; 或 &#code; 替换为对应的文本 # 需要转换文本中特定的字符(比如<, >, 或 &) # 替换文本字符串中的 ‘<’ 或者 ‘>’ ,使用 html.escape() 函数可以很容易的完成 import html s = 'Elements are written as "<tag>text</tag>".' print(html.escape(s)) # Elements are written as "<tag>text</tag>". html转义,防止html注入,传输的使用用的转义的格式 print(html.escape(s, quote=False)) # Elements are written as "<tag>text</tag>". # 如果正在处理的是ASCII文本,并且想将非ASCII文本对应的编码实体嵌入进去, # 可以给某些I/O函数传递参数 errors='xmlcharrefreplace' 来达到这个目 s = 'Spicy Jalapeño' print(s.encode('ascii', errors='xmlcharrefreplace')) # b'Spicy Jalapeño'
# 处理HTML或者XML文本,试着先使用一个合适的HTML或者XML解析器,这些工具会自动替换这些编码值 # 如果你接收到了一些含有编码值的原始文本,需要手动去做替换, 通常你只需要使用HTML或者XML解析器的一些相关工具函数/方法即可 from html.parser import HTMLParser from xml.sax.saxutils import unescape s = 'Spicy "Jalapeño".' p = HTMLParser() res = p.unescape(s) # 报错,py3.9没有这个方法了html反转义移到html里面了 print(res) # 'Spicy "Jalapeño".' t = 'The prompt is >>>' ret = unescape(t) print(ret) # The prompt is >>>
import html
s = 'Spicy "Jalapeño".'
print(html.unescape(s))
38:字符串令牌解析
# 一个字符串,想从左至右将其解析为一个令牌流。 # 为了令牌化字符串,你不仅需要匹配模式,还得指定模式的类型 # 可能想将字符串像下面这样转换为序列对 import re text = 'foo = 23 + 42 * 10' tokens = [('NAME', 'foo'), ('EQ', '='), ('NUM', '23'), ('PLUS', '+'), ('NUM', '42'), ('TIMES', '*'), ('NUM', '10')] # 第一步:利用命名捕获组的正则表达式来定义所有可能的令牌,包括空格 # ?P<TOKENNAME> 用于给一个模式命名,供后面使用 NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)' NUM = r'(?P<NUM>\d+)' PLUS = r'(?P<PLUS>\+)' TIMES = r'(?P<TIMES>\*)' EQ = r'(?P<EQ>=)' WS = r'(?P<WS>\s+)' master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS])) # master_pat = re.compile('(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)|(?P<NUM>\\d+)|(?P<PLUS>\\+)|(?P<TIMES>\\*)|(?P<EQ>=)|(?P<WS>\\s+)') # 第二步,为了令牌化,使用scanner() 方法,这个方法会创建一个 scanner 对象 # 在这个对象上不断的调用 match() 方法会一步步的扫描目标文本,每步一个匹配 scanner = master_pat.scanner('foo = 42') # 按照正则规则扫描foo = 42 这个字符串,一步步扫描返回 ret = scanner.match() # <re.Match object; span=(0, 3), match='foo'> print(ret.lastgroup, ret.group()) # NAME foo ret = scanner.match() print(ret.lastgroup, ret.group()) # WS ret = scanner.match() print(ret.lastgroup, ret.group()) # EQ = ret = scanner.match() print(ret.lastgroup, ret.group()) # WS ret = scanner.match() print(ret.lastgroup, ret.group()) # NUM 42 ret = scanner.match() # None print(ret) # while ret := scanner.match(): # 一次性输出,ret # print(ret.lastgroup, ret.group())
# 实际使用这种技术的时候,将代码打包到一个生成器中 import re from collections import namedtuple text = 'foo = 23 + 42 * 10' tokens = [('NAME', 'foo'), ('EQ', '='), ('NUM', '23'), ('PLUS', '+'), ('NUM', '42'), ('TIMES', '*'), ('NUM', '10')] def generate_tokens(pat, text): """ 扫描的生成器函数 :param pat: 正则表达式 :param text: 扫描的文本 :return: """ Token = namedtuple('Token', ['type', 'value']) # 命名元组 scanner = pat.scanner(text) # 扫描text文本得到一个对象 for m in iter(scanner.match, None): # yield Token(m.lastgroup, m.group()) NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)' NUM = r'(?P<NUM>\d+)' PLUS = r'(?P<PLUS>\+)' TIMES = r'(?P<TIMES>\*)' EQ = r'(?P<EQ>=)' WS = r'(?P<WS>\s+)' # \s用来匹配空格
master_pat = re.compile('|'.join([NAME, NUM, PLUS, TIMES, EQ, WS])) for tok in generate_tokens(master_pat, 'foo = 42'): print(tok) # 如果想过滤令牌流,你可以定义更多的生成器函数或者使用一个生成器表达式。 # 过滤所有的空白令牌 tokens = (tok for tok in generate_tokens(master_pat, text) if tok.type != 'WS') # 生成器表达式过滤type为WS类型的数据 for tok in tokens: print(tok)
iter() 返回迭代器对象。 iter(object, sentinel) object 必需。可迭代对象。 sentinel 可选。如果对象是可调用对象,则当返回值与前哨相同时,迭代将停止。 iter()的标准用法:iter(object) object:必须是支持迭代的集合对象 如何判断一个对象是否可迭代? from collections import Iterable isinstance(object,Iterable) # 使用isinstance判断object是否可以迭代 定义一个可迭代对象? 在class里实现一个 def iter(self)方法,该方法的返回值是一个支持可迭代的对象. from collections.abc import Iterable class a: list = [1, 2, 3] def __iter__(self): return self.list class b: list = [1, 2, 3] print(Iterable) # <class 'collections.abc.Iterable'> print(isinstance(a(), Iterable)) # True print(isinstance(b(), Iterable)) # False
# iter()的扩展用法:iter(object, sentinel) object:必须是一个可以callable的对象,实现了def call(self, *args, **kwargs)方法的类 # 自己实现一个可迭代的对象data class data: a_list: list = [1, 2, 3, 4, 5, 6] # 定义一个a_list列表 = [1, 2, 3, 4, 5, 6] index = 0 def __call__(self, *args, **kwargs): """ 类对象实例化后调用的返回值 :param args: :param kwargs: :return: """ item = self.a_list[self.index] self.index += 1 return item def __iter__(self): """ 外面调用__iter__方法的时候返回一个迭代器(含有__iter__方法证明这个类是可迭代器) :return: """ self.i = iter(self.a_list) return self.i for item in iter(data(), 3): # 每一次迭代都会调用一次__call__方法,当__call__的返回值等于3是停止迭代 print(item) print(list(data().__iter__())) # [1, 2, 3, 4, 5, 6] called = data() while 1: try: ret = called() print(ret) except Exception as e: print(e) break
# 令牌化是很多高级文本解析与处理的第一步。 为了使用上面的扫描方法,你需要记住这里一些重要的几点。
第一点就是你必须确认你使用正则表达式指定了所有输入中可能出现的文本序列。 如果有任何不可匹配的文本出现了,
扫描就会直接停止。这也是为什么上面例子中必须指定空白字符令牌的原因 # 令牌的顺序也是有影响的。 re 模块会按照指定好的顺序去做匹配。 因此,如果一个模式恰好是另一个更长模式的子字符串,那么你需要确定长模式写在前面 import re LT = r'(?P<LT><)' LE = r'(?P<LE><=)' # LE长模式写在前面 EQ = r'(?P<EQ>=)' master_pat = re.compile('|'.join([LE, LT, EQ])) # Correct
# 第二个模式是错的,因为它会将文本<=匹配为令牌LT紧跟着EQ,而不是单独的令牌LE,有问题
# 需要留意下子字符串形式的模式。假设你有如下两个模式 PRINT = r'(?P<PRINT>print)' NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)' master_pat = re.compile('|'.join([PRINT, NAME])) for tok in generate_tokens(master_pat, 'printer'): print(tok)
39:实现一个简单的递归下降分析器
# 根据一组语法规则解析文本并执行命令, 或者构造一个代表输入的抽象语法树 # 自己编写解析器。 # 根据特殊语法去解析文本,首先要以BNF或者EBNF形式指定一个标准语法 # 一个简单数学表达式语法
略
40: 字节字符串上的字符串操作
# 在字节字符串上执行普通的文本操作(比如移除,搜索和替换)。 # 字节字符串同样也支持大部分和文本字符串一样的内置操作 data = b"hello world" print(data[0:5]) # b'hello' print(data.startswith(b'hello')) # True print(data.split()) # [b'hello', b'world'] 默认空格切片 print(data.replace(b"hello", b"ywt")) # b'ywt world'
# bytearray() 方法返回一个新字节数组。这个数组里的元素是可变的,并且每个元素的值范围: 0 <= x < 256。 bytearray([source[, encoding[, errors]]]) 如果 source 为整数,则返回一个长度为 source 的初始化数组; 如果 source 为字符串,则按照指定的 encoding 将字符串转换为字节序列; 如果 source 为可迭代类型,则元素必须为[0 ,255] 中的整数; 如果 source 为与 buffer 接口一致的对象,则此对象也可以被用于初始化 bytearray。 如果没有输入任何参数,默认就是初始化数组为0个元素。 print(bytearray([1, 2, 3])) # bytearray(b'\x01\x02\x03') print(bytearray()) # bytearray(b'') print(bytearray('runoob', 'utf-8')) # bytearray(b'runoob')
# 字节数组也能使用常用的一些操作 data = bytearray(b'Hello World') data[0:5] # bytearray(b'Hello') data.startswith(b'Hello') # True data.split() # [bytearray(b'Hello'), bytearray(b'World')] data.replace(b'Hello', b'Hello Cruel') # bytearray(b'Hello Cruel World')
# 数据转化正二进制的函数 1:bytearray ret = bytearray([2, 5, 2, 1, 8, 3, 0, 8, 9, 8, 3, 6, 2, 3, 2, 5]) print(ret) # bytearray(b'\x02\x05\x02\x01\x08\x03\x00\x08\t\x08\x03\x06\x02\x03\x02\x05') ret = bytearray("25", encoding="utf8") print(ret) #print(ret) 可以把数字和字符串转化成二进制 2:struct函数 struct.pack() struct.unpack()
# 使用正则表达式匹配字节字符串,但是正则表达式本身必须也是字节串 import re data = b'FOO:BAR,SPAM' # re.split('[:,]',data) # 报错:TypeError: cannot use a string pattern on a bytes-like object print(re.split(b'[:,]', data)) # [b'FOO', b'BAR', b'SPAM']
# 大多数情况下,在文本字符串上的操作均可用于字节字符串 # 但是:字节字符串的索引操作返回整数而不是单独字符 a = 'Hello World' a[0] # 'H' b = b'Hello World' b[0] # 72 返回的是ord将一个字符转换为它的整数值(根据ascill码转) # 第二点,字节字符串不会提供一个美观的字符串表示,也不能很好的打印出来,除非它们先被解码为一个文本字符串 b = b"hello world" print(b) print(b.decode('ascii')) # hello world 解码成字符串了 # 不存在任何适用于字节字符串的格式化操作: b'%10s %10d %10.2f' % (b'ACME', 100, 490.1) # 报错,不能运行 # 如果你想格式化字节字符串,你得先使用标准的文本字符串,然后将其编码为字节字符串 '{:10s} {:10d} {:10.2f}'.format('ACME', 100, 490.1).encode('ascii') # 使用字节字符串可能会改变一些操作的语义,特别是那些跟文件系统有关的操作 # 使用一个编码为字节的文件名,而不是一个普通的文本字符串,会禁用文件名的编码/解码 with open('jalape\xf1o.txt', 'w') as f: f.write('spicy') import os print(os.listdir('.')) # ['jalapeño.txt'] print(os.listdir(b".")) # [b'jalape\xc3\xb1o.txt'] # 给目录名传递一个字节字符串,结果中文件名以未解码字节返回的 # 返回了\xc3\x这些未解码的数据,在目录中的文件名包含原始的UTF-8编码