炼数成金 门户 大数据 Python 查看内容

Effective Python | 用pythonic方式来思考

2020-6-28 10:34| 发布者: 炼数成金_小数| 查看: 7968| 评论: 0|原作者: Jack Stark |来自: NewBeeNLP

摘要: 目前主流的版本是Python3,但是资料或公司也可能有Python2写的历史代码。因此两者的区别还是要清楚的。区别如下:Python3中的print是函数,需要加括号,Python2中的print是语句,不需要加括号,如果想要和Python3一 ...
Python语言入门简单,但是想要写好Python程序还是需要大量经验的。最近在看谷歌工程师Brett Slatkin的《Effective Python》这本书,感觉如获至宝。本系列就是这本书的阅读笔记,希望对自己和读者有用。

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

Python3中的print是函数,需要加括号,Python2中的print是语句,不需要加括号,如果想要和Python3一样的print,可以从__future__模块中导入print_function。
# Python3
print("a", "b")  # a, b

# Python2
print("a", "b")  # ("a", "b")

from __future__ import print_function
print("a", "b")  # a, b

Python3的map函数返回一个可迭代对象,Python2的map函数返回一个列表。类似还有filter和zip。Python3中的dict.keys()、dict.values()返回的也是迭代器,dict.items()以列表返回可遍历的(键, 值) 元组数组。
# Python2
a = map(int, ['1','2']) # a是[1,2]

# Python3
a = 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 = xrange
except:
    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]: a
Out[19]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

In [20]: a[:3] = list(range(10))

In [21]: a
Out[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]: evens
Out[3]: [0, 2, 4, 6, 8]

In [4]: odds = a[1::2]

In [5]: odds
Out[5]: [1, 3, 5, 7, 9]
把以字节形式存储的字符串反转过来,这个技巧就是采用-1的步进值。
In [6]: x = b'hello world'

In [7]: y = x[::-1]

In [8]: y
Out[8]: b'dlrow olleh'
注意,这种技巧对字节串和ASCII字符有用,但是对已经编码成UTF-8字节串的Unicode字符没用。
In [9]: a = "你好"

In [10]: b = a[::-1]

In [11]: b
Out[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]: square
Out[3]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

In [4]: square2 = [x ** 2 for x in a]

In [5]: square2
Out[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_square
Out[3]: [0, 4, 16, 36, 64]

In [4]: even_square2 = [x ** 2 for x in a if x % 2 == 0]

In [5]: even_square2
Out[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]: flat
Out[29]: [1, 2, 3, 4, 5, 6]

再比如二维list求平方:
In [31]: square = [[x**2 for x in row] for row in a]

In [32]: square
Out[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]: b
Out[36]: [5, 7, 9]

In [37]: c = [x for x in a if x > 3 if x % 2 == 1]

In [38]: c
Out[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]: result
Out[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]: roots
Out[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]: a
Out[51]: [0, 1, 2, 3, 4]

In [52]: b=a

In [53]: for i, j in zip(a, b):
    ...:     print(i,j)
    ...:
0 0
1 1
2 2
3 3
4 4

注意:
Python2里的zip并不是生成器,所以可能会占用大量内存。
zip()内的迭代器长度要保证相同。当有一个迭代器耗尽时,zip就不在产生新的元组。
不要在for和while循环后面写else块

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

for i in range(5):
    print(i)
else:
    print("***")
>>>
0
1
2
3
4
***
可以看出,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("***")
>>>
0
1
2
3
因此,循环后面的else代码块没有必要且容易引起歧义,尽量不要使用。

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

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

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

声明:文章收集于网络,版权归原作者所有,为传播信息而发,如有侵权,请联系小编删除,谢谢!

欢迎加入本站公开兴趣群
软件开发技术群
兴趣范围包括:Java,C/C++,Python,PHP,Ruby,shell等各种语言开发经验交流,各种框架使用,外包项目机会,学习、培训、跳槽等交流
QQ群:26931708

Hadoop源代码研究群
兴趣范围包括:Hadoop源代码解读,改进,优化,分布式系统场景定制,与Hadoop有关的各种开源项目,总之就是玩转Hadoop
QQ群:288410967

鲜花

握手

雷人

路过

鸡蛋

相关阅读

最新评论

热门频道

  • 大数据
  • 商业智能
  • 量化投资
  • 科学探索
  • 创业

即将开课

 

GMT+8, 2020-7-9 16:07 , Processed in 0.141312 second(s), 25 queries .