优化每一个细节

函数返回值缓存/函数查询表

把函数、输入参数、返回值全部缓存下来,在函数下次调用时直接使用存储的结果(不需要重新计算所有数据)
使用情景:在处理固定参数的函数被重复调用时。这样做可以确保每次函数被调用时,直接返回缓存结果。

    
1、用列表或链表做查询表        O(N)
2、用字典做查询表            O(1)    # 只要哈希后的值不重复,就是有效的
3、二分查找                    O(logn)    # 但是要有序,因此排序也是耗费时间的

默认参数

import math
# 原始函数
def degree_sin(deg):
    return math.sin(deg * math.pi / 180)
    
# 优化的函数,因子变量是在函数创建时建立的
# 所以直接用math.sin方法查询即可
def degree_sin(deg, factor=math.pi/180, sin=math.sin):
    return sin(deg * factor)

# 但这种优化方法,因为在运行过程中,函数预先计算的项目是不能改变的,所以函数的接口容易造成混乱。

列表表达式与生成器

#########################
mult_line = [ i for i in range(100) if x % 2 == 0 ]
#########################
mult_line = []
for i in range(100):
    if x % 2 == 0:
        mult_line.append(i)
#########################

搞明白为什么列表表达式比for循环性能更好?

我们查看它们分别解析后的字节码:
    而其对应的字节码,我们识别不出来,因此我们使用dis模块把字节码转换成人能读懂的形式,分析它们执行的细节。
import dis
import inspect
import timeit


programs = dict(
    loop="""
multiples_of_two = []
for x in range(100):
    if x % 2 == 0:
        multiples_of_two.append(x)""",
    comprehension='multiples_of_two = [x for x in range(100) if x % 2 == 0]',
)
for name, text in programs.items():
    print(name, timeit.Timer(stmt=text).timeit())
    code = compile(text, '<string>', 'exec')
    dis.disassemble(code)
D:\Python35\python.exe "D:/Destop/性能分析/chapter 4/dis_list_comprehension.py"
loop 15.3982382
  2           0 BUILD_LIST               0
              3 STORE_NAME               0 (multiples_of_two)

  3           6 SETUP_LOOP              49 (to 58)
              9 LOAD_NAME                1 (range)
             12 LOAD_CONST               0 (100)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
        >>   19 FOR_ITER                35 (to 57)
             22 STORE_NAME               2 (x)

  4          25 LOAD_NAME                2 (x)
             28 LOAD_CONST               1 (2)
             31 BINARY_MODULO
             32 LOAD_CONST               2 (0)
             35 COMPARE_OP               2 (==)
             38 POP_JUMP_IF_FALSE       19

  5          41 LOAD_NAME                0 (multiples_of_two)
             44 LOAD_ATTR                3 (append)
             47 LOAD_NAME                2 (x)
             50 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             53 POP_TOP
             54 JUMP_ABSOLUTE           19
        >>   57 POP_BLOCK
        >>   58 LOAD_CONST               3 (None)
             61 RETURN_VALUE
comprehension 12.4666929
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x000002823D52F810, file "<string>", line 1>)
              3 LOAD_CONST               1 ('<listcomp>')
              6 MAKE_FUNCTION            0
              9 LOAD_NAME                0 (range)
             12 LOAD_CONST               2 (100)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 STORE_NAME               1 (multiples_of_two)
             25 LOAD_CONST               3 (None)
             28 RETURN_VALUE
Process finished with exit code 0
可以看出,for循环产生的指令集更长。在for循环里,数值是一个一个增加的,用到三个指令(LOAD_ATTR, LOAD_NAME 和 CALL_FUNCTION)。
                          但是,从图中可以看出,列表表达式只用了一个简单且已经经过优化的指令(LIST_APPEND) 但是,即便for循环需要做额外的操作,也切记不要肆意将所有的for循环都改成列表生成式,因为有时候未经优化的列表表达式,
                                                可能比for循环消耗的时间更长 注意:在处理大列表的时候,列表表达式可能就不好使了,因为列表表达式需要直接产生每一个值。因此,
  如果你要处理一个包含10万元素的列表还有一种更好的方法。你可以用生成器表达式,不需要直接返回列表,而是返回一个生成器对象,
      它会动态地生成列表元素

ctypes

具体可以看另一篇博客
可以让开发者直接进入Python的底层,借助C语言的力量进行开发。这个库只有CPython才有,因为这个版本是C语言写的。
    这个连接C语言的接口可以做很多事情,因为你可以直接加载预编译代码,并用C语言执行。也就是说通过它接入Windows系统上的kernel32.dll和msvcrt.dll动态链接库,以及Linux系统的lib.so.6库
    
    详情参考官方文档: https://docs.python.org/2/library/stypes.html
    ------------check_prime.c--------------------
    #include <stdio.h>
    #include <math.h>
     
    int check_prime(int a)
    {
      int c;
      for ( c = 2 ; c <= sqrt(a) ; c++ ) { 
        if ( a%c == 0 )
            return 0;
      }

      return 1;

    }
    #################  gcc -shared -o check_prime.so -fPIC check_prime.c
    import time
    import ctypes
    import math

    check_primes_types = ctypes.CDLL('./check_primes.so').check_prime


    def check_prime(x):
        values = range(2, int(math.sqrt(x)))
        for i in values:
            if x % i == 0:
                return False

        return True


    init = time.clock()
    numbers_py = [x for x in range(1000000) if check_prime(x)]
    print("Full python version: %s seconds" % (time.clock() - init))

    init = time.clock()
    numbers_c = [x for x in range(1000000) if check_primes_types(x)]
    print("C version: %s seconds" % (time.clock() - init))
    print(len(numbers_py))
    ################################################
    Full python version: 7.042522 seconds
    C version: 0.7749299999999995 seconds
    78705

    然后发现,我的天啊,这能比较么,效率差这么多。。。。。。。。。。。。。。。。
1
import time
import random
from ctypes import cdll

libc = cdll.LoadLibrary('libc.so.6')  # linux systems
# libc = cdll.msvcrt #windows systems

init = time.clock()
randoms = [random.randrange(1, 100) for x in range(1000000)]
print("Pure python: %s seconds" % (time.clock() - init))

init = time.clock()
randoms = [(libc.rand() % 100) for x in range(1000000)]
print("C version : %s seconds" % (time.clock() - init))
######## c的效率还是那么高
加载一个系统库

字符串拼接

因为在python内字符串是不可变的,所以每当修改都是创建了一个新的字符串,然后指向这个新的。因此处理字符串都要很小心。

import time
import sys

option = sys.argv[1]

words = [str(x) for x in range(1000000)]

if option == '1':
    full_doc = ""
    init = time.clock()
    for w in words:
        full_doc += w
    print("Time using for-loop: %s" % (time.clock() - init))

else:
    init = time.clock()
    full_doc = "".join(words)
    print("Time using join: %s" % (time.clock() - init))


########################################
ace@ace-virtual-machine:$ /usr/bin/time -f 'Memory: %M bytes' python3 string_concat.py 1
Time using for-loop: 0.13839400000000002
Memory: 77724 bytes
ace@ace-virtual-machine:$ /usr/bin/time -f 'Memory: %M bytes' python3 string_concat.py 0
Time using join: 0.014417000000000013
Memory: 77524 bytes
结论:join  消耗的内存更少,时间更短
##############################################
另外当遇到一下情况时:
    document = title + introduction + main_price + conclusion
    document = "%s%s%s%s" % (title ,introduction , main_price , conclusion)
    document = "%(title)s%(introduction)s%(main_price)s%(conclusion)s" % locals()
    这三种效率依次增加

其他优化技巧

成员关系测试
a in  b  
    b :    列表    O(n)
        集合    O(1)
        字典    O(1)
        元组    O(n)

 

不要重复发明轮子
python标准库核心组件都是经过优化的,
像 列表,元组,集合,字典,以及数组(array),迭代工具(itertools)和队列(collections.deque)这些模块都推荐使用。
如内置函数
    map(operator.add,list1,list2)也会比
    map(lamba x,y:x+y, list1, list2) 更快

 

不要忘记了队列
当需要固定一个长度的数组或可变长度的栈时,列表很合适。但是在处理pop(0)以及insert(0,value)时,可以试试collections.deque,
因为它能在列表的任何一端都能快速的完成O(1)插入和弹出操作

 

有时不定义函数更好
调用函数会增加大量的资源消耗, 因此,有时候,尤其是在时间密集的循环体体中内联函数代码,不调用外部函数,可以更加高效。

但这有时可能会损害代码的可读性和维护便利性。因此仅当必须提升性能才应该这样做。

 

尽可能用key函数排序

 

1 比 true 好

 

多元赋值
多元赋值比 单元赋值 要慢
但是在变量交换上,比普通方法要快
推荐使用链式比较
比如    x<y<z 比   x<y和y<z 更好

 

用命名元组(nametuple)替换常规对象
使用常规的类创建对象存储数据的简单对象时,实例中会有一个字典存储属性。这个存储对属性少的对象来说是一种浪费。
如果需要创建大量的简单对象,会浪费大量内存。这种情况下,可以使用命名元组。
这是一个新的tuple子类,可以轻松地构建并优化内存。
https://docs.python.org/2/library/collections.html#collections.namedtuple

import time
import collections


class Obj(object):
    def __init__(self, i):
        self.i = i
        self.l = []


all = {}

init = time.clock()
for i in range(1000000):
    all[i] = Obj(i)
print("Regular Objects: %s" % (time.clock() - init))

Obj = collections.namedtuple('Obj', 'i l')

all = {}
init = time.clock()
for i in range(1000000):
    all[i] = Obj(i, [])
print("Regular Objects: %s" % (time.clock() - init))

################################################
D:\Python35\python.exe "D:/example.py"
Regular Objects: 1.9968633
Regular Objects: 1.5068022

 

posted @ 2018-12-28 14:00  慕沁  阅读(170)  评论(0)    收藏  举报