一、大文件读取(生成器实现)
利用python的f.read(1024),一次读取1024字节到内存中,同时用yield返回读取的结果,这样当内存小于文件大小时,可以分批读取文件,并进行操作。
def bigFileRead(f, new_line):
buf = ""
while True:
while new_line in buf: # 如果一次性读取出的字符串包含多个分隔符,那么循环操作,查分分隔符,相当于做了split
pos = buf.index(new_line)
yield buf[:pos]
buf = buf[pos+len(new_line):]
chunk = f.read(5)
if not chunk: # 读取结束
yield buf
break
buf += chunk
with open("input.txt", encoding="utf-8") as f:
for line in bigFileRead(f, "{|}"):
print(line)
二、for else 用法
for后面跟else,当正常遍历完for循环后,会接着执行else里面的代码。如果再执行for循环的时候,break了,那么就不会执行else了。
for i in range(5):
if i == 3:
break
else: # 当for循环是break出来的时候,不会执行else,否则都会执行else里面的语句
print("out")
三、解开n层的list或tuple
递归方法,解开n层的list或tuple。
def splitList(lst):
ret = []
for x in lst:
if (type(x)==list) or (type(x)==tuple):
ret.extend(splitList(x)) # 处理外层
else:
ret.append(x) # 处理内层
return ret
k = [2, ['23', ['234', (5, 6, 7)], ['9823', 9239]]]
print(splitList(k))
四、GIL(全局解释器锁)
首先明确一点,每个CPU在同一时刻只能执行一个python的线程。在python多线程下,每个线程的执行方式:
- 获取GIL
- 执行代码直到sleep或等待网络I/O
- 释放GIL
可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。
在python2.x里,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100(ticks可以看作是python自身的一个计数器,专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整),进行释放。 而每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。 并且由于GIL锁存在,python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,python的多线程效率并不高。
而在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。
python对CPU密集型和IO密集型的多线程的友好性区别:
- CPU密集型代码(各种循环处理、计数等等),在这种情况下,ticks计数很快就会达到阈值,然后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好。
- IO密集型代码(文件处理、网络爬虫等),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。
多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行。但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低
python下想要充分利用多核CPU,就用多进程。这是因为每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。
五、函数传参*、**的区别 | fun(*args,**kwargs)中的*args,**kwargs什么意思?
*args是直接将value作为传入参数,**kwargs是将key和value一起作为传入参数。
def args_test(x, y, *args):
print(x, y, args)
args_test(1,2,3,4,5) # *args 用来将参数打包成tuple给函数体调用
def kwargs_test(**kwargs):
print(kwargs)
kwargs_test(a=1,b=2,c=3) # **kwargs 打包关键字参数成dict给函数体调用
def param_test(arg, *args, **kwargs):
print(arg, args, kwargs)
param_test(1,3,5,a=6,b=9) # 参数arg、*args、**kwargs三个参数的位置必须是一定的。必须是(arg,*args,**kwargs)这个顺序,否则程序会报错。
def test(*args,**kwargs):
print(args, kwargs)
l = [1,2,3,4]
d = {"a":1,"b":2}
test(l, d) # l,d这2个变量都会传递给args这个形参,作为args变量的两个元素,kwargs是一个空的字典,没有任何参数传递一个它
test(*l,**d) # 这个变量就会被赋值给args,d这个变量就会被赋值给kwargs
def foo(action=None, **kwargs):
print("action",action,sep="=================>")
print("kwargs", kwargs, sep="=================>")
d = {"a":1,"b":2}
foo(d) # d会被赋值给action参数
foo(**d) # d会被赋值给kwargs参数
d = {"action":"action","a":1}
foo(**d) # action会被d里面的action覆盖
六、装饰器作用、语法糖
装饰器是给现有的模块增添新的小功能。
import logging
def use_logging(func):
def wrapper():
logging.info("%s is running" % func.__name__)
return func() # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
return wrapper
def foo():
print('i am foo1')
foo = use_logging(foo) # 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于 foo = wrapper
foo() # 执行foo()就相当于执行 wrapper()
# 等价于使用@语法糖
def use_logging(func):
def wrapper():
logging.info("%s is running" % func.__name__)
return func()
return wrapper
@use_logging
def foo():
print("i am foo2")
foo()
# 传参装饰器 - *args
def use_logging(func):
def wrapper(*args):
logging.info("%s is running" % func.__name__)
return func(*args)
return wrapper
@use_logging
def foo(*args):
print("~~~~", args)
l = ["张三", 12]
foo(l)
# 传参装饰器 - **kwargs
def use_logging(func):
def wrapper(**kwargs):
logging.info("%s is running" % func.__name__)
return func(**kwargs)
return wrapper
@use_logging
def foo(**kwargs):
print("~~~~", kwargs)
foo(name="张三", age=12)
"""
@a
@b
@c
def f (): # 个函数还可以同时定义多个装饰器,它等效于 f=a(b(c(f)))
pass
"""
七、is 和 == 的区别
python中对象包含的三个基本要素,分别是:id(身份标识)、type(数据类型)和value(值)。此外还有其他要素,比如计数字段,该字段便于后面垃圾回收机制做参考。
is比较的是两个对象的id值是否相等,也就是比较两个对象是否为同一个实例对象,是否指向同一个内存地址。
==比较的是两个对象的内容是否相等,默认会调用对象的__eq__()方法。
a = 256
b = 256
c = 1000
d = 10*3
e = 2000
f = 2000
print(a is b) # True、出于对性能的考虑,Python内部会缓存一些整型对象,但凡是需要用些小整数时,就从这里面取,不再去临时创建新的对象。故而id相同
print(c is d) # False、1000的id 和 10*3的value一样,但是对应的id不同
print(e is f) # True、第一条
八、python内存管理
python中一切皆对象,对象又可以分为可变对象和不可变对象。如果修改后地址不变,则是可变对象,否则为不可变对象。
(1)内存池
python有内存池机制,pymalloc机制。 当创建大量消耗小内存的对象时,c语言中频繁调用new/malloc会导致大量的内存碎片,致使效率降低。 在python中使用内存池,内存池的概念就是预先在内存中申请一定数量的,大小相等的内存块留作备用,当有新的内存需求时,就先从内存池中分配内存给这个需求,不够了之后再申请新的内存。这样做最显著的优势就是能够减少内存碎片,提升效率。 查看源码,可以看到pymalloc对于小的对象,pymalloc会在内存池中申请空间,一般是少于256kb,如果是大的对象,则直接调用new/malloc来申请新的内存空间。
(2-1)垃圾回收机制
python采用GC作为自动内存管理机制,GC要做的有2件事,一是找到内存中无用的垃圾对象资源,二是清除找到的这些垃圾对象,释放内存给其他对象使用。
优点: 简单 实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。
缺点: 需要额外的空间维护引用计数。 不能解决对象的循环引用。(主要缺点) 。对象循环参考下面的代码
(2-2)标记清除
标记清除主要是解决循环引用问题。 对象之间通过引用(指针)连在一起,构成一个有向图,对象构成这个有向图的节点,而引用关系构成这个有向图的边。 从根对象(root object)出发,沿着有向边遍历对象,可达的(reachable)对象标记为活动对象,不可达的对象就是要被清除的非活动对象。根对象就是全局变量、调用栈、寄存器。 非活动对象会被GC回收。 详情可参考:https://juejin.im/post/5ca2471df265da307b2d45a3
(2-3)分代回收
python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代。 python将内存分为了3“代”,分别为年轻代(第0代)、中年代(第1代)、老年代(第2代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。 新创建的对象都会分配在年轻代,年轻代链表的总数达到上限时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉。 而那些不会回收的对象就会被移到中年代去,依此类推,老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。
a = {} # 对象a的引用计数为 1
b = {} # 对象b的引用计数为 1
a['b']=b # b的引用计数增1
b['a']=a # a的引用计数增1
del a # a的引用减 1,最后a对象的引用为 1
del b # b的引用减 1, 最后b对象的引用为 1
# 循环引用
# 执行del后,a、b对象已经没有任何引用指向这两个对象,需要被回收,但是a、b的计数器并不为0,故而无法通过计数法对两个对象回收,它们会一直驻留在内存中,就会造成了内存泄漏。
九、python魔法函数
- 魔法函数以__开始、__结束,是python内置的函数
- 自定义的类中使用魔法函数,该类就具有了该魔法函数的功能,比如使用iter魔法函数,该类就具有迭代功能。
- 魔法函数不要自己构建,而是使用python提供的魔法函数
class Company(object):
def __init__(self, employee_list):
self.employee = employee_list
def __getitem__(self, item):
return self.employee[item]
def __len__(self):
return 2
company = Company(["tom", "bob", "jane"])
# 实例化对象
employe = company.employee
for em in employe:
print(em)
print()
# 魔法函数,自动调用__getitem__
for em in company:
print(em)
print()
# 魔法函数,调用__len__,全局关键字len的优先级小于class里面的魔法函数
print(len(company))
十、理解__new__和__init__的区别
__init__方法为初始化方法、__new__方法才是真正的构造函数。
- 默认Foo类继承object类,而object类里面包含__new__方法,故而调用Foo类时,会自动调用object里面的__new__构造方法
- __new__ 方法创建实例对象供__init__ 方法使用,__init__方法定制实例对象。__new__方法必须返回值
- __init__方法为初始化方法,为类的实例提供一些属性或完成一些动作。__init__方法不需要返回值
- 如果在自定义的类中重构了__new__方法,那么调用该类时,就会使用重构的__new__方法
class Foo(object):
def __new__(cls, *agrs, **kwds):
inst = object.__new__(cls, *agrs, **kwds)
print("重构的__new__:", inst)
return inst
def __init__(self, price=50):
self.price = price
def how_much_of_book(self, n):
print("初始化__init__:", self)
return self.price * n
foo = Foo() # 当class中重构了__new__函数后,会优先调用重构的__new__函数
print(foo.how_much_of_book(8))
十一、三元表达式
为真时的结果 if 判定条件 else 为假时的结果
print(100 if 77>66 else 99)
十二、lambda表达式
lamba的代码简洁、返回结果也很简洁,但是lamba并不会带来程序运行效率的提高。
g = lambda x:x+2 # lamba传一个参数
info = [g(x) for x in range(10)]
print(info)
m = lambda x,y,z:(x-y)*z # lamba传多个参数
print(m(3,1,2))
dict1 = {"a":4, "b":3, "c":9}
dict2 = sorted(dict1.items(), key=lambda item:item[1], reverse=True)
print(dict2)
十三、assert断言
检查条件,不符合就报错终止程序。
a = -1
# assert a>0 # 报错
assert a<0
print(a)
十四、连接字符串用join还是+
join的性能明显好于+。这是因为,当用操作符+连接字符串的时候,每执行一次+都会申请一块新的内存,然后复制上一个+操作的结果和本次操作的右操作符到这块内存空间,因此用+连接字符串的时候会涉及好几次内存申请和复制。而join在连接字符串的时候,会先计算需要多大的内存存放结果,然后一次性申请所需内存并将字符串复制过去,这是为什么join的性能优于+的原因。
str1 = " ".join(["hello", "world"])
str2 = "hello " + "world"
print(str1)
print(str2)
十五、深拷贝和浅拷贝
python对象分为容器对象和非容器对象
容器对象:
列表、元组、字典和集合
非容器对象(对于非容器类型的对象没有拷贝一说):
数字、字符串和其他'原子'类型的对象
浅拷贝:
- 是将一个对象的地址赋值给一个变量,让新变量指向该地址
- 新的对象改变,旧的对象也会跟着改变
深拷贝:
- 在另一块内存地址中创建一个新的变量或容器,同时容器内的元素的地址也是新开辟的
- 新的对象改变,旧的对象不会跟着改变
from copy import deepcopy
# 浅拷贝,
a = ['hello',[1,2,3]]
b1 = a[:]
b2 = a
print([id(x) for x in a]) # 只是将新变量指向旧的变量地址
print([id(x) for x in b1])
print([id(x) for x in b2])
b2[0] = "p"
print(a) # 新的变量做了更改,旧的变量也会跟着改变
# 深拷贝
a = ['hello',[1,2,3]]
b = deepcopy(a)
print([id(x) for x in a]) # 地址不同
print([id(x) for x in b])
b[0] = "p"
print(a) # 新的对象改变,旧的对象不会跟着改变
十六、数组、链表、队列、堆栈等容器对象的区别
数组:
- python中没有数组的概念,这里把数组单独拿出来,是为了后面与列表做比较
- 在c语言中,定义一个数组,必须指定数组的长度和数据类型,例如 int a[4]={10,12,16,33}
- 相当于在内存中开辟长度为20的连续空间(注意是连续空间),a就指向该数组第一个元素的指针,每个内存空间真真实实的存储着10/12/16/33的二进制
列表:
- python中的列表可以混合存储任意数据类型,这是因为列表中存的是元素的内存地址,而不是元素的值。
- 对元素取值时,先在列表中找到元素的内存地址,再通过内存地址找到元素。因此,比起数组,对列表元素的取值要慢一些。
- 列表没有长度限制,当我们通过append方法向列表中添加元素时,如果列表满了,那么新申请2倍于原来列表的内存地址,并将原列表的值拷贝到新的内存地址中。
- 列表的删除,插入的时间复杂度是O(N)
栈:
- 栈是一个数据集合,遵循先进后出的特性
- 进栈:append
- 出栈:pop
- 查看栈顶元素:li[-1]
- 在python中通常用list来实现栈
堆栈溢出:
- 计算机中的内存可以分为堆存储、栈存储。其中程序中的变量保存在堆中,栈用来保存函数调用上下文等。一般程序嵌套层不会太多,因此堆的比例远大于栈的比例
- 我们通常说的内存溢出,就是堆溢出
- 在python中,函数调用是通过栈这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出
队列:
- 队列是一个数据集合,遵循先进先出的特性
- 在python中通常用deque来实现队列
链表
- 链表中每一个元素都是一个对象,每个对象称为一个节点,包含有数据域key和指向下一个节点的指针next。通过各个节点之间的相互连接,最终串联成一个链表
- 数组和列表都是连续存储的,删除和插入元素,其它元素需要补过去或者后移
- 不过数组删除插入要耗时一些,时间复杂度是O(n),而链表则不会这样,它的时间复杂度是O(1)
- 在python中通常定义class来实现链表
集合与字典
- 哈希表:是一种线性的存储结构(类似于数组),根据关键码的值(key, value)直接进行访问。将对象的Key通过一个哈希函数hash(key)换成一个整型数字,然后将该数字对数组长度进行取余,取余结果就当作数组的下标,将对象的值value存储在以该数字为下标的数组空间里。
- 例如:数据集合{1,7},假设存在哈希函数hash(x)使得hash(1)=0,hash(7)=4,那么这个哈希表被存储为[1,None, None, None, 7]。当我们查找7所在的位置时,通过哈希函数获得该元素在哈希表中的下标(hash(7)=4),然后在下标为4的位置取出该元素。哈希表查询速度非常的快,几乎是O(1)的时间复杂度。
- 哈希冲突:由于哈希表的下标范围是有限的,而元素关键字的值是接近无限的,因此可能会出现h(102)=56, h(2003)=56这种情况。此时,两个元素映射到同一个下标处,造成哈希冲突。
- 字典:python中的字典就是使用哈希表存储的。通过哈希函数将字典中元素的key映射为数组/列表下标。假如hash(‘name’)=3, hash(‘age’)=1, h(‘hobby’)=4, 则哈希表存储为[None, 18, None, 'Lena', '瓜子']。在字典键值对数量不多的情况下,几乎不会发生哈希冲突,此时查找一个元素的时间复杂度为O(1)。
# 16-1、括号匹配问题
def check(exp):
list1 = []
for char in exp:
if char in ["(", "[", "{"]:
list1.append(char)
elif char == ")":
if list1[-1]=="(":
list1.pop()
elif char == "]":
if list1[-1]=="[":
list1.pop()
elif char == "}":
if list1[-1]=="{":
list1.pop()
if len(list1) == 0:
return True
else:
return False
print(check("({[()]})"))
# 16-2、迷宫问题,深度遍历,栈实现
maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
# 约定按四个方向探寻:
dirs = [lambda x, y: (x-1, y), # 左
lambda x, y: (x+1, y), # 右
lambda x, y: (x, y-1), # 上
lambda x, y: (x, y+1)] # 下
def mazePath(x1, y1, x2, y2):
stack = []
stack.append((x1, y1)) # 起点位置入栈
while stack: # 栈不为空时循环;栈空代表死迷宫(找遍所有的路,直到回退到起点位置,继续回退(出栈),栈空)
curNode = stack[-1] # 栈顶,表示当前位置
if (curNode[0]==x2) and (curNode[1]==y2): # 到达终点,栈中的元素就是最终路径
for node in stack: # 打印路径
print(node)
return True
for dir in dirs: # 依次进行4个方向的探寻
nextNode = dir(curNode[0], curNode[1]) # 下一个位置坐标
if maze[nextNode[0]][nextNode[1]]==0: # 0代表通道,说明找到下一个方块
stack.append(nextNode) # 添加到栈顶,更新当前位置
maze[nextNode[0]][nextNode[1]]=-1 # 将0赋值为-1,标记为已经走过,防止死循环
break # 退出
else: # 四个方向都找完,没有通道,那么开始回退
maze[curNode[0]][curNode[1]] = -1 # 标记死路
stack.pop() # 弹出栈顶,回退
print("死迷宫")
return False # 死迷宫
mazePath(1, 1, 8, 8)
# 16-3、迷宫问题,广度遍历,队列实现
from collections import deque
maze = [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1],
[1, 0, 0, 0, 0, 1, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 0, 0, 0, 1],
[1, 0, 0, 0, 1, 0, 0, 0, 0, 1],
[1, 0, 1, 0, 0, 0, 1, 0, 0, 1],
[1, 0, 1, 1, 1, 0, 1, 1, 0, 1],
[1, 1, 0, 0, 0, 0, 0, 0, 0, 1],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]
dirs = [lambda x, y: (x-1, y), # 左
lambda x, y: (x+1, y), # 右
lambda x, y: (x, y-1), # 上
lambda x, y: (x, y+1)] # 下
def mazePath2(x1, y1, x2, y2):
queue = deque()
path = [] # 存放走过的节点
queue.append((x1, y1, -1)) # queue中元素:(1, 1, -1),前两位是坐标,第三位是其上一步在path中的索引。这里用-1标记起点。queue出了包含正确的路径,也会包含一些噪音节点。
maze[x1][y1] = -1
while queue:
curNode = queue.popleft()
path.append(curNode)
if curNode[0]==x2 and curNode[1]==y2:
printPath(path)
return True
for dir in dirs:
nextNode = dir(curNode[0], curNode[1])
if maze[nextNode[0]][nextNode[1]]==0: # 0代表通道,说明找到下一个方块,因为是广度遍历,故而上下左右都会尝试走一下,不会break
queue.append((nextNode[0], nextNode[1], len(path)-1)) # len(path)-1)记录来源索引,也就是记录当前节点的前一个节点路径
maze[nextNode[0]][nextNode[1]]=-1 # 标记该节点已经走过,防止死循环
return False
def printPath(path):
curNode = path[-1]
realpath = [] # 记录新的路径
while curNode[2] != -1: # 反溯到起始节点时,break
realpath.append(curNode[0:2])
curNode = path[curNode[2]] # 通过索引找到上一步节点
realpath.append(curNode[:2])
realpath.reverse() # list取反
print(realpath)
print(mazePath2(1, 1, 8, 8))
十七、提高python运行效率
JIT:是just in time的缩写, 也就是即时编译编译器。通常python的执行过程是,将.py的文件编译成.pyc的字节码,之后PVM将字节码编译成对应的机器指令执行。在循环语句中,编译器也会一条一条的将代码编译成字节码之后再翻译为机器指令,这非常耗时。而JIT就是解决这个问题的,针对那些频繁运行的程序,JIT会将这部分程序直接编译为机器指令并缓存起来,之后在运行时直接调用缓存的机器指令即可,大大缩减了程序运行的时间
CPython:一般说Python都是指CPython解释器
Cython: Cython将Python代码编译成C源码,再把C源码转换成Python扩展模块
Pypy: Pypy最重要的一点就是Pypy集成了JIT。Pypy的优点是对纯Python项目兼容性极好,缺点是对很多C语言库支持性不好,不能导入一些库
Numba: Numba是一个库,是一个用于编译python数组和数值计算函数的编译器,可以在运行时将Python代码编译为本地机器指令,而不会强制大幅度的改变普通的Python代码
Cython、Pypy、Numba的区别:
- 通用性:在三个方案中,Cython和Numba的兼容性都非常好,而Pypy对于部分库的支持较差(如Numpy,Scipy)
- 速度: 这三种方案的速度相差不大,通常来说Cython要快于Pypy,尤其是对于部分C扩展。Pypy要快于Numba,但针对于纯数值计算的工作,Numba甚至还要快于Cython。(通常用numba对numpy进行加速)
- 易用性:易用性最好的无疑是Pypy,Pypy是Python的解释器,我们针对纯Python使用Pypy,除了Pypy不支持的部分库外,不需要进行任何改动。然后是Numba,Numba的基本使用方法就是给函数加一个装饰器,易用性也很高,最后是Cython,因为Cython中需要使用Python+C混合编码,如果需要移植,代价会很大
import time
from numba import jit
# pypy 可以参考,https://zhuanlan.zhihu.com/p/28018577
time_start = time.time()
for i in range(100):
for j in range(i):
pass
time_end = time.time()
print("pypy执行耗时:", (time_end-time_start)) # python运行耗时3.5s、pypy运行耗时0.059s
# cPython代码可读性较差,一般很少用,故而不做详细介绍
# numba使用LLVM编译器架构将纯Python代码生成优化过的机器码,可以将面向数组和使用大量数学的python代码优化到与c,c++和Fortran类似的性能,而无需改变Python的解释器。
# python加速numpy等计算时,优先考虑numba
@jit
def f(x, y):
# A somewhat trivial example
return x + y
print("A = ", f(1,2))
# numba编译的函数可以调用其他编译函数
@jit
def square(x):
return x ** 2
@jit
def hypot(x, y):
return math.sqrt(square(x) + square(y))
print("B = ", hypot(1,2))
# numba有两种编译模式:nopython模式和object模式。前者能够生成更快的代码,但是有一些限制可能迫使numba退为后者。想要避免退为后者,而且抛出异常,可以传递nopython=True.
@jit(nopython=True)
def f(x, y):
return x + y
print("C = ", f(1,2))