10

不懂性能优化,再强的计算机也白玩

 3 years ago
source link: http://www.justdopython.com/2020/11/29/python-performance/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Python 的优秀有目共睹,不过说的性能,还真比不了 Java、C、Go,有没有提升性能的技巧或方法呢?今天我们一起学习下提升 Python 性能的方式方法,那还等啥,来吧

局部变量更好

记得刚开始学习 C 语言时,对先定义再使用,感到很痛苦,经常因为声明问题编译不通

现在用 Python,变量随用随定义,爽到不行

不过我却养成了先定义在使用的习惯,例如:

a = None  # 可以不写
if some_condition:
    a = 10
else:
    a = 0

虽然 a = None 可以不写,但是还是习惯性的写处理做变量声明,类似 C 中的 int a;

这样的习惯,促使我在写代码之前,会先考虑如何将会用到的变量,从而,对变量的使用范围做出了严格的限定,能是局部的,绝不全局

而这个习惯提高程序的性能同时,还有其他好处

  • 局部变量查找的速度更快,因为 Python 是从代码块里,向外查找变量的,里面找不到,才会去外面找,最后才是全局变量,其他语言也一样
  • 局部变量更节省内存,当代码快被执行,代码块中声明的局部变量所占用的内存就会被释放
  • 让代码更简洁,更易懂,比如可以用局部变量为冗长的命名空间中的变量起别名,如 ls = os.linesp,后面就在可以用 ls 简洁表示 os.linesp

函数虽好 尽量少调

函数是个伟大的发明,将可以被重复使用的过程集中起来,方便反复调用,而且函数的出现,使递归得以实现

不过,调用函数的时间成本比一般语句高的多,这是因为,函数的调用需要计算机做更多的调度协调工作

因此,应该尽量减少调用函数,特别是在大的循环中,更要注意

下面,列出几个典型例子,在这些情况下,可以不用调用函数

  • 使用 isinstance 代替 type,因为 Python 是面向对象语言,支持对象的继承,isinstance 可以直接检测对象的基类,不会像 type 一样对对象做全面的检测,会比 isinstance 做更多的函数调用
  • 避免在循环判断中,调用函数

      # 每次循环都需要计算 a 的长度
      while i < len(a):
          statement
    
      # 先计算出 a 的程度,避免每次循环计算
      length = len(a)
      while i < length:
          statement
    
  • 如果模块 X 中有个 Y 对象或函数,那么最后这样引入 from X import Y,而不是 import X,后者每次使用 Y 时,需要通过 X.Y 的方式,比直接使用 Y 多了一次函数调用

映射优于判断

在《编程珠玑 第二版》 第一章开篇中,描述了一个需求,需要对记录了一千多万行 7 位数据的文件中的数据排序,而且需要在很短时间内,在只使用 1M 内存的条件下完成

对于使用着现代计算机的我们来说,简直不可思议

一方面,现在的计算机动辄好几 G,几核,性能超强

另一方面,随便一个编程语言都有内置的高效排序算法

但在当时,计算机最大内存才不过几兆(M)!

你可能不会相信,就在当时的条件下,能在数十秒内完成吧

核心原理就是借用索引,来表示数值,比如 1000 是否存在,就看数组中索引为 1000 的值是否为 1,不存在则为 0,最后只需要便利一遍数组(书上实际应该的是字符串,一个字节索引表示一个数字),就能得到数据排序了

显而易见,相比判断,索引效率更高

例如,应该尽量避免第一种写法,而用第二种:

# 判断并赋值
if a == 1:
    b = 10
elif a == 2:
    b = 20
...

# 利用索引,直接存值,性能更好
d = {1:10,2:20,...}
b = d[a]

迭代器的性能更优

Python 中有很多迭代器,方便我们做各种循环

对于可以支持迭代器的对象,使用迭代器获取元素,比用索引获取元素的效率更高

a = [1, 2, 3]

for i in range(len(a)):  # 使用索引获取元素
    print(a[i])

for item in a:  # 使用迭代器获取元素
    print(item)

上面代码中,直接使用迭代器的效率更高

如果最开始接触的语言是 Python,应该比较习惯直接使用迭代器,如果从其他语言转过来,可能更习惯使用索引

另外,如果需要在循环中得到每个元素的索引,可以通过一个索引计数器来实现:

a = ['a', 'b', 'c']
index = 0  # 初始化索引
for item in a:
    print(index, item)
    index += 1  # 递增索引值

延迟享受是美德

曾经有个著名的心理学实验 —— 棉花糖实验,测试一群小孩子的延迟满足能力,最终的结论是:延迟满足能力强的孩子,未来成功的机率更高

这虽然是对人的测试,但对计算机也适用,不过,背后的逻辑有些不同

对计算机而言,没必要将还用不到的内容加载到内存里,内存就好比我们的工作太,如果放了太的的东西,查找就比较困难,从而影响工作效率

Python 中提供多种延迟加载的功能,其中生成器是个典型的应用

list 容器为例,在使用该容器迭代一组数据时,必须事先将所有数据存储到容器中,才能开始迭代

生成器 却不同,它可以实现在迭代的同时生成元素,也就是不是一次性生成所元素,只有在使用时才会生成

下面是个数字 生成器 的例子

def initNum():
    for i in range(10):
        yield i

gen = initNum()  # 得到生成器

for item in gen: # 遍历生成器
    print(item)

调用 initNum 会返回一个生成器,在 for 循环中,或者调用 next(等同于 gen.__next__()) 时才会生成下一个数字,在调用之前,不会生成所有的数据

生成器 占用的资源更少,意味着效率更高

先编译再调用

网络上有很多描述产品经理和研发直接矛盾的段子,让人啼笑皆非

最主要的原因是,需求的不确定性和研发需要的确定性是相互矛盾的

面对不到变动的需求,研发需要不断调整,因此效率不会高

相同的道理,计算机执行已经编译好的程序,比执行边解析边执行的程序效率高很多

例如,C 的程序运行效率会更高,因为需要对 C 代码,编译后才能运行

我们在享受 Python 这类动态语言带来的便利性同时,尽量让程序执行已经编译好的代码,以便提升性能

code = "for i in range(3):\n\tprint(i)"

exec(code)  # 直接执行

c = compile(code, "", "exec")
exec(c)  # 编译后再执行

更常见的是正则表达式

line = "name\t\tage\tschool"

reObj = re.compile("\t+")  # 编译
reObj.split(line)  # 使用编译后的性能更好

编译后不仅性能更好,而且在可以反复使用时,进一步提供效率

我们人类在不断理解这个世界的同时,产生了大量的信息和知识,一直无论哪个人,究其一生也无法掌握所有的知识

于是科学发展为分科之学,将知识分门别类,以便不同的人掌握了解他所关注的一点

知识是这样,我们的合作也是如此,没有一个可以做所有的事情,需要多人相互配合,各负其责

计算机是我们大脑的延伸,是用我们的思考方式、思维习惯制造的

面对大规模代码时,需要对代码进行分门别类,着不仅方便我们人类查看,还能提升程序执行效率,可以在需要时,才去加载和执行模块和方法

# module1.py

def fun(alist):
    for item in alist:
        do some thing
# 测试用
a = [1, 2, 3]
fun(a)

定义了函数 fun,之后测试了下,如果其他代码引用了 fun: from module1 import fun

这测试用的代码就会被执行

好的方式是,将测试用代码封装起来:

# module1.py

def fun(alist):
    for item in alist:
        do some thing

if __name__ == "__main__":
    # 测试用
    a = [1, 2, 3]
    fun(a)

这样就可以避免测试用代码的无意义执行,从而提升运行效率

虽然现在的计算机性能很强,编程语言提高的功能很多,为我们提高了极大的便利,但是良好的编程习惯和编码规范仍然是很重要的,首先代码更多的时候是写给人看的,另外在强的计算机性能,也解决不了思维懒惰者的低效代码,就好比 你永远叫不醒一个装睡的人 一样。

业精于勤,在日常的工作和编程中,多学多练多思考,会使编程水平的提升事半功倍,看似不起眼的小技巧,处处做好了,将带来巨大的差异,这就是与高手直接的差距

期望今天分享的一点小技巧能给您一丝启发,让您在通往高手的道路上更加顺畅,比心!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK