跳转至

python

还是一样,记录学习中的一些细节。

字符串

1 字符编码

计算机只处理数字,所以我们平常使用的字符串、字符就需要计算机进行编码转化成数字。世界上这么多语言,各国有各国的标准:计算美国是最早,他们编译了127个字符到ASCII;日本把日文编到Shift_JIS里;韩国把韩文编到Euc-kr,中国把中文编到GB2312编码......不同的语言混在一起,没有统一的标准就会出现乱码。那么如何统一?

因此,Unicode字符集应运而生。Unicode把所有语言都统一到一套编码里,这样就不会再有乱码问题了。

如果说“各个国家都在为自己文字独立编码”是百家争鸣,那么“建立世界统一的字符编码”则是一统江湖,谁都想来做这个武林盟主。早前就有两个机构试图来做这个事: (1) 国际标准化组织(ISO),他们于1984年创建ISO/IEC JTC1/SC2/WG2工作组,试图制定一份“通用字符集”(Universal Character Set,简称UCS),并最终制定了ISO 10646标准。 (2) 统一码联盟,他们由Xerox、Apple等软件制造商于1988年组成,并且开发了Unicode标准(The Unicode Standard,这个前缀Uni很牛逼哦---Unique, Universal, and Uniform)。

1991年前后,两个项目的参与者都认识到,世界不需要两个不兼容的字符集。于是,它们开始合并双方的工作成果,并为创立一个单一编码表而协同工作。从Unicode 2.0开始,Unicode采用了与ISO 10646-1相同的字库和字码;ISO也承诺,ISO 10646将不会替超出U+10FFFF的UCS-4编码赋值,以使得两者保持一致。两个项目仍都独立存在,并独立地公布各自的标准。不过由于Unicode这一名字比较好记,因而它使用更为广泛。

中国的设计史:GB2312集收纳了6763个字符,发现不够,设计了GBK,后来又加入了少数民族的字符成了GB18030

Unicode是一个字符集,同时也定义了编码规则(字符集大约是收纳的意思,每一个字符对应一个编号;而编码是指如何使用对应的编号和计算机二进制直接的转化存储)

一开始,Unicode使用UCS-2,发现还是不够就使用了UCS-4。UCS-4有个缺点,她太大了,导致当时并不流行。后来的后来,UTF-8出现,她是一种可变长的编码,解决了空间上的问题,如今十分流行。

其实还有很多种编码规则UTF-16,UTF-32......包括他们的编码规则是什么?感兴趣可以自己了解一下

对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符:

>>> ord('A')
65
>>> ord('中')
20013
>>> chr(66)
'B'
>>> chr(25991)
'文'

# 知道了字符的整数编码,使用整数的十六进制然后可以直接写出对应字符
>>> '\u6587'
'文'

由于Python的字符串类型是str,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes

Python对bytes类型的数据用带b前缀的单引号或双引号表示:

x = b'ABC'

要注意区分'ABC'b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。

以Unicode表示的str通过encode()方法可以编码为指定的bytes,例如:

>>> 'ABC'.encode('ascii')
b'ABC'
>>> '中文'.encode('utf-8')
b'\xe4\xb8\xad\xe6\x96\x87'
>>> '中文'.encode('ascii')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)

纯英文的str可以用ASCII编码为bytes,内容是一样的,含有中文的str可以用UTF-8编码为bytes。含有中文的str无法用ASCII编码,因为中文编码的范围超过了ASCII编码的范围,Python会报错。

bytes中,无法显示为ASCII字符的字节,用\x##显示。

反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法:

>>> b'ABC'.decode('ascii')
'ABC'
>>> b'\xe4\xb8\xad\xe6\x96\x87'.decode('utf-8')
'中文'

如果bytes中包含无法解码的字节,decode()方法会报错:

>>> b'\xe4\xb8\xad\xff'.decode('utf-8')
Traceback (most recent call last):
  ...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 3: invalid start byte

如果bytes中只有一小部分无效的字节,可以传入errors='ignore'忽略错误的字节:

>>> b'\xe4\xb8\xad\xff'.decode('utf-8', errors='ignore')
'中'

要计算str包含多少个字符,可以用len()函数:

>>> len('ABC')
3
>>> len('中文')
2

len()函数计算的是str的字符数,如果换成byteslen()函数就计算字节数:

>>> len(b'ABC')
3
>>> len(b'\xe4\xb8\xad\xe6\x96\x87')
6
>>> len('中文'.encode('utf-8'))
6

可见,1个中文字符经过UTF-8编码后通常会占用3个字节,而1个英文字符只占用1个字节。

在操作字符串时,我们经常遇到strbytes的互相转换。为了避免乱码问题,应当始终坚持使用UTF-8编码对strbytes进行转换。

字符串格式化

  1. %

    Python中%就是用来格式化字符串的,在字符串内部,%s代表字字符串替换,%d代表整数替换...

    ## 占位符顺序对应
    
    print('%2d-%02d' % (3, 1))
    '''
     3-01
    3.14
    '''
    
    print('%.2f' % 3.1415926)
    '''
    3.14
    '''
    
  2. format()

    另一种格式化字符串的方法是使用字符串的format()方法,它会用传入的参数依次替换字符串内的占位符{0}{1},可以自己制定顺序了。

    print('Hello, {1}, 你的幸运数字是{0}'.format('小明', 7))
    # Hello, 7, 你的幸运数字是小明
    
  3. f-string

最后一种格式化字符串的方法是使用以f开头的字符串,称之为f-string,它和普通字符串不同之处在于,字符串如果包含{xxx},就会以对应的变量替换:

>>> r = 2.5
>>> s = 3.14 * r ** 2
>>> print(f'The area of a circle with radius {r} is {s:.2f}')
The area of a circle with radius 2.5 is 19.62

上述代码中,{r}被变量r的值替换,{s:.2f}被变量s的值替换,并且:后面的.2f指定了格式化参数(即保留两位小数),因此,{s:.2f}的替换结果是19.62

List 和 Tuple

# List  
classmates = ['Michael', 'Bob', 'Tracy']

classmates.append('H')    # 追加末尾
classmates.insert(1, 'H') # 追加到指定位置
classmates.pop(0)         # 删除指定位置元素,留空表示尾部


# Tuple  无法改动(本质是指向不变)
classmates = ('Michael', 'Bob', 'Tracy')

Dict 和 Set

# Dict  key-value
d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}

if 'White' in d:      # 查找key值是否存在
d.get('White', value) # 查找key值是否存在,若不存在返回value,留空返回None
d.pop('Bob')          # 删除key

# Set 存储不重复元素,无序(内部不指定顺序)
>>> s = {1, 1, 2, 2, 3, 3}
>>> s
{1, 2, 3}
s.add(4)    # 添加元素
s.remove(4) # 删除元素

函数的参数

位置参数

按位置顺序对应

def power(x, n):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s

power(5, 2)

默认参数

将变化大的参数放在前面,变化小的参数放在后面。变化小的参数在传入时如果留空就接受默认值。

def power(x, n=2):
    s = 1
    while n > 0:
        n = n - 1
        s = s * x
    return s


power(5) # 等价于 power(5, 2)
power(5, 3)

请注意,默认参数一定要是整型,字符串等不可变对象。

不可变对象,其本质是指向不变。字符串的replace函数看似能替换修改str,但其实他只是重新创建了一个str变量赋值。

其实不可变对象调用自身任意方法都不会修改自身内容,而是创建新的对象并返回。

# 默认参数是可变对象时产生的问题
def add_end(L=[]):
    L.append('END')
    return L


add_end([1, 2, 3])
# [1, 2, 3, 'END']
add_end(['x', 'y', 'z'])
# ['x', 'y', 'z', 'END']


# 默认参数启用后,会出下面情况:
>>> add_end()
['END', 'END']
>>> add_end()
['END', 'END', 'END']

Python函数在定义的时候,默认参数L的值就被计算出来了,即[ ],因为默认参数L也是一个变量,它指向对象[ ],每次调用该函数,如果改变了L的内容,则下次调用时,默认参数的内容就变了,不再是函数定义时的[ ]了。

要修改上面的例子,我们可以用None这个不变对象来实现:

def add_end(L=None):
    if L is None:
        L = []
    L.append('END')
    return L

现在,无论调用多少次,都不会有问题:

>>> add_end()
['END']
>>> add_end()
['END']

为什么要设计strNone这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。

可变参数 *args

不确定参数的数量,使用可变参数更加方便。

当我们想讲多个数当做参数传入,普通参数:

def calc(numbers):
    sum1 = 0
    for n in numbers:
        sum1 = sum1 + n * n
    return sum1


calc([1, 2, 3]) # 此时参数需要传入list或者tuple

如果使用了可变参数:

def calc(*numbers):
    sum2 = 0
    for n in numbers:
        sum2 = sum2 + n * n
    return sum2


  calc(1, 2, 3)

在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数。

当然即便是你已经使用list,同样也使用了可变参数的函数,这时python允许再listtuple前添加*作为可变参数传入函数。表示把listtuple中的全部元素传入(如果是不加*则会将整个list作为元素装入可变参数的tuple中),此写法十分常见。

def calc(*numbers):
    sum2 = 0
    for n in numbers:
        sum2 = sum2 + n * n
    return sum2


    x = [1, 2, 3]
    calc(*x)

关键字参数 **kw

1 使用

可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:

def person(name, age, **kw):
    print('name:', name, 'age:', age, 'other:', kw)

函数person除了必选参数nameage外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数:

person('Michael', 30)
# name: Michael age: 30 other: {}

也可以传入任意个数的关键字参数:

person('Bob', 35, city='Beijing')
# name: Bob age: 35 other: {'city': 'Beijing'}


person('Adam', 45, gender='M', job='Engineer')
# name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到nameage这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。

和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:

extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, city=extra['city'], job=extra['job'])
# 关键字参数传入一定要传入参数名字

# name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

当然,上面复杂的调用可以用简化的写法:

extra = {'city': 'Beijing', 'job': 'Engineer'}
person('Jack', 24, **extra)


# name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}

**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra

2 检查关键字存在

kw相当于组成了一个tuple,那么检查用in即可。

def person(name, age, **kw):
    if 'city' in kw:
        # 有city参数
        pass
    if 'job' in kw:
        # 有job参数
        pass
    print('name:', name, 'age:', age, 'other:', kw)

3 升级:命名关键字参数

命名关键字用于限制关键字参数的名字。

如,只接受cityjob关键字的参数:

def person(name, age, **kw):


# 改成  
def person(name, age, *, city, job):

当前面已经有可变参数时,则不需要加*的特殊分割符号。

def person(name, age, *args, city, job):
    print(name, age, args, city, job)


lis = [1, 2]
person('Jack', 24, *lis, city='Beijing', job='x')

命名关键字可以有默认值

def person(name, age, *, city='Beijing', job):
    print(name, age, city, job)


person('Jack', 24, job='Engineer')
# Jack 24 Beijing Engineer

参数组合使用

使用时前后顺序需要满足:位置参数(必选),默认参数,可变参数,命名关键字参数,关键字参数

但是实际这样组合并不好,很影响理解。

奇招

切片

记住左开右闭,

L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']

L[:3] == L[0:3]
# ['Michael', 'Sarah', 'Tracy']

L[1:3]
# ['Sarah', 'Tracy']

L[-2:]
# ['Bob', 'Jack']
# [-2:0] 没有效果,也就是说,如果你想输出最后n个元素,这是唯一的招数。虽然L[-1]可以去最后一个,但是切片是左开右闭的。

L[-2:-1]
# ['Bob']

# 前7;后7;全部;每2个取1个
L[:7]
L[-7:]
L[:]
L[:7:2]

tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:

>>> (0, 1, 2, 3, 4, 5)[:3]
(0, 1, 2)

字符串'xxx'也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:

>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'

迭代

c语言的迭代,使用for配合使用指针,数组使用下标

python中针对迭代,用关键字in。这样即可不在乎下标,只拿到我想要的。

d = {'a': 1, 'b': 2, 'c': 3}
for key in d:
    print(key)

'''
a
c
b
'''

# 当然,上面这样的dict,你可能想要的东西有三种key, value, key and value
for key in dict:
  pass
for value in dict.values():
  pass
for key, value in dict.items():
  pass

# list 
for i, value in enumerate(['a', 'b', 'c']):
  pass

# enumerate /ɪˈnjuːməreɪt/ v. 枚举

字符串等也可以用in迭代。想知道一个对象能不能迭代,python中给了方法:

from collections.abc import Iterable

isinstance('hello', Iterable)
# True
isinstance([1, 2, 3], Iterable)
# True
isinstance(123, Iterable)
# False




# ps isinstance
x = 'abc'
y = 123
isinstance(x, str)
# True
isinstance(y, str)
# False

列表生成式

所谓列表生成式,就是简介的生成列表

[x * x for x in range(1, 11)]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

甚至可以加if判断

[x * x for x in range(1, 11) if x % 2 == 0]
# [4, 16, 36, 64, 100]

甚至可以使用两层循环

[m + n for m in 'ABC' for n in 'XYZ']
# ['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

甚至甚至可以用if ... else

if else就有讲究了,你如果这样使用:

[x for x in range(1, 11) if x % 2 == 0 else 0]
# ERROR!

这样使用else就是错误的,其实很好理解。跟在for后面的if作用是 筛选过滤,只有符合if的才会取出来完成for前面的表达式,加上了else就一定需要表达式来承接else的内容了,这可就不叫筛选了。

所以说,想写else就得写在表达式里,像这样:

>>> [x if x % 2 == 0 else -x for x in range(1, 11)]
# [-1, 2, -3, 4, -5, 6, -7, 8, -9, 10]

生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

创建generator

第一种方法很简单,只要把一个列表生成式的[]改成()

L = [x * x for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
g = (x * x for x in range(10))
# <generator object <genexpr> at 0x1022ef630>

创建Lg的区别仅在于最外层的[]()L是一个list,而g是一个generator。

我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?

如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:

>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

我们讲过,generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。

当然,上面这种不断调用next(g)实在是太变态了,正确的方法是使用for循环,因为generator也是可迭代对象:

>>> g = (x * x for x in range(10))
>>> for n in g:
...     print(n)
... 
0
1
4
9
16
25
36
49
64
81

所以,我们创建了一个generator后,基本上永远不会调用next(),而是通过for循环来迭代它,并且不需要关心StopIteration的错误。

何时使用generator

generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。

比如,著名的斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:

1, 1, 2, 3, 5, 8, 13, 21, 34, ...

斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'

注意,赋值语句:

a, b = b, a + b

# 上面相当于
t = (b, a + b) # t是一个tuple
a = t[0]
b = t[1]

上面的函数可以输出斐波那契数列的前N个数:

>>> fib(6)
1
1
2
3
5
8
'done'

仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。

也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator函数,只需要把print(b)改为yield b就可以了:

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator函数,调用一个generator函数将返回一个generator:

>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>

这里,最难理解的就是generator函数和普通函数的执行流程不一样。普通函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

举个简单的例子,定义一个generator函数,依次返回数字1,3,5:

def odd():
    print('step 1')
    yield 1
    print('step 2')
    yield(3)
    print('step 3')
    yield(5)

调用该generator函数时,首先要生成一个generator对象,然后用next()函数不断获得下一个返回值:

>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

可以看到,odd不是普通函数,而是generator函数,在执行过程中,遇到yield就中断,下次又继续执行。执行3次yield后,已经没有yield可以执行了,所以,第4次调用next(o)就报错。

请务必注意:调用generator函数会创建一个generator对象,多次调用generator函数会创建多个相互独立的generator。

回到fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。

同样的,把函数改成generator函数后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代:

>>> for n in fib(6):
...     print(n)
...
1
1
2
3
5
8

但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIterationvalue中:

>>> g = fib(6)
>>> while True:
...     try:
...         x = next(g)
...         print('g:', x)
...     except StopIteration as e:
...         print('Generator return value:', e.value)
...         break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done

关于如何捕获错误,后面的错误处理还会详细讲解。

迭代器

我们上面说过,使用isintance(*, Iterable)可以检验一个对象是否是可迭代对象。

而迭代器不同,迭代器对象往往是一类数据流,它在需要下一个数据时才惰性的计算。它甚至可以表示一个无限大的数据流,这点list做不到。

  • 可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator
    • 可以被for()使用的对象都是Iterable类型,但是Iterable类型未必是Iterator类型。

我们上面讲过generator,它就是第一个典型的迭代器。不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。生成器都是Iterator对象,但listdictstr虽然是Iterable,却不是Iterator

from collections.abc import Iterator

isinstance((x for x in range(10)), Iterator)
# True
isinstance([], Iterator)
# False
isinstance({}, Iterator)
# False
isinstance('abc', Iterator)
# False



# 把list、dict、str等Iterable变成Iterator可以使用iter()函数:
isinstance(iter([]), Iterator)
# True
isinstance(iter('abc'), Iterator)
# True

函数式编程

函数式编程(functional programming)是一种编程范式

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

函数式编程FP 与 面向对象编程OOP

高阶函数

  1. 变量可以指向函数
>> abs
# <built-in function abs>

# 允许
x = abs
  1. 函数名也是变量

那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!

如果把abs指向其他对象,会有什么情况发生?

>>> abs = 10
>>> abs(-10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable

abs指向10后,就无法通过abs(-10)调用该函数了!因为abs这个变量已经不指向求绝对值函数而是指向一个整数10

当然实际代码绝对不能这么写,这里是为了说明函数名也是变量。要恢复abs函数,请重启Python交互环境。

注:由于abs函数实际上是定义在import builtins模块中的,所以要让修改abs变量的指向在其它模块也生效,要用import builtins; builtins.abs = 10

  1. 函数也可以传参
def g(x, y, f):
  return f(x) + f(y)

g(-5, 6, abs)
# 11

map()/reduce()

Google 的论文: MapReduce: Simplified Data Processing on Large Clusters

map

map()函数接受两个参数,第一个是一个函数,第二个是一个Iterable

作用是:将传入的函数作用到序列的每一个元素,并且返回新的Iterable对象。

def f(x):
  return x * x;

r = map(f, [1, 2, 3])
list(r)
# [1, 4, 9]

list(map(str, [1, 2]))
# ['1', '2']
reduce

正则表达式

固长字符

在正则表达式中,如果直接给出字符,就是精确匹配。用\d可以匹配一个数字,\w可以匹配一个字母或数字,所以:

  • '00\d'可以匹配'007',但无法匹配'00A'
  • '\d\d\d'可以匹配'010'
  • '\w\w\d'可以匹配'py3'

.可以匹配任意字符,所以:

  • 'py.'可以匹配'pyc''pyo''py!'等等。

变长字符

要匹配变长的字符,在正则表达式中,用*表示任意个字符(包括0个),用+表示至少一个字符,用?表示0个或1个字符,用{n}表示n个字符,用{n,m}表示n-m个字符:

来看一个复杂的例子:\d{3}\s+\d{3,8}

我们来从左到右解读一下:

  1. \d{3}表示匹配3个数字,例如'010'
  2. \s可以匹配一个空格(也包括Tab等空白符),所以\s+表示至少有一个空格,例如匹配' '' '等;
  3. \d{3,8}表示3-8个数字,例如'1234567'

综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。

如果要匹配'010-12345'这样的号码呢?由于'-'是特殊字符,在正则表达式中,要用'\'转义,所以,上面的正则是\d{3}\-\d{3,8}

但是,仍然无法匹配'010 - 12345',因为带有空格。所以我们需要更复杂的匹配方式。

更加精确的匹配

要做更精确地匹配,可以用[]表示范围,比如:

  • [0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线;
  • [0-9a-zA-Z\_]+可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100''0_Z''Py3000'等等;
  • [a-zA-Z\_][0-9a-zA-Z\_]*可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量
  • [a-zA-Z\_][0-9a-zA-Z\_]{0, 19}更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。

A|B可以匹配A或B,所以(P|p)ython可以匹配'Python'或者'python'

^表示行的开头,^\d表示必须以数字开头。

$表示行的结束,\d$表示必须以数字结束。

你可能注意到了,py也可以匹配'python',但是加上^py$就变成了整行匹配,就只能匹配'py'了。

re module

跑不了看官方文档

  • re.compile(pattern, flags=0):将正则表达式模式编译成一个正则表达式对象。
  • re.search(pattern, string, flags=0):在字符串中搜索匹配正则表达式模式的第一个位置,并返回一个对应于匹配的 Match 对象。
  • re.match(pattern, string, flags=0):尝试从字符串的起始位置匹配正则表达式模式,并返回一个对应于匹配的 Match 对象。
  • re.findall(pattern, string, flags=0)搜索字符串,以列表形式返回其中所有与正则表达式模式匹配的字符串。
  • re.sub(pattern, repl, string, count=0, flags=0):在字符串中搜索与正则表达式模式匹配的内容,并将其替换为指定的字符串。

Tips:

强烈建议使用Python的r前缀,就不用考虑转义的问题了:

s = 'ABC\\-001' # Python的字符串

s = r'ABC\-001' # Python的字符串

# 如果使用正则表达式去对应该字符串,其实是不需要单独考虑转义'\'的,容易混淆视听
# 上述两个对应的正则表达式字符串都是:
# 'ABC\-001'

如何匹配

match()方法判断是否匹配,如果匹配成功,返回一个Match对象,否则返回None。常见的判断方法就是:

test = '用户输入的字符串'
if re.match(r'正则表达式', test):
    print('ok')
else:
    print('failed')

更强大的切分字符串

正常的切分代码:

>>> 'a b   c'.split(' ')
['a', 'b', '', '', 'c']

无法识别连续的空格,用正则表达式试试:

>>> re.split(r'\s+', 'a b   c')
['a', 'b', 'c']

无论多少个空格都可以正常分割。加入,试试:

>>> re.split(r'[\s\,]+', 'a,b, c  d')
['a', 'b', 'c', 'd']

再加入;试试:

>>> re.split(r'[\s\,\;]+', 'a,b;; c  d')
['a', 'b', 'c', 'd']

分组

除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。比如:

^(\d{3})-(\d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:

>>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
>>> m
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group(0)
'010-12345'
>>> m.group(1)
'010'
>>> m.group(2)
'12345'
  • ^:匹配文本的开头位置,表示文本必须以指定的模式开始。例如,^abc可以匹配abc开头的文本,但不能匹配其他位置出现的abc。
  • $:匹配文本的结尾位置,表示文本必须以指定的模式结束。例如,abc$可以匹配以abc结尾的文本,但不能匹配其他位置出现的abc。
  • \b:匹配单词边界,表示文本中字母和非字母符号的交界处。例如,\babc\b可以匹配单独的abc单词,但不能匹配其他位置出现的abc。

如果正则表达式中定义了组,就可以在Match对象上用group()方法提取出子串来。

注意到group(0)永远是与整个正则表达式相匹配的字符串,group(1)group(2)……表示第1、2、……个子串。

贪婪匹配

最后需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0

>>> re.match(r'^(\d+)(0*)$', '102300').groups()
('102300', '')

由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。

必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配:

>>> re.match(r'^(\d+?)(0*)$', '102300').groups()
('1023', '00')

编译

当我们在Python中使用正则表达式时,re模块内部会干两件事情:

  1. 编译正则表达式,如果正则表达式的字符串本身不合法,会报错;
  2. 用编译后的正则表达式去匹配字符串。

如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配:

>>> import re
# 编译:
>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')
# 使用:
>>> re_telephone.match('010-12345').groups()
('010', '12345')
>>> re_telephone.match('010-8086').groups()
('010', '8086')

编译后生成Regular Expression对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串。