社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

Effective Python | 用pythonic方式来思考

NewBeeNLP • 4 年前 • 611 次点击  

作者 | Jack Stark 
知乎 | https://zhuanlan.zhihu.com/p/84292970
整理 | NewBeeNLP

Python语言入门简单,但是想要写好Python程序还是需要大量经验的。最近在看谷歌工程师Brett Slatkin的《Effective Python》这本书,感觉如获至宝。本系列就是这本书的阅读笔记,希望对自己和读者有用。

确认自己的Python版本

目前主流的版本是Python3,但是资料或公司也可能有Python2写的历史代码。因此两者的区别还是要清楚的。区别如下:

  • Python3中的print是函数,需要加括号,Python2中的print是语句,不需要加括号,如果想要和Python3一样的print,可以从__future__模块中导入print_function
# Python3print("a", "b")  # a, b
# Python2print("a", "b") # ("a", "b")
from __future__ import print_functionprint("a", "b") # a, b
  • Python3的map函数返回一个可迭代对象,Python2的map函数返回一个列表。类似还有filter和zip。Python3中的dict.keys()、dict.values()返回的也是迭代器,dict.items()以列表返回可遍历的(键, 值) 元组数组。
# Python2a = map(int, ['1','2']) # a是[1,2]
# Python3a = map(int, ['1','2'])type(a) # map
# 我用Python3读取从控制台输入的一行按空格分开的数字一般都这么做nums = list(map(int, input().split()))
  • Python3的默认编码是UTF-8,Python2的默认编码是ascii码。因此在Python3中不需要在开头写# coding=utf-8了。
  • Python3中的True和False是两个关键字,指向两个固定的对象,不能被赋值。Python2中它们是全局变量,可以被重新赋值。
  • Python3中的range返回的不是list对象,而是迭代器,相当于Python2中的range和xrange的整合。Python2中的range返回的是列表。两个版本都能用的写法:
try:    range = xrangeexcept:    pass
  • Python2的迭代器必须实现next方法,Python3中是__next__方法。
  • Python3中的input得到的是字符串,Python2 中的input()在输入是数字时会自动转换为数字,有时候会引发问题,raw_input()和Python3中的input()功能类似。
  • Python3中引入了nonlocal,可以声明某个变量为非局部变量。Python2中没有这种方法。
def fun_1():    a = 1    def fun_2():        a = 2    fun_2()    print(a)
print(fun_1()) # 1

上面fun_2中的a是局部变量,不会影响外部的a。但声明为nonlocal后就不一样了,如下代码:

def fun_1():    a = 1    def fun_2():        nonlocal a        a = 2    fun_2()    print(a)
print(fun_1()) # 2
  • Python3中两种字符类型是bytes和str。前者的实例包含原始的8位值;后者的实例包含Unicode字符。Python2中那个的两种字符类型是str和unicode。前者的实例包含原始的8位值,后者的实例包好Unicode字符。但是Python3的str实例和Python2的unicode实例都没有和特定的二进制编码相关联。想要把Unicode字符转换成二进制数据,必须使用encode方法;反之,必须使用decode方法。

  • 2to3和six等工具可以方便地把Python2迁移到Python3上。

遵循PEP8的风格指南

PEP8是针对Python代码格式而编订的风格指南,遵循PEP8的要求有利于写出可读性强的代码。几条绝对应该遵循的规则,

空白

Python中的空白会影响代码的含义和清晰程度。

  • 使用space来表示缩进,而不要使用tab键。
  • 和语法相关的每一层缩进都使用四个空格。
  • 每行的字符不应该超过79.
  • 对于占据多行的长表达式来说,除了首行之外的其余各行都应该在通常的缩进级别之上再加4个空格。
  • 文件中的函数与类之间应该用两个空行隔开。
  • 同一个类的方法之间应该用一个空行隔开。
  • 在使用下标来获取列表元素、调用函数或给关键字参数赋值时,不要在两旁添加空格。
  • 为变量赋值时,赋值符号的左侧和右侧应该各自写上一个空格,而且只写一个。

命名

PEP8规定在不同的对象使用不同的命名方式,这样阅读时根据名称就可以看出它们在Python中的角色。

  • 函数、变量和属性用小写字母拼写,各单词之间用下划线相连,例如lowercase_underscore。
  • 类与异常应该以每个单词首字母大写的形式命名,例如CapitalizedWord。
  • 受保护的实例属性,应该以单个下划线开头。
  • 私有的实例属性,应该以两个下划线开头。
  • 模块级别的常量,应该全部采用大写字母来拼写,各单次之间用下划线相连。
  • 类中的实例方法(instance method),应该把首个参数命名为self,表示该对象本身。
  • 类方法(cls method)的首个参数,应该命名为cls,表示该类本身。
  • 建议:可以安装一下pep8这个库,然后再pycharm中设置一下,这样IDE就可以帮你把代码整理成pep8的风格。具体操作搜索一下即可。

了解bytes、str与Unicode的区别

具体区别在上面第1条里面已经提到了。

想要把Unicode字符转换成二进制数据,必须使用encode方法;反之,必须使用decode方法。写程序时,应该把编码和解码操作放在最外围,程序的核心部分应该使用Unicode字符类型。因此需要下面两个辅助函数,以便进行转换。

Python3中接受str或byte,并返回str的函数。

def to_str(s): # s不确定是str还是bytes    if isinstance(s, bytes):        value = s.decode('utf-8')    else:        value = s    return s  # instance of str

Python3中接受str或byte,并返回byte的函数。

def to_bytes(s):    if isinstance(s, str):        value = s.encode('utf-8')    else:        value = s    return s  # instance of bytes

在Python3中,有一个需要注意的地方。如果通过内置的open函数获取了文件句柄,那么该句柄默认会采用UTF-8编码格式来操作文件。而在Python2中,文件操作的默认编码格式则是二进制。下面这个程序功能是向文件中随机写入一些二进制数据。在Python2中可以正常运行,在Python3中却不行。

with open('/random.bin','w') as f:    f.write(os.urandom(10))# python3的错误  TypeError:must be str, not bytes

Python3给open函数添加了名为encoding的新参数,其默认值是'utf-8',因此在进行read和write操作时,必须传入Unicode字符的str实例,而不接受二进制数据的bytes实例。

解决方法是用二进制写入模式('wb')

with open('/random.bin','wb') as f:    f.write(os.urandom(10))

读的时候类似,有二进制读取模式('rb')

用辅助函数来取代复杂的表达式

表达式如果比较复杂,那么应该把它拆解为小块,并移入到辅助函数中。编写Python程序时,不要一味追求过于紧凑的写法,那样会写出非常复杂的表达式,对阅读和维护不友好。

了解切割序列的方法

切片操作(slice)用于把序列切成小块,也适用于实现了__getitem____setitem__这两个特殊方法的Python类上。

  • 不要写多余的代码,当start索引为0,或end索引为序列长度时,应该将其省略。
  • 切片操作不会计较start和end索引是否越界,这使得我们很容易从序列的前端或后端开始,对其进行范围固定的切片操作。
In [1]: a = list(range(10))
In [2]: a[:20]Out[2]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 如果对赋值操作右侧的列表使用切片,而把切片的起止索引都留空,就会产生一份原列表的拷贝。
  • 对list赋值时,如果使用切片操作,就会把原列表中处在相关范围内的值替换为新值(「即使长度不相等」)。
In [18]: a = list(range(10))
In [19]: aOut[19]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [20]: a[:3] = list(range(10))
In [21]: aOut[21]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 3, 4, 5, 6, 7, 8, 9]

在单次切片操作内,不要同时指定start、end和stride

  • Python提供了somelist[start:end:stride]形式的写法,以实现步进式切割。比如获取奇数索引或偶数索引的方法。
In [1]: a = list(range(10))
In [2]: evens = a[::2]
In [3]: evensOut[3]: [0, 2, 4, 6, 8]
In [4]: odds = a[1::2]
In [5]: oddsOut[5]: [1, 3, 5, 7, 9]
  • 把以字节形式存储的字符串反转过来,这个技巧就是采用-1的步进值。
In [6]: x = b'hello world'


    

In [7]: y = x[::-1]
In [8]: yOut[8]: b'dlrow olleh'
  • 注意,这种技巧对字节串和ASCII字符有用,但是对已经编码成UTF-8字节串的Unicode字符没用。
In [9]: a = "你好"
In [10]: b = a[::-1]
In [11]: bOut[11]: '好你'
In [12]: c = a.encode('utf-8')
In [13]: d = c[::-1]
In [14]: e = d.decode('utf-8')---------------------------------------------------------------------------UnicodeDecodeError Traceback (most recent call last) in ()----> 1 e = d.decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbd in position 0: invalid start byte
  • 既有start和end,又有stride的切片操作,可能会令人费解。尽量不要这么写。在内存满足的情况下可以做二次切片,先做步进式切片,然后把切割结果赋给某个变量。

用列表推导式来取代map和filter

list comprehension是Python中非常好用的一种写法,比map的写法看着更清楚。

In [1]: a = list(range(10))
In [2]: square = list(map(lambda x: x ** 2, a))
In [3]: squareOut[3]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
In [4]: square2 = [x ** 2 for x in a]
In [5]: square2Out[5]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

有条件时需要同时使用map和filter,这时列表推导式的优势更明显。




    
In [1]: a = list(range(10))
In [2]: even_square = list(map(lambda x: x ** 2, filter(lambda x: x % 2 == 0, a)))
In [3]: even_squareOut[3]: [0, 4, 16, 36, 64]
In [4]: even_square2 = [x ** 2 for x in a if x % 2 == 0]
In [5]: even_square2Out[5]: [0, 4, 16, 36, 64]

不要使用含有两个以上表达式的列表推导

列表推导式也支持多重循环。比如把二维list展成一维的,注意两重for循环的顺序。

In [26]: a = [[1,2,3],[4,5,6]]
In [27]: flat = [x for x in row for row in a]---------------------------------------------------------------------------NameError Traceback (most recent call last) in ()----> 1 flat = [x for x in row for row in a]
NameError: name 'row' is not defined
In [28]: flat = [x for row in a for x in row]
In [29]: flatOut[29]: [1, 2, 3, 4, 5, 6]

再比如二维list求平方:

In [31]: square = [[x**2 for x in row] for row in a]
In [32]: squareOut[32]: [[1, 4, 9], [16, 25, 36]]

列表推导式也支持多个if条件,处在同一循环中的多项条件,彼此之间默认形成and表达式。比如从列表中选出大于3的奇数,下面两种写法是等价的。

In [34]: a = list(range(10))
In [35]: b = [x for x in a if x > 3 and x % 2 ==1]
In [36]: bOut[36]: [5, 7, 9]
In [37]: c = [x for x in a if x > 3 if x % 2 == 1]
In [38]: cOut[38]: [5, 7, 9]

但是有些问题可能需要两个以上的表达式,虽然可以用列表推导式做,但是不利于阅读。比如要从原矩阵中找出是偶数,且所在行各元素之和大于等于10的数。

In [39]: a = [[1,2,3],[4,5,6]]
In [40]: result = [[x for x in row if x % 2 == 0] for row in a if sum(row) > 10]
In [41]: resultOut[41]: [[4, 6]]

像这种超过两个表达式的列表推导式虽然行数少,但是阅读起来麻烦,不推荐。

用生成器表达式来改写数据量较大的列表推导

列表推导式会一下子生成整个列表,如果输入数据较多,会消耗大量内存,可能导致程序崩溃。比如,读取一份文件并返回每行的字符数,如果用下面的列表推导式来处理,会把每一行的长度值都保存在内存中,这样没法处理大文件。

value = [len(x) for x in open('/1.txt')]print(value)

为了解决此问题,Python提供了生成器表达式(generator expression)。实现方法很简单,把列表推导式的中括号改为圆括号即可。使用时用next()函数读取下一个数据。这样可以大大减少内存占用。

value = (len(x) for x in open('/1.txt'))print(next(value) # 输出一个值

使用生成器表达式的另一个好处是可以组合。外围迭代器每次前进时,都会推动内部那个迭代器,产生连锁效应,使得每个表达式里面的逻辑都组合在一起了。

# 假设上面的文件每行的长度分别为0,1,2,3...
In [44]: roots = ((x, x**0.5) for x in value)
In [45]: rootsOut[45]: at 0x10f896518>
In [46]: next(roots)Out[46]: (0, 0.0)
In [47]: next(roots)Out[47]: (1, 1.0)
In [48]: next(roots)Out[48]: (2, 1.4142135623730951)

注意:由生成器表达式返回的那个迭代器是有状态的,用过一轮后就不能反复使用了。

尽量用enumerate取代range

enumerate的用法,可以在遍历时同时得到索引和值。

for index, value in enumerate(nums):    ....

另外,可以直接指定enumerate函数开始计数时使用的值(默认为0)。

for index, value in enumerate(nums, 1):    ....

用zip函数来同时遍历两个迭代器

在Python3中的zip函数,可以把两个或两个以上的迭代器封装为生成器,以便稍后求值。这种zip生成器,会从每个迭代器中获取该迭代器的下一个值,然后把这些值汇聚成元组。

In [51]: aOut[51]: [0, 1, 2, 3, 4]
In [52]: b=a
In [53]: for i, j in zip(a, b): ...: print(i,j) ...:0 01 12 23 34 4

注意:

  • Python2里的zip并不是生成器,所以可能会占用大量内存。
  • zip()内的迭代器长度要保证相同。当有一个迭代器耗尽时,zip就不在产生新的元组。

不要在for和while循环后面写else块

Python语言有一个很多其他语言都不支持的功能,就是在循环内部语句块后面直接编写else块。

for i in range(5):    print(i)else:    print("***")>>>01234***

可以看出,else内的语句会在循环语句结束后立即执行。但是很奇怪,为什么叫else呢?

常用的else如if/else,try/except/else等都是前面的代码块不执行才执行else语句。所以不熟悉此语法的人可能会误认为:如果循环没有正常执行完,那就执行else块。但实际上正好相反,循环正常执行完会立即执行else代码块;在循环里用break跳出(即使是最后一个循环break),会导致程序不执行else。

for i in range(5):    if i == 4:        break    print(i)else:    print("***")>>>0123

因此,循环后面的else代码块没有必要且容易引起歧义,尽量不要使用。

合理利用try/except/else/finally结构中的每个代码块

Python 异常处理可能要考虑四种不同的时机,可以用try、except、else和finally块来表述。

  • 无论try块是否发生异常,都可利用try/finally复合语句中的finally块来执行清理工作。
  • else块可以用来缩减try块中的代码量,并没有把发生异常时所要执行的语句与try/except代码块隔开。
  • 顺利运行try块后,若想使某些操作能在finally块的清理代码之前执行,则可将这些操作写到else块中。

- END -


Python中那些低调有趣的模块 2020-06-15
关于逻辑回归,面试官们都怎么问 2020-04-03
我从吴恩达AI For Everyone中学到的十个重要AI观 2020-03-25



记得把NewBeeNLP设为星标哦



等你在看

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/70702
 
611 次点击