二、字符串和文本
1、使用多个界定符分割字符串
使用 re.split()
方法:
>>> line = 'asdf fjdk; afed, fjek,asdf, foo'
>>> import re
>>> re.split(r'[;,s]s*', line)
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
使用 re.split()
函数时候,需要特别注意的是正则表达式中是否包含一个括号捕获分组。 如果使用了捕获分组,那么被匹配的文本也将出现在结果列表中。比如,观察一下这段代码运行后的结果:
>>> fields = re.split(r'(;|,|s)s*', line)
>>> fields
['asdf', ' ', 'fjdk', ';', 'afed', ',', 'fjek', ',', 'asdf', ',', 'foo']
>>>
获取分割字符在某些情况下也是有用的。 比如,你可能想保留分割字符串,用来在后面重新构造一个新的输出字符串:
>>> values = fields[::2]
>>> delimiters = fields[1::2] + ['']
>>> values
['asdf', 'fjdk', 'afed', 'fjek', 'asdf', 'foo']
>>> delimiters
[' ', ';', ',', ',', ',', '']
>>> # Reform the line using the same delimiters
>>> ''.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']
>>>
参照:【https://www.runoob.com/python/python-reg-expressions.html】
2、字符串开头和结尾匹配
你需要通过指定的文本模式去检查字符串的开头或者结尾,比如文件名后缀,URL Scheme等等
any() 函数用于判断给定的可迭代参数 iterable 是否全部为 False,则返回 False,如果有一个为 True,则返回 True。
any(['a', 'b', 'c', 'd']) # 列表list,元素都不为空或0
True
检查字符串开头或结尾的一个简单方法是使用 str.startswith()
或者是 str.endswith()
方法。比如:
>>> filename = 'spam.txt'
>>> filename.endswith('.txt')
True
>>> filename.startswith('file:')
False
>>> url = 'http://www.python.org'
>>> url.startswith('http:')
True
>>>
如果你想检查多种匹配可能,只需要将所有的匹配项放入到一个元组中去, 然后传给 startswith()
或者 endswith()
方法:
>>> import os
>>> filenames = os.listdir('.')
>>> filenames
[ 'Makefile', 'foo.c', 'bar.py', 'spam.c', 'spam.h' ]
>>> [name for name in filenames if name.endswith(('.c', '.h')) ]
['foo.c', 'spam.c', 'spam.h'
>>> any(name.endswith('.py') for name in filenames)
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()
奇怪的是,这个方法中必须要输入一个元组作为参数。 如果你恰巧有一个 list
或者 set
类型的选择项, 要确保传递参数前先调用 tuple()
将其转换为元组类型。比如:
>>> choices = ['http:', 'ftp:']
>>> url = 'http://www.python.org'
>>> url.startswith(choices)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: startswith first arg must be str or a tuple of str, not list
>>> url.startswith(tuple(choices))
True
>>>
startswith()
和 endswith()
方法提供了一个非常方便的方式去做字符串开头和结尾的检查。 类似的操作也可以使用切片来实现,但是代码看起来没有那么优雅。比如:
>>> filename = 'spam.txt'
>>> filename[-4:] == '.txt'
True
>>> url = 'http://www.python.org'
>>> url[:5] == 'http:' or url[:6] == 'https:' or url[:4] == 'ftp:'
True
>>>
你可以能还想使用正则表达式去实现,比如:
>>> import re
>>> url = 'http://www.python.org'
>>> re.match('http:|https:|ftp:', url)
<_sre.SRE_Match object at 0x101253098>
>>>
这种方式也行得通,但是对于简单的匹配实在是有点小材大用了,本节中的方法更加简单并且运行会更快些。
最后提一下,当和其他操作比如普通数据聚合相结合的时候 startswith()
和 endswith()
方法是很不错的。 比如,下面这个语句检查某个文件夹中是否存在指定的文件类型:
if any(name.endswith(('.c', '.h')) for name in listdir(dirname)):
...
3、在shell统配符匹配字符串
使用 Unix Shell 中常用的通配符(比如 *.py
, Dat[0-9]*.csv
等)去匹配文本字符串
fnmatch
模块提供了两个函数—— fnmatch()
和 fnmatchcase()
,可以用来实现这样的匹配。用法如下:
>>> from fnmatch import fnmatch, fnmatchcase
>>> fnmatch('foo.txt', '*.txt')
True
>>> fnmatch('foo.txt', '?oo.txt')
True
>>> fnmatch('Dat45.csv', 'Dat[0-9]*')
True
>>> names = ['Dat1.csv', 'Dat2.csv', 'config.ini', 'foo.py']
>>> [name for name in names if fnmatch(name, 'Dat*.csv')]
['Dat1.csv', 'Dat2.csv']
>>>
fnmatch()
函数使用底层操作系统的大小写敏感规则(不同的系统是不一样的)来匹配模式。比如:
>>> # On OS X (Mac)
>>> fnmatch('foo.txt', '*.TXT')
False
>>> # On Windows
>>> fnmatch('foo.txt', '*.TXT')
True
>>>
如果你对这个区别很在意,可以使用 fnmatchcase()
来代替。它完全使用你的模式大小写匹配。比如:
>>> fnmatchcase('foo.txt', '*.TXT')
False
>>>
这两个函数通常会被忽略的一个特性是在处理非文件名的字符串时候它们也是很有用的。 比如,假设你有一个街道地址的列表数据:
addresses = [
'5412 N CLARK ST',
'1060 W ADDISON ST',
'1039 W GRANVILLE AVE',
'2122 N CLARK ST',
'4802 N BROADWAY',
]
你可以像这样写列表推导:
>>> from fnmatch import fnmatchcase
>>> [addr for addr in addresses if fnmatchcase(addr, '* ST')]
['5412 N CLARK ST', '1060 W ADDISON ST', '2122 N CLARK ST']
>>> [addr for addr in addresses if fnmatchcase(addr, '54[0-9][0-9] *CLARK*')]
['5412 N CLARK ST']
>>>
fnmatch()
函数匹配能力介于简单的字符串方法和强大的正则表达式之间。 如果在数据处理操作中只需要简单的通配符就能完成的时候,这通常是一个比较合理的方案。
4、字符串匹配和搜索
你想匹配或者搜索特定模式的文本
如果你想匹配的是字面字符串,那么你通常只需要调用基本字符串方法就行, 比如 str.find()
, str.endswith()
, str.startswith()
或者类似的方法:
>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> # Exact match
>>> text == 'yeah'
False
>>> # Match at start or end
>>> text.startswith('yeah')
True
>>> text.endswith('no')
False
>>> # Search for the location of the first occurrence
>>> text.find('no')
10
>>>
对于复杂的匹配需要使用正则表达式和 re
模块。 为了解释正则表达式的基本原理,假设你想匹配数字格式的日期字符串比如 11/27/2012
,你可以这样做:
>>> text1 = '11/27/2012'
>>> text2 = 'Nov 27, 2012'
>>>
>>> import re
>>> # Simple matching: d+ means match one or more digits
>>> if re.match(r'd+/d+/d+', text1):
... print('yes')
... else:
... print('no')
...
yes
>>> if re.match(r'd+/d+/d+', text2):
... print('yes')
... else:
... print('no')
...
no
>>>
如果你想使用同一个模式去做多次匹配,你应该先将模式字符串预编译为模式对象。比如:
>>> datepat = re.compile(r'd+/d+/d+')
>>> if datepat.match(text1):
... print('yes')
... else:
... print('no')
...
yes
>>> if datepat.match(text2):
... print('yes')
... else:
... print('no')
...
no
>>>
match()
总是从字符串开始去匹配,如果你想查找字符串任意部分的模式出现位置, 使用 findall()
方法去代替。比如:
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
['11/27/2012', '3/13/2013']
>>>
在定义正则式的时候,通常会利用括号去捕获分组。比如:
>>> datepat = re.compile(r'(d+)/(d+)/(d+)')
>>>
捕获分组可以使得后面的处理更加简单,因为可以分别将每个组的内容提取出来。比如:
>>> m = datepat.match('11/27/2012')
>>> m
<_sre.SRE_Match object at 0x1005d2750>
>>> # Extract the contents of each group
>>> m.group(0)
'11/27/2012'
>>> m.group(1)
'11'
>>> m.group(2)
'27'
>>> m.group(3)
'2012'
>>> m.groups()
('11', '27', '2012')
>>> month, day, year = m.groups()
>>>
>>> # Find all matches (notice splitting into tuples)
>>> text
'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> datepat.findall(text)
[('11', '27', '2012'), ('3', '13', '2013')]
>>> for month, day, year in datepat.findall(text):
... print('{}-{}-{}'.format(year, month, day))
...
2012-11-27
2013-3-13
>>>
findall()
方法会搜索文本并以列表形式返回所有的匹配。 如果你想以迭代方式返回匹配,可以使用 finditer()
方法来代替,比如:
>>> for m in datepat.finditer(text):
... print(m.groups())
...
('11', '27', '2012')
('3', '13', '2013')
>>>
5、字符串搜索和替换
字符串中搜索和匹配指定的文本模式
对于简单的字面模式,直接使用 str.replace()
方法即可,比如:
>>> text = 'yeah, but no, but yeah, but no, but yeah'
>>> text.replace('yeah', 'yep')
'yep, but no, but yep, but no, but yep'
>>>
对于复杂的模式,请使用 re
模块中的 sub()
函数。 为了说明这个,假设你想将形式为 11/27/2012
的日期字符串改成 2012-11-27
。示例如下:
>>> text = 'Today is 11/27/2012. PyCon starts 3/13/2013.'
>>> import re
>>> re.sub(r'(d+)/(d+)/(d+)', r'3-1-2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'
sub()
函数中的第一个参数是被匹配的模式,第二个参数是替换模式。反斜杠数字比如 3
指向前面模式的捕获组号。
如果你打算用相同的模式做多次替换,考虑先编译它来提升性能。比如:
>>> import re
>>> datepat = re.compile(r'(d+)/(d+)/(d+)')
>>> datepat.sub(r'3-1-2', text)
'Today is 2012-11-27. PyCon starts 2013-3-13.'
>>>
对于更加复杂的替换,可以传递一个替换回调函数来代替,比如:
>>> from calendar import month_abbr
>>> def change_date(m):
... mon_name = month_abbr[int(m.group(1))]
... return '{} {} {}'.format(m.group(2), mon_name, m.group(3))
...
>>> datepat.sub(change_date, text)
'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
>>>
6、字符串忽略大小写的搜索替换
忽略大小写的方式搜索与替换文本字符串
为了在文本操作时忽略大小写,你需要在使用 re
模块的时候给这些操作提供 re.IGNORECASE
标志参数。比如:
>>> text = 'UPPER PYTHON, lower python, Mixed Python'
>>> re.findall('python', text, flags=re.IGNORECASE)
['PYTHON', 'python', 'Python']
>>> re.sub('python', 'snake', text, flags=re.IGNORECASE)
'UPPER snake, lower snake, Mixed snake'
>>>
最后的那个例子揭示了一个小缺陷,替换字符串并不会自动跟被匹配字符串的大小写保持一致。 为了修复这个,你可能需要一个辅助函数,就像下面的这样:
def matchcase(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
下面是使用上述函数的方法:
>>> re.sub('python', matchcase('snake'), text, flags=re.IGNORECASE)
'UPPER SNAKE, lower snake, Mixed Snake'
>>>
import re
text = 'UPPER PYTHON, lower python, Mixed Python'
def matchcase(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
print(re.sub('python', matchcase('snake'), text, flags=re.IGNORECASE))
matchcase('snake')
返回了一个回调函数(参数必须是 match
对象),sub()` 函数除了接受替换字符串外,还能接受一个回调函数。
7、最短匹配模式
正则表达式匹配某个文本模式,但是它找到的是模式的最长可能匹配。 而你想修改它变成查找最短的可能匹配。
这个问题一般出现在需要匹配一对分隔符之间的文本的时候(比如引号包含的字符串)。 为了说明清楚,考虑如下的例子:
>>> str_pat = re.compile(r'"(.*)"')
>>> text1 = 'Computer says "no."'
>>> str_pat.findall(text1)
['no.']
>>> text2 = 'Computer says "no." Phone says "yes."'
>>> str_pat.findall(text2)
['no." Phone says "yes.']
>>>
在这个例子中,模式 r'"(.*)"'
的意图是匹配被双引号包含的文本。 但是在正则表达式中*操作符是贪婪的,因此匹配操作会查找最长的可能匹配。 于是在第二个例子中搜索 text2
的时候返回结果并不是我们想要的。
为了修正这个问题,可以在模式中的*操作符后面加上?修饰符,就像这样:
>>> str_pat = re.compile(r'"(.*?)"')
>>> str_pat.findall(text2)
['no.', 'yes.']
>>>
这样就使得匹配变成非贪婪模式,从而得到最短的匹配,也就是我们想要的结果。
8、多行匹配模式
使用正则表达式去匹配一大块的文本,而你需要跨越多行去匹配。
这个问题很典型的出现在当你用点(.)去匹配任意字符的时候,忘记了点(.)不能匹配换行符的事实。 比如,假设你想试着去匹配C语言分割的注释:
>>> comment = re.compile(r'/*(.*?)*/')
>>> text1 = '/* this is a comment */'
>>> text2 = '''/* this is a
... multiline comment */
... '''
>>>
>>> comment.findall(text1)
[' this is a comment ']
>>> comment.findall(text2)
[]
>>>
为了修正这个问题,你可以修改模式字符串,增加对换行的支持。比如:
>>> comment = re.compile(r'/*((?:.|
)*?)*/')
>>> comment.findall(text2)
[' this is a
multiline comment ']
>>>
在这个模式中, (?:.|
)
指定了一个非捕获组 (也就是它定义了一个仅仅用来做匹配,而不能通过单独捕获或者编号的组)。
re.compile()
函数接受一个标志参数叫 re.DOTALL
,在这里非常有用。 它可以让正则表达式中的点(.)匹配包括换行符在内的任意字符。比如:
>>> comment = re.compile(r'/*(.*?)*/', re.DOTALL)
>>> comment.findall(text2)
[' this is a
multiline comment ']
9、将Unicode文本标准化
import unicodedata
s1 = 'Spicy Jalapeu00f1o'
s2 = 'Spicy Jalapenu0303o'
print(unicodedata.normalize("NFD",s2))
t1 = unicodedata.normalize('NFD',s1)
print("".join(c for c in t1 if not unicodedata.combining(c)))
这里的文本”Spicy Jalapeño”使用了两种形式来表示。 第一种使用整体字符”ñ”(U+00F1),第二种使用拉丁字母”n”后面跟一个”~”的组合字符(U+0303)。
标准化对于任何需要以一致的方式处理Unicode文本的程序都是非常重要的。 当处理来自用户输入的字符串而你很难去控制编码的时候尤其如此。
在清理和过滤文本的时候字符的标准化也是很重要的。 比如,假设你想清除掉一些文本上面的变音符的时候(可能是为了搜索和匹配):
>>> t1 = unicodedata.normalize('NFD', s1)
>>> ''.join(c for c in t1 if not unicodedata.combining(c))
'Spicy Jalapeno'
>>>
最后一个例子展示了 unicodedata
模块的另一个重要方面,也就是测试字符类的工具函数。combining()
函数可以测试一个字符是否为和音字符。 在这个模块中还有其他函数用于查找字符类别,测试是否为数字字符等等。
10、正则式中使用Unicode
默认情况下 re
模块已经对一些Unicode字符类有了基本的支持。 比如, \d
已经匹配任意的unicode数字字符了:
>>> import re
>>> num = re.compile('d+')
>>> # ASCII digits
>>> num.match('123')
<_sre.SRE_Match object at 0x1007d9ed0>
>>> # Arabic digits
>>> num.match('u0661u0662u0663')
<_sre.SRE_Match object at 0x101234030>
>>>
如果你想在模式中包含指定的Unicode字符,你可以使用Unicode字符对应的转义序列(比如 uFFF
或者 UFFFFFFF
)。 比如,下面是一个匹配几个不同阿拉伯编码页面中所有字符的正则表达式:
>>> arabic = re.compile('[u0600-u06ffu0750-u077fu08a0-u08ff]+')
>>>
当执行匹配和搜索操作的时候,最好是先标准化并且清理所有文本为标准化格式(参考2.9小节)。 但是同样也应该注意一些特殊情况,比如在忽略大小写匹配和大小写转换时的行为。
>>> pat = re.compile('strau00dfe', re.IGNORECASE)
>>> s = 'straße'
>>> pat.match(s) # Matches
<_sre.SRE_Match object at 0x10069d370>
>>> pat.match(s.upper()) # Doesn't match
>>> s.upper() # Case folds
'STRASSE'
>>>
11、删除字符串中不需要的字符
strip()
方法能用于删除开始或结尾的字符。 lstrip()
和 rstrip()
分别从左和从右执行删除操作。 默认情况下,这些方法会去除空白字符,但是你也可以指定其他字符。
>>> # Whitespace stripping
>>> s = ' hello world
'
>>> s.strip()
'hello world'
>>> s.lstrip()
'hello world
'
>>> s.rstrip()
' hello world'
>>>
>>> # Character stripping
>>> t = '-----hello====='
>>> t.lstrip('-')
'hello====='
>>> t.strip('-=')
'hello'
>>>
这些 strip()
方法在读取和清理数据以备后续处理的时候是经常会被用到的。 比如,你可以用它们来去掉空格,引号和完成其他任务。
但是需要注意的是去除操作不会对字符串的中间的文本产生任何影响。比如:
>>> s = ' hello world
'
>>> s = s.strip()
>>> s
'hello world'
>>>
如果你想处理中间的空格,那么你需要求助其他技术。比如使用 replace()
方法或者是用正则表达式替换。示例如下:
s = ' hello world
'
import re
print(re.sub("s+","",s))
通常情况下你想将字符串 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()
方法
12、审查清理文本字符串
文本清理问题会涉及到包括文本解析与数据处理等一系列问题。 在非常简单的情形下,你可能会选择使用字符串函数(比如 str.upper()
和 str.lower()
)将文本转为标准格式。 使用 str.replace()
或者 re.sub()
的简单替换操作能删除或者改变指定的字符序列。
可能想消除整个区间上的字符或者去除变音符。 为了这样做,你可以使用经常会被忽视的 str.translate()
方法。
s = 'pýtĥöñfis awesome
第一步是清理空白字符。为了这样做,先创建一个小的转换表格然后使用 translate()
方法
s = 'pýtĥöñfis awesome
'
remp = {
ord(" "):"",
ord("f"):"",
ord("
"):None,
}
a = s.translate(remp)
print(a)
空白字符
和 f
已经被重新映射到一个空格。回车字符r直接被删除。
我们删除所有的和音符:
import unicodedata
import sys
print(sys.maxunicode)
cmb = dict.fromkeys(c for c in range(sys.maxunicode) if unicodedata.combining(chr(c)))
print(cmb)
s = 'pýtĥöñfis awesome
'
b = unicodedata.normalize("NFD",s)
# print(b)
print(b.translate(cmb))
上面例子中,通过使用 dict.fromkeys()
方法构造一个字典,每个Unicode和音符作为键,对应的值全部为 None
。
然后使用 unicodedata.normalize()
将原始输入标准化为分解形式字符。 然后再调用 translate
函数删除所有重音符。 同样的技术也可以被用来删除其他类型的字符(比如控制字符等)。
这里构造一个将所有Unicode数字字符映射到对应的ASCII字符上的表格
digitmap = {
c:ord("0") + unicodedata.digit(chr(c))
for c in range(sys.maxunicode)
if unicodedata.category(chr(c)) == "Nd"
}
print(len(digitmap))
x = 'u0661u0662u0663'
print(x.translate(digitmap))
另一种清理文本的技术涉及到I/O解码与编码函数。这里的思路是先对文本做一些初步的清理, 然后再结合 encode()
或者 decode()
操作来清除或修改它。比如:
>>> a
'pýtĥöñ is awesome
'
>>> b = unicodedata.normalize('NFD', a)
>>> b.encode('ascii', 'ignore').decode('ascii')
'python is awesome
'
>>>
这里的标准化操作将原来的文本分解为单独的和音符。接下来的ASCII编码/解码只是简单的一下子丢弃掉那些字符。 当然,这种方法仅仅只在最后的目标就是获取到文本对应ACSII表示的时候生效。
文本字符清理一个最主要的问题应该是运行的性能。一般来讲,代码越简单运行越快。 对于简单的替换操作, str.replace()
方法通常是最快的,甚至在你需要多次调用的时候。 比如,为了清理空白字符,你可以这样做:
def clean_spaces(s):
s = s.replace('
', '')
s = s.replace(' ', ' ')
s = s.replace('f', ' ')
return s
如果你去测试的话,你就会发现这种方式会比使用 translate()
或者正则表达式要快很多。
另一方面,如果你需要执行任何复杂字符对字符的重新映射或者删除操作的话, tanslate()
方法会非常的快。
从大的方面来讲,对于你的应用程序来说性能是你不得不去自己研究的东西。 不幸的是,我们不可能给你建议一个特定的技术,使它能够适应所有的情况。 因此实际情况中需要你自己去尝试不同的方法并评估它。
13、字符串对齐
对于基本的字符串对齐操作,可以使用字符串的 ljust()
, rjust()
和 center()
方法
text = "Hello word"
print(len(text))
print(len(text.ljust(10)))
print(text.rjust(20))
print(text.center(20))
填充:
text = "Helloword"
print(len(text))
print(text.ljust(10,"+"))
print(text.rjust(20,"*"))
print(text.center(20,"*"))
"""
9
Helloword+
***********Helloword
*****Helloword******
"""
函数 format() 同样可以用来很容易的对齐字符串。 你要做的就是使用 <,> 或者 ^ 字符后面紧跟一个指定的宽度。比如:
>>> format(text, '>20')
' Hello World'
>>> format(text, '<20')
'Hello World '
>>> format(text, '^20')
' Hello World '
>>>
如果你想指定一个非空格的填充字符,将它写到对齐字符的前面即可:
>>> format(text, '=>20s')
'=========Hello World'
>>> format(text, '*^20s')
'****Hello World*****'
>>>
当格式化多个值的时候,这些格式代码也可以被用在 format() 方法中。比如:
>>> '{:>10s} {:>10s}'.format('Hello', 'World')
' Hello World'
>>>
format() 函数的一个好处是它不仅适用于字符串。它可以用来格式化任何值,使得它非常的通用。 比如,你可以用它来格式化数字:
>>> x = 1.2345
>>> format(x, '>10')
' 1.2345'
>>> format(x, '^10.2f')
' 1.23 '
>>>
text = "Helloword"
print("%-20s"%text) #左对齐
print("%20s"%text) #右对齐
"""
Helloword
Helloword
"""
在新版本代码中,你应该优先选择 format()
函数或者方法。 format()
要比 %
操作符的功能更为强大。 并且 format()
也比使用 ljust()
, rjust()
或 center()
方法更通用, 因为它可以用来格式化任意对象,而不仅仅是字符串。
14、合并拼接字符串
#简单的采用此方式
a = "hello" "wrod"
print(a)
#补充:
+
join
当我们使用加号(+)操作符去连接大量的字符串的时候是非常低效率的, 因为加号连接会引起内存复制以及垃圾回收操作。 特别的,你永远都不应像下面这样写字符串连接代码:
s = ''
for p in parts:
s += p
这种写法会比使用 join()
方法运行的要慢一些,因为每一次执行+=操作的时候会创建一个新的字符串对象。 你最好是先收集所有的字符串片段然后再将它们连接起来。
data = ["alex","wusir","hyc","66"]
print(",".join(str(datum)for datum in data))
# Version 1 (string concatenation)
f.write(chunk1 + chunk2)
# Version 2 (separate I/O operations)
f.write(chunk1)
f.write(chunk2)
"""
如果两个字符串很小,那么第一个版本性能会更好些,因为I/O系统调用天生就慢。 另外一方面,如果两个字符串很大,那么第二个版本可能会更加高效, 因为它避免了创建一个很大的临时结果并且要复制大量的内存块数据
"""
准备编写构建大量小字符串的输出代码, 你最好考虑下使用生成器函数,利用yield语句产生输出片段
def sample():
yield 'Is'
yield 'Chicago'
yield 'Not'
yield 'Chicago?'
text = "".join(sample())
print(text)
IsChicagoNotChicago?
#文件操作优化:
def sample():
yield 'Is'
yield 'Chicago'
yield 'Not'
yield 'Chicago?'
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)
#这里的关键点在于原始的生成器函数并不需要知道使用细节,它只负责生成字符串片段就行了。
14、指定列宽格式化字符串
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."
import textwrap
print(textwrap.fill(s)) #默认70个字符
print(textwrap.wrap(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.
"""
"""
['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."]
"""
textwrap 链接:https://docs.python.org/3.6/library/textwrap.html#textwrap.indent
生成器
深入理解Yield from
yield from
是在Python3.3才出现的语法。所以这个特性在Python2中是没有的。
yield from
后面需要加的是可迭代对象,它可以是普通的可迭代对象,也可以是迭代器,甚至是生成器。
简单应用:拼接可迭代对象
我们可以用一个使用yield
和一个使用yield from
的例子来对比看下。
使用yield
# 字符串astr='ABC'# 列表alist=[1,2,3]# 字典adict={"name":"wangbm","age":18}# 生成器agen=(i for i in range(4,8))def gen(*args, **kw): for item in args: for i in item: yield inew_list=gen(astr, alist, adict, agen)print(list(new_list))# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]
使用yield from
# 字符串astr='ABC'# 列表alist=[1,2,3]# 字典adict={"name":"wangbm","age":18}# 生成器agen=(i for i in range(4,8))def gen(*args, **kw): for item in args: yield from itemnew_list=gen(astr, alist, adict, agen)print(list(new_list))# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]
由上面两种方式对比,可以看出,yield from后面加上可迭代对象,他可以把可迭代对象里的每个元素一个一个的yield出来,对比yield来说代码更加简洁,结构更加清晰。
复杂应用:生成器的嵌套
如果你认为只是 yield from
仅仅只有上述的功能的话,那你就太小瞧了它,它的更强大的功能还在后面。
当 yield from
后面加上一个生成器后,就实现了生成的嵌套。
当然实现生成器的嵌套,并不是一定必须要使用yield from
,而是使用yield from
可以让我们避免让我们自己处理各种料想不到的异常,而让我们专注于业务代码的实现。
如果自己用yield
去实现,那只会加大代码的编写难度,降低开发效率,降低代码的可读性。既然Python已经想得这么周到,我们当然要好好利用起来。
讲解它之前,首先要知道这个几个概念
1、
调用方
:调用委派生成器的客户端(调用方)代码
2、委托生成器
:包含yield from表达式的生成器函数
3、子生成器
:yield from后面加的生成器函数
你可能不知道他们都是什么意思,没关系,来看下这个例子。
这个例子,是实现实时计算平均值的。
比如,第一次传入10,那返回平均数自然是10.
第二次传入20,那返回平均数是(10+20)/2=15
第三次传入30,那返回平均数(10+20+30)/3=20
# 子生成器def average_gen(): total = 0 count = 0 average = 0 while True: new_num = yield average count += 1 total += new_num average = total/count# 委托生成器def proxy_gen(): while True: yield from average_gen()# 调用方def main(): calc_average = proxy_gen() next(calc_average) # 预激下生成器 print(calc_average.send(10)) # 打印:10.0 print(calc_average.send(20)) # 打印:15.0 print(calc_average.send(30)) # 打印:20.0if __name__ == '__main__': main()
认真阅读以上代码,你应该很容易能理解,调用方、委托生成器、子生成器之间的关系。我就不多说了
委托生成器的作用是:在调用方与子生成器之间建立一个双向通道
。
所谓的双向通道是什么意思呢?
调用方可以通过send()
直接发送消息给子生成器,而子生成器yield的值,也是直接返回给调用方。
你可能会经常看到有些代码,还可以在yield from
前面看到可以赋值。这是什么用法?
你可能会以为,子生成器yield回来的值,被委托生成器给拦截了。你可以亲自写个demo运行试验一下,并不是你想的那样。
因为我们之前说了,委托生成器,只起一个桥梁作用,它建立的是一个双向通道
,它并没有权利也没有办法,对子生成器yield回来的内容做拦截。
为了解释这个用法,我还是用上述的例子,并对其进行了一些改造。添加了一些注释,希望你能看得明白。
按照惯例,我们还是举个例子。
# 子生成器def average_gen(): total = 0 count = 0 average = 0 while True: new_num = yield average if new_num is None: break count += 1 total += new_num average = total/count # 每一次return,都意味着当前协程结束。 return total,count,average# 委托生成器def proxy_gen(): while True: # 只有子生成器要结束(return)了,yield from左边的变量才会被赋值,后面的代码才会执行。 total, count, average = yield from average_gen() print("计算完毕!!
总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))# 调用方def main(): calc_average = proxy_gen() next(calc_average) # 预激协程 print(calc_average.send(10)) # 打印:10.0 print(calc_average.send(20)) # 打印:15.0 print(calc_average.send(30)) # 打印:20.0 calc_average.send(None) # 结束协程 # 如果此处再调用calc_average.send(10),由于上一协程已经结束,将重开一协程if __name__ == '__main__': main()
运行后,输出
10.015.020.0计算完毕!!总共传入 3 个数值, 总和:60,平均数:20.0
为什么要使用yield from#
学到这里,我相信你肯定要问,既然委托生成器,起到的只是一个双向通道的作用,我还需要委托生成器做什么?我调用方直接调用子生成器不就好啦?
高能预警~~~
下面我们来一起探讨一下,到底yield from 有什么过人之处,让我们非要用它不可。
因为它可以帮我们处理异常#
如果我们去掉委托生成器,而直接调用子生成器。那我们就需要把代码改成像下面这样,我们需要自己捕获异常并处理。而不像使yield from
那样省心。
# 子生成器# 子生成器
def average_gen():
total = 0
count = 0
average = 0
while True:
new_num = yield average
if new_num is None:
break
count += 1
total += new_num
average = total/count
return total,count,average
# 调用方
def main():
calc_average = average_gen()
next(calc_average)
# 预激协程
print(calc_average.send(10)) # 打印:10.0
print(calc_average.send(20)) # 打印:15.0
print(calc_average.send(30)) # 打印:20.0
# ----------------注意-----------------
try:
calc_average.send(None)
except StopIteration as e:
total, count, average = e.value
print("计算完毕!!
总共传入 {} 个数值, 总和:{},平均数:{}".format(count, total, average))
# ----------------注意-----------------
if __name__ == '__main__': main()
此时的你,可能会说,不就一个StopIteration
的异常吗?自己捕获也没什么大不了的。
你要是知道yield from
在背后为我们默默无闻地做了哪些事,你就不会这样说了。
具体yield from
为我们做了哪些事,可以参考如下这段代码。
#一些说明
"""_i:子生成器,同时也是一个迭代器
_y:子生成器生产的值
_r:yield from 表达式最终的值
_s:调用方通过send()发送的值
_e:异常对象"""
_i = iter(EXPR)
try:
_y = next(_i)
except StopIteration as _e:
_r = _e.value
else:
while 1:
try:
_s = yield _y
except GeneratorExit as _e:
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
raise _e
except BaseException as _e:
_x = sys.exc_info()
try:
_m = _i.throw
except AttributeError:
raise _e
else:
try:
_y = _m(*_x)
except StopIteration as _e:
_r = _e.value
break
else:
try:
if _s is None:
_y = next(_i)
else:
_y = _i.send(_s)
except StopIteration as _e:
_r = _e.value
break
RESULT = _r
以上的代码,稍微有点复杂,有兴趣的同学可以结合以下说明去研究看看。
- 迭代器(即可指子生成器)产生的值直接返还给调用者
- 任何使用send()方法发给委派生产器(即外部生产器)的值被直接传递给迭代器。如果send值是None,则调用迭代器next()方法;如果不为None,则调用迭代器的send()方法。如果对迭代器的调用产生StopIteration异常,委派生产器恢复继续执行yield from后面的语句;若迭代器产生其他任何异常,则都传递给委派生产器。
- 子生成器可能只是一个迭代器,并不是一个作为协程的生成器,所以它不支持.throw()和.close()方法,即可能会产生AttributeError 异常。
- 除了GeneratorExit 异常外的其他抛给委派生产器的异常,将会被传递到迭代器的throw()方法。如果迭代器throw()调用产生了StopIteration异常,委派生产器恢复并继续执行,其他异常则传递给委派生产器。
- 如果GeneratorExit异常被抛给委派生产器,或者委派生产器的close()方法被调用,如果迭代器有close()的话也将被调用。如果close()调用产生异常,异常将传递给委派生产器。否则,委派生产器将抛出GeneratorExit 异常。
- 当迭代器结束并抛出异常时,yield from表达式的值是其StopIteration 异常中的第一个参数。
- 一个生成器中的return expr语句将会从生成器退出并抛出 StopIteration(expr)异常。
只要知道,yield from
帮我们做了很多的异常处理,而且全面,而这些如果我们要自己去实现的话,一个是编写代码难度增加,写出来的代码可读性极差,这些我们就不说了,最主要的是很可能有遗漏,只要哪个异常没考虑到,都有可能导致程序崩溃什么的。