Python编程小技巧(2)

字符串处理


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

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

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

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

可以做做这样的小实验:

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

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

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

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

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

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

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

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

正则确实强大, 但是今天不会来讨论正则, 如果想要了解, 请前往正则表达式入门教程.

正则模块需要引入.

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

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

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

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

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

举个例子:

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接受三个参数, 分别是['正则表达式', '替换后的字符串', '需要替换的字符串'].

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

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

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\d{2})/(?P[a-zA-Z]{3})/(?P\d{4})', r'\g-\g-\g', log).

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

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

l = ["Hel", "lo,", "Worl", "d"]
greeting = ''
for part in l:
  greeting += part
print(greeting)
# Hello,World

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

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

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

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

先试一试吧:

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

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

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

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

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

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

因为他的开销更小.

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

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

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

{
    "name":'J',
    "age":22,
    "sex":1
}

我们期望的输出是:

name :  'J'
age  :  22
sex  :  1

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

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

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

s = 'abc'
s.ljust(10)
# '       abc'
s.ljust(10,'=')
# '=======abc'
s.center(10)
# '   abc    '

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

format(s, '>10')
# '       abc'
format(s,, '^10')
# '   abc    '

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

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一类方法

s = "!  ++Hello==  ?"
s.strip("? !") # "++Hello=="
s.lstrip("+") # "Hello=="
s.rstrip("=") # "Hello"

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

比如:

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

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

s = "123\n456\n789"
s = s.replace("\n", "")
s
# "123456789"

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

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

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

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

就可以了.

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

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

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

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

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

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

函数


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

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

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

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

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

一种优化方案是:

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的阶梯, 在不能后退的情况下, 走完楼梯一共有多少种方法?

传统的递归解法是:

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))试试, 你的咖啡已经凉了.

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

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

本来的过程是这样的:

func = wraaper(func)

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

@wraaper
def func():
  pass

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

def caching(func):
  cache = {}
  def wrap(*args):
    if args not in cache:
      cache[args] = func(*args)
    return cache
  return wrap

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

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

import time
def record(func):
  def wrap(*args):
    start = time.time()
    func(*args)
    end = time.time()
    print(end-start)
  return wrap

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

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()

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

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

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

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

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

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

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

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

@log("execute")
def now():
  print("2017-05-05")
now()
# execute now():
# 2017-05-05

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

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

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

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

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

你可能感兴趣的:(Python编程小技巧(2))