Python 是一门很是容易上手的语言,经过查阅资料和教程,也许一夜就能写出一个简单的爬虫。但 Python 也是一门很难精通的语言,由于简洁的语法背后隐藏了许多黑科技。本文主要针对的读者是:python
固然, 用一篇文章来说完某个语言是不可能的事情,我但愿读完本文的读者能够:swift
若是以上介绍符合你对本身的定位,在开始阅读前,还须要明确几点:数组
请不要在学习 Python2 仍是 Python3 之间犹豫了,除非你很明确本身只接触 Python2,不然就从 Python3 学起,新版本的语言老是意味着进步的生产力(Swift 和 Xcode 除外)。Python 2 和 3 之间语法不兼容,但这并不影响熟悉 Python3 的开发者迅速写出 Python 2 的代码,反之亦然。因此与其在反复纠结中浪费时间,不如马上行动起来。数据结构
推荐使用 CodeRunner 来运行本文中的 demo,它比文本编辑器功能更强大,好比支持自动补全和断点调试,又比 PyCharm 轻量得多。app
若是要对数组中的全部内容作一些修改,能够用 for 循环或者 map 函数:编辑器
array = [1, 2, 3, 4, 5, 6] small = [] for n in array: if n < 4: small.append(n * 2) print(small) # [2, 4, 6]
比较地道的 Python 写法是使用列表推导:ide
array = [1, 2, 3, 4, 5, 6] small = [n * 2 for n in array if n < 4]
for in
能够写两次,相似于嵌套的 for 循环,会获得一个笛卡尔积:函数
signs = ['+', '-'] numbers = [1, 2] ascii = ['{sign}{number}'.format(sign=sign, number=number) for sign in signs for number in numbers] # 获得:['+1', '+2', '-1', '-2']
元组能够简单的理解为不可变的数组,也就是没有 append
、del
等方法,一旦建立,就没法新增或删除元素,元素自身的值也不能改变,但元素内部的属性是否可变并不受元组的影响,这一点符合其余语言中的常识。工具
t = (1, []) t[0] = 3 # 抛出错误 TypeError: 'tuple' object does not support item assignment t[1].append(2) # 正常运行,如今的 t 是 (1, [2])
除了不可变性之外,有时候元组也会被当作不具名的数据结构,这时候元素的位置就再也不是无关紧要的了: coordinate = (33.9425, -118.408056) # coordinate 的第一个位置用来表示精度,第二个位置表示维度 在解析元组数据时,能够一一对应的写上变量名: t = (1, 2) a, b = t # a = 1, b = 2 有时候变量名比较长, 但我只关心其中某一个,能够这样写: t = (1, 2) a, _ = t # a = 1 若是元组中元素特别多,即便挨个写下划线也比较累,能够用 * 来批量解包: t = (1, 2, 3, 4, 5) first, *middle, last = t # first = 1 # middle = [2, 3, 4] # last = 5 固然,若是元素数量较多,含义较复杂,我仍是建议使用具名元组: import collections People = collections.namedtuple('People', ['name', 'age']) p = People('bestswifter', '22') p.name # 22 具名元组更像是一个不能定义方法的简化版的类,能提供友好的数据展现。 元组的一个小技巧是能够避免用临时变量来交换两个数的值: a = 1 b = 2 a, b = b, a # a = 2, b = 1
切片的基本格式是 array[start:end:step]
,表示对 array 在 start 到 end 以前以 step 为间隔取切片。注意这里的区间是 [start, end),也就是左闭右开。好比:性能
s = 'hello' s[0:5:2] # 表示取 s 的第 0、2、4 个字符,结果是 'hlo'
再举几个例子
s[0:5] # 不写 step 默认就是 1,所以获得 'hello' s[1:] # 不写 end 默认到结尾,所以仍是获得 'ello' s[n:] # 获取 s 的最后 len(s) - n 个元素 s[:2] # 不写 start 默认从 0 开始,所以获得 'he' s[:n] # 获取 s 的前 n 个元素 s[:-1] # 负数表示倒过来数,所以这会刨除最后一个字符,获得 'hell' s[-2:] # 同上,表示获取最后两个字符,获得 'lo' s[::-1] # 获取字符串的倒序排列,至关于 reverse 函数
step 和它前面的冒号要么同时写,要么同时不写,但 start 和 end 之间的冒号不能省,不然就不是切片而是获取元素了。再次强调 array[start:end]
表示的区间是 [a, b),也许你会以为这很难记,但一样的,这会得出如下美妙的公式:
array[:n] + array[n:] = array (0 <= n <= len(array))
用代码来表示就是:
s = 'hello' s[:2] + s[2:] == s # True,由于 s[:2] 是 'he',s[2:] 是 'llo'
切片不只能够用来获取数组的一部分值,修改切片也能够直接修改数组的对应部分,好比:
a = [1, 2, 3, 4, 5, 6] a[1:3] = [22, 33, 44] # a = [1, 22, 33, 44, 4, 5, 6]
并无人规定切片的新值必须和原来的长度一致:
a = [1, 2, 3, 4, 5, 6] a[1:3] = [3] # a = [1, 3, 4, 5, 6] a[1:4] = [] # a = [1, 6],至关于删除了中间的三个数字
但切片的新值必须也是可迭代的对象,好比这样写是不合法的:
a = [1, 2, 3, 4, 5, 6] a[1:3] = 3 # TypeError: can only assign an iterable
1.1.4 循环与遍历
通常来讲,在 Python 中咱们不会写出 for (int i = 0; i < len(array); ++i)
这种风格的代码,而是使用 for in
这种语法:
for i in [1, 2, 3]: print(i)
虽然你们都知道 for in
语法,但它的某些灵活用法或许就不是那么众所周知了。
有时候,咱们会在 if 语句中对某个变量的值作屡次判断,只要知足一个条件便可: name = 'bs' if name == 'hello' or name == 'hi' or name == 'bs' or name == 'admin': print('Valid') 这种状况推荐用 in 来代替: name = 'bs' if name in ('hello', 'hi', 'bs', 'admin'): print('Valid') 有时候,若是咱们想要把某件事重复固定的次数,用 for in 会显得有些啰嗦,这时候能够借助 range 类型: for i in range(5): print('Hi') # 打印五次 'Hi' range 的语法和切片相似,好比咱们须要访问数组全部奇数下标的元素,能够这么写: a = [1, 2, 3, 4, 5] for i in range(0, len(a), 2): print(a[i]) 在这种写法中,咱们不只能得到元素,还能知道元素的下标,这与使用 enumerate(iterable [, start ]) 函数相似: a = [1, 2, 3, 4, 5] for i, n in enumerate(a): print(i, n)
也许你已经注意到了,数组和字符串都支持切片,并且语法高度统一。这在某些强类型语言(好比我常常接触的 Objective-C 和 Java)中是不可能的,事实上,Python 可以支持这样统一的语法,并不是巧合,而是由于全部用中括号进行下标访问的操做,其实都是调用这个类的 __getitem__
方法。
好比咱们彻底可让本身的类也支持经过下标访问:
class Book: def __init__(self): self.chapters = [1, 2, 3] def __getitem__(self, n): return self.chapters[n] b = Book() print(b[1]) # 结果是 2
须要注意的是,这段代码几乎不会出问题(除非数组越界),这是由于咱们直接把下标传到了内部的 self.chapters
数组上。但若是要本身处理下标,须要牢记它不必定是数字,也能够是切片,所以更完整的逻辑应该是:
def __getitem__(self, n): if isinstance(n, int): # n是索引 # 处理索引 if isinstance(n, slice): # n是切片 # 经过 n.start,n.stop 和 n.step 来处理切片
与静态语言不一样的是,任何实现了 __getitem__
都支持经过下标访问,而不用声明为实现了某个协议,这种特性也被称为 “鸭子类型”。鸭子类型并不要求某个类 是什么,仅仅要求这个类 能作什么。
顺便说一句,实现了 __getitem__
方法的类都是可迭代的,好比:
b = Book() for c in b: print(c)
后续的章节还会介绍更多 Python 中的魔术方法,这种方法的名称先后都有两个下划线,若是读做 “下划线-下划线-getitem” 会比较拗口,所以能够读做 “dunder-getitem” 或者 “双下-getitem”,相似的,我想每一个人都能猜到 __setitem__
的做用和用法。
最简单的建立一个字典的方式就是直接写字面量: {'a': 61, 'b': 62, 'c': 63, 'd': 64, 'e': 65} 字典字面量由大括号包住(注意区别于数组的中括号),键值对之间由逗号分割,每一个键值对内部用冒号分割键和值。 若是数组的每一个元素都是二元的元组,这个数组能够直接转成字典: dict([('a', 61), ('b', 62), ('c', 63), ('d', 64), ('e', 65)]) 就像数组能够推导同样,字典也能够推导: a = [('a', 61), ('b', 62), ('c', 63), ('d', 64), ('e', 65)] d = {letter: number for letter, number in a} # 这里用到了元组拆包 只要记得外面仍是大括号就好了。 两个独立的数组能够被压缩成一个字典: numbers = [61, 62, 63, 64, 65] letters = ['a', 'b', 'c', 'd', 'e'] dict(zip(letters, numbers)) 正如 zip 的意思所表示的,超出长处的那部分数组会被抛弃。 1.2.2 查询字典 最简单方法是直接写键名,但若是键名不存在会抛出 KeyError: d = {'a': 61} d['a'] # 值是 61 d['b'] # KeyError: 'b' 能够用 if key in dict 的判断来检查键是否存在,甚至能够先 try 再 catch KeyError,但更加优雅简洁一些的写法是用
get(k, default) 方法来提供默认值: d = {'a': 61} d.get('a', 62) # 获得 61 d.get('b', 62) # 获得 62 不过有时候,咱们可能不只仅要读出默认属性,更但愿能把这个默认属性能写入到字典中,好比: d = {} # 咱们想对字典中某个 Value 作操做,若是 Key 不存在,就先写入一个空值 if 'list' not in d: d['list'] = [] d['list'].append(1) 这种状况下,seddefault(key, default) 函数或许更合适: d.setdefault('key', []).append(1) 这个函数虽然名为 set,但做用实际上是查找,仅仅在查找不到时才会把默认值写入字典。
1.2.3 遍历字典
直接遍历字典其实是遍历了字典的键,所以也能够经过键获取值: d = {'a': 61, 'b': 62, 'c': 63, 'd': 64, 'e': 65} for i in d: print(i, d[i]) #b 62 #a 61 #e 65 #d 64 #c 63 咱们也能够用字典的 keys() 或者 values() 方法显式的获取键和值。字典还有一个 items() 方法,它返回一个数组,
每一个元素都是由键和值组成的二元元组: d = {'a': 61, 'b': 62, 'c': 63, 'd': 64, 'e': 65} for (k, v) in d.items(): print(k, v) #e 65 #d 64 #a 61 #c 63 #b 62
可见 items()
方法和字典的构造方法互为逆操做,由于这个公式老是成立的:
dict(d.items()) == d
在 1.1.4 节中介绍过,经过下标访问最终都会由 __getitem__
这个魔术方法处理,所以字典的 d[key]
这种写法也不例外, 若是键不存在,则会走到 __missing__
方法,再给一次挽救的机会。好比咱们能够实现一个字典, 自动忽略键的大小写:
class MyDict(dict): def __missing__(self, key): if key.islower(): raise KeyError(key) else: return self[key.lower()] d = MyDict({'a': 61}) d['A'] # 返回 61 'A' in d # False
这个字典比较简陋,好比 key 可能不是字符串,不过我没有处理太多状况,由于它主要是用来演示 __missing__
的用法,若是想要最后一行的 in
语法正确工做,须要重写 __contains__
这个魔术方法,过程相似,就不赘述了。
虽然经过自定义的函数也能实现类似的效果,不过这个自定义字典对用户更加透明,若是不在文档中说明,调用方很难察觉到字典的内部逻辑被修改了。 Python 有不少强大的功能,能够具有这种内部进行修改,可是对外保持透明的能力。这多是咱们第一次体会到,后续还会不断的经历。
集合更像是不会有重复元素的数组,但它的本质是以元素的哈希值做为 Key,从而实现去重的逻辑。所以,集合也能够推导,不过得用字典的语法: a = [1,2,3,4,5,4,3,2,1] d = {i for i in a if i < 5} # d = {1, 2, 3, 4},注意这里的大括号 回忆一下,二进制逻辑运算一共有三个运算符,按位或 |,按位与 & 和异或 ^,这三个运算符也能够用在集合之间,并且含义变化不大。好比: a = {1, 2, 3} b = {3, 4, 5} c = a | b # c = {1, 2, 3, 4, 5} 这里的 | 运算表示交集,也就是 c 中的任意元素,要么在 a,要么在 b 集合中。相似的,按位与 & 运算求的就是交集: a = {1, 2, 3} b = {3, 4, 5} c = a & b # c = {3} 而异或则表示那些只在 a 不在 b 或者只在 b 不在 a 的元素。或者换个说法,表示那些在集合 a 和 b 中出现了且仅出现了一次的元素: a = {1, 2, 3} b = {3, 4, 5} c = a ^ b # c = {1, 2, 4, 5} 还有一个差集运算 -,表示在集合 a 中但不在集合 b 中的元素: a = {1, 2, 3} b = {3, 4, 5} c = a - b # c = {1, 2}
回忆一下韦恩图,就会获得如下公式(虽然并无什么卵用):
A | B = (A ^ B) | (A & B)
A ^ B = (A - B) | (B - A)
用 Python 写过爬虫的人都应该感觉过被字符串编码支配的恐惧。简单来讲,编码指的是将可读的字符串转换成不太可读的数字,用来存储或者传输。解码则指的是将数字还原成字符串的过程。常见的编码有 ASCII、GBK 等。
ASCII 编码是一个至关小的字符集合,只有一百多个经常使用的字符,所以只用一个字节(8 位)就能表示,为了存储本国语言,各个国家都开发出了本身的编码,好比中文的 GBK。这就带来了一个问题,若是我想要在一篇文章中同时写中文和日文,就没法实现了,除非能对每一个字符指定编码,这个成本高到没法接受。
Unicode 则是一个最全的编码方式,每一个 Unicode 字符占据 6 个字节,能够表示出 2 ^ 48 种字符。但随之而来的是 Unicode 编码后的内容不适合存储和发送,所以诞生了基于 Unicode 的再次编码,目的是为了更高效的存储。
更详细的概念分析和配图说明能够参考个人这篇文章:字符串编码入门科普,这里咱们主要聊聊 Python 对字符串编码的处理。
首先,编码的函数是 encode
,它是字符串的方法:
s = 'hello' s.encode() # 获得 b'hello' s.encode('utf16') # 获得 b'\xff\xfeh\x00e\x00l\x00l\x00o\x00'
encode
函数有两个参数,第一个参数不写表示使用默认的 utf8
编码,理论上会输出二进制格式的编码结果,但在终端打印时,被自动还原回字符串了。若是用 utf16
进行编码,则会看到编码之后的二进制结果。
前面说过,编码是字符转到二进制的转化过程,有时候在某个编码规范中,并无指定某个字符是如何编码的,也就是找不到对应的数字,这时候编码就会报错:
city = 'São Paulo' b_city = city.encode('cp437') # UnicodeEncodeError: 'charmap' codec can't encode character '\xe3' in position 1: character maps to <undefined> 此时须要用到 encode 函数的第二个参数,用来指定遇到错误时的行为。它的值能够是 'ignore',表示忽略这个不能编码的字符,
也能够是 'replace',表示用默认字符代替: b_city = city.encode('cp437', errors='ignore') # b'So Paulo' b_city = city.encode('cp437', errors='replace') # b'S?o Paulo'
decode
彻底是 encode
的逆操做,只有二进制类型才有这个函数。它的两个参数含义和 encode
函数彻底一致,就再也不介绍了。
从理论上来讲,仅从编码后的内容上来看,是没法肯定编码方式的,也没法解码出原来的字符。但不一样的编码有各自的特色,虽然没法彻底倒推,但能够从几率上来猜想,若是发现某个二进制内容,有 99% 的可能性是 utf8
编码生成的,咱们就能够用 utf8
进行解码。Python 提供了一个强大的工具包 Chardet
来完成这一任务:
octets = b'Montr\xe9al' chardet.detect(octets) # {'encoding': 'ISO-8859-1', 'confidence': 0.73, 'language': ''} octets.decode('ISO-8859-1') # Montréal
返回结果中包含了猜想的编码方式,以及可信度。可信度越高,说明是这种编码方式的可能性越大。
有时候,咱们拿到的是二进制的字符串字面量,好比 68 65 6c 6c 6f
,前文说过只有二进制类型才有 decode
函数,因此须要经过二进制的字面量生成二进制变量:
s = '68 65 6c 6c 6f' b = bytearray.fromhex(s) b.decode() # hello
字符串的 split(sep, maxsplit)
方法能够以指定的分隔符进行分割,有点相似于 Shell 中的 awk -F ' '
',第一个 sep
参数表示分隔符,不填则为空格:
s = 'a b c d e' a = s.split() # a = ['a', 'b', 'c', 'd', 'e']
第二个参数 maxsplit 表示最多分割多少次,所以返回数组的长度是 maxsplit + 1。举个例子说明下: s = 'a;b;c;d;e' a = s.split(';') # a = ['a', 'b', 'c', 'd', 'e'] b = s.split(';', 2) # b = ['a', 'b', 'c;d;e'] 若是想批量替换,则能够用 replace(old, new[, count]) 方法,由中括号括起来的参数表示选填。 old = 'a;b;c;d;e' new = old.replace(';', ' ', 3) # new = 'a b c d;e' strip[chars] 用于移除指定的字符们: old = "*****!!!Hello!!!*****" new = old.strip('*') # 获得 '!!!Hello!!!' new = old.strip('*!') # 获得 'Hello' 若是不传参数,则默认移除空格。其实 strip 等价于分别执行 lstrip() 和 rstrip(),即分别从左侧和右侧进行移除。
好比 lstrip() 表示从左侧第一个字符开始,移除空格,直到第一个非空格字符为止,因此字符串中间的空格,不管是 lstrip
仍是 strip() 都是没法移除的。 old = ' Hello world ' new = old.strip() # 获得 'Hello wrold' new = old.lstrip() # 获得 'Hello world ' 最后一个经常使用方法是 join,其实这个能够理解为字符串的构造方法,它能够把数组转换成字符串: array = 'a b c d e'.split() # 以前说过,结果是 ['a', 'b', 'c', 'd', 'e'] s = ';'.join(array) # 以分号为链接符,把数组中的元素链接起来 # s = 'a;b;c;d;e'
因此 join
能够理解为 split
的逆操做,这个公式始终是成立的:
c.join(string.split(c)) = string
上面这些字符串处理的函数,大多返回的仍是字符串,所以能够链式调用,避免使用临时变量和多行代码,但也要避免过长(超过 3 个)的链式调用,以避免影响可读性。
最初级的字符串格式化方法是使用 +
来拼接:
class Person: def __init__(self): self.name = 'bestswifter' self.age = 22 self.sex = 'm' p = Person() print('Name: ' + p.name + ', Age: ' + str(p.age) + ', Sex: ' + p.sex) # 输出:Name: bestswifter, Age: 22, Sex: m
这里必需要把 int
类型的年龄转成字符串之后才能进行拼接,这是由于 Python 是强类型语言,不支持类型的隐式转换。
这种作法的缺点在于若是输出结构比较复杂,极容易出现引号匹配错误的问题,可读性很是低。
Python 2 中的作法是使用占位符,相似于 C 语言中 printf
:
content = 'Name: %s, Age: %i, Sex: %c' % (p.name, p.age, p.sex) print(content)
从结构上看,要比上一种写法清楚得多, 但每一个变量都须要指定类型,这和 Python 的简洁不符。实际上每一个对象均可以经过 str()
函数转换成字符串,这个函数的背后是 __str__
魔术方法。
Python 3 中的写法是使用 format
函数,好比咱们来实现一下 __str__
方法:
class Person: def __init__(self): self.name = 'bestswifter' self.age = 22 self.sex = 'm' def __str__(self): return 'Name: {user.name}, Age: {user.age}, Sex: {user.sex}' .format(user=self) p = Person() print(p) # 输出:Name: bestswifter, Age: 22, Sex: m 除了把对象传给 format 函数并在字符串中展开之外, 也能够传入多个参数,而且经过下标访问他们: print('{0}, {1}, {0}'.format(1, 2)) # 输出:1, 2, 1,这里的 {1} 表示第二个参数
Heredoc 不是 Python 特有的概念, 命令行和各类脚本中都会见到,它表示一种所见即所得的文本。
假设咱们在写一个 HTML 的模板,绝大多数字符串都是常量,只有有限的几个地方会用变量去替换,那这个字符串该如何表示呢?
一种写法是直接用单引号去定义: s = '<HTML><HEAD><TITLE>\nFriends CGI Demo</TITLE></HEAD>\n<BODY><H3>ERROR </H3>\n<B>%s</B><P>\n<FORM><INPUT TYPE=button VALUE=Back\nONCLICK=\'window .history .back()\'></FORM>\n</BODY></HTML>' 这段代码是自动生成的还好,若是是手动维护的,那么可读性就很是差,由于换行符和转义后的引号增长了理解的难度。
若是用 heredoc 来写,就很是简单了: s = '''<HTML><HEAD><TITLE> Friends CGI Demo</TITLE></HEAD> <BODY><H3>ERROR</H3> <B>%s</B><P> <FORM><INPUT TYPE=button VALUE=Back ONCLICK='window.history.back()'></FORM> </BODY></HTML> '''
Heredoc 主要是用来书写大段的字符串常量,好比 HTML 模板,SQL语句等等。