文章目录
- Python语言特性
- 1 Python的函数参数传递
- 2 Python中的元类(metaclass)
- 3 @staticmethod和@classmethod
- 4 类变量和实例变量
- 5 Python自省
- 6 Python推导式-列表推导式、元组推导式、字典推导式和集合推导式
- 7 Python中单下划线和双下划线
- 8 字符串格式化:%和.format
- 9 迭代器和生成器
- 10 `*args` and `**kwargs`
- 11 面向切面编程AOP和装饰器
- 12 鸭子类型
- 13 Python中重载
- 14 浅析Python运算符重载
- 15 `__new__`和`__init__`的区别
- 16 单例模式
- 17 Python中的作用域与闭包
- 18 进程、线程、GIL线程全局锁
- 19 协程
- Gevent
- 总结
- 20 闭包
- 21 lambda函数
- 22 Python函数式编程
- 23 Python里的拷贝
- 24 Python垃圾回收机制
- 25 Python的List
- 26 Python的is
- 27 read,readline和readlines
- 28 Python2和3的区别
- 29 super init
- 30 range and xrange
- 31 __str__和repr__的区别
- 32 * 和 ** 的语法多义性,具体来说是有四类用法。
- 33.python中内置的数据结构有几种?
- 34. 类的内置成员函数
- 1. `__doc__`表示类的描述信息
- 2. `__module__ `和 ` __class__ `
- 3.` __init__`
- 4. `__del__`
- 5. `__call__`
- 6. `__dict__`
- 7. `__str__`
- 8、`__getitem__`、`__setitem__`、`__delitem__`
- 9、`__getslice__`、`__setslice__`、`__delslice__`
- 10.` __iter__ `
- 11.`__contains__`
- 12.`__lt__`,`__le__`,`__eq__`,`__ne__`,`__gt__`,`__ge__`
- 13. `__new__ `和 `__metaclass__`
- 35 Python数据结构直接的区别
Python语言特性
1 Python的函数参数传递
-
有的变量都可以理解是内存中一个对象的**“引用”,或者,也可以看似c中void***的感觉。
-
类型是属于***对象***的,而不是***变量***。
-
对象有两种,“可更改”(mutable)与“不可更改”(immutable)对象。
-
在python中,strings, tuples, 和numbers是不可更改的对象,
-
list, dict, set 等则是可以修改的对象。
- 当一个引用传递给函数的时候,函数自动复制一份引用,这个函数里的引用和外边的引用没有半毛关系了。
- 第一个例子:函数把引用指向了一个不可变对象,当函数返回的时候,外面的引用没半毛感觉.
- 第二个例子:函数内的引用指向的是可变对象,对它的操作就和定位了指针地址一样,在内存里进行修改.
-
-
引用传递和值传递(函数传值)
-
说明:可变对象为引用传递,不可变对象为值传递
-
引用传递:传递列表或者字典时,如果改变引用的值,就修改了原始的对象
-
通过id
来看引用a
的内存地址可以比较理解:
a = 1
def fun(a):
print "func_in",id(a) # func_in 41322472
a = 2
print "re-point",id(a), id(2) # re-point 41322448 41322448
print "func_out",id(a), id(1) # func_out 41322472 41322472
fun(a)
print a # 1
注:具体的值在不同电脑上运行时可能不同。
可以看到,在执行完a = 2
之后,a
引用中保存的值,即内存地址发生变化,由原来1
对象的所在的地址变成了2
这个实体对象的内存地址。
而第2个例子a
引用保存的内存值就不会发生变化:
a = []
def fun(a):
print "func_in",id(a) # func_in 53629256
a.append(1)
print "func_out",id(a) # func_out 53629256
fun(a)
print a # [1]
如果还不明白的话,这里有更好的解释: http://stackoverflow.com/questions/986006/how-do-i-pass-a-variable-by-reference
2 Python中的元类(metaclass)
(1) 概述
-
Python虽然是多范式的编程语言,但它的数据模型却是 纯面向对象 的。与那些仅在语法层面声称纯OO的编程语言(如Java)相比,Python的这种纯粹性更加深入骨髓。
-
在Python的世界里,一切皆为对象:数值、序列、字典、函数、模块、文件、类、类实例 等等,无一例外(参考 Data model)。其中,“类也是对象” 的概念最让人匪夷所思,这完全超越了传统的OO思想。
-
元类(metaclass)是Python 2.2中引入的概念,利用元类可以 定制类的创建行为(Customizing class creation)。“元类” 的概念同样让人难以理解,然而理解 “元类” 是理解 “类也是对象” 的关键。
(2) 详细描述
<1> 类的创建过程
对于类定义:
class Foo(Base):
def say(self):
print 'hello'
-
Python解释器 执行class语句 时,处理步骤如下:
-
确定元类
mcls
。元类的查找优先级为:- 首先查找 类Foo 是否拥有属性
__metaclass__
- 否则查找 类Foo的父类 是否具有属性
__metaclass__
- 否则查找 类Foo所在模块 是否具有全局变量
__metaclass__
- 否则使用默认元类(经典类:
types.ClassType
;新式类:type
)
- 首先查找 类Foo 是否拥有属性
-
使用元类
mcls
创建类Foo
。创建语意等价于:
-
def say(self):
print 'hello'
# 元类的参数:mcls(name, bases, dict)
Foo = mcls('Foo', (Base,), {'say': say})
3. 创建成功后,类Foo
是 元类mcls
的 实例。
综上:创建类 其实是一种更高级别的 实例化过程,本质上与 创建类实例 相似。
实例化过程 | 类 | 实例 | 语意形式 |
---|---|---|---|
创建类Foo | 元类mcls | 类Foo | class Foo: pass <=> Foo = mcls(‘Foo’, (), {}) |
创建类实例foo | 类Foo | 类实例foo | foo = Foo() |
<2> 元类的使用惯例
-
原则上,元类可以是:任何接受参数 name, bases, dict 并返回 类 的 可调用对象(参考 metaclass)。
- 例如元类可以是 函数:
def metacls_func(name, bases, dict): # do customizing here return type(name, bases, dict
-
根据最佳实践指导,更好的习惯是使用 类 作为元类,典型风格如下:
class MetaCls(type): def __new__(cls, name, bases, dict): # do customizing here return super(MetaCls, cls).__new__(cls, name, bases, dict)
-
注意:
- 元类可以继承自另一个元类,也可以使用其他元类
- 除了常用的
__new__
,还可以借助__init__
和__call__
来定制被创建的类
3 @staticmethod和@classmethod
Python其实有3个方法
- 静态方法(staticmethod)
- 类方法(classmethod)
- 实例方法。如下:
def foo(x):
print "executing foo(%s)"%(x)
class A(object):
def foo(self,x):
print "executing foo(%s,%s)"%(self,x)
@classmethod
def class_foo(cls,x):
print "executing class_foo(%s,%s)"%(cls,x)
@staticmethod
def static_foo(x):
print "executing static_foo(%s)"%x
a=A()
- 函数参数里面的self和cls。
- self和cls是对***类或者实例***的绑定,
- 对于一般的函数来说,我们可以这么调用
foo(x)
,这个函数就是最常用的。它的工作跟类、实例无关。 - 对于实例方法,我们知道在类里每次定义方法的时候都需要绑定这个实例,就是
foo(self, x)
。实例方法的调用离不开实例,我们需要把实例自己传给函数,调用的时候是:a.foo(x)
(其实是foo(a, x)
)。类方法一样。只不过它传递的是类而不是实例,A.class_foo(x)
。- 注意这里的self和cls可以替换别的参数,但是python的约定是这俩,还是不要改的好。
- 对于一般的函数来说,我们可以这么调用
- self和cls是对***类或者实例***的绑定,
对于静态方法其实和普通的方法一样,不需要对谁进行绑定,唯一的区别是调用的时候需要使用a.static_foo(x)
或者A.static_foo(x)
来调用.
\ | 实例方法 | 类方法 | 静态方法 |
---|---|---|---|
a = A() | a.foo(x) | a.class_foo(x) | a.static_foo(x) |
A | 不可用 | A.class_foo(x) | A.static_foo(x) |
更多关于这个问题:
- http://stackoverflow.com/questions/136097/what-is-the-difference-between-staticmethod-and-classmethod-in-python
- https://realpython.com/blog/python/instance-class-and-static-methods-demystified/
4 类变量和实例变量
-
类变量:
是可在类的所有实例之间共享的值(也就是说,它们不是单独分配给每个实例的)。例如下例中, num_of_instance 就是类变量,用于跟踪存在着多少个Test 的实例。
-
实例变量:
实例化之后,每个实例单独拥有的变量。
参考:http://stackoverflow.com/questions/6470428/catch-multiple-exceptions-in-one-line-except-block
5 Python自省
Python自省-------在运行时能够获得对象的类型
方法 | 作用 |
---|---|
help() | 查看函数或模块用途的详细说明 |
dir() | 返回对象所有属性 |
type() | 查看对象类型 |
hasattr() | 查看对象是否有特定属性 |
getattr() | 得到对象的特定属性 |
seetattr() | 设置对象的特定属性 |
isinstance() | 判断一个对象是否是一个已知的类型 |
issubclass() | 判断一个类是不是另一个类的子类 |
id() | 返回地址值 |
callable() | 判断对象是否可调用 |
a = [1,2,3]
b = {'a':1,'b':2,'c':3}
c = True
print type(a),type(b),type(c) # <type 'list'> <type 'dict'> <type 'bool'>
print isinstance(a,list) # True
6 Python推导式-列表推导式、元组推导式、字典推导式和集合推导式
d = {key: value for (key, value) in iterable}
- 推导式(又称解析器),是 Python 独有的一种特性。
- 使用推导式可以快速生成列表、元组、字典以及集合类型的数据
- 因此推导式又可细分为:
- 列表推导式、
- 元组推导式、
- 字典推导式
- 集合推导式。
<1> 列表推导式
-
列表推导式可以利用 range 区间、元组、列表、字典和集合等数据类型,快速生成一个满足指定需求的列表。
-
列表推导式的语法格式如下:
-
[表达式 for 迭代变量 in 可迭代对象 [if 条件表达式] ]
-
此格式中,[if 条件表达式] 不是必须的,可以使用,也可以省略。
-
列表推导式的语法格式,除去 [if 条件表达式] 部分,其余各部分的***含义以及执行顺序***和 for 循环是完全一样的(表达式其实就是 for 循环中的循环体),即它的执行顺序如下所示:
-
for 迭代变量 in 可迭代对象
表达式
-
-
-
-
这样认为,它只是对 for 循环语句的格式做了一下简单的变形,并用 [] 括起来而已,只不过最大的不同之处在于,列表推导式最终会将循环过程中,计算表达式得到的一系列值组成一个列表。
-
例如如下代码(程序一):
a_range = range(10) # 对a_range执行for表达式 a_list = [x * x for x in a_range] # a_list集合包含10个元素 print(a_list) # 运行上面代码,可以看到如下输出结果: [0 , 1 , 4 , 9 , 16 , 25 , 36 , 49 , 64, 81]
我们还可以在列表推导式中添加 if 条件语句,这样列表推导式将只迭代那些符合条件的元素。例如如下代码:
b_list = [x * x for x in a_range if x % 2 == 0] # a_list集合包含5个元素 print(b_list) # 运行上面代码,可以看到如下输出结果: [0 ,4 , 16, 36, 64]
另外,以上所看到的列表推导式都只有一个循环,实际上它可使用多个循环,就像嵌套循环一样。例如如下代码:
d_list = [(x, y) for x in range(5) for y in range(4)] # d_list列表包含20个元素 print(d_list) #运行上面代码,可以看到如下输出结果: [(0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1), (1, 2), (1, 3), (2, 0), (2, 1), (2, 2), (2, 3), (3, 0), (3, 1), (3, 2), (3, 3), (4, 0), (4, 1), (4, 2), (4, 3)]
上面代码中,x 是遍历 range(5) 的迭代变量(计数器),因此该 x 可迭代 5 次;y 是遍历 range(4) 的计数器,因此该 y 可迭代 4 次。因此,该(x,y)表达式一共会迭代 20 次。上面的 for 表达式相当于如下嵌套循环:
dd_list = [] for x in range(5): for y in range(4): dd_list.append((x, y))
当然,也支持类似于三层嵌套的 for 表达式,例如如下代码:
e_list = [[x, y, z] for x in range(5) for y in range(4) for z in range(6)] # e_list列表包含120个元素 print(e_list)
对于包含多个循环的 for 表达式,同样可指定 if 条件。假如我们有一个需求:程序要将两个列表中的数值按“能否整除”的关系配对在一起。比如 src_a 列表中包含 30,src_b 列表中包含 5,其中 30 可以整除 5,那么就将 30 和 5 配对在一起。对于上面的需求使用 for 表达式来实现非常简单,例如如下代码:
src_a = [30, 12, 66, 34, 39, 78, 36, 57, 121] src_b = [3, 5, 7, 11] # 只要y能整除x,就将它们配对在一起 result = [(x, y) for x in src_b for y in src_a if y % x == 0] print(result)
运行上面代码,可以看到如下输出结果:
[(3, 30), (3, 12), (3, 66), (3, 39), (3, 78), (3, 36), (3, 57), (5, 30), (11, 66), (11, 121)]
<2> Python元组推导式
-
元组推导式可以利用 range 区间、元组、列表、字典和集合等数据类型,快速生成一个满足指定需求的元组。
-
元组推导式的语法格式如下:
(表达式 for 迭代变量 in 可迭代对象 [if 条件表达式] )
其中,用 [] 括起来的部分,可以使用,也可以省略。
例如,我们可以使用下面的代码生成一个包含数字 1~9 的元组:
a = (x for x in range(1,10)) print(a) 运行结果为: <generator object <genexpr> at 0x0000020BAD136620>
从上面的执行结果可以看出,使用元组推导式生成的结果并不是一个***元组***,而是一个***生成器对象***(后续会介绍),这一点和列表推导式是不同的。
如果我们想要使用元组推导式获得新元组或新元组中的元素,有以下三种方式:
-
使用 tuple() 函数,可以直接将生成器对象转换成元组,例如:
a = (x for x in range(1,10)) print(tuple(a)) 运行结果为: (1, 2, 3, 4, 5, 6, 7, 8, 9)
-
直接使用 for 循环遍历生成器对象,可以获得各个元素,例如:
a = (x for x in range(1,10)) for i in a: print(i,end=' ') print(tuple(a)) # 运行结果为: 1 2 3 4 5 6 7 8 9 ()
-
使用 ***next() 方法***遍历生成器对象,也可以获得各个元素,例如:
a = (x for x in range(3)) print(a.__next__()) print(a.__next__()) print(a.__next__()) a = tuple(a) print("转换后的元组:",a) 运行结果为: 0 1 2 转换后的元组: ()
注意,无论是使用 for 循环遍历生成器对象,还是使用 next() 方法遍历生成器对象,遍历后***原生成器对象将不复存在***,这就是遍历后转换原生成器对象却得到空元组的原因。
<3> Python字典推导式
-
Python 中,使用字典推导式可以借助列表、元组、字典、集合以及 range 区间,快速生成符合需求的字典。
-
字典推导式的语法格式如下:
-
{表达式 for 迭代变量 in 可迭代对象 [if 条件表达式]}
其中,用 [] 括起来的部分,可以使用,也可以省略。
可以看到,和其它推导式的语法格式相比,唯一不同在于,字典推导式用的是大括号{}。
-
【例 1】
listdemo = ['C语言中文网','c.biancheng.net']
#将列表中各字符串值为键,各字符串的长度为值,组成键值对
newdict = {key:len(key) for key in listdemo}
print(newdict)
# 运行结果为:
{'C语言中文网': 6, 'c.biancheng.net': 15}
【例 2】交换现有字典中各键值对的键和值。
olddict={'C语言中文网': 6, 'c.biancheng.net': 15}
newdict = {v: k for k, v in olddict.items()}
print(newdict)
# 运行结果为:
{6: 'C语言中文网', 15: 'c.biancheng.net'}
【例 3】使用 if 表达式筛选符合条件的键值对。
olddict={'C语言中文网': 6, 'c.biancheng.net': 15}
newdict = {v: k for k, v in olddict.items() if v>10}
print(newdict)
# 运行结果为:
{15: 'c.biancheng.net'}
<4> Python集合推导式
-
Python中,使用集合推导式可以借助列表、元组、字典、集合以及 range 区间,快速生成符合需求的集合。
-
集合推导式的语法格式和字典推导式完全相同,如下所示:
{ 表达式 for 迭代变量 in 可迭代对象 [if 条件表达式] }
其中,用 [] 括起来的部分,可以使用,也可以省略。
【例 1】
setnew = {i**2 for i in range(3)}
print(setnew)
运行结果为:
{0, 1, 4}
【例 2】既然生成的是集合,那么其保存的元素必须是唯一的。
tupledemo = (1,1,2,3,4,5,6,6)
setnew = {x**2 for x in tupledemo if x%2==0}
print(setnew)
运行结果为:
{16, 4, 36}
【例 3】
dictdemo = {'1':1,'2':2,'3':3}
setnew = {x for x in dictdemo.keys()}
print(setnew)
运行结果为:
{'2', '1', '3'}
7 Python中单下划线和双下划线
>>> class MyClass():
... def __init__(self):
... self.__superprivate = "Hello"
... self._semiprivate = ", world!"
...
>>> mc = MyClass()
>>> print mc.__superprivate
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: myClass instance has no attribute '__superprivate'
>>> print mc._semiprivate
, world!
>>> print mc.__dict__
{'_MyClass__superprivate': 'Hello', '_semiprivate': ', world!'}
-
__foo__
:一种约定,Python内部的名字,用来区别其他用户自定义的命名,以防冲突,就是例如__init__()
,__del__()
,__call__()
这些特殊方法 -
_foo
:一种约定,用来***指定变量私有***。程序员用来指定私有变量的一种方式。不能用from module import * 导入,其他方面和公有一样访问; -
__foo
:这个有真正的意义:解析器用***_classname__foo
***来代替这个名字,以区别和其他类相同的命名,它无法直接像公有成员一样随便访问,通过对象名._类名__xxx这样的方式可以访问。 -
foo_
:一个变量的***最合适的名称***已经被一个***关键字***所占用。 因此,像class或def这样的名称不能用作Python中的变量名称。 在这种情况下,你可以附加一个下划线来解决命名冲突。 -
单下划线 _:有时候单个独立下划线是用作一个名字,来表示某个变量是临时的或无关紧要的。
例如,在下面的循环中,我们不需要访问正在运行的索引,我们可以使用"_"来表示它只是一个临时值:
>>> for _ in range(32): ... print('Hello, World.')
在***拆分(unpacking)表达式***中将单个下划线用作"不关心的"变量,以忽略特定的值。 同样,这个含义只是"依照约定",并不会在Python解释器中触发特殊的行为。 单个下划线仅仅是一个有效的变量名称,会有这个用途而已。
详情见:http://stackoverflow.com/questions/1301346/the-meaning-of-a-single-and-a-double-underscore-before-an-object-name-in-python
或者: http://www.zhihu.com/question/19754941
8 字符串格式化:%和.format
.format
更便利。%
无法同时传递一个变量和元组,现在不推荐
.format
用法
* 该函数把字符串当成一个模板,通过传入的参数进行格式化,并且使用大括号‘{}’作为特殊字符代替‘%’
位置匹配
(1)不带编号,即“{}”
(2)带数字编号,可调换顺序,即“{1}”、“{2}”
(3)带关键字,即“{a}”、“{tom}”
-
不带字段
print('{} {}'.format('hello','world')) # hello world
-
带数字编号
print('{0} {1}'.format('hello','world')) # hello world # 打乱顺序 print('{0} {1} {0}'.format('hello','world')) # hello world
-
带关键字
print('{a} {tom} {a}'.format(tom='hello',a='world')) # world hello world
-
通过下标或key匹配参数
coord = (3, 5) 'X: {0[0]}; Y: {0[1]}'.format(coord) # 'X: 3; Y: 5' a = {'a': 'test_a', 'b': 'test_b'} 'X: {0[a]}; Y: {0[b]}'.format(a)# 'X: test_a; Y: test_b'
格式转换
- ‘b’ - 二进制。将数字以2为基数进行输出。
- ‘c’ - 字符。在打印之前将整数转换成对应的Unicode字符串。
- ‘d’ - 十进制整数。将数字以10为基数进行输出。
- ‘o’ - 八进制。将数字以8为基数进行输出。
- ‘x’ - 十六进制。将数字以16为基数进行输出,9以上的位数用小写字母。
- ‘e’ - 幂符号。用科学计数法打印数字。用’e’表示幂。
- ‘g’ - 一般格式。将数值以fixed-point格式输出。当数值特别大的时候,用幂形式打印。
- ‘n’ - 数字。当值为整数时和’d’相同,值为浮点数时和’g’相同。不同的是它会根据区域设置插入数字分隔符。
- ‘%’ - 百分数。将数值乘以100然后以fixed-point(‘f’)格式打印,值后面会有一个百分号。
print('{0:b}'.format(3))# 11
print('{:c}'.format(20))#
print('{:d}'.format(20))#20
print('{:o}'.format(20))#24
print('{:x}'.format(20))#14
print('{:e}'.format(20))#2.000000e+01
print('{:g}'.format(20.1))# 20.1
print('{:f}'.format(20))#20.000000
print('{:n}'.format(20))#20
print('{:%}'.format(20))#2000.000000%
进阶用法
(1)进制转换
"int: {0:d}; hex: {0:x}; oct: {0:o}; bin: {0:b}".format(42) #'int: 42; hex: 2a; oct: 52; bin: 101010
- 在前面加“#”,则带进制前缀
"int: {0:d}; hex: {0:#x}; oct: {0:#o}; bin: {0:#b}".format(42)
'int: 42; hex: 0x2a; oct: 0o52; bin: 0b101010
(2) 左中右对齐及位数补全
-
<
(默认)左对齐 -
>
右对齐 -
^
中间对齐、 -
=
(只用于数字)在小数点后进行补齐 -
取位数“{:4s}”、"{:.2f}"等
print('{} and {}'.format('hello','world')) # 默认左对齐hello and world
print('{:10s} and {:>10s}'.format('hello','world')) # 取10位左对齐,取10位右对齐hello and world
print('{:^10s} and {:^10s}'.format('hello','world')) # 取10位中间对齐hello and world print('{} is {:.2f}'.format(1.123,1.123)) # 取2位小数1.123 is 1.12
print('{0} is {0:>10.2f}'.format(1.123)) # 取2位小数,右对齐,取10位1.123 is 1.12
(3)正负符号显示
(4)百分数%
(5)时间
(6)逗号","分隔金钱,没以前进位
(7)占位符嵌套
(8)占位符%s和%r
如何在 .format 中使用大括号
- 你可以使用
{{
和}}
>>> x = " {{ Hello }} {0} "
>>> print x.format(42)
' { Hello } 42 '
- 不可以使用
\{
和\}
文档在这里 Python documentation for format string syntax"
- 格式化中的大括号
{}
表示替代字段。任何不再大括号中的将被看做文本,会被原样输出。如果你在文本中包含大括号, 你可以使用{{
和}}
。
9 迭代器和生成器
生成器
-
生成器(generator):使用了关键字 yield 的函数被称为生成器(generator)。
-
跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。
- 在调用生成器运行的过程中,每次遇到 yield 时,函数会***暂停并保存当前所有***的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。
- 调用一个***生成器函数***,返回的是一个***迭代器对象***。
#!/usr/bin/python3
import sys
def fibonacci(n): # 生成器函数 - 斐波那契
a, b, counter = 0, 1, 0
while True:
if (counter > n):
return
yield a
a, b = b, a + b
counter += 1
f = fibonacci(10) # f 是一个迭代器,由生成器返回生成
while True:
try:
print (next(f), end=" ")
except StopIteration:
sys.exit()
# 0 1 1 2 3 5 8 13 21 34 55
- 获取生成器的三种方式:
- 生成器函数。
- 生成器表达式。
- python内部提供的一些。
迭代器
-
迭代器:一个可以记住遍历的位置的对象。
-
迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能***往前***不会***后退***。
-
迭代器有两个基本的方法:
- iter()
- next()。
-
字符串,列表或元组对象都可用于创建迭代器
创建一个迭代器
-
把一个类作为一个迭代器使用需要在类中实现两个方法
__ iter__()
与__next__()
以及Python 的构造函数为__init__()
, 它会在对象初始化的时候执行。 -
__iter__()
方法返回一个特殊的迭代器对象, 这个迭代器对象实现了__next__()
方法并通过StopIteration
异常标识迭代的完成。 -
__next__()
方法会返回下一个迭代器对象。
-
创建一个返回数字的迭代器,初始值为 1,逐步递增 1:
class MyNumbers: def __iter__(self): self.a = 1
return self
def __next__(self): x = self.a
self.a += 1
return x
myclass = MyNumbers()myiter = iter(myclass)
print(next(myiter))
print(next(myiter))
print(next(myiter))
print(next(myiter))
print(next(myiter))
StopIteration
- StopIteration 异常用于标识迭代的完成,防止出现无限循环的情况,在 next() 方法中我们可以设置在完成指定循环次数后触发 StopIteration 异常来结束迭代。
在 20 次迭代后停止执行:
class MyNumbers:
def __iter__(self):
self.a = 1
return self
def __next__(self):
if self.a <= 20:
x = self.a
self.a += 1
return x
else:
raise StopIteration
myclass = MyNumbers()
myiter = iter(myclass)
for x in myiter: print(x)
10 *args
and **kwargs
-
用
*args
和**kwargs
只是为了方便并没有强制使用它们. -
当你不确定你的函数里将要传递多少参数时,你可以用
*args
。
例如,它可以传递任意数量的参数:
>>> def print_everything(*args):
for count, thing in enumerate(args):
... print '{0}. {1}'.format(count, thing)
...
>>> print_everything('apple', 'banana', 'cabbage')
0. apple
1. banana
2. cabbage
**kwargs
允许你使用没有事先定义的参数名:
>>> def table_things(**kwargs):
... for name, value in kwargs.items():
... print '{0} = {1}'.format(name, value)
...
>>> table_things(apple = 'fruit', cabbage = 'vegetable')
cabbage = vegetable
apple = fruit
- 命名参数,首先获得参数值,然后所有的其他参数都传递给
*args
和**kwargs
。命名参数在列表的最前端.例如:
def table_things(titlestring, **kwargs)
-
*args
和**kwargs
可以同时在函数的定义中,但是*args
必须在**kwargs
前面. -
并不是必须写成
*args
和**kwargs
。*(星号)
才是*必须的***. 你也可以写成 *ar 和**k
。而写成 *args 和kwargs 只是一个通俗的命名约定。 -
当调用函数时你也可以用
*
和**
语法.例如:
>>> def print_three_things(a, b, c):
... print 'a = {0}, b = {1}, c = {2}'.format(a,b,c)
...
>>> mylist = ['aardvark', 'baboon', 'cat']
>>> print_three_things(*mylist)
a = aardvark, b = baboon, c = cat
就像你看到的一样,它可以传递列表(或者元组)的每一项并把它们解包.注意必须与它们在函数里的参数相吻合.当然,你也可以在函数定义或者函数调用时用*.
11 面向切面编程AOP和装饰器
AOP
-
AOP:在运行时,编译时,类和方法加载时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。***切入到指定类指定方法的代码片段***称为切面,
-
装饰器的作用就是为已经存在的对象添加额外的功能。
-
函数就是对象.因此,对象:
- 可以赋值给一个变量
- 可以在其他函数里定义
- 函数可以返回另一个函数
- 函数作为参数传递
装饰器
- 装饰器就是把其他函数作为参数的函数
def my_new_decorator(a_function_to_decorate):
# 在函数里面,装饰器在运行中定义函数: 包装.
# 这个函数将被包装在原始函数的外面,所以可以在原始函数之前和之后执行其他代码..
def the_wrapper_function():
# 把要在原始函数被调用前的代码放在这里
print "Before the function runs"
# 调用原始函数(用括号)
a_function_to_decorate()
# 把要在原始函数调用后的代码放在这里
print "After the function runs"
# 在这里"a_function_to_decorate" 函数永远不会被执行
# 在这里返回刚才包装过的函数
# 在包装函数里包含要在原始函数前后执行的代码.
return the_wrapper_function
# 加入你建了个函数,不想修改了
def a_stand_alone_function():
print "I am a stand alone function, don't you dare modify me"
a_stand_alone_function()
#输出: I am a stand alone function, don't you dare modify me
# 现在,你可以装饰它来增加它的功能
# 把它传递给装饰器,它就会返回一个被包装过的函数.
a_function_decorated = my_new_decorator(a_stand_alone_function)
# 执行
a_function_decorated()
#输出s:
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs
真实装饰器
@my_new_decorator
def another_stand_alone_function():
print "Leave me alone"
another_stand_alone_function()
#输出:
#Before the function runs
#Leave me alone
#After the function runs
从这里可以看出@decorator就是下面的简写:
another_stand_alone_function = my_new_decorator(another_stand_alone_function)
在装饰器函数里传入参数
def a_decorator_passing_arguments(function_to_decorate):
def a_wrapper_accepting_arguments(arg1, arg2):
print "I got args! Look:", arg1, arg2
function_to_decorate(arg1, arg2)
return a_wrapper_accepting_arguments
# 当你调用装饰器返回的函数时,也就调用了包装器,把参数传入包装器里,
# 它将把参数传递给被装饰的函数里.
@a_decorator_passing_arguments
def print_full_name(first_name, last_name):
print "My name is", first_name, last_name
print_full_name("Peter", "Venkman")
# 输出:
#I got args! Look: Peter Venkman
#My name is Peter Venkman
把参数传给装饰器
# 装饰器就是一个'平常不过'的函数
def my_decorator(func):
print "I am an ordinary function"
def wrapper():
print "I am function returned by the decorator"
func()
return wrapper
# 因此你可以不用"@"也可以调用他
def lazy_function():
print "zzzzzzzz"
decorated_function = my_decorator(lazy_function)
#输出: I am an ordinary function
# 之所以输出 "I am an ordinary function"是因为你调用了函数,
# 并非什么魔法.
@my_decorator
def lazy_function():
print "zzzzzzzz"
#输出: I am an ordinary function
这里调用decorated_function()才会输出装饰器里面的方法,建一个装饰器.它只是一个新函数,去掉中间变量他就会变为真正的装饰器,那么如何去掉中间变量
def decorator_maker_with_arguments(decorator_arg1, decorator_arg2):
print "I make decorators! And I accept arguments:", decorator_arg1, decorator_arg2
def my_decorator(func):
print "I am the decorator. Somehow you passed me arguments:", decorator_arg1, decorator_arg2
def wrapped(function_arg1, function_arg2) :
print ("I am the wrapper around the decorated function.\n"
"I can access all the variables\n"
"\t- from the decorator: {0} {1}\n"
"\t- from the function call: {2} {3}\n"
"Then I can pass them to the decorated function"
.format(decorator_arg1, decorator_arg2,
function_arg1, function_arg2))
return func(function_arg1, function_arg2)
return wrapped
return my_decorator
@decorator_maker_with_arguments("Leonard", "Sheldon")
def decorated_function_with_arguments(function_arg1, function_arg2):
print ("I am the decorated function and only knows about my arguments: {0}"
" {1}".format(function_arg1, function_arg2))
decorated_function_with_arguments("Rajesh", "Howard")
#输出:
#I make decorators! And I accept arguments: Leonard Sheldon
#I am the decorator. Somehow you passed me arguments: Leonard Sheldon
#I am the wrapper around the decorated function.
#I can access all the variables
# - from the decorator: Leonard Sheldon
# - from the function call: Rajesh Howard
#Then I can pass them to the decorated function
#I am the decorated function and only knows about my arguments: Rajesh Howard
装饰器的知识点
- 装饰器使函数调用变慢了.一定要记住.
- 装饰器不能被取消(有些人把装饰器做成可以移除的但是没有人会用)所以一旦一个函数被装饰了.所有的代码都会被装饰.
- Python自身提供了几个装饰器,像property, staticmethod.
- Django用装饰器管理缓存和试图的权限.
- Twisted用来修改异步函数的调用.
12 鸭子类型
“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”
我们并不关心对象是什么类型,到底是不是鸭子,只关心行为。
比如在python中,有很多file-like的东西,比如StringIO,GzipFile,socket。它们有很多相同的方法,我们把它们当作文件使用。
又比如list.extend()方法中,我们并不关心它的参数是不是list,只要它是可迭代的,所以它的参数可以是list/tuple/dict/字符串/生成器等.
鸭子类型在动态语言中经常使用,非常灵活,使得python不像java那样专门去弄一大堆的设计模式。
13 Python中重载
-
函数重载主要是为了解决两个问题。
- 可变参数类型。
- 可变参数个数。
- 仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函数的功能其实不同,那么不应当使用重载,而应当使用一个名字不同的函数。
-
对于情况 1 ,函数功能相同,但是参数类型不同
- 因为 python 可以接受任何类型的参数,如果函数的功能相同,那么不同的参数类型在 python 中很可能是相同的代码,没有必要做成两个不同函数。
-
对于情况 2 ,函数功能相同,但参数个数不同。缺省参数。对那些缺少的参数设定为缺省参数即可解决问题。因为你假设函数功能相同,那么那些缺少的参数终归是需要用的。
python 自然就不需要函数重载了。
14 浅析Python运算符重载
Python语言提供了运算符重载功能,增强了语言的灵活性。
-
Python语言本身提供了很多内置方法,它的运算符重载就是通过重写这些Python内置方法实现的。
-
这些内置方法都是以双下划线开头和结尾的,类似于
__X__
的形式,python通过这种特殊的命名方式来拦截操作符,以实现重载。当Python的内置操作运用于类对象时,Python会去搜索并调用对象中指定的方法完成操作。 -
类可以重载加减运算、打印、函数调用、索引等内置运算,运算符重载使我们的对象的行为与内置对象的一样。Python在调用操作符时会自动调用这样的方法,例如,如果类实现了__add__方法,当类的对象出现在+运算符中时会调用这个方法。
*常见运算符重载方法*
方法名 | 重载说明 | 运算符调用方式 |
---|---|---|
init | 构造函数 | 对象创建: X = Class(args) |
del | 析构函数 | X对象收回 |
add/sub | 加减运算 | X+Y, X+=Y/X-Y, X-=Y |
or | 运算符| | X|Y, X|=Y |
repr_/str | 打印/转换 | print(X)、repr(X)/str(X) |
bool | 布尔测试 | bool(X) |
lt, gt, le, ge, eq, ne | 特定的比较 | 依次为X<Y,X>Y,X<=Y,X>=Y, X==Y,X!=Y 注释:(lt: less than, gt: greater than, le: less equal, ge: greater equal, eq: equal, ne: not equal ) |
radd | 右侧加法 | other+X |
iadd | 实地(增强的)加法 | X+=Y(or else add) |
iter, next | 迭代 | I=iter(X), next() |
contains | 成员关系测试 | item in X(X为任何可迭代对象) |
index | 整数值 | hex(X), bin(X), oct(X) |
enter, exit | 环境管理器 | with obj as var: |
get, set, delete | 描述符属性 | X.attr, X.attr=value, del X.attr |
new | 创建 | 在__init__之前创建对象 |
15 __new__
和__init__
的区别
这个__new__
确实很少见到,先做了解吧.
__new__
是一个静态方法,而__init__
是一个实例方法.__new__
方法会返回一个创建的实例,而__init__
什么都不返回.- 只有在
__new__
返回一个cls的实例时后面的__init__
才能被调用. - 当创建一个新实例时调用
__new__
,初始化一个实例时用__init__
.
ps: __metaclass__
是创建类时起作用.所以我们可以分别使用__metaclass__
,__new__
和__init__
来分别在类创建,实例创建和实例初始化的时候做一些小手脚.
16 单例模式
单例模式***是一种常用的软件设计模式。在它的核心结构中只包含一个被称为***单例类***的***特殊类。通过单例模式可以保证系统中一个类只有一个实例,而且该实例易于外界访问。从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
__new__()
在__init__()
之前被调用,用于生成实例对象。利用这个方法和类的属性的特点可以实现设计模式的单例模式。单例模式是指创建唯一对象,单例模式设计的类只能实例
1 使用__new__
方法
class Singleton(object):
def __new__(cls, *args, **kw):
if not hasattr(cls, '_instance'):
orig = super(Singleton, cls)
cls._instance = object.__new__(cls, *args, **kw)
return cls._instance
class MyClass(Singleton):
a = 1
2 共享属性
创建实例时把所有实例的__dict__
指向同一个字典,这样它们具有相同的属性和方法.
class Borg(object):
_state = {}
def __new__(cls, *args, **kw):
ob = super(Borg, cls).__new__(cls, *args, **kw)
ob.__dict__ = cls._state
return ob
class MyClass2(Borg):
a = 1
3 装饰器版本
def singleton(cls):
instances = {}
def getinstance(*args, **kw):
if cls not in instances:
instances[cls] = cls(*args, **kw)
return instances[cls]
return getinstance
@singleton
class MyClass:
...
4 import方法
作为python的模块是天然的单例模式
# mysingleton.py
class My_Singleton(object):
def foo(self):
pass
my_singleton = My_Singleton()
# to use
from mysingleton import my_singleton
my_singleton.foo()
- 补充:第一种方法:使用装饰器
def singleton(cls):
instances = {}
def wrapper(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrapper
@singleton
class Foo(object):
pass
foo1 = Foo()
foo2 = Foo()
print(foo1 is foo2) # True
第二种方法:使用基类
New 是真正创建实例对象的方法,所以重写基类的new 方法,以此保证创建对象的时候只生成一个实例
class Singleton(object):
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
cls._instance = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instance
class Foo(Singleton):
pass
foo1 = Foo()
foo2 = Foo()
print(foo1 is foo2) # True
第三种方法:元类,元类是用于创建类对象的类,类对象创建实例对象时一定要调用call方法,因此在调用call时候保证始终只创建一个实例即可,type是python的元类
class Singleton(type):
def __call__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
cls._instance = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instance
# Python2
class Foo(object):
__metaclass__ = Singleton
# Python3
class Foo(metaclass=Singleton):
pass
foo1 = Foo()
foo2 = Foo()
print(foo1 is foo2) # True
17 Python中的作用域与闭包
Python 中,一个变量的作用域总是由在代码中被赋值的地方所决定的。
闭包
定义
- 闭包:如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。
def addx(x):
def adder(y): return x + y
return adder
c = addx(8)
>>> c.__name__
'adder'
>>> c(10)
18
-
如果在一个内部函数里:adder(y)就是这个内部函数,对在外部作用域(但不是在全局作用域)的变量进行引用:x就是被引用的变量,x在外部作用域addx里面,但不在全局作用域里,则这个内部函数adder就是一个闭包。
闭包=函数块+定义函数时的环境,
-
adder就是函数块,x就是环境。
二,使用闭包注意事项
- 闭包中是不能修改外部作用域的局部变量的
>>> def foo():
... m = 0
... def foo1():
... m = 1
... print m
...
... print m
... foo1()
... print m
...
>>> foo()
0
1
0
从执行结果可以看出,虽然在闭包里面也定义了一个变量m,但是其不会改变外部函数中的局部变量m。
- 以下这段代码是在python中使用闭包时一段经典的错误代码
def foo():
a = 1
def bar():
a = a + 1
return a
return bar
这段程序的本意是要通过在每次调用闭包函数时都对变量a进行递增的操作。但在实际使用时
>>> c = foo()
>>> print c()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in bar
UnboundLocalError: local variable 'a' referenced before assignment
这是因为在执行代码 c = foo()时,python会导入全部的闭包函数体bar()来分析其的局部变量,python规则指定所有在赋值语句左面的变量都是局部变量,则在闭包bar()中,变量a在赋值符号"="的左面,被python认为是bar()中的局部变量。再接下来执行print c()时,程序运行至a = a + 1时,因为先前已经把a归为bar()中的局部变量,所以python会在bar()中去找在赋值语句右面的a的值,结果找不到,就会报错。解决的方法很简单。
def foo():
a = [1]
def bar():
a[0] = a[0] + 1
return a[0]
return bar
只要将a设定为一个容器就可以了。这样使用起来多少有点不爽,所以在python3以后,在a = a + 1 之前,使用语句nonloacal a就可以了,该语句显式的指定a不是闭包的局部变量。
for i in range(3):
print i
- 在程序里面经常会出现这类的循环语句,Python的问题就在于,当循环结束以后,循环体中的***临时变量i不会销毁***,而是继续存在于执行环境中。
- 还有一个python的现象是,python的函数只有在执行时,才会去找函数体里的变量的值。
三,作用
用途1,当闭包执行完后,仍然能够保持住当前的运行环境。
比如说,如果你希望函数的每次执行结果,都是基于这个函数上次的运行结果。我以一个类似棋盘游戏的例子来说明。假设棋盘大小为50*50,左上角为坐标系原点(0,0),我需要一个函数,接收2个参数,分别为方向(direction),步长(step),该函数控制棋子的运动。棋子运动的新的坐标除了依赖于方向和步长以外,当然还要根据原来所处的坐标点,用闭包就可以保持住这个棋子原来所处的坐标。
origin = [0, 0] # 坐标系统原点
legal_x = [0, 50] # x轴方向的合法坐标
legal_y = [0, 50] # y轴方向的合法坐标
def create(pos=origin):
def player(direction,step):
# 这里应该首先判断参数direction,step的合法性,比如direction不能斜着走,step不能为负等
# 然后还要对新生成的x,y坐标的合法性进行判断处理,这里主要是想介绍闭包,就不详细写了。
new_x = pos[0] + direction[0]*step
new_y = pos[1] + direction[1]*step
pos[0] = new_x
pos[1] = new_y
#注意!此处不能写成 pos = [new_x, new_y],原因在上文有说过
return pos
return player
player = create() # 创建棋子player,起点为原点
print player([1,0],10) # 向x轴正方向移动10步
print player([0,1],20) # 向y轴正方向移动20步
print player([-1,0],10) # 向x轴负方向移动10步
输出为
[10, 0]
[10, 20]
[0, 20]
用途2,闭包可以根据外部作用域的局部变量来得到不同的结果,这有点像一种类似配置功能的作用,我们可以修改外部的变量,闭包根据这个变量展现出不同的功能。比如有时我们需要对某些文件的特殊行进行分析,先要提取出这些特殊行。
def make_filter(keep):
def the_filter(file_name):
file = open(file_name)
lines = file.readlines()
file.close()
filter_doc = [i for i in lines if keep in i]
return filter_doc
return the_filter
如果我们需要取得文件"result.txt"中含有"pass"关键字的行,则可以这样使用例子程序
filter = make_filter("pass")
filter_result = filter("result.txt")
以上两种使用场景,用面向对象也是可以很简单的实现的,但是在用Python进行函数式编程时,闭包对数据的持久化以及按配置产生不同的功能,是很有帮助的。
python变量搜素
-
当 Python 遇到一个变量的话他会按照这样的顺序进行搜索:
-
本地作用域(Local)→当前作用域被嵌入的本地作用域(Enclosing locals)→全局/模块作用域(Global)→内置作用域(Built-in)
作用域
-
作用域:命名空间,python创建、改变或查找变量名都是在所谓的命名空间中。***变量在赋值创建***时,python中***代码赋值***的地方决定了这个变量存在于哪个命名空间,也就是他的可见范围。
-
对于函数,函数为程序增加一个额外的命名空间层来最小化相同变量之间的冲突:默认情况下,一个函数内赋值的所有变量名都与该函数的命名空间相互关联。这条规则意味着:
- 在***def内赋值的变量名***只能被***def内的代码*** 使用。不能在函数的外部引用该变量名。
- 在***def内赋值的变量名***与***在def外赋值的变量名*** 并***不冲突***,即使是相同的变量名,也完全时两个完全不同的变量。
- 如果一个变量在def内赋值,它对于该函数是***局部的***
- 如果一个变量在外层的def中赋值,它对于内层的函数时***非局部***的
- 如果一个变量在所有的def外赋值,它对整个文件来说时***全局***的。
可见,一个变量的作用域取决于它在代码中被赋值的位置,而与函数调用完全无关
作用域细节
- 函数的命名空间时可嵌套的,以便函数内部用的变量名不会与函数外(同一模块或另一函数中)的变量名冲突。函数定义了局部作用域,而模块定义了全局作用域:
- 外围模块是全局作用域。这里的全局指的是***文件顶层的变量名***对于这个文件是***全局*** 的,全局作用域的作用范围仅限于单个文件。
- python中不存在一个跨文件的单一且无所不包的全局作用域的概念,全局是对模块而言的。
- 赋值的变量名***除非被声明为global或nonlocal,否则均为局部变量***。在函数内部想更改顶层变量的值,就不许声明为全局变量。
- 函数的每次调用就会创建一个新的局部作用域。可以认为每一个def及lambda语句都定义了一个新的局部作用域,但局部作用域实际上对应于一次函数的激活,每一次激活的调用都能拥有一套自己的局部变量副本。
注意:一个函数内部任何类型的赋值都会把一个名称划定为局部的,包括=语句,import的模块名,def的函数名,函数形式参数名等。如果你在def中以任何方式赋值一个名称,他都会默认为该函数的局部名称。
但是,要注意原位置改变对象并不会把变量划分为局部变量,只有对变量赋值才可以。例如L在模块顶层被赋值为列表,在一个函数内类似L.append(X)的语句并不会将L划分为局部变量,而L = X却可以。append是修改对象,而=是赋值,要明白修改一个对象并不是对一个变量名赋值。
变量名解析:LEGB规则
总结三条简单的规则
- 在默认情况下,变量名赋值会创建改变局部变量
- 变量名引用至多可以在四种作用域内进行查找:首先是局部,其次是外层的函数,之后是全局,然后是内置
- 使用global和nonlocal语句声明的名称将赋值的变量名分别映射到外围的模块和函数的作用域,变成全局。
变量名解析机制称为LEGB规则,也是有作用域的命名而来的:
- 当你在函数内使用未限定的变量名时,python将查找4个作用域并在找到该变量名的地方停下:分别是局部作用域L,向外一层的def或lambda的局部作用域E,全局作用域G,最后时内置作用域B。如果都没有找到就会报错。
- 当你在函数中给一个变量名赋值时,python会创建或改变局部作用域的变量名,除非该变量名已经在该函数中被声明为全局变量。
- 当你在所有函数的外面给一个变量名赋值时,就是在模块文件的顶层或时交互式命令行下,此时的局部作用域就是全局作用域,即该模块的命名空间。
内置作用域
是一个名为builtins的内置模块,但是必须要导入之后才能使用内置作用域
global语句
global语句告诉python生成一个或多个全局变量,总结一下全局变量:
- 全局变量是在外层模块文件的顶层被赋值的变量名
- 全局变量如果是在函数内赋值的话,必须经过声明
global OLD_URL
- 全局变量名在函数的内部不经过声明也可以被引用
程序设计中最少化全局变量,因为你可能也不知道语句在什么时候最后一次执行,值是多少;最小化跨文件的修改,变量的值可能化发生难以预测的改变甚至报错。
这里的作用域主要是***作用域查找规则LEGB的第二个层次E,即def或lambda定义的外层函数的局部作用域。嵌套函数产生相对应的嵌套作用域,对应嵌套的代码结构。***
嵌套的函数作用域中变量查找规则,在一个函数中:
- 引入一个变量X,X依然遵循LEGB规则,然而global声明会直接从全局(模块文件)作用域查找。
- 赋值(X=value)会创建或改变当前作用域中的变量名X。若X在函数的声明为全局变量,它会创建或改变整个模块中作用域的X;若在函数内声明为非局部变量,会修改最近的嵌套函数作用域中的X。
在存在嵌套函数时,若global存在,global声明会将变量映射到外层模块,嵌套函数中变量也许会被引用,但他们必须要nonlocal声明才能修改。
nonlocal语句
nonlocal和global都遵循LEGB准则,但是限制了查找规则。
使用global语句意味着名称存在于外层的模块中,global使得作用域查找从外围的作用域开始,并且允许对那里 的名称赋值。如果名称不存在该模块中,作用域查找会继续进行到内置作用域,但是全局名称的赋值总是在模块的作用域中创建或修改它们。
nonlocal语句将作用域查找限制为只在外层的def中,同时要求名称已经存在,并且允许对它们赋值,作用域不会继续查找到全局或内置作用域。nonlocal也意味着忽略局部作用域。
18 进程、线程、GIL线程全局锁
(1)形象描述
- 做个简单的比喻:进程=火车,线程=车厢线程在进程下行进(单纯的车厢无法运行)
- 一个进程可以包含多个线程(一辆火车可以有多个车厢)
- 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
- 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)
- 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源)
- 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢)
- 进程可以拓展到多机,进程最多适合多核(不同火车可以开在多个轨道上,同一火车的车厢不能在行进的不同的轨道上)
- 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。(比如火车上的洗手间)-“互斥锁” 进程使用的内存地址可以限定使用量
- 单个CPU在任一时刻只能执行单个线程,只有多核CPU还能真正做到多个线程同时运行
- 一个进程包含多个线程,这些线程可以分布在多个CPU上
- 多核CPU同时运行的线程可以属于单个进程或不同进程
- 所以,在大多数编程语言中,因为切换消耗的资源更少,多线程比多进程效率更高,Python是个特例
(2)多线程vs多进程
- CPU密集型操作使用多进程比较合适,例如海量运算
- IO密集型操作使用多线程比较合适,例如爬虫,文件处理,批量ssh操作服务器等等
线程全局锁(Global Interpreter Lock),即Python为了保证线程安全而采取的独立线程运行的限制,说白了就是一个核只能在同一时间运行一个线程。对于io密集型任务,python的多线程起到作用,但对于cpu密集型任务,python的多线程几乎占不到任何优势,还有可能因为争夺资源而变慢。
(3) 线程
什么是线程
- 线程(Thread)也叫轻量级进程,是操作系统能够进行运算调度的最小单位,它被包涵在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
线程的优点
- 进程之间不能共享内存,但线程之间共享内存非常容易。
- 操作系统在创建进程时,需要为该进程重新分配系统资源,但创建线程的代价则小得多。因此,使用多线程来实现多任务并发执行比使用多进程的效率高。
- Python 语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了 Python 的多线程编程。
线程的类型
线程的因作用可以划分为不同的类型。大致可分为:
- 主线程
- 子线程
- 守护线程(后台线程)
- 前台线程
解决办法就是多进程和下面的协程(协程也只是单CPU,但是能减小切换代价提升性能)。
threading模块
- 普通创建方式
import threading
import time
def run(n):
print("task", n)
# time.sleep(1)
print('2s')
# time.sleep(1)
print('1s')
#time.sleep(1)
print('0s')
# time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=run, args=("t1",))
t2 = threading.Thread(target=run, args=("t2",))
t1.start()
t2.start()
----------------------------------
task t1
2s
1s
0s
task t2
2s
1s
0s
import threading
import time
class MyThread(threading.Thread):
def __init__(self, n):
super(MyThread, self).__init__() # 重构run函数必须要写
self.n = n
def run(self):
print("task", self.n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
if __name__ == "__main__":
t1 = MyThread("t1")
t2 = MyThread("t2")
t1.start()
t2.start()
----------------------------------
>>> task t1
>>> task t2
>>> 2s
>>> 2s
>>> 1s
>>> 1s
>>> 0s
>>> 0s
自定义线程
- 继承threading.Thread来自定义线程类,其本质是重构Thread类中的run方法
import threading
import time
class MyThread(threading.Thread):
def __init__(self, n):
super(MyThread, self).__init__() # 重构run函数必须要写
self.n = n
def run(self):
print("task", self.n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)
if __name__ == "__main__":
t1 = MyThread("t1")
t2 = MyThread("t2")
t1.start()
t2.start()
----------------------------------
>>> task t1
>>> task t2
>>> 2s
>>> 2s
>>> 1s
>>> 1s
>>> 0s
>>> 0s
守护线程
- 我们看下面这个例子,这里使用setDaemon(True)把所有的子线程都变成了主线程的守护线程,因此当主进程结束后,子线程也会随之结束。所以当主线程结束后,整个程序就退出了。
import threading
import time
def run(n):
print("task", n)
time.sleep(1) #此时子线程停1s
print('3')
time.sleep(1)
print('2')
time.sleep(1)
print('1')
if __name__ == '__main__':
t = threading.Thread(target=run, args=("t1",))
t.setDaemon(True) #把子进程设置为守护线程,必须在start()之前设置
t.start()
print("end")
----------------------------------
>>> task t1
>>> end
- 我们可以发现,设置守护线程之后,当主线程结束时,子线程也将立即结束,不再执行。
主线程等待子线程结束
- 为了让守护线程执行结束之后,主线程再结束,我们可以使用join方法,让主线程等待子线程执行。
import threading
import time
def run(n):
print("task", n)
time.sleep(1) #此时子线程停1s
print('3')
time.sleep(1)
print('2')
time.sleep(1)
print('1')
if __name__ == '__main__':
t = threading.Thread(target=run, args=("t1",))
t.setDaemon(True) #把子进程设置为守护线程,必须在start()之前设置
t.start()
t.join() # 设置主线程等待子线程结束
print("end")
----------------------------------
>>> task t1
>>> 3
>>> 2
>>> 1
>>> end
多线程共享全局变量
- 线程是进程的执行单元,进程是系统分配资源的最小单位,所以在同一个进程中的多线程是共享资源的。
import threading
import time
g_num = 100
def work1():
global g_num
for i in range(3):
g_num += 1
print("in work1 g_num is : %d" % g_num)
time.sleep(1)
def work2():
global g_num
for i in range(3):
print("in work2 g_num is : %d" % g_num)
time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=work1)
t1.start()
# time.sleep(1)
t2 = threading.Thread(target=work2)
t2.start()
# t1.join()
# t2.join()
---------------------------------
in work1 g_num is : 101
in work2 g_num is : 101
in work2 g_num is : 101
in work1 g_num is : 102
in work1 g_num is : 103
in work2 g_num is : 103
互斥锁
-
由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁,即同一时刻允许一个线程执行操作。线程锁用于锁定资源,你可以定义多个锁, 像下面的代码, 当你需要独占某一资源时,任何一个锁都可以锁这个资源,就好比你用不同的锁都可以把相同的一个门锁住是一个道理。
-
由于线程之间是进行随机调度,如果有多个线程同时操作一个对象,如果没有很好地保护该对象,会造成程序结果的不可预期,我们也称此为“线程不安全”。
-
为了方式上面情况的发生,就出现了互斥锁(Lock)
from threading import Thread,Lock
import os,time
def work():
global n
lock.acquire()
temp=n
time.sleep(0.1)
n=temp-1
lock.release()
if __name__ == '__main__':
lock=Lock()
n=100
l=[]
for i in range(100):
p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
递归锁
- RLcok类的用法和Lock类一模一样,但它支持嵌套,在多个锁没有释放的时候一般会使用RLcok类。
import threading
import time
def Func(lock):
global gl_num
lock.acquire()
gl_num += 1
time.sleep(1)
print(gl_num)
lock.release()
if __name__ == '__main__':
gl_num = 0
lock = threading.RLock()
for i in range(10):
t = threading.Thread(target=Func, args=(lock,))
t.start()
信号量(BoundedSemaphore类)
- 互斥锁同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 .
import threading
import time
def run(n, semaphore):
semaphore.acquire() #加锁
time.sleep(1)
print("run the thread:%s\n" % n)
semaphore.release() #释放
if __name__ == '__main__':
num = 0
semaphore = threading.BoundedSemaphore(5) # 最多允许5个线程同时运行
for i in range(22):
t = threading.Thread(target=run, args=("t-%s" % i, semaphore))
t.start()
while threading.active_count() != 1:
pass # print threading.active_count()
else:
print('-----all threads done-----')
事件(Event类)
-
事件处理的机制:全局定义了一个“Flag”,当flag值为“False”,那么event.wait()就会阻塞,当flag值为“True”,那么event.wait()便不再阻塞。
-
事件对象管理一个内部标志,通过
set()
方法将其设置为True
,并使用clear()
方法将其设置为False
。wait()
方法阻塞,直到标志为True
。该标志初始为False
。-
is_set()
当且仅当内部标志为True
时返回True
。 -
set()
将内部标志设置为
True
。所有等待它成为True
的线程都被唤醒。当标志保持在True
的状态时,线程调用wait()
是不会阻塞的。 -
clear()
将内部标志重置为False
。随后,调用wait()
的线程将阻塞,直到另一个线程调用set()
将内部标志重新设置为True
。 -
wait(timeout=None)
阻塞直到内部标志为真。如果内部标志在wait()
方法调用时为True
,则立即返回。否则,则阻塞,直到另一个线程调用set()
将标志设置为True
,或发生超时。
该方法总是返回True
,除非设置了timeout
并发生超时。
-
#利用Event类模拟红绿灯
import threading
import time
event = threading.Event()
def lighter():
count = 0
event.set() #初始值为绿灯 Flag=True
while True:
if 5 < count <=10 :
event.clear() # 红灯,清除标志位 Flag=False
print("\33[41;1mred light is on...\033[0m")
elif count > 10:
event.set() # 绿灯,设置标志位 Flag = True
count = 0
else:
print("\33[42;1mgreen light is on...\033[0m")
time.sleep(1)
count += 1
def car(name):
while True:
if event.is_set(): #判断是否设置了标志位 Flag==True
print("[%s] running..."%name)
time.sleep(1)
else: # Flag==False
print("[%s] sees red light,waiting..."%name)
event.wait()
print("[%s] green light is on,start going..."%name)
light = threading.Thread(target=lighter,)
light.start()
car = threading.Thread(target=car,args=("MINI",))
car.start()
GIL
-
其他语言,CPU 是多核时是支持多个线程同时执行。但在 Python 中,无论是单核还是多核,同时只能由一个线程在执行。其根源是 GIL 的存在。
-
GIL 的全称是 ***Global Interpreter Lock(全局解释器锁)***,来源是 Python 设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到 GIL,我们可以把 GIL 看作是“通行证”,并且在一个 Python 进程中,GIL 只有一个。拿不到通行证的线程,就不允许进入 CPU 执行。
-
而目前 Python 的解释器有多种,例如:
- CPython:CPython 是用C语言实现的 Python 解释器。 作为官方实现,它是最广泛使用的 Python 解释器。
- PyPy:PyPy 是用RPython实现的解释器。RPython 是 Python 的子集, 具有静态类型。这个解释器的特点是即时编译,支持多重后端(C, CLI, JVM)。PyPy 旨在提高性能,同时保持最大兼容性(参考 CPython 的实现)。
- Jython:Jython 是一个将 Python 代码编译成 Java 字节码的实现,运行在JVM (Java Virtual Machine) 上。另外,它可以像是用 Python 模块一样,导入 并使用任何Java类。
- IronPython:IronPython 是一个针对 .NET 框架的 Python 实现。它 可以用 Python 和 .NET framewor k的库,也能将 Python 代码暴露给 .NET 框架中的其他语言。
- GIL 只在 CPython 中才有,而在 PyPy 和 Jython 中是没有 GIL 的。
每次释放 GIL锁,线程进行锁竞争、切换线程,会消耗资源。这就导致打印线程执行时长,会发现耗时更长的原因。
并且由于 GIL 锁存在,Python 里一个进程永远只能同时执行一个线程(拿到 GIL 的线程才能执行),
(4)进程
-
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
-
狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。
-
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
进程概念
- 进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
- 进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
- 进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。
(1) 引入进程原因
- 为了提高资源利用率和系统处理能力,现阶段计算机系统都是多道程序系统,即多道程序并发执行。
- 优化系统资源,方便计算机调度,避免系统运算紊乱。
- 进程是一种数据结构,能够清晰的刻画动态系统的内在规律,增加程序运行时的动态性。
(2)进程特征
- 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
- 并发性:任何进程都可以同其他进程一起并发执行。
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
- 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。
结构组成:程序、数据和进程控制块
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
(3 )进程与程序区别
- 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
- 而进程是程序在处理机上的一次执行过程,它是一个动态的概念。
- 程序可以作为一种软件资料长期存在,而进程是有一定生命期的。
- 程序是永久的,进程是暂时的。
并行 并发
(1)并行(Parallelism)
并行:指两个或两个以上事件(或线程)在同一时刻发生,是真正意义上的不同事件或线程在同一时刻,在不同CPU资源呢上(多核),同时执行。
特点
- 同一时刻发生,同时执行。
- 不存在像并发那样竞争,等待的概念。
(2)并发(Concurrency)
指一个物理CPU(也可以多个物理CPU) 在若干道程序(或线程)之间多路复用,并发性是对有限物理资源强制行使多用户共享以提高效率。
特点
- 微观角度:所有的并发处理都有排队等候,唤醒,执行等这样的步骤,在微观上他们都是序列被处理的,如果是同一时刻到达的请求(或线程)也会根据优先级的不同,而先后进入队列排队等候执行。
- 宏观角度:多个几乎同时到达的请求(或线程)在宏观上看就像是同时在被处理。
进程调度
-
FCFS 先来先服务调度法(First Come First Service):按照先后顺序处理事件的一种算法。
- 该算法既可用于作业调度,也可用于进程调度。FCFS算法比较有利于长作业(进程),而不利于短作业(进程)。由此可知,本算法适合于CPU繁忙型作业,而不利于I/O繁忙型的作业(进程)。
-
SJF/SPN 短作业优先法(Shortest Job First):又称为短进程优先算法(SPN,Shortest Process Next),能有效减少平均周转时间。
- 该算法既可用于作业调度,也可用于进程调度。但其对长作业不利;不能保证紧迫性作业(进程)被及时处理;作业的长短只是被估算出来的。
-
RR 时间片轮转法(Round Robin):让每个进程在就绪队列中的等待时间与享受服务的时间成比例,也就是需要将CPU的处理时间分成固定大小的时间片,如果一个进程在被调度选中之后用完了系统规定的时间片,但又未完成要求的任务,则它自行释放自己所占有的CPU而排到就绪队列的末尾,等待下一次调度。同时,进程调度程序又去调度当前就绪队列中的第一个进程。
- 显然,轮转法只能用来调度分配一些可以抢占的资源。这些可以抢占的资源可以随时被剥夺,而且可以将它们再分配给别的进程。CPU是可抢占资源的一种。但打印机等资源是不可抢占的。由于作业调度是对除了CPU之外的所有系统硬件资源的分配,其中包含有不可抢占资源,所以作业调度不使用轮转法。
- 在轮转法中,时间片长度的选取非常重要。首先,时间片长度的选择会直接影响到系统的开销和响应时间。如果时间片长度过短,则调度程序抢占处理机的次数增多。这将使进程上下文切换次数也大大增加,从而加重系统开销。反过来,如果时间片长度选择过长,例如,一个时间片能保证就绪队列中所需执行时间最长的进程能执行完毕,则轮转法变成了先来先服务法。时间片长度的选择是根据系统对响应时间的要求和就绪队列中所允许最大的进程数来确定的。
- 在轮转法中,加入到就绪队列的进程有3种情况:
- 分给某进程的时间片用完,但进程还未完成,回到就绪队列的末尾等待下次调度去继续执行。
- 分给该进程的时间片并未用完,只是因为请求I/O或由于进程的互斥与同步关系而被阻塞。当阻塞解除之后再回到就绪队列。
- 新创建进程进入就绪队列。
多级反馈队列
-
如果对这些进程区别对待,给予不同的优先级和时间片从直观上看,可以进一步改善系统服务质量和效率。例如,我们可把就绪队列按照进程到达就绪队列的类型和进程被阻塞时的阻塞原因分成不同的就绪队列,每个队列按FCFS原则排列,各队列之间的进程享有不同的优先级,但同一队列内优先级相同。这样,当一个进程在执行完它的时间片之后,或从睡眠中被唤醒以及被创建之后,将进入不同的就绪队列。
-
多级反馈队列调度算法则不必事先知道各种进程所需的执行时间,而且还可以满足各种类型进程的需要,因而它是目前被公认的一种较好的进程调度算法。在采用多级反馈队列调度算法的系统中,调度算法的实施过程如下所述。
- 应设置多个就绪队列,并为各个队列赋予不同的优先级。第一个队列的优先级最高,第二个队列次之,其余各队列的优先权逐个降低。该算法赋予各个队列中进程执行时间片的大小也各不相同,在优先权愈高的队列中,为每个进程所规定的执行时间片就愈小。例如,第二个队列的时间片要比第一个队列的时间片长一倍,……,第i+1个队列的时间片要比第i个队列的时间片长一倍。
- 当一个新进程进入内存后,首先将它放入第一队列的末尾,按FCFS原则排队等待调度。当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第二队列的末尾,再同样地按FCFS原则等待调度执行;如果它在第二队列中运行一个时间片后仍未完成,再依次将它放入第三队列,……,如此下去,当一个长作业(进程)从第一队列依次降到第n队列后,在第n 队列便采取按时间片轮转的方式运行。
- 仅当第一队列空闲时,调度程序才调度第二队列中的进程运行;仅当第1~(i-1)队列均空时,才会调度第i队列中的进程运行。如果处理机正在第i队列中为某进程服务时,又有新进程进入优先权较高的队列(第1~(i-1)中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第i队列的末尾,把处理机分配给新到的高优先权进程。
进程状态介绍
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N8qoSbkT-1614861271631)(C:\Users\Administrator\Desktop\interview_python-master\img\进程调度.png)]
状态描述
-
就绪(Ready)状态:当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。
-
执行/运行(Running)状态:当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。
-
阻塞(Blocked)状态:正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。
同步/异步
-
同步(synchronous): 所谓同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。
-
异步(asynchronous):所谓异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。
进程创建/结束
(1)创建
但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,一些操作系统只为一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在。
- 对于通用系统(跑很多应用程序),需要有系统运行过程中创建或撤销进程的能力,主要分为4中形式创建新的进程:
- 系统初始化(查看进程linux中用ps命令,windows中用任务管理器,前台进程负责与用户交互,后台运行的进程与用户无关,运行在后台并且只在需要时才唤醒的进程,称为守护进程,如电子邮件、web页面、新闻、打印)。
- 一个进程在运行过程中开启了子进程(如nginx开启多进程,os.fork,subprocess.Popen等)。
- 用户的交互式请求,而创建一个新进程(如用户双击暴风影音)。
- 一个批处理作业的初始化(只在大型机的批处理系统中应用)。
无论哪一种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的。
(2)结束
- 正常退出(自愿,如用户点击交互式页面的叉号,或程序执行完毕调用发起系统调用正常退出,在linux中用exit,在windows中用ExitProcess)。
- 出错退出(自愿,python a.py中a.py不存在)。
- 严重错误(非自愿,执行非法指令,如引用不存在的内存,1/0等,可以捕捉异常,try…except…)。
- 被其他进程杀死(非自愿,如kill -9)。
Python中进程操作
(1) multiprocess.Process模块
process模块是一个创建进程的模块,借助这个模块,就可以完成进程的创建。
语法:Process([group [, target [, name [, args [, kwargs]]]]])
由该类实例化得到的对象,表示一个子进程中的任务(尚未启动)。
注意:1. 必须使用关键字方式来指定参数;2. args指定的为传给target函数的位置参数,是一个元祖形式,必须有逗号。
参数介绍:
group:参数未使用,默认值为None。
target:表示调用对象,即子进程要执行的任务。
args:表示调用的位置参数元祖。
kwargs:表示调用对象的字典。如kwargs = {‘name’:Jack, ‘age’:18}。
name:子进程名称。
返回值:实例化对象
方法/属性 | 说明 |
---|---|
start() | 启动进程,调用进程中的run()方法。 |
run() | 进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 。 |
terminate() | 强制终止进程,不会进行任何清理操作。如果该进程终止前,创建了子进程,那么该子进程在其强制结束后变为僵尸进程;如果该进程还保存了一个锁那么也将不会被释放,进而导致死锁。使用时,要注意。 |
is_alive() | 判断某进程是否存活,存活返回True,否则False。 |
join([timeout]) | 主线程等待子线程终止。timeout为可选择超时时间;需要强调的是,p.join只能join住start开启的进程,而不能join住run开启的进程 。 |
daemon | 默认值为False,如果设置为True,代表该进程为后台守护进程;当该进程的父进程终止时,该进程也随之终止;并且设置为True后,该进程不能创建子进程,设置该属性必须在start()之前 |
name | 进程名称。 |
pid | 进程pid |
exitcode | 进程运行时为None,如果为-N,表示被信号N结束了。 |
authkey | 进程身份验证,默认是由os.urandom()随机生成32字符的字符串。这个键的用途是设计涉及网络连接的底层进程间的通信提供安全性,这类连接只有在具有相同身份验证才能成功。 |
实例1:
import os
from multiprocessing import Process
def func_one():
print("This is son_one")
print("son_one:%s father:%s" % (os.getpid(), os.getppid()))
def func_two():
print("This is son_two")
print("son_two:%s father:%s" % (os.getpid(), os.getppid()))
if __name__ == '__main__':
p_one = Process(target=func_one)
P_two = Process(target=func_two)
p_one.start()
P_two.start()
print("son:%s father:%s" % (os.getpid(), os.getppid())) # father是pycharm
结果:
son:14560 father:8040
This is son_one
This is son_two
son_one:5228 father:14560
son_two:9736 father:14560Process finished with exit code 0
实例2:
我们也可以使用迭代来处理进程的执行:
import time
from multiprocessing import Process
def func_one(name):
print("My name is", name)
time.sleep(1)
def func_two(age):
print("My age is", age)
time.sleep(5)
if __name__ == '__main__':
lst_one = []
lst_two = []
for i in range(5):
p_one = Process(target=func_one, args=('Mike',))
p_two = Process(target=func_two, args=(18,))
p_one.start()
p_two.start()
lst_two.append(p_two)
# [p_two.join() for p_two in lst_two] # 父进程要等子进程结束
print("End")
结果:
End
My name is Mike
My age is 18
My age is 18
My age is 18
My age is 18
My name is Mike
My name is Mike
My age is 18
My name is Mike
My name is MikeProcess finished with exit code 0
去掉注释结果:
My name is Mike
My age is 18
My name is Mike
My name is Mike
My age is 18
My age is 18
My age is 18
My name is Mike
My name is Mike
My age is 18
EndProcess finished with exit code 0
除了上面这些开启进程的方法之外,还有一种以继承Process的方式开启进程的方式:
import os
from multiprocessing import Process
class MyProcess(Process):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print("进程为%s,父进程为%s" % (os.getpid(), os.getppid()))
print("我的名字是%s" % self.name)
if __name__ == '__main__':
p_one = MyProcess('张三')
p_two = MyProcess('李四')
p_thr = MyProcess('王五')
p_one.start() # 自动调用run()
p_two.start()
p_thr.run() # 直接调用run()
p_one.join()
p_two.join()
# p_thr.join() # 调用run()函数的不可以调用join()
print("主进程结束")
结果:
进程为22376,父进程为17808
我的名字是王五
进程为15212,父进程为22376
我的名字是张三
进程为20588,父进程为22376
我的名字是李四
主进程结束Process finished with exit code 0
注意:直接调用run()函数的进程不能使用join()方法。
下面我们来演示进程之间数据隔离的问题:
from multiprocessing import Process
def func_one():
global n
n = 0
print("在func_one中的n为%s" % n)
def func_two():
global n
n = 1
print("在func_two中的n为%s" % n)
if __name__ == '__main__':
n = 100
p_one = Process(target=func_one)
p_two = Process(target=func_two)
p_one.start()
p_two.start()
print("主进程的n为%s" % n)
结果:
主进程的n为100
在func_one中的n为0
在func_two中的n为1Process finished with exit code 0
守护进程
守护进程就是会随着主进程的结束而结束的进程,具有以下两个特点:
- 守护进程会在主进程代码执行结束后就终止。
- 守护进程内无法再开启子进程,否则抛出异常。
实例1:
import os
import time
from multiprocessing import Process
class MyProcess(Process):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print("进程为%s,父进程为%s" % (os.getpid(), os.getppid()))
print("我的名字是%s" % self.name)
if __name__ == '__main__':
p_one = MyProcess('张三')
p_two = MyProcess('李四')
p_two.daemon = True # 默认为False,必须在start()之前设置
p_one.start()
p_two.start()
time.sleep(5)
print("主进程结束")
结果:
进程为22452,父进程为22344
我的名字是张三
进程为11336,父进程为22344
我的名字是李四
主进程结束Process finished with exit code 0
多进程与socket
服务端:
from socket import *
from multiprocessing import Process
HOST = '127.0.0.1'
PORT = 8080
ADDRESS = (HOST, PORT)
BUFF_SIZE = 1024
ss = socket(AF_INET, SOCK_STREAM)
ss.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
ss.bind(ADDRESS)
ss.listen(5)
def talk(conn, add):
while 1:
try:
msg = conn.recv(BUFF_SIZE)
if not msg:
break
conn.send(msg.upper())
except Exception:
break
if __name__ == '__main__':
while 1:
conn, add = ss.accept()
p = Process(target=talk, args=(conn, add))
p.start()
客户端:
from socket import *
HOST = '127.0.0.1'
PORT = 8080
ADDRESS = (HOST, PORT)
BUFF_SIZE = 1024
sc = socket(AF_INET,SOCK_STREAM)
sc.connect(ADDRESS)
while 1:
msg = input(">>>".strip())
if not msg:
continue
sc.send(msg.encode('utf-8'))
msg = sc.recv(BUFF_SIZE)
print(msg.decode('utf-8'))
多进程中的其它方法
terminate()和is_alive(): 结束进程和判断进程是否存活。如,
锁——Lock
当多个进程使用同一份数据资源的时候,就会引发数据安全或顺序混乱问题:
from multiprocessing import Process, Lock
import time
import json
import random
def search():
dic = json.load(open('db'))
time.sleep(random.random()) # 模拟读取数据
print("\033[43m剩余票数:%s\033[0m" % dic['count'])
def get():
dic = json.load(open('db'))
time.sleep(random.random()) # 模拟网络延迟
if dic['count'] > 0:
dic['count'] -= 1 # 购票成功后减一
time.sleep(1)
json.dump(dic, open('db', 'w'))
print("\033[43m购票成功\033[0m")
else:
print("尚无余票")
def task():
search()
get()
if __name__ == '__main__':
for i in range(100):
p = Process(target=task)
p.start()
没有使用锁,以上代码会造成错误和紊乱。下面是加锁改进版:
from multiprocessing import Process, Lock
import time
import json
import random
def search():
dic = json.load(open('db'))
time.sleep(random.random()) # 模拟读取数据
print("\033[43m剩余票数:%s\033[0m" % dic['count'])
def get():
dic = json.load(open('db'))
time.sleep(random.random()) # 模拟网络延迟
if dic['count'] > 0:
dic['count'] -= 1 # 购票成功后减一
time.sleep(1)
json.dump(dic, open('db', 'w'))
print("\033[32m购票成功\033[0m")
else:
print("\033[31m尚无余票\033[0m")
def task(lock):
lock.acquire()
search()
get()
lock.release()
if __name__ == '__main__':
lock = Lock()
for i in range(10):
p = Process(target=task, args=(lock,))
p.start()
结果(db文件为{“count”: 3}):
剩余票数:3
购票成功
剩余票数:2
购票成功
剩余票数:1
购票成功
剩余票数:0
尚无余票
剩余票数:0
尚无余票
剩余票数:0
尚无余票
剩余票数:0
尚无余票
剩余票数:0
尚无余票
剩余票数:0
尚无余票
剩余票数:0
尚无余票Process finished with exit code 0
另一个版本:
from multiprocessing import Process, Lock
import time
import json
import random
def search(num):
dic = json.load(open('db'))
time.sleep(random.random())
print("第%s个人查到余票还剩下%s张" % (num, dic['count']))
def get(num, lock):
lock.acquire()
dic = json.load(open('db'))
time.sleep(random.random())
if dic['count'] > 0:
print('\033[31m 第%s个人买到票了\033[0m' % num)
dic['count'] -= 1
json.dump(dic, open('db', 'w'))
time.sleep(random.random())
else:
print('\033[32m 第%s个人没有买到票\033[0m' % num)
lock.release()
if __name__ == '__main__':
l = Lock()
for i in range(100):
p_search = Process(target=search, args=(i + 1,))
p_search.start()
for i in range(10):
p_get = Process(target=get, args=(i + 1, l))
p_get.start()
结果:(略)
- 加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改。加锁牺牲了速度,但是却保证了数据的安全。
#因此我们最好找寻一种解决方案能够兼顾:1、效率高(多个进程共享一块内存的数据)2、帮我们处理好锁问题。
mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道。 队列和管道都是将数据存放于内存中 队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来, 我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。
事件
-
事件(Event),用于线程间通信,即程序中的其一个线程需要通过判断某个线程的状态来确定自己下一步的操作,就用到了event对象。
-
事件处理的机制:
- 全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。
- clear:将“Flag”设置为False。
- set:将“Flag”设置为True。
红绿灯实例:
from multiprocessing import Process, Event
import time
import random
def traffic_light(e):
'''信号灯函数'''
while 1: # 红绿灯得一直亮着,要么是红灯要么是绿灯
if e.is_set(): # True,代表绿灯亮,那么此时代表可以过车
time.sleep(5) # 所以在这让灯等5秒钟,这段时间让车过
print('\033[31m红灯亮!\n车辆等待中...\033[0m') # 绿灯亮了5秒后应该提示到红灯亮
e.clear() # 把is_set设置为False
else:
time.sleep(5) # 此时代表红灯亮了,此时应该红灯亮5秒,在此等5秒
print('\033[32m绿灯亮!\n车辆通过中...\033[0m') # 红的亮够5秒后,该绿灯亮了
e.set() # 将is_set设置为True
def car_status(num, e):
e.wait() # 车等在红绿灯,此时要看是红灯还是绿灯,如果is_set为True就是绿灯,此时可以过车
print('第%s辆车过去了' % num)
if __name__ == '__main__':
event = Event()
tra_light = Process(target=traffic_light, args=(event,)) # 信号灯的进程
tra_light.start()
for i in range(50): # 描述50辆车的进程
if i % 3 == 0:
time.sleep(random.randint(1, 5)) # 车辆出现时间随机
car = Process(target=car_status, args=(i + 1, event,))
car.start()
结果:(略)
队列和管道
- 队列和管道属于进程之间的通信机制。
(1)队列(Queue)
-
创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
-
Queue[maxsize]:maxsize
是队列中允许的最大项数。如果省略此参数,则无大小限制。底层队列使用管道和锁定实现。另外,还需要运行支持线程以便队列中的数据传输到底层管道中。
设q为队列实例化对象:
方法 | 说明 |
---|---|
q.get( [ block [ ,timeout ] ] ) | 返回q中的一个项目。如果q为空,此方法将阻塞,直到队列中有项目可用为止。block用于控制阻塞行为,默认为True. 如果设置为False,将引发Queue.Empty异常(定义在Queue模块中)。timeout是可选超时时间,用在阻塞模式中。如果在制定的时间间隔内没有项目变为可用,将引发Queue.Empty异常。 |
q.get_nowait( ) | 同q.get(False)方法。 |
q.put(item [, block [,timeout ] ] ) | 将item放入队列。如果队列已满,此方法将阻塞至有空间可用为止。block控制阻塞行为,默认为True。如果设置为False,将引发Queue.Empty异常(定义在Queue库模块中)。timeout指定在阻塞模式中等待可用空间的时间长短。超时后将引发Queue.Full异常。 |
q.qsize() | 返回队列中目前项目的正确数量。此函数的结果并不可靠,因为在返回结果和在稍后程序中使用结果之间,队列中可能添加或删除了项目。在某些系统上,此方法可能引发NotImplementedError异常。 |
q.empty() | 如果调用此方法时 q为空,返回True。如果其他进程或线程正在往队列中添加项目,结果是不可靠的。也就是说,在返回和使用结果之间,队列中可能已经加入新的项目。 |
q.full() | 如果q已满,返回为True. 由于线程的存在,结果也可能是不可靠的(参考q.empty()方法)。 |
q.close() | 关闭队列,防止队列中加入更多数据。调用此方法时,后台线程将继续写入那些已入队列但尚未写入的数据,但将在此方法完成时马上关闭。如果q被垃圾收集,将自动调用此方法。关闭队列不会在队列使用者中生成任何类型的数据结束信号或异常。例如,如果某个使用者正被阻塞在get()操作上,关闭生产者中的队列不会导致get()方法返回错误。 |
q.cancel_join_thread() | 不会再进程退出时自动连接后台线程。这可以防止join_thread()方法阻塞。 |
q.join_thread() | 连接队列的后台线程。此方法用于在调用q.close()方法后,等待所有队列项被消耗。默认情况下,此方法由不是q的原始创建者的所有进程调用。调用q.cancel_join_thread()方法可以禁止这种行为。 |
import time
from multiprocessing import Process, Queue
def func(queue):
queue.put([time.asctime(), 'Hello', 'Python'])
if __name__ == '__main__':
q = Queue()
p = Process(target=func, args=(q,))
p.start()
print(q.get())
p.join()
结果:
[‘Mon Aug 27 15:26:47 2018’, ‘Hello’, ‘Python’]
Process finished with exit code 0
import os
import time
import multiprocessing
def input_queue(q):
info = str(os.getpid()) + '(put):' + str(time.asctime())
q.put(info)
print(info)
def output_queue(q):
info = q.get()
print('\033[32m%s\033[0m' % info)
if __name__ == '__main__':
multiprocessing.freeze_support()
record_one = []
record_two = []
queue = multiprocessing.Queue(3)
# 放入数据
for i in range(10):
p = multiprocessing.Process(target=input_queue, args=(queue,))
p.start()
record_one.append(p)
# 取出数据
for i in range(10):
p = multiprocessing.Process(target=output_queue, args=(queue,))
p.start()
record_one.append(p)
for p in record_one:
p.join()
for p in record_two:
p.join()
结果:
328(put):Mon Aug 27 15:45:34 2018
328(put):Mon Aug 27 15:45:34 2018
27164(put):Mon Aug 27 15:45:34 2018
27164(put):Mon Aug 27 15:45:34 2018
8232(put):Mon Aug 27 15:45:34 2018
8232(put):Mon Aug 27 15:45:34 2018
23804(put):Mon Aug 27 15:45:34 2018
23804(put):Mon Aug 27 15:45:34 2018
23004(put):Mon Aug 27 15:45:34 2018
23004(put):Mon Aug 27 15:45:34 2018
22184(put):Mon Aug 27 15:45:34 2018
22184(put):Mon Aug 27 15:45:34 2018
24948(put):Mon Aug 27 15:45:34 2018
24948(put):Mon Aug 27 15:45:34 2018
19704(put):Mon Aug 27 15:45:34 2018
19704(put):Mon Aug 27 15:45:34 2018
8200(put):Mon Aug 27 15:45:34 2018
8200(put):Mon Aug 27 15:45:34 2018
14380(put):Mon Aug 27 15:45:34 2018
14380(put):Mon Aug 27 15:45:34 2018Process finished with exit code 0
(2)管道(Pipe)
-
Pipe([duplex]):在线程之间创建一条管道,并返回元祖(con1,con2),其中con1,con2表示管道两端连接的对象。
-
duplex:默认管道为全双工的,如果将duplex映射为False,con1只能用于接收,con2只能由于发送。
注意:必须在产生Process之前产生管道。
方法 | 说明 |
---|---|
con1.recv() | 接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。 |
con1.send(obj) | 通过连接发送对象。obj是与序列化兼容的任意对象。 |
con1.close() | 关闭连接。如果conn1被垃圾回收,将自动调用此方法。 |
con1.fileno() | 返回连接使用的整数文件描述符。 |
conn1.poll([timeout]) | 如果连接上的数据可用,返回True。timeout指定等待的最长时限。如果省略此参数,方法将立即返回结果。如果将timeout射成None,操作将无限期地等待数据到达。 |
conn1.recv_bytes([maxlength]) | 接收c.send_bytes()方法发送的一条完整的字节消息。maxlength指定要接收的最大字节数。如果进入的消息,超过了这个最大值,将引发IOError异常,并且在连接上无法进行进一步读取。如果连接的另外一端已经关闭,再也不存在任何数据,将引发EOFError异常。 |
conn.send_bytes(buffer [, offset [, size]]) | 通过连接发送字节数据缓冲区,buffer是支持缓冲区接口的任意对象,offset是缓冲区中的字节偏移量,而size是要发送字节数。结果数据以单条消息的形式发出,然后调用c.recv_bytes()函数进行接收。 |
conn1.recv_bytes_into(buffer [, offset]) | 接收一条完整的字节消息,并把它保存在buffer对象中,该对象支持可写入的缓冲区接口(即bytearray对象或类似的对象)。offset指定缓冲区中放置消息处的字节位移。返回值是收到的字节数。如果消息长度大于可用的缓冲区空间,将引发BufferTooShort异常。 |
实例:
from multiprocessing import Process, Pipe
def func(conn):
conn.send("Hello,This is Python!") # 发送
conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=func, args=(child_conn,))
p.start()
print(parent_conn.recv()) # 接收
p.join()
结果:
Hello,This is Python!
Process finished with exit code 0
- 管道端点的正确管理问题。如果是生产者或消费者中都没有使用管道的某个端点,就应将它关闭。这也说明了为何在生产者中关闭了管道的输出端,在消费者中关闭管道的输入端。如果忘记执行这些步骤,程序可能在消费者中的recv()操作上挂起。管道是由操作系统进行引用计数的,必须在所有进程中关闭管道后才能生成EOFError异常。因此,在生产者中关闭管道不会有任何效果,除非消费者也关闭了相同的管道端点。
下面的操作将引发EOFError:
from multiprocessing import Process, Pipe
def func(parent_conn, child_conn):
# parent_conn.close() # 写了close()将引发OSError
while 1:
try:
print(child_conn.recv())
except EOFError:
child_conn.close()
if __name__ == '__main__':
parent_conn, child_conn = Pipe()
p = Process(target=func, args=(parent_conn, child_conn))
p.start()
child_conn.close()
parent_conn.send("Hello,This is Python!")
parent_conn.close()
p.join()
结果:
Hello,This is Python!
Manager
- 进程间数据是独立的,可以借助于队列或管道实现通信,二者都是基于消息传递的。虽然进程间数据独立,但可以通过Manager实现数据共享,事实上Manager的功能远不止于此:
from multiprocessing import Manager, Process, Lock
def work(d, lock):
with lock: # 不加锁而操作共享的数据,数据会出乱
d['count'] -= 1
if __name__ == '__main__':
lock = Lock()
with Manager() as m:
dic = m.dict({'count': 100})
p_l = []
for i in range(100):
p = Process(target=work, args=(dic, lock))
p_l.append(p)
p.start()
for p in p_l:
p.join()
print(dic)
结果:(略)
进程池
问题:为什么要有进程池?进程池的概念?
在程序实际处理问题过程中,忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务开启或者结束进程。那么我们要怎么做呢?
在这里,要给大家介绍一个进程池的概念,定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务,等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务。如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。也就是说,池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行。这样不会增加操作系统的调度难度,还节省了开闭进程的时间,也一定程度上能够实现并发效果。
语法:Pool([numprocess [,initializer [, initargs]]]):创建进程池
参数:
- numprocess:要创建的进程数,默认为cpu_count()的值。
- initializer:是每个工作进程启动时要执行的可调用对象,默认为None。
- initargs:是要传给initializer的参数组。
方法 | 说明 |
---|---|
apply(func[, args=()[, kwds={}]]) | 该函数用于传递不定参数,主进程会被阻塞直到函数执行结束(不建议使用,并且3.x以后不在出现)。 |
apply_async(func[, args=()[, kwds={}[, callback=None]]]) | 与apply用法一样,但它是非阻塞且支持结果返回进行回调。 |
map(func, iterable[, chunksize=None]) | Pool类中的map方法,与内置的map函数用法行为基本一致,它会使进程阻塞直到返回结果。 注意,虽然第二个参数是一个迭代器,但在实际使用中,必须在整个队列都就绪后,程序才会运行子进程。 |
close() | 关闭进程池(pool),使其不在接受新的任务。 |
terminate() | 结束工作进程,不在处理未处理的任务。 |
join() | 主进程阻塞等待子进程的退出,join方法必须在close或terminate之后使用。 |
下面演示一些实例:
import time
from multiprocessing import Pool
def run(num):
'''
计算num的num次方
:param num: 数字
:return: num的num次方
'''
time.sleep(1)
return num ** num
if __name__ == '__main__':
lst = [1, 2, 3, 4, 5, 6]
print("顺序:")
t_one = time.time()
for fn in lst:
run(fn)
t_two = time.time()
print("执行时间:", t_two - t_one)
print("多进程:")
pool = Pool(5)
res = pool.map(run, lst)
pool.close()
pool.join()
t_thr = time.time()
print("执行时间:", t_thr - t_two)
print(res)
结果:
顺序:
执行时间: 6.0030517578125
多进程:
执行时间: 2.216660976409912
[1, 4, 27, 256, 3125, 46656]Process finished with exit code 0
上例是一个创建多个进程并发处理与顺序执行处理同一数据,所用时间的差别。从结果可以看出,并发执行的时间明显比顺序执行要快很多,但是进程是要耗资源的,所以平时工作中,进程数也不能开太大。
程序中的res表示全部进程执行结束后全局的返回结果集,run函数有返回值,所以一个进程对应一个返回结果,这个结果存在一个列表中,也就是一个结果堆中,实际上是用了队列的原理,等待所有进程都执行完毕,就返回这个列表(列表的顺序不定)。
对Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),让其不再接受新的Process了。
再来分析一个例子:
import time
from multiprocessing import Pool
def run(num):
time.sleep(2)
print(num ** num)
if __name__ == '__main__':
start_time = time.time()
lst = [1, 2, 3, 4, 5, 6]
pool = Pool(10)
pool.map(run, lst)
pool.close()
pool.join()
end_time = time.time()
print("时间:", end_time - start_time)
结果:
42561
466563125
27
时间: 2.455129384994507
Process finished with exit code 0
再运行:
14
27
256
3125
46656
时间: 2.4172260761260986Process finished with exit code 0
问题:结果中为什么还有空行和没有折行的数据呢?
其实这跟进程调度有关,当有多个进程并行执行时,每个进程得到的时间片时间不一样,哪个进程接受哪个请求以及执行完成时间都是不定的,所以会出现输出乱序的情况。那为什么又会有没这行和空行的情况呢?因为有可能在执行第一个进程时,刚要打印换行符时,切换到另一个进程,这样就极有可能两个数字打印到同一行,并且再次切换回第一个进程时会打印一个换行符,所以就会出现空行的情况。
利用Python进行系统管理的时候,特别是同时操作多个文件目录,或者远程控制多台主机,并行操作可以节约大量的时间。当被操作对象数目不大时,可以直接利用multiprocessing中的Process动态成生多个进程,几个还好,但如果是上百个,上千个目标,手动的去限制进程数量却又太过繁琐,这时候进程池Pool发挥作用的时候就到了。
19 协程
1、协程的概念
-
协程,又称微线程,纤程。英文名Coroutine。
-
线程是系统级别的,它们由操作系统调度。
-
而协程则是程序级别的由程序根据需要自己调度。在一个线程中会有很多函数,我们把这些函数称为子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程就称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似与yield操作。
-
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
-
协程的优点:
- (1)无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力)
- (2)无需原子操作锁定及同步的开销
- (3)方便切换控制流,简化编程模型
- (4)高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
-
协程的缺点:
- (1)无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- (2)进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
2、 协程的发展阶段
-
Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:
- 最初的生成器变形yield/send
- 引入@asyncio.coroutine
- yield from * 引入async/await关键字
Python2.x协程
yield + send(利用生成器实现协程)
生产者生产消息后,直接通过yield跳转到消费者开始执行,
待消费者执行完毕后,
切换回生产者继续生产。
#-*- coding:utf8 -*- def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER]Consuming %s...' % n) r = '200 OK' def producer(c): # 启动生成器 c.send(None) n = 0 while n < 5: n = n + 1 print('[PRODUCER]Producing %s...' % n) r = c.send(n) print('[PRODUCER]Consumer return: %s' % r) c.close() if __name__ == '__main__': c = consumer() producer(c)
-
send(msg)
与next()
的区别在于:send
可以传递参数给yield
表达式,这时传递的参数会作为yield
表达式的值,而yield
的参数是返回给调用者的值。换句话说,就是send
可以强行修改上一个yield
表达式的值。上述例子运行之后输出结果如下:[PRODUCER]Producing 1... [CONSUMER]Consuming 1... [PRODUCER]Consumer return: 200 OK [PRODUCER]Producing 2... [CONSUMER]Consuming 2... [PRODUCER]Consumer return: 200 OK [PRODUCER]Producing 3... [CONSUMER]Consuming 3... [PRODUCER]Consumer return: 200 OK [PRODUCER]Producing 4... [CONSUMER]Consuming 4... [PRODUCER]Consumer return: 200 OK [PRODUCER]Producing 5... [CONSUMER]Consuming 5... [PRODUCER]Consumer return: 200 OK
Python3.x协程
除了Python2.x中协程的实现方式,Python3.x还提供了如下方式实现协程: asyncio + yield from (python3.4+) asyncio + async/await (python3.5+)
Python3.4以后引入了asyncio模块,可以很好的支持协程。
asyncio + yield from
asyncio是Python3.4版本引入的标准库,直接内置了对异步IO的支持。asyncio的异步操作,需要在coroutine中通过
yield from
完成。看如下代码(需要在Python3.4以后版本使用):#-*- coding:utf8 -*- import asyncio @asyncio.coroutine def test(i): print('test_1', i) r = yield from asyncio.sleep(1) print('test_2', i) if __name__ == '__main__': loop = asyncio.get_event_loop() tasks = [test(i) for i in range(3)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
@asyncio.coroutine
把一个generator标记为coroutine类型,然后就把这个coroutine扔到EventLoop中执行。test()会首先打印出test_1,然后yield from
语法可以让我们方便地调用另一个generator。由于asyncio.sleep()
也是一个coroutine,所以线程不会等待asyncio.sleep()
,而是直接中断并执行下一个消息循环。当asyncio.sleep()
返回时,线程就可以从yield from
拿到返回值(此处是None),然后接着执行下一行语句。把asyncio.sleep(1)
看成是一个耗时1秒的IO操作,在此期间主线程并未等待,而是去执行EventLoop中其他可以执行的coroutine了,因此可以实现并发执行。asyncio + async/await
为了简化并更好地标识异步IO,从Python3.5开始引入了新的语法async和await,可以让coroutine的代码更简洁易读。请注意,async和await是coroutine的新语法,使用新语法只需要做两步简单的替换:
- 把@asyncio.coroutine替换为async
- 把yield from替换为await
看如下代码(在Python3.5以上版本使用):
#-*- coding:utf8 -*- import asyncio async def test(i): print('test_1', i) await asyncio.sleep(1) print('test_2', i) if __name__ == '__main__': loop = asyncio.get_event_loop() tasks = [test(i) for i in range(3)] loop.run_until_complete(asyncio.wait(tasks)) loop.close()
运行结果与之前一致。与前一节相比,这里只是把yield from换成了await,@asyncio.coroutine换成了async,其余不变。
Gevent
Gevent是一个基于Greenlet实现的网络库,通过greenlet实现协程。基本思想是一个greenlet就认为是一个协程,当一个greenlet遇到IO操作的时候,比如访问网络,就会自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO操作。
Greenlet是作为一个C扩展模块,它封装了libevent事件循环的API,可以让开发者在不改变编程习惯的同时,用同步的方式写异步IO的代码。
#-*- coding:utf8 -*- import gevent def test(n): for i in range(n): print(gevent.getcurrent(), i) if __name__ == '__main__': g1 = gevent.spawn(test, 3) g2 = gevent.spawn(test, 3) g3 = gevent.spawn(test, 3) g1.join() g2.join() g3.join()
运行结果:
<Greenlet at 0x10a6eea60: test(3)> 0 <Greenlet at 0x10a6eea60: test(3)> 1 <Greenlet at 0x10a6eea60: test(3)> 2 <Greenlet at 0x10a6eed58: test(3)> 0 <Greenlet at 0x10a6eed58: test(3)> 1 <Greenlet at 0x10a6eed58: test(3)> 2 <Greenlet at 0x10a6eedf0: test(3)> 0 <Greenlet at 0x10a6eedf0: test(3)> 1 <Greenlet at 0x10a6eedf0: test(3)> 2
可以看到3个greenlet是依次运行而不是交替运行。要让greenlet交替运行,可以通过
gevent.sleep()
交出控制权:def test(n): for i in range(n): print(gevent.getcurrent(), i) gevent.sleep(1)
运行结果:
<Greenlet at 0x10382da60: test(3)> 0 <Greenlet at 0x10382dd58: test(3)> 0 <Greenlet at 0x10382ddf0: test(3)> 0 <Greenlet at 0x10382da60: test(3)> 1 <Greenlet at 0x10382dd58: test(3)> 1 <Greenlet at 0x10382ddf0: test(3)> 1 <Greenlet at 0x10382da60: test(3)> 2 <Greenlet at 0x10382dd58: test(3)> 2 <Greenlet at 0x10382ddf0: test(3)> 2
当然在实际的代码里,我们不会用
gevent.sleep()
去切换协程,而是在执行到IO操作时gevent会自动完成,所以gevent需要将Python自带的一些标准库的运行方式由阻塞式调用变为协作式运行。这一过程在启动时通过monkey patch完成:#-*- coding:utf8 -*- from gevent import monkey; monkey.patch_all() from urllib import request import gevent def test(url): print('Get: %s' % url) response = request.urlopen(url) content = response.read().decode('utf8') print('%d bytes received from %s.' % (len(content), url)) if __name__ == '__main__': gevent.joinall([ gevent.spawn(test, 'http://httpbin.org/ip'), gevent.spawn(test, 'http://httpbin.org/uuid'), gevent.spawn(test, 'http://httpbin.org/user-agent') ])
运行结果:
Get: http://httpbin.org/ip Get: http://httpbin.org/uuid Get: http://httpbin.org/user-agent 53 bytes received from http://httpbin.org/uuid. 40 bytes received from http://httpbin.org/user-agent. 31 bytes received from http://httpbin.org/ip.
从结果看,3个网络操作是并发执行的,而且结束顺序不同,但只有一个线程。
总结
至此Python中的协程就介绍完毕了,示例程序中都是以sleep代表异步IO的,在实际项目中可以使用协程异步的读写网络、读写文件、渲染界面等,而在等待协程完成的同时,CPU还可以进行其他的计算,协程的作用正在于此。那么协程和多线程的差异在哪里呢?多线程的切换需要靠操作系统来完成,当线程越来越多时切换的成本会很高,而协程是在一个线程内切换的,切换过程由我们自己控制,因此开销小很多,这就是协程和多线程的根本差异。
20 闭包
闭包(closure)是函数式编程的重要的语法结构。闭包也是一种组织代码的结构,它同样提高了代码的可重复使用性。
当一个内嵌函数引用其外部作作用域的变量,我们就会得到一个闭包. 总结一下,创建一个闭包必须满足以下几点:
- 必须有一个内嵌函数
- 内嵌函数必须引用外部函数中的变量
- 外部函数的返回值必须是内嵌函数
21 lambda函数
其实就是一个匿名函数,为什么叫lambda?因为和后面的函数式编程有关.
推荐: 知乎
22 Python函数式编程
这个需要适当的了解一下吧,毕竟函数式编程在Python中也做了引用.
推荐: 酷壳
python中函数式编程支持:
filter 函数的功能相当于过滤器。调用一个布尔函数bool_func
来迭代遍历每个seq中的元素;返回一个使bool_seq
返回值为true的元素的序列。
>>>a = [1,2,3,4,5,6,7]
>>>b = filter(lambda x: x > 5, a)
>>>print b
>>>[6,7]
map函数是对一个序列的每个项依次执行函数,下面是对一个序列每个项都乘以2:
>>> a = map(lambda x:x*2,[1,2,3])
>>> list(a)
[2, 4, 6]
reduce函数是对一个序列的每个项迭代调用函数,下面是求3的阶乘:
>>> reduce(lambda x,y:x*y,range(1,4))
6
23 Python里的拷贝
引用和copy(),deepcopy()的区别
import copy
a = [1, 2, 3, 4, ['a', 'b']] #原始对象
b = a #赋值,传对象的引用
c = copy.copy(a) #对象拷贝,浅拷贝
d = copy.deepcopy(a) #对象拷贝,深拷贝
a.append(5) #修改对象a
a[4].append('c') #修改对象a中的['a', 'b']数组对象
print 'a = ', a
print 'b = ', b
print 'c = ', c
print 'd = ', d
输出结果:
a = [1, 2, 3, 4, ['a', 'b', 'c'], 5]
b = [1, 2, 3, 4, ['a', 'b', 'c'], 5]
c = [1, 2, 3, 4, ['a', 'b', 'c']]
d = [1, 2, 3, 4, ['a', 'b']]
24 Python垃圾回收机制
Python GC主要使用引用计数(reference counting)来跟踪和回收垃圾。在引用计数的基础上,通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用问题,通过“分代回收”(generation collection)以空间换时间的方法提高垃圾回收效率。
1 引用计数
PyObject是每个对象必有的内容,其中ob_refcnt
就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt
就会增加,当引用它的对象被删除,它的ob_refcnt
就会减少.引用计数为0时,该对象生命就结束了。
优点:
- 简单
- 实时性
缺点:
- 维护引用计数消耗资源
- 循环引用
2 标记-清除机制
基本思路是先按需分配,等到没有空闲内存的时候从寄存器和程序栈上的引用出发,遍历以对象为节点、以引用为边构成的图,把所有可以访问到的对象打上标记,然后清扫一遍内存空间,把所有没标记的对象释放。
3 分代技术
分代回收的整体思想是:将系统中的所有内存块根据其存活时间划分为不同的集合,每个集合就成为一个“代”,垃圾收集频率随着“代”的存活时间的增大而减小,存活时间通常利用经过几次垃圾回收来度量。
Python默认定义了三代对象集合,索引数越大,对象存活时间越长。
举例:
当某些内存块M经过了3次垃圾收集的清洗之后还存活时,我们就将内存块M划到一个集合A中去,而新分配的内存都划分到集合B中去。当垃圾收集开始工作时,大多数情况都只对集合B进行垃圾回收,而对集合A进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制需要处理的内存少了,效率自然就提高了。在这个过程中,集合B中的某些内存块由于存活时间长而会被转移到集合A中,当然,集合A中实际上也存在一些垃圾,这些垃圾的回收会因为这种分代的机制而被延迟。
25 Python的List
推荐: http://www.jianshu.com/p/J4U6rR
26 Python的is
is是对比地址,==是对比值
27 read,readline和readlines
- read 读取整个文件
- readline 读取下一行,使用生成器方法
- readlines 读取整个文件到一个迭代器以供我们遍历
28 Python2和3的区别
推荐:Python 2.7.x 与 Python 3.x 的主要差异
29 super init
super() lets you avoid referring to the base class explicitly, which can be nice. But the main advantage comes with multiple inheritance, where all sorts of fun stuff can happen. See the standard docs on super if you haven’t already.
Note that the syntax changed in Python 3.0: you can just say super().__init__
() instead of super(ChildB, self).__init__
() which IMO is quite a bit nicer.
http://stackoverflow.com/questions/576169/understanding-python-super-with-init-methods
30 range and xrange
都在循环时使用,xrange内存性能更好。
for i in range(0, 20):
for i in xrange(0, 20):
31 __str__和repr__的区别
-
__repr__
的目的是明确的 -
__str__
的目的是可读性 -
__str__
的用法包含__repr__
32 * 和 ** 的语法多义性,具体来说是有四类用法。
1. 算数运算
-
*
代表乘法 -
**
代表乘方
>>> 2 * 5
10
>>> 2 ** 5
32
2. 函数形参
-
*args 和 **kwargs 主要用于函数定义。
-
并不是必须写成
*args
和**kwargs
。*(星号)
才是*必须的***. 你也可以写成 *ar 和**k
。而写成 *args 和kwargs 只是一个通俗的命名约定。 -
python函数传递参数的方式有两种:
-
位置参数(positional argument)
-
关键词参数(keyword argument)
-
*args 与 **kwargs 的区别,两者都是 python 中的可变参数:
- *args 表示任何多个无名参数,它本质是一个 tuple
- **kwargs 表示关键字参数,它本质上是一个 dict
-
如果同时使用 *args
和 **kwargs
时,必须 *args
参数列要在 **kwargs
之前。
3. 函数实参
如果函数的形参是定长参数,也可以使用 *args 和 **kwargs 调用函数,类似对元组和字典进行解引用:
>>> def fun(data1, data2, data3):
... print("data1: ", data1)
... print("data2: ", data2)
... print("data3: ", data3)
...
>>> args = ("one", 2, 3)
>>> fun(*args)
data1: one
data2: 2
data3: 3
>>> kwargs = {"data3": "one", "data2": 2, "data1": 3}
>>> fun(**kwargs)
data1: 3
data2: 2
data3: one
4. 序列解包
序列解包 往期博客有写过,这里只列出一个例子,序列解包没有 **。
>>> a, b, *c = 0, 1, 2, 3
>>> a
0
>>> b
1
>>> c
[2, 3]
33.python中内置的数据结构有几种?
-
a. 整型 int、 长整型 long、浮点型 float、 复数 complex
-
b. 字符串 str、 列表 list、 元祖 tuple
-
c. 字典 dict 、 集合 set
-
d. Python3 中没有 long,只有无限精度的 int
34. 类的内置成员函数
1. __doc__
表示类的描述信息
2. __module__
和 __class__
__module__
表示当前操作的对象在那个模块
__class__
表示当前操作的对象的类是什么
3.__init__
构造方法,通过类创建对象时,自动触发执行。
4. __del__
- 析构方法,当对象在内存中被释放时,自动触发执行。
- 注:此方法一般无须定义,因为Python是一门高级语言,程序员在使用时无需关心内存的分配和释放,因为此工作都是交给Python解释器来执行,所以,析构函数的调用是由解释器在进行垃圾回收时自动触发执行的。
5. __call__
- 对象后面加括号,触发执行。
- 注:构造方法的执行是由创建对象触发的,即:对象 = 类名() ;而对于 call 方法的执行是由对象后加括号触发的,即:对象() 或者 类()()
6. __dict__
- 类或对象中的所有成员
- 类的普通字段属于对象;类中的静态字段和方法等属于类,即:
class Province:
country = 'China'
def __init__(self, name, count):
self.name = name
self.count = count
def func(self, *args, **kwargs):
print 'func'
# 获取类的成员,即:静态字段、方法、
print Province.__dict__
# 输出:{'country': 'China', '__module__': '__main__', 'func': <function func at 0x10be30f50>, '__init__': <function __init__ at 0x10be30ed8>, '__doc__': None}
obj1 = Province('HeBei',10000)
print obj1.__dict__
# 获取 对象obj1 的成员
# 输出:{'count': 10000, 'name': 'HeBei'}
obj2 = Province('HeNan', 3888)
print obj2.__dict__
# 获取 对象obj1 的成员
# 输出:{'count': 3888, 'name': 'HeNan'}
7. __str__
- 如果一个类中定义了__str__方法,那么在打印 对象 时,默认输出该方法的返回值。
class Foo:
def __str__(self):
return 'wupeiqi'
obj = Foo()
print obj
# 输出:wupeiqi
8、__getitem__
、__setitem__
、__delitem__
用于索引操作,如字典。以上分别表示获取、设置、删除数据
#!/usr/bin/env python
# -*- coding:utf-8 -*-
class Foo(object):
def __getitem__(self, key):
print '__getitem__',key
def __setitem__(self, key, value):
print '__setitem__',key,value
def __delitem__(self, key):
print '__delitem__',key
obj = Foo()
result = obj['k1'] # 自动触发执行 __getitem__
obj['k2'] = 'wupeiqi' # 自动触发执行 __setitem__
del obj['k1'] # 自动触发执行 __delitem__
9、__getslice__
、__setslice__
、__delslice__
该三个方法用于分片操作,如:列表
#!/usr/bin/env python
# -*- coding:utf-8 -*-
class Foo(object):
def __getslice__(self, i, j):
print '__getslice__',i,j
def __setslice__(self, i, j, sequence):
print '__setslice__',i,j
def __delslice__(self, i, j):
print '__delslice__',i,j
obj = Foo()
obj[-1:1] # 自动触发执行 __getslice__
obj[0:1] = [11,22,33,44] # 自动触发执行 __setslice__
del obj[0:2] # 自动触发执行 __delslice__
10.__iter__
用于迭代器,之所以列表、字典、元组可以进行for循环,是因为类型内部定义了 iter
class Foo(object):
pass
obj = Foo()
for i in obj:
print i
# 报错:TypeError: 'Foo' object is not iterable
#!/usr/bin/env python
# -*- coding:utf-8 -*-
class Foo(object):
def __iter__(self):
pass
obj = Foo()
for i in obj:
print i
# 报错:TypeError: iter() returned non-iterator of type 'NoneType'
#!/usr/bin/env python
# -*- coding:utf-8 -*-
class Foo(object):
def __init__(self, sq):
self.sq = sq
def __iter__(self):
return iter(self.sq)
obj = Foo([11,22,33,44])
for i in obj:
print i
11.__contains__
- 当使用 in 时,解释器会去调用 对象的 contains 方法。
class NoisyString(str):
def __contains__(self, other):
print('testing if "{0}" in "{1}"'.format(other, self))
return super(NoisyString, self).__contains__(other)
ns = NoisyString('a string with a substring inside')
>>> 'substring' in ns
testing if "substring" in "a string with a substring inside"
True
12.__lt__
,__le__
,__eq__
,__ne__
,__gt__
,__ge__
- x<y 执行 x.lt(y),
- x<=y 执行 x.le(y),
- x==y 执行 x.eq(y),
- x!=y 执行 x.ne(y),
- x>y 执行 x.gt(y),
- x>=y 执行 x.ge(y).
13. __new__
和 __metaclass__
阅读以下代码:
class Foo(object):
def __init__(self):
pass
obj = Foo() # obj是通过Foo类实例化的对象
- 上述代码中,obj 是通过 Foo 类实例化的对象,其实,不仅 obj 是一个对象,Foo类本身也是一个对象,因为在Python中一切事物都是对象。
print type(obj) # 输出:<class '__main__.Foo'>
表示,obj 对象由Foo类创建print type(Foo) # 输出:<type 'type'>
表示,Foo类对象由 type 类创建
所以,obj对象是Foo类的一个实例,Foo类对象是 type 类的一个实例,即:Foo类对象 是通过type类的构造方法创建。
那么,创建类就可以有两种方式:
a). 普通方式
class Foo(object):
def func(self):
print 'hello wupeiqi'
b).特殊方式(type类的构造函数)
def func(self):
print 'hello wupeiqi'
Foo = type('Foo',(object,), {'func': func})
#type第一个参数:类名
#type第二个参数:当前类的基类
#type第三个参数:类的成员
==》 类 是由 type 类实例化产生
type类中如何实现的创建类?类又是如何创建对象?
答:类中有一个属性 metaclass,其用来表示该类由 谁 来实例化创建,所以,我们可以为 __metaclass__
设置一个type类的派生类,从而查看 类 创建的过程。
class MyType(type):
def __init__(self, what, bases=None, dict=None):
super(MyType, self).__init__(what, bases, dict)
def __call__(self, *args, **kwargs):
obj = self.__new__(self, *args, **kwargs)
self.__init__(obj)
class Foo(object):
__metaclass__ = MyType
def __init__(self, name):
self.name = name
def __new__(cls, *args, **kwargs):
return object.__new__(cls, *args, **kwargs)
# 第一阶段:解释器从上到下执行代码创建Foo类
# 第二阶段:通过Foo类创建obj对象
obj = Foo()
类对象与实例对象
Python中对象分两种
- 类对象:声明类时会默认有一个类对象,这个对象的名与类名相同。
- 实例对象:通过 变量名 = 类名(形参列表) 这样的语法创建的对象称为实例对象
__new__
、__init__
、以及__call__
的执行顺序__new__
__init__
__call__
35 Python数据结构直接的区别
- 定义方式:list:[]、tuple:()、dict:{}、set:{}
- 是否有序:list:有序,tuple:有序,dict:无序,set:无序
- 是否允许出现重复元素:list:允许,tuple:允许,dict:key键值不允许,但是value的值允许,set:不允许
- 都属于可迭代对象
- set相当于存储了字典中的key
- 可以向互转化