Python小技巧_2

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

字符串处理

字符串最主要的处理之一就是分割.

最基础简单的分割方式就是使用split函数.

1
2
"Hello".split("l")
# ["He", "", "o"]

哎?出现了一个空字符串!, 这并不是我们想要的, 再讲如何消去它之前, 我们先来说一下为什么会产生.

可以做做这样的小实验:

1
2
"aaaa".split("a")
# ['','','','','']

产生了五个空字符串, 然而我们只传了四个呀. 那么就开始猜测, 会不会是两个字符串的叠加效果呢?(此时可以试试"a".split("a"))

所以说, 这个情况是在寻找符合条件的字符串的两边时,发现要么没有了, 要么还是原来的条件.

现在提供一个可以快速消去空字符串的方法,不仅仅在这里适用,只要是为了剔除空数据, 都是可以的!

1
2
[x for x in ["","","Only",""] if x]
# ["Only"]

这种基本方法还是有一点的笨拙的, 如果遇到字符串中含有多个split点时, 就懵逼了.

所以,还是要请出主角–正则表达式re模块

当你遇到了一个问题的时候, 为了解决它, 你想到了使用正则表达式, 这时, 你就有了两个问题. :)

正则模块需要引入.

1
2
3
4
5
6
import re
text = ''
for n in re.split("[,./|]", "Hel.lo,W|or/ld!"):
text += n
print(text)
# HelloWorld!

小结一下:当字符串不是很复杂的时候,仍然建议使用默认的字符串的split函数, 因为他更快速.如果遇到较为棘手的切割, 最好使用正则的切割.

想象这么一种场景: 你需要获得文件的后缀名/你想知道你个网址使用的协议是什么.

抽象出来就是判断字符串的开头或者结尾.

就如同方法名一样简单, 两个方法分别用来检测头和尾. startswithendswith.

举个例子:

1
2
3
4
5
6
import os, stat
os.listdir('.')
# ['e.py', 'c.java', 'd.cpp', 'b.sh', 'f.sh', 'a.py', 'g.js'] 事先写好了有一些测试用数据
# 接下来使用列表解析找出所有的py和sh
[ file for file in os.listdir('.') if file.endswith(('.py', '.sh')) ]
# ['e.py', 'b.sh', 'f.sh', 'a.py']

方法内可以传入元组进行多适配, 但一定不接受列表.

接下来就可以使用这样的方法找到一堆数据中所有的ftp://的服务器地址.(略 :) )

下面说一个超实用的正则魔法!

re.sub可以进行替换, 如果继续利用正则的捕获组就能进行格式的变换,(比如把04/05/2017变成2017-05-04).

sub接受三个参数, 分别是['正则表达式', '替换后的字符串', '需要替换的字符串'].

接下来再说一下什么是正则中的捕获组.

捕获组简单的说就是一个小整体,还是用实例来说明下:

1
2
3
import re
log = open("/var/log/nginx/access.log", 'r').read()
print(re.sub(r'(\d{2})/([a-zA-Z]{3})/(\d{4})', r'\3-\2-\1', log))

一个一个来说明, access.log的文件类似

5?.6?.2?.?1? - - [03/May/2017:23:19:25 +0800] "GET /??/ HTTP/1.1" 304 0 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36"

这个实例是将所有的时间改为2017-May-03的形式.

接着来看一下捕获组, 就是那个用括号抱起来的东西. 被抱起来的从0开始计数, 在后面使用反斜线加上组号就可以进行引用.

另外, 正则的捕获组可以添加名字的, 像这样使用: ?P<名字>, 引用时这样: \g<名字>.

将上面的例子进行一下更改就变成这样: re.sub(r'(?P<day>\d{2})/(?P<month>[a-zA-Z]{3})/(?P<year>\d{4})', r'\g<day>-\g<month>-\g<year>', log).

说完了分割, 接下来再来看另一个重要的操作–拼接.

传统的拼接方法上面也展示过了:

1
2
3
4
5
6
l = ["Hel", "lo,", "Worl", "d"]
greeting = ''
for part in l:
greeting += part
print(greeting)
# Hello,World

这样拼接还是有一点麻烦的, 更好地方法是使用str.join方法.

这个方法接受一个可迭代对象作为参数, 调用者(self)就是分割符, 比如:

1
2
",".join(['Hello', 'world'])
# Hello,world

但是,我们遗漏了一种情况, 如果传入的可迭代对象包含多个类型的数据怎么办呢?

先试一试吧:

1
"".join(['123', 456, '789'])

呀!报错了! 看样子还是要处理这个问题的.

一种解决方法是使用列表解析:

1
2
example = ['123', 456, '789']
"".join([str(x) for x in example])

这样虽然可以处理, 但如果数据很大的话, 会占用很多资源.

所以, 使用Generator是更优的选择.

因为他的开销更小.

1
2
example = ['123', 456, '789']
"".join((str(x) for x in example))

接着再来说说文本的排版.

我们经常遇到的一种情况是, 对一个字典进行输出.

1
2
3
4
5
{
"name":'J',
"age":22,
"sex":1
}

我们期望的输出是:

1
2
3
name :  'J'
age : 22
sex : 1

由于键的长度并不一样, 所以直接输出是做不到的.

str有几个方法可以帮助我们达成这个目标.

准确的说, 这些方法只是填充字符串而已.

1
2
3
4
5
6
7
s = 'abc'
s.ljust(10)
# ' abc'
s.ljust(10,'=')
# '=======abc'
s.center(10)
# ' abc '

另外一个类似的方法是使用format函数, format接受一个字符串和格式字符串, 使用起来的效果就像是这样:

1
2
3
4
format(s, '>10')
# ' abc'
format(s,, '^10')
# ' abc '

那么现在来完成开头提出的问题, 排版输出键值长度不一致的字典:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kv = {
'name':'Python',
'createTime':'2017-04-26',
'mtime':'2017-05-03',
'author':'Justin'
}
# 得到字典
m = max(map(len, kv.keys()))
# 得到键的最大长度
for k in d:
print(k.ljust(m), ":", kv[k])
# name : Python
# createTime : 2017-04-26
# mtime : 2017-05-03
# author : Justin

除了分割和拼接, 字符串另一个重要的处理就是删除(叫剔除应该更好),

比如剔除两端的空格, 剔除指定字符,等等..

str类仍然封装了很多丰富的方法.

*strip一类方法

1
2
3
4
s = "!  ++Hello==  ?"
s.strip("? !") # "++Hello=="
s.lstrip("+") # "Hello=="
s.rstrip("=") # "Hello"

*strip不能搞定中间的字符, 我们可以通过切片加拼接的方式完成.

比如:

1
2
3
4
s = "Hel:lo"
s = s[:3] + s[4:]
print(s)
# Hello

但是切片的方法略显笨重,来看一下字符串的replace方法(别忘了正则的sub方法呀.)

1
2
3
4
s = "123\n456\n789"
s = s.replace("\n", "")
s
# "123456789"

但是, 你现在也注意到了, replace方法只能匹配一个字符, 如果遇到多个就不行了.

比如剔除字符串s = "123.456,789;"中所有的特殊字符.

这个时候还是需要靠强大的正则了, 也就是之前说的sub函数.

1
re.sub("[.,;]", "",s)

就可以了.

最后再来说一下translate方法, strre, 他们的作用稍有不同, 我们分开来说.

首先, translate方法可以实现 伪加密(更换顺序)和剔除指定字符的效果. 一个联动的函数是str.maketrans.

其中, translate接受一个参数, 一个映射表(需要实现__getitem__接口), 这个映射表一般有str.maketrans产生.

str.maketrans最多接受三个参数, 前两个为映射字符串, 最后一个是将选定的字符串映射成None, 被映射成None的字符串会在translate的过程中被删除.

1
2
3
s = "123\n456\t789\r"
s.translate(str.maketrans("123789", "789123","\n\r\t"))
# "789456123"

到此, 字符串处理的基本就结束了.

函数

Python有个特别好用的函数特性, 叫做装饰器.

装饰器可以帮助我们减少大量代码的冗余, 使得函数的利用更加便利.

一个非常的经典的例子就是这样:

1
2
3
4
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)

这是一个非常经典的斐波那契数列递归,但如果使用这个函数进行fib(50)的话, 也许你可以先去泡杯咖啡.

一种优化方案是:

1
2
3
4
5
6
7
8
9
def fib(n, cache=None):
if cache is None:
cache = {}
if n <= 1:
return 1
if n in cache:
return cache[n]
cache[n] = fib(n-1, cache) + fib(n-2, cache)
return cache[n]

在这里,我们建立了一个缓存池(原函数缓慢的原因就是因为大量的重复计算, 计算得到的值都没有得到保存.), 如果能从缓存池中读取, 就不再计算而直接读取.

对于这个问题,事实上好多递归函数都存在这个问题.

比如爬楼梯问题:

一个共有10个台阶的楼梯, 从下面走到上面, 一次只能迈1-3的阶梯, 在不能后退的情况下, 走完楼梯一共有多少种方法?

传统的递归解法是:

1
2
3
4
5
6
7
8
def climb(n, steps):
if n == 0:
return 1
count = 0
if n >0:
for step in steps:
count += climb(n - step, steps)
return count

计算climb(50, (1,2,3))试试, 你的咖啡已经凉了.

我们仍然可以考虑加上缓存的方法, 但是那就意味着还要再重新书写一遍近乎是一样的代码.

这时引入装饰器, 装饰器也是个函数, 接受一个函数, 在他的内部生产一个符合要求的函数(比如生成缓存池, 记录日志等等), 再调用参数(也就是传进来的函数), 接着将这个包装的函数返回出去.就成为了一个满足要求的函数.

本来的过程是这样的:

1
func = wraaper(func)

这样有点麻烦, 所以Python提供了这样的语法糖:

1
2
3
@wraaper
def func():
pass

为上述递归问题写一个装饰器就像是这样:

1
2
3
4
5
6
7
def caching(func):
cache = {}
def wrap(*args):
if args not in cache:
cache[args] = func(*args)
return cache
return wrap

这样再调用fibclimb, 就可秒出结果.

再比如说一个很常用的功能, 我想知道我的函数执行了多长时间.我就可以也写个装饰器:

1
2
3
4
5
6
7
8
import time
def record(func):
def wrap(*args):
start = time.time()
func(*args)
end = time.time()
print(end-start)
return wrap

但是这个装饰器不是太实用, 我们可以添加让他在超过一定时间就记录日志的功能.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time, logging, random
def record(func):
def wrapper(*args):
start = time.time()
res = func(*args)
used = time.time() - start
if used > 2.0:
logging.warn("%s : %s > %s" % (func.__name__, str(used), str(2.0)))
return res
return wrapper
@record
def test():
if randint(0,1):
time.sleep(2.5)
print("Test")
for n in range(10):
test()

接着运行的结果就像是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Test                                         
Test
WARNING:root:test : 2.500215768814087 > 2.0
Test
Test
Test
Test
Test
WARNING:root:test : 2.501840591430664 > 2.0
Test
WARNING:root:test : 2.5007271766662598 > 2.0
Test
Test
WARNING:root:test : 2.5005836486816406 > 2.0

但是这个装饰器仍然不够灵活, 他的时间阈值是个固定值,如果是个可调节的值就更好了.

为了达成装饰器同样可以接受参数, 我们就需要写个三层嵌套的装饰器.

也就是说: 一个返回装饰器的函数.

1
2
3
4
5
6
7
8
def log(text):
def decorator(func):
def wrapper(*args **kw):
print("%s %s():" % (text, func.__name__))
res = func(*args, **kw)
return res
return wraaper
return decorator

这就是一个最简单的展示接受参数的装饰器.

使用中你会发现这样的情况.

1
2
3
4
5
6
@log("execute")
def now():
print("2017-05-05")
now()
# execute now():
# 2017-05-05

看起来很正常是吗.但是如果你试试查看now.__name__的时候就会发现, now已经被wrapper污染了.

所以返回的是wrapper.这并不是我们所希望的.

Pythonfunctools包内的wraps提供了我们需要的功能.

由于wraps也是一个装饰器, 所以使用起来也非常方便, 在原基础上加上这样的一行代码即可解决问题 :

1
2
3
4
5
6
7
8
9
10
import functools
def log(text):
def decorator(func):
@functools.wraps(func)
def wrapper(*args **kw):
print("%s %s():" % (text, func.__name__))
res = func(*args, **kw)
return res
return wraaper
return decorator