Python学习笔记
1.Python的函数参数传递
看两个例子:
a = 1
def fun(a):
a = 2
fun(a)
print(a) # 1
a = []
def fun(a):
a.append(1)
fun(a)
print(a) # [1]
所有的变量都可以理解是内存中一个对象的“引用”,或者,也可以看似c中void*的感觉。
这里记住的是类型是属于对象的,而不是变量。而对象有两种,“可更改”(mutable)与“不可更改”(immutable)对象。在python中,strings, tuples, 和numbers是不可更改的对象,而list,dict等则是可以修改的对象。(这就是这个问题的重点)
当一个引用传递给函数的时候,函数自动复制一份引用,这个函数里的引用和外边的引用没有半毛关系了.所以第一个例子里函数把引用指向了一个不可变对象,当函数返回的时候,外面的引用没半毛感觉.而第二个例子就不一样了,函数内的引用指向的是可变对象,对它的操作就和定位了指针地址一样,在内存里进行修改.
2.@staticmethod和@classmethod
Python其实有3个方法,即静态方法(staticmethod),类方法(classmethod)和实例方法,如下:
def foo(x):
print("executing foo(%s)" % (x))
class A:
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的约定是这俩,还是不要改的好.
对于静态方法其实和普通的方法一样,不需要对谁进行绑定,唯一的区别是调用的时候需要使用a.static_foo(x)
或者A.static_foo(x)
来调用.
- 实例方法:由对象调用;至少一个self参数;执行实例方法时,自动将调用该方法的对象赋值给self;
- 类方法:由类调用; 至少一个cls参数;执行类方法时,自动将调用该方法的类赋值给cls;
- 静态方法:由类调用;无默认参数;
\ | 实例方法 | 类方法 | 静态方法 |
---|---|---|---|
a = A() | a.foo(x) | a.class_foo(x) | a.static_foo(x) |
A | 不可用 | A.class_foo(x) | A.static_foo(x) |
3.类变量和实例变量
class Person:
name = "aaa"
p1 = Person()
p2 = Person()
p1.name = "bbb"
print(p1.name) # bbb
print(p2.name) # aaa
print(Person.name) # aaa
类变量就是供类使用的变量,实例变量就是供实例使用的.
这里p1.name="bbb"
是实例调用了类变量,这其实和上面第一个问题一样,就是函数传参的问题,p1.name
一开始是指向的类变量name="aaa"
,但是在实例的作用域里把类变量的引用改变了,就变成了一个实例变量,self.name不再引用Person的类变量name了.
可以看看下面的例子:
class Person:
name=[]
p1 = Person()
p2 = Person()
p1.name.append(1)
print(p1.name) # [1]
print(p2.name) # [1]
print(Person.name) # [1]
4.Python中单下划线和双下划线
>>> class MyClass:
def __init__(self):
self.__superprivate = "Hello"
self._semiprivate = ", world!"
>>> mc = MyClass()
>>> print(mc.__superprivate)
Traceback (most recent call last):
File "", line 1, in
AttributeError: myClass instance has no attribute '__superprivate'
>>> print(mc._semiprivate)
, world!
>>> print(mc.__dict__)
{'_MyClass__superprivate': 'Hello', '_semiprivate': ', world!'}
__foo__
:一种约定,Python内部的名字,用来区别其他用户自定义的命名,以防冲突.
_foo
:一种约定,用来指定变量私有.程序员用来指定私有变量的一种方式,不能用from module import * 导入.
__foo
:这个有真正的意义:解析器用_classname__foo
来代替这个名字,以区别和其他类相同的命名,通过对象名._类名__xxx这样的方式可以访问.
5.字符串格式化:%和.format
.format在许多方面看起来更便利.对于%
最烦人的是它无法同时传递一个变量和元组.你可能会想下面的代码不会有什么问题:
"hi there %s" % name
但是,如果name恰好是(1,2,3),它将会抛出一个TypeError异常.为了保证它总是正确的,你必须这样做:
"hi there %s" % (name,) # 提供一个单元素的数组而不是一个参数
但是有点丑…format就没有这些问题.你给的第二个问题也是这样,.format好看多了.
迭代器和生成器
这里有个关于生成器的创建问题面试官有考: 问: 将列表生成式中[]改成() 之后数据结构是否改变? 答案:是,从列表变为生成器
>>> L = [x*x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x*x for x in range(10))
>>> g
<generator object <genexpr> at 0x0000028F8B774200>
通过列表生成式,可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含百万元素的列表,不仅是占用很大的内存空间,如:我们只需要访问前面的几个元素,后面大部分元素所占的空间都是浪费的。因此,没有必要创建完整的列表(节省大量内存空间)。在Python中,我们可以采用生成器:边循环,边计算的机制—>generator
6.Python中重载
引自知乎:http://www.zhihu.com/question/20053359
函数重载主要是为了解决两个问题。
- 可变参数类型。
- 可变参数个数。
另外,一个基本的设计原则是,仅仅当两个函数除了参数类型和参数个数不同以外,其功能是完全相同的,此时才使用函数重载,如果两个函数的功能其实不同,那么不应当使用重载,而应当使用一个名字不同的函数。
好吧,那么对于情况 1 ,函数功能相同,但是参数类型不同,python 如何处理?答案是根本不需要处理,因为 python 可以接受任何类型的参数,如果函数的功能相同,那么不同的参数类型在 python 中很可能是相同的代码,没有必要做成两个不同函数。
那么对于情况 2 ,函数功能相同,但参数个数不同,python 如何处理?大家知道,答案就是缺省参数。对那些缺少的参数设定为缺省参数即可解决问题。因为你假设函数功能相同,那么那些缺少的参数终归是需要用的。
好了,鉴于情况 1 跟 情况 2 都有了解决方案,python 自然就不需要函数重载了。
7.__new__
和__init__
的区别
这个__new__
确实很少见到,先做了解吧.
__new__
是一个静态方法,而__init__
是一个实例方法.__new__
方法会返回一个创建的实例,而__init__
什么都不返回.- 只有在
__new__
返回一个cls的实例时后面的__init__
才能被调用. - 当创建一个新实例时调用
__new__
,初始化一个实例时用__init__
.
ps: __metaclass__
是创建类时起作用.所以我们可以分别使用__metaclass__
,__new__
和__init__
来分别在类创建,实例创建和实例初始化的时候做一些小手脚.
8.单例模式
单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在。当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场。
在 Python 中,我们可以用多种方法来实现单例模式:
- 使用模块
- 使用
__new__
- 使用装饰器(decorator)
- 使用元类(metaclass)
使用模块
其实,Python 的模块就是天然的单例模式,因为模块在第一次导入时,会生成 .pyc
文件,当第二次导入时,就会直接加载 .pyc
文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。如果我们真的想要一个单例类,可以考虑这样做:
# mysingleton.py
class My_Singleton:
def foo(self):
pass
my_singleton = My_Singleton()
将上面的代码保存在文件 mysingleton.py
中,然后这样使用:
from mysingleton import my_singleton
my_singleton.foo()
使用__new__
为了使类只能出现一个实例,我们可以使用 __new__
来控制实例的创建过程,代码如下:
class Singleton:
def __new__(cls, *args, **kwargs):
if not hasattr(cls, '_instance'):
cls._instance = super().__new__(cls)
return cls._instance
在上面的代码中,我们将类的实例和一个类变量 _instance
关联起来,如果 cls._instance
为 None 则创建实例,否则直接返回 cls._instance
。
执行情况如下:
>>> one = Singleton()
>>> two = Singleton()
>>> one == two
True
>>> one is two
True
>>> id(one), id(two)
(4303862608, 4303862608)
使用装饰器
我们知道,装饰器(decorator)可以动态地修改一个类或函数的功能。这里,我们也可以使用装饰器来装饰某个类,使其只能生成一个实例,代码如下:
from functools import wraps
def singleton(cls):
instances = {}
@wraps(cls)
def getinstance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances
return getinstance
@singleton
class Bar:
pass
在上面,我们定义了一个装饰器 singleton
,它返回了一个内部函数 getinstance
,该函数会判断某个类是否在字典 instances
中,如果不存在,则会将 cls
作为 key,cls(*args, **kw)
作为 value 存到 instances
中,否则,直接返回 instances[cls]
。
>>> one = Bar()
>>> two = Bar()
>>> one == two
True
使用 metaclass
元类(metaclass)可以控制类的创建过程,它主要做三件事:
- 拦截类的创建
- 修改类的定义
- 返回修改后的类
使用元类实现单例模式的代码如下:
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super().__call__(*args, **kwargs)
return cls._instances[cls]
# Python2
# class MyClass(object):
# __metaclass__ = Singleton
# Python3
class MyClass(metaclass=Singleton):
pass
9.Python里的拷贝
import copy
a = [1, 2, 3, 4, ['a', 'b']] #原始对象
b = a #赋值,传对象的引用
c = copy.copy(a) #浅拷贝:拷贝父对象,不会拷贝对象的内部的子对象。
# 浅拷贝是在另一块地址中创建一个新的变量或容器,但是容器内的元素的地址均是源对象的元素的地址的拷贝。也就是说新的容器中指向了旧的元素( 新瓶装旧酒 )。
d = copy.deepcopy(a) #深拷贝:完全拷贝了父对象及其子对象。
# 深拷贝是在另一块地址中创建一个新的变量或容器,同时容器内的元素的地址也是新开辟的,仅仅是值相同而已,是完全的副本。也就是说( 新瓶装新酒 )。
a.append(5) #修改对象a
print('a = ', a)
print('b = ', b)
print('c = ', c)
print('d = ', d)
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'], 5]
b = [1, 2, 3, 4, ['a', 'b'], 5]
c = [1, 2, 3, 4, ['a', 'b']]
d = [1, 2, 3, 4, ['a', 'b']]
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']]
10.Python垃圾回收机制
Python GC主要使用引用计数(reference counting)来跟踪和回收垃圾。在引用计数的基础上,通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用问题,通过“分代回收”(generation collection)以空间换时间的方法提高垃圾回收效率。
引用计数
PyObject是每个对象必有的内容,其中ob_refcnt
就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt
就会增加,当引用它的对象被删除,它的ob_refcnt
就会减少.引用计数为0时,该对象生命就结束了。
优点:
- 简单
- 实时性
缺点:
- 维护引用计数消耗资源
- 循环引用
标记-清除机制
基本思路是先按需分配,等到没有空闲内存的时候从寄存器和程序栈上的引用出发,遍历以对象为节点、以引用为边构成的图,把所有可以访问到的对象打上标记,然后清扫一遍内存空间,把所有没标记的对象释放。
分代技术
分代回收的整体思想是:将系统中的所有内存块根据其存活时间划分为不同的集合,每个集合就成为一个“代”,垃圾收集频率随着“代”的存活时间的增大而减小,存活时间通常利用经过几次垃圾回收来度量。
Python默认定义了三代对象集合,索引数越大,对象存活时间越长。
举例: 当某些内存块M经过了3次垃圾收集的清洗之后还存活时,我们就将内存块M划到一个集合A中去,而新分配的内存都划分到集合B中去。当垃圾收集开始工作时,大多数情况都只对集合B进行垃圾回收,而对集合A进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制需要处理的内存少了,效率自然就提高了。在这个过程中,集合B中的某些内存块由于存活时间长而会被转移到集合A中,当然,集合A中实际上也存在一些垃圾,这些垃圾的回收会因为这种分代的机制而被延迟。
11.read, readline和 readlines
- read 读取整个文件
- readline 读取下一行,使用生成器方法
- readlines 读取整个文件到一个迭代器以供我们遍历
12.bisect模块
Python 有一个 bisect
模块,用于维护有序列表。bisect
模块实现了一个算法用于插入元素到有序列表。在一些情况下,这比反复排序列表或构造一个大的列表再排序的效率更高。Bisect 是二分法的意思,这里使用二分法来排序,它会将一个元素插入到一个有序列表的合适位置,这使得不需要每次调用 sort 的方式维护有序列表。
下面是一个简单的使用示例:
import bisect
import random
random.seed(1)
print('New Pos Contents')
print('--- --- --------')
l = []
for i in range(1, 15):
r = random.randint(1, 100)
position = bisect.bisect(l, r)
bisect.insort(l, r)
print('%3d %3d' % (r, position), l)
输出结果:
New Pos Contents
--- --- --------
14 0 [14]
85 1 [14, 85]
77 1 [14, 77, 85]
26 1 [14, 26, 77, 85]
50 2 [14, 26, 50, 77, 85]
45 2 [14, 26, 45, 50, 77, 85]
66 4 [14, 26, 45, 50, 66, 77, 85]
79 6 [14, 26, 45, 50, 66, 77, 79, 85]
10 0 [10, 14, 26, 45, 50, 66, 77, 79, 85]
3 0 [3, 10, 14, 26, 45, 50, 66, 77, 79, 85]
84 9 [3, 10, 14, 26, 45, 50, 66, 77, 79, 84, 85]
44 4 [3, 10, 14, 26, 44, 45, 50, 66, 77, 79, 84, 85]
77 9 [3, 10, 14, 26, 44, 45, 50, 66, 77, 77, 79, 84, 85]
1 0 [1, 3, 10, 14, 26, 44, 45, 50, 66, 77, 77, 79, 84, 85]
bisect模块提供的函数有:
- bisect.bisect_left(a,x, lo=0, hi=len(a)) :
查找在有序列表 a 中插入 x 的index。lo 和 hi 用于指定列表的区间,默认是使用整个列表。如果 x 已经存在,在其左边插入。返回值为 index。
- bisect.bisect_right(a,x, lo=0, hi=len(a))
- bisect.bisect(a, x,lo=0, hi=len(a)) :
这2个函数和 bisect_left 类似,但如果 x 已经存在,在其右边插入。
- bisect.insort_left(a,x, lo=0, hi=len(a)) :
在有序列表 a 中插入 x。和 a.insert(bisect.bisect_left(a,x, lo, hi), x) 的效果相同。
- bisect.insort_right(a,x, lo=0, hi=len(a))
- bisect.insort(a, x,lo=0, hi=len(a)) :
和 insort_left 类似,但如果 x 已经存在,在其右边插入。
我们可以用 bisect 模块实现二分查找:
def binary_search_bisect(lst, x):
from bisect import bisect_left
i = bisect_left(lst, x)
if i != len(lst) and lst[i] == x:
return i
return None
13.list, tuple, dictionary, set的底层细节
python中的列表的英文名是list,因此很容易和其它语言(C++, Java等)标准库中常见的链表混淆。事实上CPython的列表根本不是列表(可能换成英文理解起来容易些:python中的list不是list)。在CPython中,列表被实现为长度可变的数组。
从细节上看,Python中的列表是由对其它对象的引用组成的连续数组。指向这个数组的指针及其长度被保存在一个列表头结构中。这意味着,每次添加或删除一个元素时,由引用组成的数组需要该标大小(重新分配)。幸运的是,Python在创建这些数组时采用了指数过分配,所以并不是每次操作都需要改变数组的大小。但是,也因为这个原因添加或取出元素的平摊复杂度较低。
不幸的是,在普通链表上“代价很小”的其它一些操作在Python中计算复杂度相对过高。
利用 list.insert方法在任意位置插入一个元素——复杂度O(N)
利用 list.delete或del删除一个元素——复杂度O(N)
操作 | 复杂度 |
---|---|
复制 | O(N) |
添加元素(在尾部添加) | O(1) |
插入元素(在指定位置插入) | O(N) |
获取元素 | O(1) |
修改元素 | O(1) |
删除元素 | O(N) |
遍历 | O(N) |
获取长度为k的切片 | O(k) |
删除切片 | O(N) |
列表扩展 | O(k) |
测试是否在列表中 | O(N) |
min()/max() | O(n) |
获取列表长度 | O(1) |
字典是python中最通用的数据结构之一。dict可以将一组唯一的键映射到相应的值。
实现细节
CPython使用伪随机探测(pseudo-random probing)的散列表(hash table)作为字典的底层数据结构。由于这个实现细节,只有可哈希的对象才能作为字典的键。
Python中所有不可变的内置类型都是可哈希的。可变类型(如列表,字典和集合)就是不可哈希的,因此不能作为字典的键。
字典的三个基本操作(添加元素,获取元素和删除元素)的平均事件复杂度为O(1),但是他们的平摊最坏情况复杂度要高得多,为O(N).
操作 | 平均复杂度 | 平摊最坏情况复杂度 |
---|---|---|
获取元素 | O(1) | O(n) |
修改元素 | O(1) | O(n) |
删除元素 | O(1) | O(n) |
复制 | O(n) | O(n) |
遍历 | O(n) | O(n) |
还有一点很重要,在复制和遍历字典的操作中,最坏的复杂度中的n是字典曾经达到的最大元素数目,而不是当前的元素数目。换句话说,如果一个字典曾经元素个数很多,后来又大大减小了,那么遍历这个字典可能会花费相当长的事件。因此在某些情况下,如果需要频繁的遍历某个词典,那么最好创建一个新的字典对象,而不是仅在旧字典中删除元素。
集合
集合是一种鲁棒性很好的数据结构,当元素顺序的重要性不如元素的唯一性和测试元素是否包含在集合中的效率时,大部分情况下这种数据结构极其有用。
python的内置集合类型有两种:
-
set(): 一种可变的、无序的、有限的集合,其元素是唯一的、不可变的(可哈希的)对象。
-
frozenset(): 一种不可变的、可哈希的、无序的集合,其元素是唯一的,不可变的哈希对象。
实现细节
CPython中集合和字典非常相似。事实上,集合被实现为带有空值的字典,只有键才是实际的集合元素。此外,集合还利用这种没有值的映射做了其它的优化。
由于这一点,可以快速的向集合中添加元素、删除元素、检查元素是否存在。平均时间复杂度为O(1),最坏的事件复杂度是O(n)。
14.Python中is和==的区别
Python中有很多运算符,今天我们就来讲讲is和==两种运算符在应用上的本质区别是什么。
在讲is和==这两种运算符区别之前,首先要知道Python中对象包含的三个基本要素,分别是:id(身份标识)、type(数据类型)和value(值)。
is和==都是对对象进行比较判断作用的,但对对象比较判断的内容并不相同。下面来看看具体区别在哪。
==比较操作符和is同一性运算符区别
==是python标准操作符中的比较操作符,用来比较判断两个对象的value(值)是否相等,例如下面两个字符串间的比较:
例1.
>>> a = 'cheesezh'
>>> b = 'cheesezh'
>>> a == b
True
is也被叫做同一性运算符,这个运算符比较判断的是对象间的唯一身份标识,也就是id是否相同。通过对下面几个list间的比较,你就会明白is同一性运算符的工作原理:
例2.
>>> x = y = [4,5,6]
>>> z = [4,5,6]
>>> x == y
True
>>> x == z
True
>>> x is y
True
>>> x is z
False
>>>
>>> print(id(x))
3075326572
>>> print(id(y))
3075326572
>>> print(id(z))
3075328140
前三个例子都是True,这什么最后一个是False呢?x、y和z的值是相同的,所以前两个是True没有问题。至于最后一个为什么是False,看看三个对象的id分别是什么就会明白了。
下面再来看一个例子,例3中同一类型下的a和b的(a==b)都是为True,而(a is b)则不然。
例3.
>>> a = 1 #a和b为数值类型
>>> b = 1
>>> a is b
>>> True
>>> id(a)
>>> 14318944
>>> id(b)
>>> 14318944
>>> a = 'cheesezh' #a和b为字符串类型
>>> b = 'cheesezh'
>>> a is b
>>> True
>>> id(a)
>>> 42111872
>>> id(b)
>>> 42111872
>>> a = (1,2,3) #a和b为元组类型
>>> b = (1,2,3)
>>> a is b
>>> False
>>> id(a)
>>> 15001280
>>> id(b)
>>> 14790408
>>> a = [1,2,3] #a和b为list类型
>>> b = [1,2,3]
>>> a is b
>>> False
>>> id(a)
>>> 42091624
>>> id(b)
>>> 42082016
>>> a = {'cheese':1,'zh':2} #a和b为dict类型
>>> b = {'cheese':1,'zh':2}
>>> a is b
>>> False
>>> id(a)
>>> 42101616
>>> id(b)
>>> 42098736
>>> a = set([1,2,3])#a和b为set类型
>>> b = set([1,2,3])
>>> a is b
>>> False
>>> id(a)
>>> 14819976
>>> id(b)
>>> 14822256
>>>
通过例3可看出,只有数值型和字符串型的情况下,a is b才为True,当a和b是tuple,list,dict或set型时,a is b为False。
15.协程
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。
所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。
子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:
def A():
print('1')
print('2')
print('3')
def B():
print('x')
print('y')
print('z')
假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:
1
2
x
y
3
z
但是在A中是没有调用B的,所以协程的调用比函数调用理解起来要难一些。
看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
Python对协程的支持是通过generator实现的。
在generator中,我们不但可以通过for
循环来迭代,还可以不断调用next()
函数获取由yield
语句返回的下一个值。
但是Python的yield
不但可以返回一个值,它还可以接收调用者发出的参数。
来看例子:
传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield
跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(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()
c = consumer()
produce(c)
执行结果:
[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
注意到consumer
函数是一个generator
,把一个consumer
传入produce
后:
- 首先调用
c.send(None)
启动生成器; - 然后,一旦生产了东西,通过
c.send(n)
切换到consumer
执行; consumer
通过yield
拿到消息,处理,又通过yield
把结果传回;produce
拿到consumer
处理的结果,继续生产下一条消息;produce
决定不生产了,通过c.close()
关闭consumer
,整个过程结束。
整个流程无锁,由一个线程执行,produce
和consumer
协作完成任务,所以称为“协程”,而非线程的抢占式多任务。
最后套用Donald Knuth的一句话总结协程的特点:
“子程序就是协程的一种特例。”
16下面的代码会输出什么
def f(x,l=[]):
for i in range(x):
l.append(i*i)
print l
f(2)
f(3,[3,2,1])
f(3)
# [0, 1]
# [3, 2, 1, 0, 1, 4]
# [0, 1, 0, 1, 4]
第一步调用f(2)时,由于传入的第二个参数l是列表,为可变对象,调用之后l=[0, 1]
第二步调用f(3,[3,2,1])时,第二个传入参数变成了[3, 2, 1],结果为[3, 2, 1, 0, 1, 4]
第三步调用f(3)时,由于此时l=[0, 1],于是结果变成了[0, 1, 0, 1, 4]
17.Python多继承,并且包含super()函数
super() 函数是用于调用父类(超类)的一个方法。
super 是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题。
method resolution order (MRO)就是类的方法解析顺序表, 其实也就是继承父类方法时的顺序表。
本地优先级:指声明时父类的顺序,比如C(A,B),如果访问C类对象属性时,应该根据声明顺序,优先查找A类,然后再查找B类。
单调性:如果在C的解析顺序中,A排在B的前面,那么在C的所有子类里,也必须满足这个顺序
class A:
def go(self):
print("go A go!")
def stop(self):
print("stop A stop!")
def pause(self):
raise Exception("Not Implemented")
class B(A):
def go(self):
super().go()
print("go B go!")
class C(A):
def go(self):
super().go()
print("go C go!")
def stop(self):
super().stop()
print("stop C stop!")
class D(B,C):
def go(self):
super().go()
print("go D go!")
def stop(self):
super().stop()
print("stop D stop!")
def pause(self):
print("wait D wait!")
# 调用顺序
# D.mro()
# [__main__.D, __main__.B, __main__.C, __main__.A, object]
class E(B,C):
pass
a = A()
b = B()
c = C()
d = D()
e = E()
# 说明下列代码的输出结果
a.go()
b.go()
c.go()
d.go()
e.go()
a.stop()
b.stop()
c.stop()
d.stop()
e.stop()
a.pause()
b.pause()
c.pause()
d.pause()
e.pause()
结果为
a.go()
# go A go!
b.go()
# go A go!
# go B go!
c.go()
# go A go!
# go C go!
d.go()
# go A go!
# go C go!
# go B go!
# go D go!
e.go()
# go A go!
# go C go!
# go B go!
a.stop()
# stop A stop!
b.stop()
# stop A stop!
c.stop()
# stop A stop!
# stop C stop!
d.stop()
# stop A stop!
# stop C stop!
# stop D stop!
e.stop()
# stop A stop!
# stop C stop!
a.pause()
# ... Exception: Not Implemented
b.pause()
# ... Exception: Not Implemented
c.pause()
# ... Exception: Not Implemented
d.pause()
# wait D wait!
e.pause()
# ...Exception: Not Implemented
18.collections模块的Counter类
collections模块自Python 2.4版本开始被引入,包含了dict、set、list、tuple以外的一些特殊的容器类型,分别是:
- OrderedDict类:排序字典,是字典的子类。
- namedtuple()函数:命名元组,是一个工厂函数。
- Counter类:为hashable对象计数,是字典的子类。
- deque:双向队列。
- defaultdict:使用工厂函数创建字典,使不用考虑缺失的字典键。
统计元素出现次数
from collections import Counter
l = [1, 2, 1, 2, 3]
d = Counter(l)
for key, value in d.items():
print(key, value)
计数器的更新(update和subtract)
可以使用一个iterable对象或者另一个Counter对象来更新键值。
计数器的更新包括增加和减少两种。其中,增加使用update()方法:
>>> c = Counter('which')
>>> c.update('witch') # 使用另一个iterable对象更新
>>> c['h']
3
>>> d = Counter('watch')
>>> c.update(d) # 使用另一个Counter对象更新
>>> c['h']
4
减少则使用subtract()方法:
>>> c = Counter('which')
>>> c.subtract('witch') # 使用另一个iterable对象更新
>>> c['h']
1
>>> d = Counter('watch')
>>> c.subtract(d) # 使用另一个Counter对象更新
>>> c['a']
-1
elements()
返回一个迭代器。元素被重复了多少次,在该迭代器中就包含多少个该元素。元素排列无确定顺序,个数小于1的元素不被包含。
>>> c = Counter(a=4, b=2, c=0, d=-2)
>>> list(c.elements())
['a', 'a', 'a', 'a', 'b', 'b']
most_common([n])
返回一个TopN列表。如果n没有被指定,则返回所有元素。当多个元素计数值相同时,排列是无确定顺序的。
>>> c = Counter('abracadabra')
>>> c.most_common()
[('a', 5), ('r', 2), ('b', 2), ('c', 1), ('d', 1)]
>>> c.most_common(3)
[('a', 5), ('r', 2), ('b', 2)]