Python细节
[on true] if [expression] else [false]
例子:
a,b = 1,2
num_max = a if a > b else b
英文:Global Interpreter Lock 缩写GIL 它是计算机程序语言解释器用于同步线程的一种机制,它使得在任何时刻仅有一个线程在执行,即使是在多核处理器上!
作用:
python为了利用多核,开始支持多线程,但线程是非独立的,所以同一进程里线程是数据共享,当各个线程访问数据资源时会出现竞状态,即数据可能会同时被多个线程占用,造成数据混乱,这就是线程的不安全。而解决多线程之间数据完整性和状态同步最简单的方式就是加锁。GIL能限制多线程同时执行,保证同一时间内只有一个线程在执行。
坏处:
GIL无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序
解决办法:
用进程+协程 代替 多线程的方式 在多进程中,由于每个进程都是独立的存在,所以每个进程内的线程都拥有独立的GIL锁,互不影响。但是,由于进程之间是独立的存在,所以进程间通信就需要通过队列的方式来实现。
当一个类继承自另一个类,它就被称为一个子类/派生类,继承自父类/基类/超类。它会继承/获取所有类成员(属性和方法)。
继承能让我们重新使用代码,也能更容易的创建和维护应用。Python支持如下种类的继承:
单继承:一个类继承自单个基类 多继承:一个类继承自多个基类 多级继承:一个类继承自单个基类,后者则继承自另一个基类 分层继承:多个类继承自单个基类 混合继承:两种或多种类型继承的混合
定义:环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。环境变量是在操作系统中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息。
获取环境变量
import os
# 程序名'my_env'
# 方式一
os.environ.get('my_env')
# 方式二
os.getenv('my_env')
当我们不知道函数传递多少个参数时,比如向函数传递一个列表或元组,就使用*args
def funx(*args):
for i in args:
print(i)
func([1,2,3,4])
当我们不知道该传递多少个关键字参数时,就使用**kwargs
def func(**kwargs):
for i in kwargs:
print(i,kwargs[i])
func({a:1,b:2,c:3})
str = ' a b c'
去除字符串左边的空格
str.lstrip()
去除字符串右边的空格
str.rstrip()
去除字符串两边的空格
str.strip()
去除字符串中的空格
str.strip().replace(' ','')
在用Python写代码时,有时可能还没想好函数怎么写,只写了函数声明,但为了保证语法正确,必须输入一些东西,在这种情况下,我们会使用pass语句。
def func(*args):
pass
break语句能让我们跳出循环
for i in range(7):
if i==3:
break
print(i)
# output:0,1,2
continue语句能让我们跳到下个循环
for i in range(7):
if i==3:
continue
print(i)
# output:0,1,2,4,5,6
当一个嵌套函数在其外部区域引用了一个值时,该嵌套函数就是一个闭包。其意义就是会记录这个值。
用比较容易懂的话说,就是当某个函数被当成对象返回时,夹带了外部变量,就形成了一个闭包
def A(x):
def B():
print(x) # 外部变量X
return B # 返回函数
装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
总而言之,装饰器的作用就是为已存在的函数或对象添加额外的功能
不带参数:
def funcs(func):
def eat():
print('i can eat')
return func()
return eat
@funcs
def dog():
print('i can wangwang')
dog()
带参数:
def funcs(func):
def eat(*args):
print('i can eat')
return func(*args)
return eat
@funcs
def dog(something):
print('i can wangwang')
print(something)
dog('i can watch the house')
- Numbers(数字)——用于保存数值
- Strings(字符串)——字符串是一个字符序列。我们用单引号或双引号来声明字符串。
- Lists(列表)——列表就是一些值的有序集合,我们用方括号声明列表。
- Tuples(元组)——元组和列表一样,也是一些值的有序集合,区别是元组是不可变的,意味着我们无法改变元组内的值。
- Dictionary(字典)——字典是一种数据结构,含有键值对。我们用大括号声明字典
切片是Python中的一种方法,能让我们只检索列表、元素或字符串的一部分。在切片时,我们使用切片操作符[]。
(1,2,3,4,5)[2:4]
集合就是一系列数据项的合集,不存在任何副本。另外,集合是无序的,这就意味着我们无法索引它,不过,集合是可变的。而不可变集合却不可变,这意味着我们无法改变它的值,从而也使其无法作为字典的键值
myset=frozenset([1,3,2,2])
myset
如果我们需要一个只有单一表达式的函数,我们可以匿名定义它。拉姆达表达式通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数。
max = lambda a,b : a if a > b else b
print(max(2,3))
在调用一个函数的过程中,直接或间接地调用了函数本身这个就叫递归。但为了避免出现死循环,必须要有一个结束条件
def facto(n):
if n==1:
return 1
return n*facto(n-1)
facto(4)
定义:生成器会生成一系列的值用于迭代,这样看它又是一种可迭代对象。它是在for循环的过程中不断计算出下一个元素,并在适当的条件结束for循环。
def intNum():
print("开始执行")
for i in range(5):
yield i
print("继续执行")
num = intNum()
定义:迭代器是访问集合元素的一种方式。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。
gen = (x for x in range(10))
print(gen) # <generator object <genexpr> at 0x7ff85a550660>
print(next(gen)) # 0
print(next(gen)) # 1
在使用生成器时,我们创建一个函数;在使用迭代器时,我们使用内置函数iter()和next()。 在生成器中,我们使用关键字‘yield’来每次生成/返回一个对象。 生成器中有多少‘yield’语句,你可以自定义。 每次‘yield’暂停循环时,生成器会保存本地变量的状态。而迭代器并不会使用局部变量,它只需要一个可迭代对象进行迭代。 使用类可以实现你自己的迭代器,但无法实现生成器。 生成器运行速度快,语法简洁,更简单。 迭代器更能节约内存。
Python使用按引用传递(pass-by-reference)将参数传递到函数中。如果你改变一个函数内的参数,会影响到函数的调用。这是Python的默认操作。不过,如果我们传递字面参数,比如字符串、数字或元组,它们是按值传递,这是因为它们是不可变的。
python不允许程序员选择采用传值还是传引用。Python参数传递采用的肯定是“传对象引用”的方式。这种方式相当于传值和传引用的一种综合。如果函数收到的是一个可变对象(比如字典或者列表)的引用,就能修改对象的原始值--相当于通过“传引用”来传递对象。如果函数收到的是一个不可变对象(比如数字、字符或者元组)的引用,就不能直接修改原始对象--相当于通过“传值’来传递对象。
深拷贝就是将一个对象拷贝到另一个对象中,这意味着如果你对一个对象的拷贝做出改变时,不会影响原对象。在Python中,我们使用函数deepcopy()执行深拷贝
import copy
b = copy.deepcopy(a)
浅拷贝则是将一个对象的引用拷贝到另一个对象上,所以如果我们在拷贝中改动,会影响到原对象。我们使用函数function()执行浅拷贝
b = copy.copy(a)
切片操作是浅拷贝
通常函数是浅拷贝:lst1 = list(lst)
copy函数:lst1 = copy.copy(lst)
注意一点,浅拷贝要分三种情况进行讨论
1.拷贝不可变对象:只是增加一个指向原对象的引用,改变会互相影响
>>> a = (1, 2, [3, 4])
>>> b = copy.copy(a)
>>> b
... (1, 2, [3, 4])
# 改变一方,另一方也改变
>>> b[2].append(5)
>>> a
... (1, 2, [3, 4, 5])
2.拷贝可变对象(一层结构):产生新的对象,开辟新的内存空间,改变互不影响。
>>> import copy
>>> a = [1, 2, 3]
>>> b = copy.copy(a)
>>> b
... [1, 2, 3]
# 查看两者的内存地址,不同,开辟了新的内存空间
>>> id(b)
... 1833997595272
>>> id(a)
... 1833997595080
>>> a is b
... False
# 改变了一方,另一方关我卵事
a = [1, 2, 3] b = [1, 2, 3]
>>> b.append(4)
>>> a
... [1, 2, 3]
>>> a.append(5)
>>> b
... [1, 2, 3, 4]
3.拷贝可变对象(多层结构):产生新的对象,开辟新的内存空间,不改变包含的子对象则互不影响、改变包含的子对象则互相影响。
>>> import copy
>>> a = [1, 2, [3, 4]]
>>> b = copy.copy(a)
>>> b
... [1, 2, [3, 4]]
# 查看两者的内存地址,不同,开辟了新的内存空间
>>> id(b)
1833997596488
>>> id(a)
1833997596424
>>> a is b
... False
# 1.没有对包含的子对象进行修改,另一方关我卵事
a = [1, 2, [3, 4]] b = [1, 2, [3, 4]]
>>> b.append(5)
>>> a
... [1, 2, [3, 4]]
>>> a.append(6)
>>> b
... [1, 2, [3, 4], 5]
# 2.对包含的子对象进行修改,另一方也随之改变
a = [1, 2, [3, 4]] b = [1, 2, [3, 4]]
>>> b[2].append(5)
>>> a
... [1, 2, [3, 4, 5]]
>>> a[2].append(6)
>>> b
... [1, 2, [3, 4, 5, 6]]
可变类型(mutable):列表,字典
不可变类型(unmutable):数字,字符串,元组
列表与元组
二者的主要区别是列表是可变的,而元组是不可变的
不同点一:不可变 VS 可变 两种类型除了字面上的区别(括号与方括号)之外,最重要的一点是tuple是不可变类型,大小固定,而 list 是可变类型、数据可以动态变化,这种差异使得两者提供的方法、应用场景、性能上都有很大的区别。
不同点二:同构 VS 异构 tuple 用于存储异构(heterogeneous)数据,当做没有字段名的记录来用,比如用 tuple 来记录一个人的身高、体重、年龄。
person = (“zhangsan”, 20, 180, 80) 比如记录坐标上的某个点
point = (x, y) 而列表一般用于存储同构数据(homogenous),同构数据就是具有相同意义的数据,比如下面的都是字符串类型
[“zhangsan”, “Lisi”, “wangwu”] 再比如 list 存放的多条用户记录
[(“zhangsan”, 20, 180, 80), (“wangwu”, 20, 180, 80)] 数据库操作中查询出来的记录就是由元组构成的列表结构。
因为 tuple 作为没有名字的记录来使用在某些场景有一定的局限性,所以又有了一个 namedtuple 类型的存在,namedtuple 可以指定字段名,用来当做一种轻量级的类来使用
列表与字典
列表是序列,可以理解为数据结构中的数组,字典可以理解为数据结构中的hashmap
他俩都可以作为集合来存储数据
从差异特征上来说
- list是有序的,dict是无需的
- list通过索引访问,dict使用key访问
- list随着数量的正常增长要想查找元素的时间复杂度为O(n), dict不随数量而增长而变化,时间负责都为O(1)
- dict的占用内存稍比list大,会在1.5倍左右
特征决定用途:
list一般可作为队列、堆栈使用,而dict一般作为聚合统计或者快速使用特征访问等
集合
集合是一种鲁棒性很好的数据结构,当元素顺序的重要性不如元素的唯一性和测试元素是否包含在集合中的效率时,大部分情况下这种数据结构极其有用。
python的内置集合类型有两种:
set(): 一种可变的、无序的、有限的集合,其元素是唯一的、不可变的(可哈希的)对象。 frozenset(): 一种不可变的、可哈希的、无序的集合,其元素是唯一的,不可变的哈希对象。
set里的元素必须是唯一的,不可变的。但是set是可变的,所以set作为set的元素会报错。
实现细节
CPython中集合和字典非常相似。事实上,集合被实现为带有空值的字典,只有键才是实际的集合元素。此外,集合还利用这种没有值的映射做了其它的优化。
由于这一点,可以快速的向集合中添加元素、删除元素、检查元素是否存在。平均时间复杂度为O(1),最坏的事件复杂度是O(n)。
print语句被python3废弃,只能使用print函数
Python3中字符串是Unicode (utf-8)编码,支持中文做标识符。
python2中是ASCII编码,需要更改字符集才能正常支持中文,所以在.py文件中会看到#-- coding: UTF-8
异常处理 Python2中try:…except Exception, e:…,在Python3中改为了try:…except Exception as e:…
Python3中不再使用xrange方法,只有range方法。
range在Python2中返回列表,而在Python3中返回range可迭代对象。
在Python2中有两个不等运算符!=和<>,在Python3中去掉了<>,只有!=符号表示不等
在Python2中双反引号可以替代repr函数,在Python3中去掉了双反引号的表是方法,只能用repr
方法。
StringIO模块现在被合并到新的io模组内。new, md5, gopherlib等模块被删除。
httplib, BaseHTTPServer, CGIHTTPServer, SimpleHTTPServer, Cookie, cookielib被合并到http包内。
取消了exec语句,只剩下exec()函数。
在Python2中long是比int取值范围更大的整数,Python3中取消了long类型,int的取值范围扩大到之前的long类型范围。
列表推导 不再支持[n for n in a,b]语法,改为[n for n in (a,b)]或[n for n in [a,b]]
python 2 中通过input输入的类型是int,只有通过raw_input()输入的类型才是str。
python 3中通过input输入的类型都是str,去掉了row_input()方法。
range返回的是一个包含所有元素的列表,xrange返回的是一个生成器,生成器是一个可迭代对象,在对生成器进行迭代时,元素是逐个被创建的。而列表需要根据列表长度而开辟出相应的内存空间用来遍历,一般来看,在对大序列进行迭代的时候,因为xrange的特性,所以它会比较节约内存。
普通实例方法,第一个参数需要是self,它表示一个具体的实例本身。
如果用了staticmethod,那么就可以无视这个self,而将这个方法当成一个普通的函数使用。
而对于classmethod,它的第一个参数不是self,是cls,它表示这个类本身。
@classmethod修饰符对应的函数不需要实例化,不需要self参数,第一个参数需要是表示自身类的cls参数,cls参数可以用来调用类的属性,类的方法,实例化对象等。
@staticmethod返回函数的静态方法,该方法不强制要求传递参数
示例:
class Cls(object):
"""类三种方法语法形式"""
def instance_method(self):
print('类的实例方法,只能被实例对象调用')
@staticmethod
def static_method():
print('静态方法,当属性使用')
@classmethod
def class_method(cls):
print('类方法')
cls = Cls()
cls.instance_method()
cls.static_method()
cls.class_method()
class Foo(object):
@staticmethod
def f():
print('我是静态函数')
Foo.f() # 静态方法无需实例化,也可实例化后调用
class A(object):
# 属性默认为类属性(可以给直接被类本身调用)
num = "类属性"
# 实例化方法(必须实例化类之后才能被调用)
def func1(self): # self : 表示实例化类后的地址id
print("func1")
print(self)
# 类方法(不需要实例化类就可以被类本身调用)
@classmethod
def func2(cls): # cls : 表示没用被实例化的类本身
print("func2")
print(cls)
print(cls.num)
cls().func1()
# 不传递默认self参数的方法(该方法也是可以直接被类调用的,但是这样做不标准)
def func3():
print("func3")
print(A.num) # 属性是可以直接用类本身调用的
1、map
一般情况map()函数接收两个参数,一个函数(该函数接收一个参数),一个序列,将传入的函数依次作用到序列的每个元素,并返回一个新的Iterator(迭代器)。 例如有这样一个list:[‘pYthon’, ‘jaVa’, ‘kOtlin’],现在要把list中每个元素首字母改为大写,其它的改为小写,可以这样操作:
>>> def f(s):
... return s.title()
...
>>> l = map(f, ['pYthon', 'jaVa', 'kOtlin'])
>>> list(l)
['Python', 'Java', 'Kotlin']
2、reduce
和map()用法类似,reduce把传入的函数作用在一个序列上,但传入的函数需要接收两个参数,传入函数的计算结果继续和序列的下一个元素做累积计算。
例如有一个list,里边的元素都是字符串,要把它拼接成一个字符串:
>>> from functools import reduce
>>> def f(x, y):
... return x + y
...
>>> reduce(f, ['ab', 'c', 'de', 'f'])
'abcdef'
3、filter
filter()同样接收一个函数和一个序列,然后把传入的函数依次作用于序列的每个元素,如果传入的函数返回true则保留元素,否则丢弃,最终返回一个Iterator。
例如一个list中元素有纯字母、纯数字、字母数字组合的,我们要保留纯字母的:
>>> def f(s):
... return s.isalpha()
...
>>> l = filter(f, ['abc', 'xyz', '123kg', '666'])
>>> list(l)
['abc', 'xyz']
4、sorted
sorted()函数就是用来排序的,同时可以自己定义排序的规则。
>>> sorted([6, -2, 4, -1])
[-2, -1, 4, 6]
>>> sorted([6, -2, 4, -1], key=abs)
[-1, -2, 4, 6]
>>> sorted([6, -2, 4, -1], key=abs, reverse=True)
[6, 4, -2, -1]
>>> sorted(['Windows', 'iOS', 'Android'])
['Android', 'Windows', 'iOS']
>>> d = [('Tom', 170), ('Jim', 175), ('Andy', 168), ('Bob', 185)]
>>> def by_height(t):
... return t[1]
...
>>> sorted(d, key=by_height)
[('Andy', 168), ('Tom', 170), ('Jim', 175), ('Bob', 185)]
进程
进程,是执行中的计算机程序。而每个程序,本身首先是一个进程
运行中每个进程都拥有自己的地址空间、内存、数据栈及其它资源。
操作系统本身自动管理着所有的进程(不需要用户代码干涉),并为这些进程合理分配可以执行时间。
进程可以通过派生新的进程来执行其它任务,不过每个进程还是都拥有自己的内存和数据栈等。
进程间可以通讯(发消息和数据),采用 进程间通信(IPC) 方式。
说明:
多个进程可以在不同的 CPU 上运行,互不干扰
同一个CPU上,可以运行多个进程,由操作系统来自动分配时间片
由于进程间资源不能共享,需要进程间通信,来发送数据,接受消息等
多进程,也称为“并行”。
进程间通信:
进程彼此之间互相隔离,要实现进程间通信(IPC),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的。
进程队列queue
不同于线程queue,进程queue的生成是用multiprocessing模块生成的。
在生成子进程的时候,会将代码拷贝到子进程中执行一遍,及子进程拥有和主进程内容一样的不同的名称空间。
multiprocess.Queue 是跨进程通信队列
常用方法
q.put方法用以插入数据到队列中,put方法还有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。
q.get方法可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked和timeout。如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.
q.get_nowait():同q.get(False)
q.put_nowait():同q.put(False)
q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样
管道pipe
管道就是管道,就像生活中的管道,两头都能进能出
默认管道是全双工的,如果创建管道的时候映射成False,左边只能用于接收,右边只能用于发送,类似于单行道
import multiprocessing
def foo(sk):
sk.send('hello world')
print(sk.recv())
if __name__ == '__main__':
conn1,conn2=multiprocessing.Pipe() #开辟两个口,都是能进能出,括号中如果False即单向通信
p=multiprocessing.Process(target=foo,args=(conn1,)) #子进程使用sock口,调用foo函数
p.start()
print(conn2.recv()) #主进程使用conn口接收
conn2.send('hi son') #主进程使用conn口发送
常用方法
conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。
conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象
注意:send()和recv()方法使用pickle模块对对象进行序列化
进程池
开多进程是为了并发,通常有几个cpu核心就开几个进程,但是进程开多了会影响效率,主要体现在切换的开销,所以引入进程池限制进程的数量。
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。
线程
线程,是在进程中执行的代码。
一个进程下可以运行多个线程,这些线程之间共享主进程内申请的操作系统资源。
在一个进程中启动多个线程的时候,每个线程按照顺序执行。现在的操作系统中,也支持线程抢占,也就是说其它等待运行的线程,可以通过优先级,信号等方式,将运行的线程挂起,自己先运行。
使用
用户编写包含线程的程序(每个程序本身都是一个进程)
操作系统“程序切换”进入当前进程
当前进程包含了线程,则启动线程
多个线程,则按照顺序执行,除非抢占
特性
线程,必须在一个存在的进程中启动运行
线程使用进程获得的系统资源,不会像进程那样需要申请CPU等资源
线程无法给予公平执行时间,它可以被其他线程抢占,而进程按照操作系统的设定分配执行时间
每个进程中,都可以启动很多个线程
说明
多线程,也被称为”并发“执行。
线程池
系统启动一个新线程的成本是比较高的,因为它涉及与操作系统的交互。在这种情形下,使用线程池可以很好地提升性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。
此外,使用线程池可以有效地控制系统中并发线程的数量。当系统中包含有大量的并发线程时,会导致系统性能急剧下降,甚至导致 Python 解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。
共享变量
创建全局变量,多个线程公用一个全局变量,方便简单。但是坏处就是共享变量容易出现数据竞争,不是线程安全的,解决方法就是使用互斥锁。
变量共享引申出线程同步问题
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。 使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。
队列
线程间使用队列进行通信,因为队列所有方法都是线程安全的,所以不会出现线程竞争资源的情况
Queue.Queue 是进程内非阻塞队列
进程与线程的区别
一个进程中的各个线程与主进程共享相同的资源,与进程间互相独立相比,线程之间信息共享和通信更加容易(都在进程中,并且共享内存等)。
线程一般以并发执行,正是由于这种并发和数据共享机制,使多任务间的协作成为可能。
进程一般以并行执行,这种并行能使得程序能同时在多个CPU上运行;
区别于多个线程只能在进程申请到的的“时间片”内运行(一个CPU内的进程,启动了多个线程,线程调度共享这个进程的可执行时间片),进程可以真正实现程序的“同时”运行(多个CPU同时运行)。
进程和线程的常用应用场景
一般来说,在Python中编写并发程序的经验:
计算密集型任务使用多进程
IO密集型(如:网络通讯)任务使用多线程,较少使用多进程.
这是由于 IO操作需要独占资源,比如:
网络通讯(微观上每次只有一个人说话,宏观上看起来像同时聊天)每次只能有一个人说话
文件读写同时只能有一个程序操作(如果两个程序同时给同一个文件写入 ‘a’, ‘b’,那么到底写入文件的哪个呢?)
都需要控制资源每次只能有一个程序在使用,在多线程中,由主进程申请IO资源,多线程逐个执行,哪怕抢占了,也是逐个运行,感觉上“多线程”并发执行了。
如果多进程,除非一个进程结束,否则另外一个完全不能用,显然多进程就“浪费”资源了。
当然如上解释可能还不足够立即理解问题所在,让我们通过不断的实操来体验其中的“门道”。
协程
协程: 协程,又称微线程,纤程,英文名Coroutine。协程的作用,是在执行函数A时,可以随时中断,去执行函数B,然后中断继续执行函数A(可以自由切换)。但这一过程并不是函数调用(没有调用语句),这一整个过程看似像多线程,然而协程只有一个线程执行.
协程由于由程序主动控制切换,没有线程切换的开销,所以执行效率极高。对于IO密集型任务非常适用,如果是cpu密集型,推荐多进程+协程的方式。
协程,又称微线程。
说明
协程的主要特色是:
协程间是协同调度的,这使得并发量数万以上的时候,协程的性能是远远高于线程。
注意这里也是“并发”,不是“并行”。
常用库:greenlet gevent
协程优点:
1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
2. 单线程内就可以实现并发的效果,最大限度地利用cpu
协程缺点:
1.协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
2.协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
python中的协程
一个协程是一个函数/子程序(可以认为函数和子程序是指一个东西)。这个函数可以暂停执行, 把执行权让给 YieldInstruction,等 YieldInstruction 执行完成后,这个函数可以继续执行。 这个函数可以多次这样的暂停与继续。
注:这里的 YieldInstruction, 我们其实也可以简单理解为函数。
协程可以在“卡住”的时候可以干其它事情。
async def long_task():
... print('long task started')
... await asyncio.sleep(1)
... print('long task finished')
...
>>> loop.create_task(long_task())
<Task pending coro=<long_task() running at <stdin>:1>>
>>> loop.create_task(job1()) >>>>> loop.create_task(job1())
<Task pending coro=<job1() running at <stdin>:1>>
>>>
>>> try:
... loop.run_forever()
... except KeyboardInterrupt:
... pass
...
long task started
job1 started...
job1 paused
hello world
job1 resumed
job1 finished
long task finished
从这段程序的输出可以看出,程序本来是在执行 long task 协程,但由于 long task 要 await sleep 1 秒,于是 long task 自动暂停了,hello_world 协程自动开始执行, hello world 执行完之后,long task 继续执行。
协程有两种定义的方法, 其中使用生成器形式定义的协程叫做 generator-based coroutine, 通过 async/await 声明的协程叫做 native coroutine,两者底层实现都是生成器。接着, 我们阐述了协程的概念,从概念和例子出发,讲了协程和生成器最主要的特征:可以暂停执行和恢复执行。
协程异常处理
使用协程的时候一定加了很多的异常,但百密而一疏,总是会有想象不到的异常发生,这个时候为了不让程序整体崩溃应该使用协程的额外异常处理方法,这个方法会去执行绑定的回调函数.
g_dict=dict{}
g = gevent.spawn(self._g_fetcher, feed_name) # 创建协程
g_dict[feed_name] = [g,False]
g.link_exception(self._link_exception_callback) # 给该协程添加出现处理不了的异常时候的回调函数
def _link_exception_callback(self, g):
# 可能遇到无法修复的错误,需要修改代码 todo 报警
# 可以在这个函数里面做一些错误异常的打印,或者文件的关闭,连接的关闭.
self.terminated_flag = True # 停止整个程序 让 supervior重启
logger.info("_link_exception_callback {0} {1}".format(g, g.exception))
self._kill_sleep_gevent() # 轮询结束休眠的协程
def _kill_sleep_gevent(self):
for i,is_sleep in g_dict.items():
if is_sleep[1] == "True":
gevent.kill(is_sleep[0])
greenlet框架实现协程(封装yield的基础库)
greenlet机制的主要思想是:生成器函数或者协程函数中的yield语句挂起函数的执行,直到稍后使用next()或send()操作进行恢复为止。可以使用一个调度器循环在一组生成器函数之间协作多个任务。greentlet是python中实现我们所谓的"Coroutine(协程)"的一个基础库。
基于greenlet框架的高级库gevent模块
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
原生协程
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存,在调度回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合
async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖
import time
def job(t):
time.sleep(t)
print('用了%s' % t)
def main():
[job(t) for t in range(1,3)]
start = time.time()
main()
print(time.time()-start)
import time
import asyncio
async def job(t): # 使用 async 关键字将一个函数定义为协程
await asyncio.sleep(t) # 等待 t 秒, 期间切换执行其他任务
print('用了%s秒' % t)
async def main(loop): # 使用 async 关键字将一个函数定义为协程
tasks = [loop.create_task(job(t)) for t in range(1,3)] # 创建任务, 不立即执行
await asyncio.wait(tasks) # 执行并等待所有任务完成
start = time.time()
loop = asyncio.get_event_loop() # 建立 loop
loop.run_until_complete(main(loop)) # 执行 loop
loop.close() # 关闭 loop
print(time.time()-start)