Python2 & Python3
- py2的不等于可以用
!=
和<>
。py3 只能用!=
- py2的
print
是特殊语句,不用加括号。py3的print
是函数,必须加括号。 - py2默认编码是ASCII。py3默认编码是utf-8。使用
print sys.getdefaultencoding()
可以查看。 - py2有Unicode数据类型,py3取消了。
- py2有long类型,所以整数后面可以加L。py3取消了long类型。
- py2有
input
和raw_input
。py3中取消了py2的input
,把raw_input
更名为input
。 - py2的
range()
直接返回列表。py3的range()
返回的是 range 类型的迭代器。 - py2的 “/” 是整除,想得到浮点型的精确商需要把被除数或除数变成浮点数。py3的 “/” 是精确除法,“//” 是整除,即使把整除的两个数换成浮点数,结果也只是整除结果的浮点数表示,不是精确商。
- py2有经典类和新式类。py3只有新式类。
- py2的
super()
需要输入本身类名和自身作为参数。py3的super()
不用输入参数,可以直接使用替代上一级父类。
Python 中的作用域
- Python的作用域一共有4种: L (Local, 局部作用域),E (Enclosing, 闭包函数外的函数),G (Global, 全局作用域),B (Built-in, 内建作用域)。以 L –> E –> G –>B 的规则查找。
闭包(Closure)
闭包的使用
- 如下,b 是 a 的内部函数,b 调用了 a 的局部变量 x,a 的返回值是函数 b,当调用函数 a 的时候,得到的 c 就是所谓的闭包。闭包包括两部分,一是函数 b,二是 b 所调用的外部变量 x,称为自由变量。
def a(): |
- 函数 b 查询变量 x 的顺序是:b 的内部变量 -> a 的内部变量 -> 全局变量。如下:
x = 1 |
x = 1 |
x = 1 |
- 函数 b 中仅能使用自由变量 x,而不能修改 x 的值,一旦对 x 赋值,x 就会被当做 b 的内部变量。解决办法是在 b 中声明
nonlocal x
,就可以对自由变量 x 进行修改。如下:
def a(): |
def a(): |
- global 关键字可以无视变量的查找顺序,直接声明对全局变量的引用。如下:
x = 1 |
闭包调用自由变量的原理
- Python 的函数都有一个
__closure__
属性,如果该函数是闭包的话,这个参数里就会保存可使用的自由变量,存储形式是 Cell 对象。如下:
def a(): |
闭包的优点
- 封装性好,可以不使用全局变量来为内部函数提供可使用的外部变量。函数 a 就像一个工厂函数,如果其中有多个内部函数 b1、b2、b3,就可以根据用户提供的参数,包装指定的内部函数,提供用户需要的闭包,而这一切都被封装在函数 a 中,不需要泄露到全局。基于同一函数的两个闭包的自由变量是独立、互不影响的,从而提高了闭包的可用性,如下:
def a(): |
闭包的应用
- 装饰器
装饰器
闭包的一种应用
- 因为闭包返回的是一个函数,所以可以用闭包的原理去包装函数,然后返回包装后的函数。如下,实现的是在每个函数运行前都输出日志,只需要把函数传给 use_logging,用其内部函数 wrapper 进行包装,这样就避免了在每个函数中都加一行 print,方便维护。有两种使用方式:
def use_logging(func): |
def use_logging(func): |
装饰器
- 所谓的装饰器就是这种闭包应用的语法糖,下面的代码和上面第一段代码是等价的。好处就是省略了调用 use_logging 对 foo 赋值的步骤,把闭包的应用实现在了函数的定义阶段,而不是使用阶段。
def use_logging(func): |
面向切片编程
- Aspect Oriented Programming(AOP) 是一种编程范式,和 OOP 一样,只是一种设计理念,并没有指定实现的具体方式。AOP 的初衷是将日志记录、性能统计、安全控制、事务处理、异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。显然,AOP 可以基于装饰器实现。
GC
- Python中的垃圾回收器主要使用引用计数进行垃圾回收,通过标记-清理解决容器对象产生循环引用的问题,再通过分代回收以空间换时间的方式来提高垃圾回收的效率。
引用计数
- 在 CPython 源码中,Python 对象的核心是 PyObject 这个结构体,该结构体内部通过 ob_refcnt 变量实现引用计数。对象在创建时引用计数值为 1,当对象的引用计数值为 0 时, 它的内存就会被立即释放掉,即被垃圾回收。
typedef struct_object { |
- 引用加一的情况:
- 对象被创建,
a = 1
- 对象被引用,
b = a
- 对象被作为参数,传入到一个函数中
- 对象作为一个元素,存储在容器中
- 对象被创建,
- 引用减一的情况:
- 对象被显式销毁,
del a
- 对象的引用被赋予新的对象,
b=123
- 一个对象离开他的作用域,比如函数返回后会释放局部变量
- 对象所在的容器被销毁或者是从容器中删除对象
- 对象被显式销毁,
标记-清理
- 引用计数的一个问题是循环引用,如下。a 和 b 通过循环嵌套,都成为了嵌套深度无穷无尽的两个列表,二者的引用计数值永远不可能为 0 ,所以单凭引用计数无法回收。
In [0]: a=[] |
- 只有容器对象才会产生循环引用的情况,因为 Python 里引用和赋值的语法相同,两个变量之间的相互引用要么是交换二者的值,要么是变成一个对象的引用,总之简单类型不会出现循环引用。
- 标记-清理的原理很简单,直接把所有对象的引用计数值复制一份,再把所有副本的值减一。此时把副本当做引用值来考虑,如果存在两个容器对象循环引用,一定表现为一个引用值为 0 的对象引用了一个引用值为 0 的对象(两个对象可以相同,也就是自引用),所以就很容易辨认出了。的阶段,标记-清理过程会暂停整个应用程序,等待结束后才会恢复应用程序的运行。
分代回收
- 引用计数和标记-清理的过程速度很慢,所以 Python 采用了和 Java 差不多的分代回收策略,把存活对象分为了三代。对象每逃过一轮的 gc ,就会移动到下个世代,当某一世代被分配的对象与被释放的对象之差达到某一阈值的时候,就会触发 gc 对某一世代的扫描,查看每个世代阈值的方法如下:
In [0]: import gc |
- 分代回收所依据的基本假设是,存活时间越久的对象,越不可能在后面的程序中变成垃圾。出于信任和效率,对于相对长寿的对象,我们相信它们的用处,所以会减少在垃圾回收中扫描它们的频率。
GIL
GIL 是什么?
- 全局解释器锁(Global Interpreter Lock) 不是 Python 语言本身的特性,而是 CPython 解释器所采用的一种机制,CPython 只是一种 Python 解释器的实现,其他的有 Jython、IronPython 等。
- 采用 GIL 的人正是 Python 的创始者 Guido van Rossum,当时多核芯片与并发编程刚刚兴起,编程语言的开发者需要考虑如何快速适应多核硬件,来提高软件性能和编程语言的市场占有率,Python也不例外受到冲击。但当年依然是单核计算机占绝对的主导地位,所以 Guido van Rossum 真正考虑的并不是如何提升多核 cpu 的利用率,而是如何保证并发编程在单核 cpu 下的线程安全。于是他采用了 GIL,一把在解释器层面的全局互斥锁,使得单个程序只能在单核上运行,同一时刻也只能有一个线程在运行,这是一种最简洁、最优雅的实现线程安全的方法。
- GIL 带来的直接好处是可靠性,开发者不需要再加任何细粒度的锁,就能保证线程安全,保证了单线程性能的最优化。但随着多核 cpu 的广泛应用,GIL 的缺点愈发突出,由于只能在一个核上运行,多线程的程序并没有比单线程程序多利用了 cpu 资源,同时多线程的程序还有额外的线程切换的时间消耗,所以在 GIL 的禁锢下,Python 的多线程反而比单线程还要慢,多个任务并发执行反倒比串行执行的效率低。
- 对于I/O密集型的多线程程序,如网络爬虫,多线程的性能也许不会比单线程低太多。但对于CPU密集型的多线程程序,GIL 就是灾难。
为什么没有移除 GIL?
- GIL 对单线程的性能优化是显著的。尽管并发编程已经相当普及,单线程的程序依然是广泛使用、不可替代的。如果移除了 GIL,很多常用的函数库都要进行重写,使用细粒度的锁做并发控制,一定会影响程序的性能。作为一种解释型语言,Python 的速度本身就是性能瓶颈,再慢的话恐怕就真的没人用了。因此没有必要牺牲单线程的性能去提升多线程的效率,这种取长补短反而会让 Python 丧失优势。
- GIL 采用地太早了,后来开发出的很多第三方库都显式或隐式地依赖了 GIL 的存在,移除 GIL 势必要重构整个 Python 生态,实在是划不来。
多线程的替代方案
- GIL 的影响无非是多线程无法运行在多核上,其实可以有多种替代方案。
- 换语言:没有什么是只能用 Python 写的,想用多线程就换 Java。
- 多进程:Python2.6引入了MultiProcess库来弥补Threading库中GIL带来的缺陷,基于此开发多进程程序,每个进程有单独的GIL,避免了多进程之间对GIL的竞争,从而实现多核的利用。虽然也带来一些同步和通信问题,但至少逃离了GIL的禁锢。
- 协程:相当于单核上的多线程,但协程的切换代价没有线程大,因为协程是在应用程序层面进行切换,而不是在操作系统层面。如果不是非要用到多核,可以使用协程。
- 换解释器:GIL 只是 CPython 的问题,可以换 JPython,JPython 基于 JVM,可能也有自己的鸡肋之处吧。或者用 PyPy,支持 JIT,速度据说比 CPython 快。
- Ctypes:CPython的优势就是与C模块的结合,而 ctypes 库可以让 Python 直接调用任意的 C 动态库的导出函数,关键在于,ctypes 会在调用 C 函数前释放 GIL,从而摆脱单核的限制。因此,通过把计算任务丢给 ctypes 也可以有效利用到多核。
鸭子类型(Duck Typing)
- 如果走起路来像鸭子,叫起来也像鸭子,那么它就是鸭子(If it walks like a duck and quacks like a duck, it must be a duck)。鸭子类型是动态类型语言中的一种设计风格,不关注对象的类型,而是关注对象具有的行为。
- 比如在下面的函数 a 中,因为Python是动态类型语言,所以不需要声明参数 b 的类型。无论 b 是什么类型,只要它能调用 foo 函数,函数 a 就能正常执行,相反,只要 b 不能调用 foo 函数,不管它是什么类型,运行到
b.foo()
都会报错。这就是所谓的关注行为而不关注类型。
def a(b): |
猴子补丁(Monkey Patch)
- 在运行时对已有的代码进行修改,达到hot patch的目的。虽然很便利,但也确实搞乱了源代码,感觉没啥用。
class Foo(object): |
函数式编程
map
- map 函数接收两个参数,一个是函数,一个是可迭代对象,map将传入的函数依次作用到序列的每个元素,返回结果的迭代器。可以用 next 函数逐个取出迭代器中的元素,也可以直接把迭代器对象转换成列表对象。
In [0]: r = map(lambda x:x*x, [1,2,3,4,5]) |
reduce
- reduce 函数 把一个函数作用在一个序列上,这个函数必须接收两个参数,reduce 把结果继续和序列的下一个元素做累积计算。在 Python3 中,reduce不再是内建函数,需要从 functools 模块导入。
In [0]: from functools import reduce |
filter
- filter 函数用于过滤序列,接收一个函数和一个序列,把传入的函数依次作用于每个元素,然后根据返回值是 True 还是 False 决定保留还是丢弃该元素。返回值是个迭代器。
In [0]: r = filter(lambda x:x>3, [1,2,3,4,5]) |
可迭代对象、迭代器、生成器
- 可迭代对象只有
__iter__
方法,迭代器有__iter__
和__next__
方法,可以使用 iter 函数把可迭代对象转换成迭代器。
In [0]: a=[1,2,3,4] |
- 生成器是一种特殊的迭代器,用 yield 关键字来实现迭代数据,但迭代器仍然有
__next__
方法。只要有 yield 的函数就是生成器函数,生成器函数的返回值是一个生成器。
In [0]: def d(): |
- 生成器的强悍之处在于可以传入数据进行计算,当 yield 关键字前有赋值语句时,必须使用 send 函数传入参数,否则赋值失败将抛出 error 。特别的,在执行第一次迭代时,由于只执行到第一个 yield 处就中断了,并没有执行 yield 左侧的赋值语句,所以第一次迭代可以使用原来的
next
函数或send(None)
来实现,而第二次迭代时用 send 传进的参数,其实就是为第一个 yield 所在的赋值语句提供右值。
In [0]: def d(): |
协程
- 协程就是利用了生成器的原理,因为 yield 相当于中断程序,而调用 next 和 send 函数又相当于回到此前中断的位置继续执行,所以能够用这种机制实现多个任务之间的切换。协程的切换只是单纯地操作CPU的上下文,而线程的切换除了保存和恢复CPU上下文,系统还要处理缓存Cache、数据恢复等问题,所以十分耗时。这就是协程相对于线程的优势。
- 协程的并发本质上和多线程一样,还是只能在单核上运行,同一时刻只能有一个协程在运行,所以对于CPU密集型任务,使用协程还是一样的鸡肋,协程并发主要用于优化IO密集型任务。比如在并发运行多个网络爬虫时,若某个爬虫访问URL后由于网络问题陷入等待,就可以切换到其它爬虫执行,这样就高效利用了IO阻塞的时间,往往可以比单线程串行执行速度更快。
- 当协程数过多时,手动使用 yield 和 send 进行切换十分复杂,于是就有了asyncio,一个基于协程原理实现异步IO通信的第三方库,类似的还有tornado、gevent等库。
- 下面用一个例子对比一下串行和并发协程的速度:
import asyncio |
import time |
多态
- 重写:和其他面向对象语言一样,子类可以重写父类的方法。
- 重载:Python 采用鸭子类型的设计理念,不需要支持重载。单个函数就可以支持任何类型的参数,同时可以使用 args 和 *kwargs 进行参数打包,所以也能支持任意数量的参数,只需要在函数内部添加对参数的判别逻辑就够了。
多继承
MRO
- Python 支持多继承,为了避免父类同名方法导致的二义性问题,Python 使用 C3 算法给每个类生成了一个方法解析顺序列表(Method Resolution Order, MRO),MRO 规定了类继承的顺序,调用某个方法时,会按照 MRO 的顺序逐个访问父类,所以不会出现二义性。换言之,继承的多个父类看似是同一级别的,但 MRO 已经偷偷给他们排好了顺序,分出了亲爹干爹。
In [0]: class A(object): |
C3 算法
- MRO 的计算流程如下。注意,表头是指列表的第一个元素,表尾是指列表第一个元素之后的所有元素。
对于上面的例子,MRO 的计算公式为:
可以看出,merge操作是C3算法的核心,正是由于删表头的操作,使得菱形继承的情况下 MRO 中不会有重复的类。
例外
- 虽然Python支持多继承,但对于内置类型的继承却是有限制的,通常情况下,对于list、set、dict等内置类型,至多可以继承一个。
In [0]: class A(list,set): |
可变对象 & 不可变对象
- 可变对象(mutable):对象指向的内存中的值可以改变,当更改这个变量的时候,还是指向原来内存中的值,并且在原来的内存值进行原地修改,并没有开辟新的内存。list、dict、set是可变对象。
- 不可变对象(immutable):对象指向的内存中的值不可以改变,当改变这个变量的时候,原来指向的内存中的值不变,变量不再指向原来的值,而是开辟一块新的内存,变量指向新的内存。int、float、str 、tuple、bool 是不可变对象。
@staticmethod 和 @classmethod
- @staticmethod 或 @classmethod 修饰的函数,可以不需要类的实例,直接类名.方法名()来调用。
- 以下所说的 self 和 cls 只是约定俗成的参数名,并不是Python强制规定的,其实用什么参数名都可以,只要保证是第一个参数就够了。
- @staticmethod 不需要表示自身对象的self或自身类的cls参数,就跟使用函数一样。
- @classmethod也不需要self参数,但第一个参数需要是表示自身类的cls参数。
- 如果在 @staticmethod 中调用这个类的一些属性方法,只能用类名.属性名或类名.方法名。而 @classmethod 因为持有cls参数,因此既可以用 cls 来调用类的属性和方法,也可以用类名来调用。
反射 & 自省
- 反射(Reflection)包括自省(Introspection)和调解(Intercession)。自省的作用是运行的时候检查程序本身,调解的作用是运行的时候修改程序本身,所以反射和自省可以看成是一回事,只是提到自省时一般指的是读取属性,提到反射时一般指的是修改属性。
- 反射就是通过字符串的形式,导入模块;通过字符串的形式,去模块寻找指定函数,并执行。利用字符串的形式去对象(模块)中操作(查找/获取/删除/添加)成员,是一种基于字符串的事件驱动。
- 反射的相关方法:
- getattr:获取指定字符串名称的对象属性
- setattr:为对象设置一个属性或方法
- hasattr:判断对象是否有对应的属性或方法
- delattr:删除指定属性或方法
- 自省的相关方法:
- dir:返回对象的属性名称经过排序的列表
- type:返回对象的类型
- isinstance:判断对象是否是某个特定类型或定制类的实例
下划线命名法
- 单下划线开头:在一个模块中以单下划线开头的变量和函数被默认当作内部变量和函数,无法用
from a_module import *
的方式导入,但如果使用import a_module
方式导入了整个模块,仍然可以用a_module._some_var
的形式访问到内部对象。 - 单下划线结尾:Python 官方推荐的一种代码样式,用来避免与关键字冲突,比如想定义一个叫 class 的变量,就可以定义成 class_
- 双下划线开头但非双下划线结尾:称为名称改写(Name Mangling),会给成员自动加上类名前缀,使其不能被子类和类外直接访问,但如果手动加上类名就可以访问,所以感觉没什么用,如下。
In [0]: class A: |
- 双下划线开头且双下划线结尾:Python约定俗成的魔术对象的命名方法,虽然也可以用作自定义的变量和函数名,但官方不推荐这么用。
args 和 *kwargs
- 都用于函数参数的打包。args 把参数打包成元组,**kwargs 把参数打包成字典,同时使用时 args 必须在 **kwargs 的前面。当然,args 和 kwargs 都是约定俗成的名字,换成任何名字都可以。
In [0]: def a(x, *y, **z): |
- 此外,也可以反向使用 和 * ,在调用函数时对参数进行解包,然后分发给函数。如下:
In [0]: def a(x, y, z): |
新式类 & 旧式类(经典类)
- Python3 全部是新式类,反正 Python2 已经被彻底淘汰了,没必要关心旧式类。
__new__
和 __init__
- new 是一个静态方法,init 是一个实例方法
- new 方法会返回一个创建的实例,init 什么都不返回,负责对 new 创建出的实例做初始化
- 只有在 new 返回一个 cls 的实例时后面的 init 才能被调用
常量池
- 和 Java 一样,Python 也有基本类型的常量池。
- is 比较对象的地址,== 比较对象的值
In [0]: a = 123 |
object 和 type
- 面向对象思想中有两条主线关系,一条是类与类的关系,即继承,另一条是类与实例的关系,即实例化。object 是继承关系的顶峰,是所有类的基类(父类),而 type 是实例化关系的顶峰,Python 中的一切都是 type 类的实例(包括 type 本身)。
- 因为这两条线无法合并,所以作为各自主线的顶峰,object 和 type 的关系十分尴尬。从继承关系的角度看,type 是 object 的子类,从实例化角度看,object 又是 type 的一个实例,二者就像鸡生蛋蛋生鸡的关系一样。究其原因,还是因为在底层C的实现中没有把这两条线统一。
元类(metaclass)
- 元类,顾名思义,就是创建类的类。除了一般的定义类的方式,Python中还有两种创建类的方式,一种是使用type函数,另一种是使用元类。
- type一般用于获取对象的类型,但由于Python中的一切都是type的实例,所以自然能够通过type来创建新的类。如下,type有三个参数,第一个参数是类的
__name__
属性,一般定义类的时候,__name__
默认和类的标识符同名,但这里可以更改,类的标识符是A,类的名字是 newClass,第二个参数是父类(mro 元组),第三个参数是类的属性和方法的字典。根据结果来看,A 是创建出来的一个类,类型是type,对A进行实例化得到a,a的类型是newClass。
In [0]: A = type('newClass',(object,),{}) |
- 元类创建类的原理和type函数完全一样,如下。A就是元类,元类必须继承自type,new函数用于创建实例,而所有的类都是type的实例,所以A的new函数就是用来创建类的,三个参数和上面所说的type函数的三个参数完全一样。这里通过给attrs字典赋值,为新的类添加了一个foo方法。而在B中,通过指定
metaclass=A
,就可以获得A赋予的foo方法。
In [0]: class A(type): |
- 继承元类与继承一般类的区别在于,继承一般类时,子类只是复制了父类的全部属性和方法,父类并不能干预子类的创建过程。而在继承元类时,元类的new方法中可以指定子类的名字、mro 元组、属性与方法,相当于完全操纵着子类的创建过程。
list、tuple、dict、set 的底层实现原理
- 有时间再看源码吧~
- 留坑~