Python进阶知识点整理

Posted by Jingh on November 27, 2015
本文阅读量:

参数传递是值传递还是引用传递?

引用传递

都是引用传递,对于不可变的数据类型来说,值不能被改变,如果修改了,事实上是新建了一个新对象。

或者说

Python 中有可变对象(比如列表 List)和不可变对象(比如字符串),在参数传递时分为两种情况:

  1. 对于不可变对象作为函数参数,相当于 C 系语言的值传递;
  2. 对于可变对象作为函数参数,相当于 C 系语言的引用传递。

再或者

传值方式等价于 python 赋值号 (=),但不应该说是浅拷贝。
以 list 为例,浅拷贝可变对象时,会创建一个新的 list 对象,并让新对象内部的每一个元素指向原对象每个元素的地址;

而赋值号不会创建新对象,而是直接创建一个引用连接到原对象。

函数传值是后者,可以写一个函数,在函数里面打印传入参数的 id(),与原值的 id() 是一样的,因此是直接赋值而不是浅拷贝。

在Java中

  1. 当调用方法时,如果传入的数值为基本数据类型(包含String类型),形式参数的改变对实际参数不影响,就是值传递。

  2. 当调用方法时,如果传入的数值为引用数据类型(String类型除外),形式参数的改变对实际参数有影响,就是引用传递。

深拷贝与浅拷贝

  • Python 中对象的赋值都是进行对象引用(内存地址)传递
  • 使用 copy.copy(),可以进行对象的浅拷贝,它复制了对象,但对于对象中的元素,依然使用原始的引用.
  • 如果需要复制一个容器对象,以及它里面的所有元素(包含元素的子元素),可以使用 copy.deepcopy() 进行深拷贝
  • 对于非容器类型(如数字、字符串、和其他 ‘原子’ 类型的对象)没有被拷贝一说
  • 如果元祖变量只包含原子类型对象,则不能深拷贝

引用: 浅拷贝: 深拷贝:

垃圾回收机制

引用计数

python 里每一个东西都是对象,它们的核心就是一个结构体:PyObject

 typedef struct_object 
 { 
     int ob_refcnt;
     struct_typeobject *ob_type;
} PyObject;

PyObject 是每个对象必有的内容,其中ob_refcnt就是做为引用计数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用它的对象被删除,它的ob_refcnt就会减少

#define Py_INCREF(op)   ((op)->ob_refcnt++) //增加计数 
#define Py_DECREF(op)  //减少计数

if (--(op)->ob_refcnt != 0) ;  
else  __Py_Dealloc((PyObject *)(op))

当引用计数为 0 时,该对象生命就结束了。

引用计数机制的优点:

  1. 简单
  2. 实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。

引用计数机制的缺点:

  1. 维护引用计数消耗资源
  2. 循环引用
    list1 = []
    list2 = []
    list1.append(list2)
    list2.append(list1)
    

    list1 与 list2 相互引用,如果不存在其他对象对它们的引用,list1 与 list2 的引用计数也仍然为 1,所占用的内存永远无法被回收,这将是致命的。

对于如今的强大硬件,缺点 1 尚可接受,但是循环引用导致内存泄露,注定 python 还将引入新的回收机制。

标记清除和分代收集

基于零代算法的循环引用检测及垃圾回收机制

Python 中的循环数据结构以及引用计数

在 Python 中,每个对象都保存了一个称为引用计数的整数值,来追踪到底有多少引用指向了这个对象。无论何时,如果我们程序中的一个变量或其他对象引用了目标对象,Python 将会增加这个计数值,而当程序停止使用这个对象,则 Python 会减少这个计数值。一旦计数值被减到零,Python 将会释放这个对象以及回收相关内存空间。

从六十年代开始,计算机科学界就面临了一个严重的理论问题,那就是针对引用计数这种算法来说,如果一个数据结构引用了它自身,即如果这个数据结构是一个循环数据结构,那么某些引用计数值是肯定无法变成零的。为了更好地理解这个问题,让我们举个例子。下面的代码展示了一些所用到的节点类:

我们有一个构造器 (在 Python 中叫做 init),在一个实例变量中存储一个单独的属性。在类定义之后我们创建两个节点,ABC 以及 DEF,在图中为左边的矩形框。两个节点的引用计数都被初始化为 1,因为各有两个引用指向各个节点 (n1 和 n2)。

现在,让我们在节点中定义两个附加的属性,next 以及 prev: 跟 Ruby 不同的是,Python 中你可以在代码运行的时候动态定义实例变量或对象属性。这看起来似乎有点像 Ruby 缺失了某些有趣的魔法。(声明下我不是一个 Python 程序员,所以可能会存在一些命名方面的错误)。我们设置 n1.next 指向 n2,同时设置 n2.prev 指回 n1。现在,我们的两个节点使用循环引用的方式构成了一个双端链表。同时请注意到 ABC 以及 DEF 的引用计数值已经增加到了 2。这里有两个指针指向了每个节点:首先是 n1 以及 n2,其次就是 next 以及 prev。

现在,假定我们的程序不再使用这两个节点了,我们将 n1 和 n2 都设置为 null(Python 中是 None)。 好了,Python 会像往常一样将每个节点的引用计数减少到 1。 但此时这两个对象中内部互相引用着对方。

在 Python 中的零代 (Generation Zero)

请注意在以上刚刚说到的例子中,我们以一个不是很常见的情况结尾:我们有一个“孤岛”或是一组未使用的、互相指向的对象,但是谁都没有外部引用。换句话说,我们的程序不再使用这些节点对象了,所以我们希望 Python 的垃圾回收机制能够足够智能去释放这些对象并回收它们占用的内存空间。但是这不可能,因为所有的引用计数都是 1 而不是 0。Python 的引用计数算法不能够处理互相指向自己的对象。

当然,上边举的是一个故意设计的例子,但是你的代码也许会在不经意间包含循环引用并且你并未意识到。事实上,当你的 Python 程序运行的时候它将会建立一定数量的“浮点数垃圾”,Python 的 GC 不能够处理未使用的对象因为应用计数值不会到零。

这就是为什么 Python 要引入Generational GC 算法的原因!正如 Ruby 使用一个链表 (free list) 来持续追踪未使用的、自由的对象一样,Python 使用一种不同的链表来持续追踪活跃的对象。而不将其称之为“活跃列表”,Python 的内部 C 代码将其称为零代 (Generation Zero)。每次当你创建一个对象或其他什么值的时候,Python 会将其加入零代链表

从上边可以看到当我们创建 ABC 节点的时候,Python 将其加入零代链表。请注意到这并不是一个真正的列表,并不能直接在你的代码中访问,事实上这个链表是一个完全内部的 Python 运行时。 相似的,当我们创建 DEF 节点的时候,Python 将其加入同样的链表:

现在零代包含了两个节点对象。(他还将包含 Python 创建的每个其他值,与一些 Python 自己使用的内部值。)

检测循环引用

随后,Python 会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数。在这个过程中,Python 会一个接一个的统计内部引用的数量以防过早地释放对象。

为了便于理解,来看一个例子:

从上面可以看到 ABC 和 DEF 节点包含的引用数为 1. 有三个其他的对象同时存在于零代链表中,蓝色的箭头指示了有一些对象正在被零代链表之外的其他对象所引用。(接下来我们会看到,Python 中同时存在另外两个分别被称为一代和二代的链表)。这些对象有着更高的引用计数因为它们正在被其他指针所指向着。

接下来你会看到 Python 的 GC 是如何处理零代链表的。

通过识别内部引用,Python 能够减少许多零代链表对象的引用计数。在上图的第一行中你能够看见 ABC 和 DEF 的引用计数已经变为零了,这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表:一代链表。

从某种意义上说,Python 的 GC 算法类似于 Ruby 所用的标记回收算法。周期性地从一个对象到另一个对象追踪引用以确定对象是否还是活跃的,正在被程序所使用的,这正类似于 Ruby 的标记过程。

Python 中的 GC 阈值

Python 什么时候会进行这个标记过程?随着你的程序运行,Python 解释器保持对新创建的对象,以及因为引用计数为零而被释放掉的对象的追踪。从理论上说,这两个值应该保持一致,因为程序新建的每个对象都应该最终被释放掉。

当然,事实并非如此。因为循环引用的原因,并且因为你的程序使用了一些比其他对象存在时间更长的对象,从而被分配对象的计数值与被释放对象的计数值之间的差异在逐渐增长。一旦这个差异累计超过某个阈值,则 Python 的收集机制就启动了,并且触发上边所说到的零代算法,释放“浮动的垃圾”,并且将剩下的对象移动到一代列表。

随着时间的推移,程序所使用的对象逐渐从零代列表移动到一代列表。而 Python 对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python 会将剩下的活跃对象移动到二代列表。

通过这种方法,你的代码所长期使用的对象,那些你的代码持续访问的活跃对象,会从零代链表转移到一代再转移到二代。通过不同的阈值设置,Python 可以在不同的时间间隔处理这些对象。Python 处理零代最为频繁,其次是一代然后才是二代。

弱代假说

来看看代垃圾回收算法的核心行为:垃圾回收器会更频繁的处理新对象。一个新的对象即是你的程序刚刚创建的,而一个来的对象则是经过了几个时间周期之后仍然存在的对象。Python 会在当一个对象从零代移动到一代,或是从一代移动到二代的过程中提升 (promote) 这个对象。

为什么要这么做?这种算法的根源来自于弱代假说 (weak generational hypothesis)。这个假说由两个观点构成:首先是年轻的对象通常死得也快,而老对象则很有可能存活更长的时间。

假定现在我用 Python 或是 Ruby 创建一个新对象:

根据假说,我的代码很可能仅仅会使用 ABC 很短的时间。这个对象也许仅仅只是一个方法中的中间结果,并且随着方法的返回这个对象就将变成垃圾了。大部分的新对象都是如此般地很快变成垃圾。然而,偶尔程序会创建一些很重要的,存活时间比较长的对象 - 例如 web 应用中的 session 变量或是配置项。

通过频繁的处理零代链表中的新对象,Python 的垃圾收集器将把时间花在更有意义的地方:它处理那些很快就可能变成垃圾的新对象。同时只在很少的时候,当满足阈值的条件,收集器才回去处理那些老变量。

总结:

python采用的是引用计数机制为主,标记-清除和分代收集两种机制为辅的策略

引用计数的优点是简单且具有实效性, 一旦没有引用, 内存就直接释放了, 不用像其他机制需要等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时

缺点是需要维护引用计数消耗资源, 且容易形成循环引用

对于如今的强大硬件,消耗资源尚可接受,但是循环引用导致内存泄露, 这将是致命的

因此python引入注定引入新的回收机制, 也就是标记清除和分代收集

每当你创建一个对象,Python 会将其加入零代链表

随后,Python 会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数。

引用计数一旦变为零,收集器将释放它们并回收内存空间。剩下的活跃对象则被移动到一个新的链表:一代链表。

Python 对于一代列表中对象的处理遵循同样的方法,一旦被分配计数值与被释放计数值累计到达一定阈值,Python 会将剩下的活跃对象移动到二代列表。

Python 处理零代最为频繁,其次是一代然后才是二代。

del

删除元素

>>> a = [1, "two", 3, "four"]
>>> del a[0]         #删除列表a中,下标为0的元素
>>> a
['two', 3, 'four']
>>> a.append("five")
>>> a.append(6)
>>> a
['two', 3, 'four', 'five', 6]
>>> del a[2:4]        #删除a从下标为2到4的元素,含头不含尾
>>> a

删除引用


>>> x = 1
>>> del x
>>> x
Traceback (most recent call last):
File "<pyshell#6>", line 1, in <module>
x
NameError: name 'x' is not defined


>>> x = ['Hello','world']
>>> y = x
>>> y
['Hello', 'world']
>>> x
['Hello', 'world']
>>> del x
>>> x
Traceback (most recent call last):
File "<pyshell#12>", line 1, in <module>
x
NameError: name 'x' is not defined
>>> y
['Hello', 'world']
>>>

可以看到 x 和 y 指向同一个列表,但是删除 x 后,y 并没有受到影响。del 的是引用,而不是对象

元类 (metaclass)

类也是对象

在理解元类之前,你需要先掌握 Python 中的类。Python 中类的概念借鉴于Smalltalk,这显得有些奇特。在大多数编程语言中,类就是一组用来描述如何生成一个对象的代码段。在 Python 中这一点仍然成立:

>>>  class  ObjectCreator(object):
        pass

>>>  my_object  =  ObjectCreator()
>>>  print  my_object
<__main__.ObjectCreator object  at  0x8974f2c>

但是,Python 中的类还远不止如此。类同样也是一种对象。是的,没错,就是对象。只要你使用关键字 class,Python 解释器在执行的时候就会创建一个对象。下面的代码段:

>>> class ObjectCreator(object):
       pass

将在内存中创建一个对象,名字就是 ObjectCreator这个对象(类)自身拥有创建对象(类实例)的能力,而这就是为什么它是一个类的原因。但是,它的本质仍然是一个对象,于是乎你可以对它做如下的操作:

  1. 你可以将它赋值给一个变量
  2. 你可以拷贝它
  3. 你可以为它增加属性
  4. 你可以将它作为函数参数进行传递 下面是示例:
# 你可以打印一个类,因为它其实也是一个对象
>>> print ObjectCreator    
<class '__main__.ObjectCreator'>

# 你可以将类做为参数传给函数
>>> def echo(o):
…       print o
…
>>> echo(ObjectCreator)                
<class '__main__.ObjectCreator'>

#  你可以为类增加属性
>>> print hasattr(ObjectCreator, 'new_attribute')
Fasle
>>> ObjectCreator.new_attribute = 'foo' 
>>> print hasattr(ObjectCreator, 'new_attribute')
True

>>> print ObjectCreator.new_attribute
foo

# 你可以将类赋值给一个变量
>>> ObjectCreatorMirror = ObjectCreator 

>>> print ObjectCreatorMirror()
<__main__.ObjectCreator object at 0x8997b4c>

动态地创建类

因为类也是对象,你可以在运行时动态的创建它们,就像其他任何对象一样。首先,你可以在函数中创建类,使用 class 关键字即可。

>>> def choose_class(name):
…       if name == 'foo':
…           class Foo(object):
…               pass
…           return Foo # 返回的是类,不是类的实例else:
…           class Bar(object):
…               pass
…           return Bar
…
>>> MyClass = choose_class('foo')
>>> print MyClass # 函数返回的是类,不是类的实例
<class '__main__'.Foo>

>>> print MyClass() # 你可以通过这个类创建类实例,也就是对象
<__main__.Foo object at 0x89c6d4c>

但这还不够动态,因为你仍然需要自己编写整个类的代码。由于类也是对象,所以它们必须是通过什么东西来生成的才对。当你使用 class 关键字时,Python 解释器自动创建这个对象。但就和 Python 中的大多数事情一样,Python 仍然提供给你手动处理的方法。还记得内建函数 type 吗?这个古老但强大的函数能够让你知道一个对象的类型是什么,就像这样:

>>> print type(1)
<type 'int'>
>>> print type("1")
<type 'str'>
>>> print type(ObjectCreator)
<type 'type'>
>>> print type(ObjectCreator())
<class '__main__.ObjectCreator'>

这里,type 有一种完全不同的能力,它也能动态的创建类。

type 可以接受一个类的描述作为参数,然后返回一个类。(我知道,根据传入参数的不同,同一个函数拥有两种完全不同的用法是一件很傻的事情,但这在 Python 中是为了保持向后兼容性)

type 可以像这样工作:

type(类名,  父类的元组(针对继承的情况,可以为空),包含属性的字典(名称和值))

比如下面的代码:

>>> class MyShinyClass(object):
…       pass

可以手动像这样创建:

>>> MyShinyClass = type('MyShinyClass', (), {})  # 返回一个类对象
>>> print MyShinyClass
<class '__main__.MyShinyClass'>
>>> print MyShinyClass()  #  创建一个该类的实例
<__main__.MyShinyClass object at 0x8997cec>

你会发现我们使用“MyShinyClass”作为类名,并且也可以把它当做一个变量来作为类的引用。类和变量是不同的,这里没有任何理由把事情弄的复杂。

type 接受一个字典来为类定义属性,因此:

>>> class Foo(object):
…       bar = True

可以翻译为:

>>>  Foo  =  type('Foo',  (),  {'bar':True})

并且可以将 Foo 当成一个普通的类一样使用:

>>> print Foo
<class '__main__.Foo'>
>>> print Foo.bar
True
>>> f = Foo()
>>> print f
<__main__.Foo object at 0x8a9b84c>
>>> print f.bar
True

当然,你可以向这个类继承,所以,如下的代码:

>>> class FooChild(Foo):
…       pass

就可以写成:

>>> FooChild = type('FooChild', (Foo,),{})
>>> print FooChild
<class '__main__.FooChild'>
>>> print FooChild.bar   # bar属性是由Foo继承而来
True

最终你会希望为你的类增加方法。只需要定义一个有着恰当签名的函数并将其作为属性赋值就可以了。

>>> def echo_bar(self):
…       print self.bar
…
>>> FooChild = type('FooChild', (Foo,), {'echo_bar': echo_bar})
>>> hasattr(Foo, 'echo_bar')
False
>>> hasattr(FooChild, 'echo_bar')
True
>>> my_foo = FooChild()
>>> my_foo.echo_bar()
True

你可以看到,在 Python 中,类也是对象,你可以动态的创建类。这就是当你使用关键字 class 时 Python 在幕后做的事情,而这就是通过元类来实现的。

到底什么是元类

元类就是用来创建类的“东西”。你创建类就是为了创建类的实例对象,不是吗?但是我们已经学习到了 Python 中的类也是对象。好吧,元类就是用来创建这些类(对象)的,元类就是类的类,你可以这样理解 为:

MyClass = MetaClass()
MyObject = MyClass()

你已经看到了 type 可以让你像这样做:

MyClass = type('MyClass', (), {})

这是因为函数 type 实际上是一个元类。type 就是 Python 在背后用来创建所有类的元类。现在你想知道那为什么 type 会全部采用小写形式而不是 Type 呢?好吧,我猜这是为了和 str 保持一致性,str 是用来创建字符串对象的类,而 int 是用来创建整数对象的类。type 就是创建类对象的类。你可以通过检查 class 属性来看到这一点。Python 中所有的东西,注意,我是指所有的东西——都是对象。这包括整数、字符串、函数以及类。它们全部都是对象,而且它们都是从一个类创建而来。

>>> age = 35
>>> age.__class__
<type 'int'>
>>> name = 'bob'
>>> name.__class__
<type 'str'>
>>> def foo(): pass
>>>foo.__class__
<type 'function'>
>>> class Bar(object): pass
>>> b = Bar()
>>> b.__class__
<class '__main__.Bar'>

现在,对于任何一个 classclass 属性又是什么呢?

>>> a.__class__.__class__
<type 'type'>
>>> age.__class__.__class__
<type 'type'>
>>> foo.__class__.__class__
<type 'type'>
>>> b.__class__.__class__
<type 'type'>

因此,元类就是创建类这种对象的东西。如果你喜欢的话,可以把元类称为“类工厂”(不要和工厂类搞混了:D) type 就是 Python 的内建元类,当然了,你也可以创建自己的元类。

__metaclass__ 属性

你可以在写一个类的时候为其添加 __metaclass__ 属性。

class Foo():
    __metaclass__ = something

这里要额外注意 (原文翻译有问题):

自定义元类不能从 ‘object’ 继承,metaclass 是类的模板,所以必须从type类型派生。可以这么定义一个元类 class foo(type),然后这么用 class myfoo(metaclass=foo) 全局 __metaclass__ 只对旧式类有效。加上 object 作为参数就不对了。

如果你这么做了,Python 就会用元类来创建类 Foo。小心点,这里面有些技巧。你首先写下 class Foo(object),但是类对象 Foo 还没有在内存中创建。Python 会在类的定义中寻找 __metaclass__ 属性,如果找到了,Python 就会用它来创建类 Foo,如果没有找到,就会用内建的 type 来创建这个类。把下面这段话反复读几次。当你写如下代码时 :

class Foo(Bar):
    pass

Python 做了如下的操作:

Foo 中有 metaclass 这个属性吗?如果是,Python 会在内存中通过 metaclass 创建一个名字为 Foo 的类对象(我说的是类对象,请紧跟我的思路)。如果 Python 没有找到 metaclass,它会继续在 Bar(父类)中寻找 metaclass 属性,并尝试做和前面同样的操作。如果 Python 在任何父类中都找不到 metaclass,它就会在模块层次中去寻找 metaclass,并尝试做同样的操作。如果还是找不到 metaclass,Python 就会用内置的 type 来创建这个类对象。

现在的问题就是,你可以在 metaclass 中放置些什么代码呢?答案就是:可以创建一个类的东西。那么什么可以用来创建一个类呢? type,或者任何使用到 type 或者子类化 type 的东东都可以。

自定义元类

元类的主要目的就是为了当创建类时能够自动地改变类。通常,你会为 API 做这样的事情,你希望可以创建符合当前上下文的类。假想一个很傻的例子,你决定在你的模块里所有的类的属性都应该是大写形式。有好几种方法可以办到,但其中一种就是通过在模块级别设定 metaclass。采用这种方法,这个模块中的所有类都会通过这个元类来创建,我们只需要告诉元类把所有的属性都改成大写形式就万事大吉了。

幸运的是,metaclass 实际上可以被任意调用,它并不需要是一个正式的类(我知道,某些名字里带有‘class’的东西并不需要是一个 class,画画图理解下,这很有帮助)。所以,我们这里就先以一个简单的函数作为例子开始。

# 元类会自动将你通常传给‘type’的参数作为自己的参数传入

def upper_attr(future_class_name, future_class_parents, future_class_attr):
    '''返回一个类对象,将属性都转为大写形式'''
    #  选择所有不以'__'开头的属性
    attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
    # 将它们转为大写形式
    uppercase_attr = dict((name.upper(), value) for name, value in attrs)

    # 通过'type'来做类对象的创建
    return type(future_class_name, future_class_parents, uppercase_attr)

__metaclass__ = upper_attr  #  这会作用到这个模块中的所有类

class Foo(object):
    # 我们也可以只在这里定义__metaclass__,这样就只会作用于这个类中
    bar = 'bip'

print hasattr(Foo, 'bar')
# 输出: False
print hasattr(Foo, 'BAR')
# 输出:True

f = Foo()
print f.BAR
# 输出:'bip'

现在让我们再做一次,这一次用一个真正的 class 来当做元类。

# 请记住,'type'实际上是一个类,就像'str'和'int'一样
# 所以,你可以从type继承

class UpperAttrMetaClass(type):
    # __new__ 是在__init__之前被调用的特殊方法
    # __new__是用来创建对象并返回之的方法
    # 而__init__只是用来将传入的参数初始化给对象
    # 你很少用到__new__,除非你希望能够控制对象的创建
    # 这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__
    # 如果你希望的话,你也可以在__init__中做些事情
    # 还有一些高级的用法会涉及到改写__call__特殊方法,但是我们这里不用
    def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):
        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)
        return type(future_class_name, future_class_parents, uppercase_attr)

但是,这种方式其实不是 OOP。我们直接调用了 type,而且我们没有改写父类的 new 方法。现在让我们这样去处理:

class UpperAttrMetaclass(type):
    def __new__(upperattr_metaclass, future_class_name, future_class_parents, future_class_attr):
        attrs = ((name, value) for name, value in future_class_attr.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)

        # 复用type.__new__方法
        # 这就是基本的OOP编程,没什么魔法
        return type.__new__(upperattr_metaclass, future_class_name, future_class_parents, uppercase_attr)

你可能已经注意到了有个额外的参数 upperattr_metaclass,这并没有什么特别的。类方法的第一个参数总是表示当前的实例,就像在普通的类方法中的 self 参数一样。当然了,为了清晰起见,这里的名字我起的比较长。但是就像 self 一样,所有的参数都有它们的传统名称。因此,在真实的产品代码中一个元类应该是像这样的:

class UpperAttrMetaclass(type):
    def __new__(cls, name, bases, dct):
        attrs = ((name, value) for name, value in dct.items() if not name.startswith('__')
        uppercase_attr  = dict((name.upper(), value) for name, value in attrs)
        return type.__new__(cls, name, bases, uppercase_attr)

如果使用 super 方法的话,我们还可以使它变得更清晰一些,这会缓解继承(是的,你可以拥有元类,从元类继承,从 type 继承)

class UpperAttrMetaclass(type):
    def __new__(cls, name, bases, dct):
        attrs = ((name, value) for name, value in dct.items() if not name.startswith('__'))
        uppercase_attr = dict((name.upper(), value) for name, value in attrs)
        return super(UpperAttrMetaclass, cls).__new__(cls, name, bases, uppercase_attr)

就是这样,除此之外,关于元类真的没有别的可说的了。使用到元类的代码比较复杂,这背后的原因倒并不是因为元类本身,而是因为你通常会使用元类去做一些晦涩的事情,依赖于自省,控制继承等等。确实,用元类来搞些“黑暗魔法”是特别有用的,因而会搞出些复杂的东西来。但就元类本身而言,它们其实是很简单的:

  1. 拦截类的创建
  2. 修改类
  3. 返回修改之后的类

为什么要用 metaclass 类而不是函数?

由于 metaclass 可以接受任何可调用的对象,那为何还要使用类呢,因为很显然使用类会更加复杂啊?这里有好几个原因:

  1. 意图会更加清晰。当你读到 UpperAttrMetaclass(type) 时,你知道接下来要发生什么。
  2. 你可以使用 OOP 编程。元类可以从元类中继承而来,改写父类的方法。元类甚至还可以使用元类。
  3. 你可以把代码组织的更好。当你使用元类的时候肯定不会是像我上面举的这种简单场景,通常都是针对比较复杂的问题。将多个方法归总到一个类中会很有帮助,也会使得代码更容易阅读。
  4. 你可以使用 new, init 以及 call 这样的特殊方法。它们能帮你处理不同的任务。就算通常你可以把所有的东西都在 new 里处理掉,有些人还是觉得用 init 更舒服些。
  5. 哇哦,这东西的名字是 metaclass,肯定非善类,我要小心!

究竟为什么要使用元类?

现在回到我们的大主题上来,究竟是为什么你会去使用这样一种容易出错且晦涩的特性?好吧,一般来说,你根本就用不上它:

“元类就是深度的魔法,99% 的用户应该根本不必为此操心。如果你想搞清楚究竟是否需要用到元类,那么你就不需要它。那些实际用到元类的人都非常清楚地知道他们需要做什么,而且根本不需要解释为什么要用元类。” —— Python 界的领袖 Tim Peters

元类的主要用途是创建 API。一个典型的例子是 Django ORM。它允许你像这样定义:

class Person(models.Model):
    name = models.CharField(max_length=30)
    age = models.IntegerField()

但是如果你像这样做的话:

guy  =  Person(name='bob',  age='35')
print  guy.age

这并不会返回一个 IntegerField 对象,而是会返回一个 int,甚至可以直接从数据库中取出数据。这是有可能的,因为 models.Model 定义了 metaclass, 并且使用了一些魔法能够将你刚刚定义的简单的 Person 类转变成对数据库的一个复杂 hook。Django 框架将这些看起来很复杂的东西通过暴露出一个简单的使用元类的 API 将其化简,通过这个 API 重新创建代码,在背后完成真正的工作。

结语

首先,你知道了类其实是能够创建出类实例的对象。好吧,事实上,类本身也是实例,当然,它们是元类的实例。

>>>class Foo(object): pass
>>> id(Foo)
142630324

Python 中的一切都是对象,它们要么是类的实例,要么是元类的实例,除了 type。type 实际上是它自己的元类,在纯 Python 环境中这可不是你能够做到的,这是通过在实现层面耍一些小手段做到的。其次,元类是很复杂的。对于非常简单的类,你可能不希望通过使用元类来对类做修改。你可以通过其他两种技术来修改类:

  1. 猴子补丁 (Monkey patching)

     class Foo(object):
     def bar(self):
        
      print 'Foo.bar' 
     def bar(self):
     print 'Modified bar' Foo().bar()
        
     Foo.bar = bar
        
     Foo().bar()
    
  2. 类装饰器(class decorators)

新式类和旧式类

在Python 2及以前的版本中,由任意内置类型派生出的类(只要一个内置类型位于类树的某个位置),都属于“新式类”,都会获得所有“新式类”的特性;反之,即不由任意内置类型派生出的类,则称之为“经典类”。

class A:
    pass

class B(object):
    pass

Python 2.x 中默认都是经典类,只有显式继承了 object 才是新式类
Python 3.x 中默认都是新式类,不必显式的继承 object

“新式类”和“经典类”的区分在 Python 3 之后就已经不存在,在 Python 3.x 之后的版本,因为所有的类都派生自内置类型 object(即使没有显示的继承 object 类型),即所有的类都是“新式类”。

继承顺序的区别

以钻石继承为例, 经典类的钻石继承是深度优先,即从下往上搜索;新式类的继承顺序是采用 C3 算法(非广度优先)。

class ClassicClassA():
    var = 'Classic Class A'


class ClassicClassB(ClassicClassA):
    pass


class ClassicClassC():
    var = 'Classic Class C'


class SubClassicClass(ClassicClassB, ClassicClassC):
    pass


if __name__ == '__main__':
    print(SubClassicClass.var)

>>> Classic Class A

在 SubClassicClass 对 var 属性进行搜索的过程中,根据从下到上的原则,会优先搜索 ClassicClassB,而 ClassicClassB 没有 var 属性,会继续往上搜索 ClassicClassB 的超类 ClassicClassA(深度优先),在 ClassicClassA 中发现 var 属性后停止搜索,var 的值为 ClassicClassA 中 var 的值;而 ClassicClassC 的 var 属性从始至终都未被搜索到。

从运行结果可以看出,输出的是 Classic Class A,可见类继承的搜索是深度优先,由下至上进行搜索。

新式类的继承顺序并非是广度优先,而是C3算法只是在部分情况下,C3 算法的结果恰巧与广度优先的结果相同。

对新式类的继承搜索顺序进行代码验证,新式类中,可以使用 mro 函数来查看类的搜索顺序 (这也算是一个区别),如 SubNewStyleClass.mro()

统一了 python 中的类型机制,保持 class 与 type 的统一
对新式类的实例执行 a.class 与 type(a) 的结果是一致的,对于旧式类来说就不一样了。
经典类的实例是 instance 类型,而内置类的实例却不是,无法统一

class A():pass
class B():pass

a = A()
b = B()

if __name__ == '__main__':
    print(type(a))
    print(type(b))
    print(type(a) == type(b))
    print(a.__class__)
    print(b.__class__)
    print(a.__class__ == b.__class__)

Python 2.6.9 及 2.7.10 的运行结果:

在 Python 2.x 及以前的版本,所有经典类的实例都是“instance”(实例类型)。所以比较经典类实例的类型 (type) 毫无意义,因为所有的经典类实例都是 instance 类型,比较的结果通常为 True。更多情况下需要比较经典类实例的 class 属性来获得我们想要的结果(或使用 isinstance 函数)。

Python 3.5.1 运行结果

在 Python 3.x 及之后的版本,类和类型已经合并。类实例的类型是这个实例所创建自的类(通常是和类实例的 class 相同),而不再是 Python 2.x 版本中的“instance”实例类型。

需要注意的是,在 Python 2.x 版本中,“经典类的实例都是 instance 类型”,这个结论只适用于经典类。对新式类和内置类型的实例,它们的类型要更加明确。

所有的类型都是 type 类。从另一个角度理解,类就是 type 类的实例,所有的新式类,都是由 type 类实例化创建而来,并且显式或隐式继承自 object。

在 Python 3.x 中,所有的类都显式或隐式的派生自 object 类,type 类也不例外。类型自身派生自 object 类,而 object 类派生自 type,二者组成了一个循环的关系。

总结:

类用于描述如何生成一个对象, 而类同样也是一种对象, 这个类对象自身拥有创建对象的能力

类也是对象, 所以可以动态的创建类, 而这就是通过元类来实现的

元类就是用来创建类的, 元类就是类的类

函数 type 实际上是一个元类。type 就是 Python 在背后用来创建所有类的元类

str 是用来创建字符串对象的类,而 int 是用来创建整数对象的类。type 就是创建类对象的类。

type 与 object

引言

父子关系,即继承关系,表现为子类继承于父类,如『蛇』类继承自『爬行动物』类,我们说『蛇是一种爬行动物』,英文说『snake is a kind of reptile』。在 python 里要查看一个类型的父类,使用它的 bases 属性可以查看。

类型实例关系,表现为某个类型的实例化,例如『萌萌是一条蛇』,英文说『萌萌 is an instance of snake』。在 python 里要查看一个实例的类型,使用它的 class 属性可以查看,或者使用 type() 函数查看。

这两种关系使用下面这张图简单示意,继承关系使用实线从子到父连接,类型实例关系使用虚线从实例到类型连接:

我们将使用一块白板来描述一下 Python 里面对象的关系,白板划分成三列:

先来看看 type 和 object

>>> object
<type 'object'>
>>> type
<type 'type'>
它们都是 type 的一个实例,表示它们都是类型对象。

在 Python 的世界中,object 是父子关系的顶端,所有的数据类型的父类都是它; type 是类型实例关系的顶端,所有对象都是它的实例的。它们两个的关系可以这样描述:

  • object 是一个 type,object is and instance of type。即 Object 是 type 的一个实例。

>>> object.__class__<type 'type'>
>>> object.__bases__  # object 无父类,因为它是链条顶端。
()
  • type 是一种 object, type is kind of object。即 Type 是 object 的子类。

>>> type.__bases__(<type 'object'>,)
>>> type.__class__  # type的类型是自己<type 'type'>

我们再引入 list, dict, tuple 这些内置数据类型来看看

>>> list.__bases__(<type 'object'>,)
>>> list.__class__<type 'type'>
>>> dict.__bases__(<type 'object'>,)
>>> dict.__class__<type 'type'>
>>> tuple.__class__<type 'type'>
>>> tuple.__bases__(<type 'object'>,)

它们的父类都是 object,类型都是 type。

再实例化一个 list 看看

>>> mylist = [1,2,3]
>>> mylist.__class__<type 'list'>
>>> mylist.__bases__
Traceback (most recent call last):  
File "<stdin>", line 1, in <module>
AttributeError: 'list' object has no attribute '__bases__'

实例化的 list 的类型是, 而没有了父类。把它们加到白板上去:

白板上的虚线表示源是目标的实例,实线表示源是目标的子类。即,左边的是右边的类型,而上面的是下面的父亲。 虚线是跨列产生关系,而实线只能在一列内产生关系。除了 type 和 object 两者外。

当我们自己去定个一个类及实例化它的时候,和上面的对象们又是什么关系呢?试一下

>>> class C(object):
...     pass
... 
>>> C.__class__<type 'type'>
>>> C.__bases__(<type 'object'>,)
# 实例化
>>> c = C()
>>> c.__class__<class '__main__.C'>
>>> c.__bases__
Traceback (most recent call last):  
File "<stdin>", line 1, in <module>
AttributeError: 'C' object has no attribute '__bases__'

这个实例化的 C 类对象也是没有父类的属性的。 再更新一下白板:

第一列,元类列,type 是所有元类的父亲。我们可以通过继承 type 来创建元类。
第二列,TypeObject 列,也称类列,object 是所有类的父亲,大部份我们直接使用的数据类型都存在这个列的。
第三列,实例列,实例是对象关系链的末端,不能再被子类化和实例化。

object 是父子关系的顶端,所有的数据类型的父类都是它;
type 是类型实例关系的顶端,所有对象都是它的实例的
即 object 是 type 的一个实例。
type 是 object 的子类。

全局解释器锁

全局解释器锁 GIL(Global Interpreter Lock)

简介

  1. 设置 GIL
  2. 切换到一个线程去运行
  3. 运行:

    a. 指定数量的字节码指令,或者
    b. 线程主动让出控制(I/O 或者可以调用 time.sleep(0)

  4. 把线程设置为睡眠状态
  5. 解锁 GIL
  6. 再次重复以上所有步骤

    引言

GIL 是什么

首先需要明确的一点是GIL并不是 Python 的特性,它是在实现 Python 解析器 (CPython) 时所引入的一个概念。就好比 C++ 是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如 GCC,INTEL C++,Visual C++ 等。Python 也一样,同样一段代码可以通过 CPython,PyPy,Psyco 等不同的 Python 执行环境来执行。像其中的 JPython 就没有 GIL。然而因为 CPython 是大部分环境下默认的 Python 执行环境。所以在很多人的概念里 CPython 就是 Python,也就想当然的把GIL归结为 Python 语言的缺陷。所以这里要先明确一点:GIL 并不是 Python 的特性,Python 完全可以不依赖于 GIL

那么 CPython 实现中的 GIL 又是什么呢?GIL 全称Global Interpreter Lock为了避免误导,我们还是来看一下官方给出的解释:

In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)

好吧,是不是看上去很糟糕?一个防止多线程并发执行机器码的一个 Mutex,乍一看就是个 BUG 般存在的全局锁嘛!别急,我们下面慢慢的分析

为什么会有 GIL

由于物理上得限制,各 CPU 厂商在核心频率上的比赛已经被多核所取代。为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。即使在 CPU 内部的 Cache 也不例外,为了有效解决多份缓存之间的数据同步时各厂商花费了不少心思,也不可避免的带来了一定的性能损失。

Python 当然也逃不开,为了利用多核,Python 开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了 GIL 这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认 python 内部对象是 thread-safe 的,无需在实现时考虑额外的内存锁和同步操作)。

慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除 GIL 的时候,发现大量库代码开发者已经重度依赖 GIL 而非常难以去除了。有多难?做个类比,像 MySQL 这样的“小项目”为了把 Buffer Pool Mutex 这把大锁拆分成各个小锁也花了从 5.5 到 5.6 再到 5.7 多个大版为期近 5 年的时间,并且仍在继续。MySQL 这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况 Python 这样核心开发和代码贡献者高度社区化的团队呢?

所以简单的说 GIL 的存在更多的是历史原因。如果推到重来,多线程的问题依然还是要面对,但是至少会比目前 GIL 这种方式会更优雅。

GIL 的影响

从上文的介绍和官方的定义来看,GIL 无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于 Python 是个单线程的程序。 那么读者就会说了,全局锁只要释放的勤快效率也不会差啊。只要在进行耗时的 IO 操作的时候,能释放 GIL,这样也还是可以提升运行效率的嘛。或者说再差也不会比单线程的效率差吧。理论上是这样,而实际上呢?Python 比你想的更糟。

下面我们就对比下 Python 在多线程和单线程下得效率对比。测试方法很简单,一个循环 1 亿次的计数器函数。一个通过单线程执行两次,一个多线程执行。最后比较执行总时间。测试环境为双核的 Mac pro。注:为了减少线程库本身性能损耗对测试结果带来的影响,这里单线程的代码同样使用了线程。只是顺序的执行两次,模拟单线程。

顺序执行的单线程 (single_thread.py)



#! /usr/bin/python

from threading import Thread
import time

def my_counter():
    i = 0
    for _ in range(100000000):
        i = i + 1
    return True

def main():
    thread_array = {}
    start_time = time.time()
    for tid in range(2):
        t = Thread(target=my_counter)
        t.start()
        t.join()
    end_time = time.time()
    print("Total time: {}".format(end_time - start_time))

if __name__ == '__main__':
    main()
    

同时执行的两个并发线程 (multi_thread.py)

#! /usr/bin/python

from threading import Thread
import time

def my_counter():
    i = 0
    for _ in range(100000000):
        i = i + 1
    return True

def main():
    thread_array = {}
    start_time = time.time()
    for tid in range(2):
        t = Thread(target=my_counter)
        t.start()
        thread_array[tid] = t
    for i in range(2):
        thread_array[i].join()
    end_time = time.time()
    print("Total time: {}".format(end_time - start_time))

if __name__ == '__main__':
    main()

下图就是测试结果

可以看到 python 在多线程的情况下居然比单线程整整慢了 45%。按照之前的分析,即使是有 GIL 全局锁的存在,串行化的多线程也应该和单线程有一样的效率才对。那么怎么会有这么糟糕的结果呢?

让我们通过 GIL 的实现原理来分析这其中的原因。

当前 GIL 设计的缺陷

基于 pcode 数量的调度方式

按照 Python 社区的想法,操作系统本身的线程调度已经非常成熟稳定了,没有必要自己搞一套。所以 Python 的线程就是 C 语言的一个 pthread,并通过操作系统调度算法进行调度(例如 linux 是 CFS)。为了让各个线程能够平均利用 CPU 时间,python 会计算当前已执行的微代码数量,达到一定阈值后就强制释放 GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。

伪代码

while True:
    acquire GIL
    for i in 1000:
        do something
    release GIL
    /* Give Operating System a chance to do thread scheduling */

这种模式在只有一个 CPU 核心的情况下毫无问题。任何一个线程被唤起时都能成功获得到 GIL(因为只有释放了 GIL 才会引发线程调度)。但当 CPU 有多个核心的时候,问题就来了。从伪代码可以看到,从release GILacquire GIL之间几乎是没有间隙的。所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经又再一次获取到 GIL 了。这个时候被唤醒执行的线程只能白白的浪费 CPU 时间,看着另一个线程拿着 GIL 欢快的执行着。然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。

PS:当然这种实现方式是原始而丑陋的,Python 的每个版本中也在逐渐改进 GIL 和线程调度之间的互动关系。例如先尝试持有 GIL 在做线程上下文切换,在 IO 等待时释放 GIL 等尝试。但是无法改变的是 GIL 的存在使得操作系统线程调度的这个本来就昂贵的操作变得更奢侈了。 关于 GIL 影响的扩展阅读

为了直观的理解 GIL 对于多线程带来的性能影响,这里直接借用的一张测试结果图(见下图)。图中表示的是两个线程在双核 CPU 上得执行情况。两个线程均为 CPU 密集型运算线程。绿色部分表示该线程在运行,且在执行有用的计算,红色部分为线程被调度唤醒,但是无法获取 GIL 导致无法进行有效运算等待的时间。 由图可见,GIL 的存在导致多线程无法很好的立即多核 CPU 的并发处理能力。

那么 Python 的 IO 密集型线程能否从多线程中受益呢?我们来看下面这张测试结果。颜色代表的含义和上图一致。白色部分表示 IO 线程处于等待。可见,当 IO 线程收到数据包引起终端切换后,仍然由于一个 CPU 密集型线程的存在,导致无法获取 GIL 锁,从而进行无尽的循环等待。

简单的总结下就是:Python 的多线程在多核 CPU 上,只对于 IO 密集型计算产生正面效果;而当有至少有一个 CPU 密集型线程存在,那么多线程效率会由于 GIL 而大幅下降。

如何避免受到 GIL 的影响

说了那么多,如果不说解决方案就仅仅是个科普帖,然并卵。GIL 这么烂,有没有办法绕过呢?我们来看看有哪些现成的方案。

用 multiprocessing 替代 Thread
multiprocessing 库的出现很大程度上是为了弥补 thread 库因为 GIL 而低效的缺陷。它完整的复制了一套 thread 所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的 GIL,因此也不会出现进程之间的 GIL 争抢。

当然 multiprocessing 也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于 thread 来说,申明一个 global 变量,用 thread.Lock 的 context 包裹住三行就搞定了。而 multiprocessing 由于进程之间无法看到对方的数据,只能通过在主线程申明一个 Queue,put 再 get 或者用 share memory 的方法。 这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。具体难点在哪有兴趣的读者可以扩展阅读这篇文章

用其他解析器
之前也提到了既然 GIL 只是 CPython 的产物,那么其他解析器是不是更好呢?没错,像 JPython 和 IronPython 这样的解析器由于实现语言的特性,他们不需要 GIL 的帮助。然而由于用了 Java/C# 用于解析器实现,他们也失去了利用社区众多 C 语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。毕竟功能和性能大家在初期都会选择前者,Done is better than perfect

所以没救了么?
当然 Python 社区也在非常努力的不断改进 GIL,甚至是尝试去除 GIL。并在各个小版本中有了不少的进步。有兴趣的读者可以扩展阅读这个 Slide 另一个改进Reworking the GIL

  • 将切换颗粒度从基于 opcode 计数改成基于时间片计数
  • 避免最近一次释放 GIL 锁的线程再次被立即调度
  • 新增线程优先级功能(高优先级线程可以迫使其他线程释放所持有的 GIL 锁)

Python 的编码

在 Python2.x 中,有两种字符串类型:str 和 unicode 类型。str 存 bytes 数据,unicode 类型存 unicode 数据 在 Python3.x 中,也只有两种字符串类型:str 和 bytes 类型。str 类型存 unicode 数据,bytse 类型存 bytes 数据,

python2 的 encode 和 decode

我们把从 Unicode 到字节码 (byte string) 称之为 encode encode : Unicode -> byte string

把从字节码 (byte string) 到 Unicode 码称之为 decode decode: byte string-> Unicode

unicode 是离用户更近的数据,bytes 是离计算机更近的数据。

with(上下文管理)

上下文

处理程序代码的前后语境 简而言之,有一个特殊的语句块,在执行这个语句块之前需要先执行一些准备动作;当语句块执行完成后,需要继续执行一些收尾动作。

用法

上下文管理常用于读写文件及短暂的操作数据库中

with open(r'somefileName') as somefile:
    for line in somefile:
        print line
        # ...more code

with open(r'somefileName') as somefile:
    for line in somefile:
        print line
        # ...more code

这里使用了 with 语句,不管在处理文件过程中是否发生异常,都能保证 with 语句执行完毕后已经关闭了打开的文件句柄。如果使用传统的 try/finally 范式

实现

通过实现两个魔法方法 __enter____exit__ 来实现,just show the code

class Contextor:
    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        """
        exc_type    : 错误类型
        exc_val        : 错误值
        exc_tb        : Traceback
        """
        pass

contextor = Contextor()

with contextor [as var]:
    with_body

class Contextor:
    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        """
        exc_type    : 错误类型
        exc_val        : 错误值
        exc_tb        : Traceback
        """
        pass

contextor = Contextor()

with contextor [as var]:
    with_body

lambda(匿名函数)

lambda x,y : x + y

# lambda 表达式当然也是对象
t = lambda x,y : x + y
print t    # <function <lambda> at 0x7f7c3d02a668>

t(1,2)     # 3

lambda x,y : x + y

# lambda 表达式当然也是对象
t = lambda x,y : x + y
print t    # <function <lambda> at 0x7f7c3d02a668>

t(1,2)     # 3

常用与被 Python 其他高级函数调用,map()filter() etc…

高阶函数

map() 函数

python 内置的一个高阶函数,它接收一个函数 f 和一个 list, 并且把 list 的元素以此传递给函数 f,然后返回一个函数 f 处理完所有 list 元素的列表,如下:

map(lambda x:x**2,[1,2,3,4,5])

# [1, 4, 9, 16, 25]

map(lambda x:x**2,[1,2,3,4,5])

# [1, 4, 9, 16, 25]

reduce() 函数

reduce()函数也是 python 的内置高阶函数,reduce() 函数接收的的参数和 map()类似,一个函数 f,一个 list,但行为和 map() 不同,reduce() 传入的参数 f 必须接受 2 个参数,reduce() 函数还接收第三个参数,作为返回结果的初始值,

第一次调用是把 list 的前两个元素传递给 f, 第二次调用时,就是把前两个 list 元素的计算结果当成第一个参数,list 的第三个元素当成第二个参数,传入 f 进行操作,以此类推,并最终返回结果;

reduce(lambda x,y:x+y,[1,2,3,4,5],10)

# 25

reduce(lambda x,y:x+y,[1,2,3,4,5],10)

# 25

filter() 函数

filter()函数接收一个函数 f 和一个 list, 这个函数 f 的作用是对每个元素进行判断,返回 true 或 false,filter() 根据判断结果自动过滤掉不符合条件的元素,返回由符合条件的元素组成的 list; 例

filter(lambda x:x%2==1,[1,2,3,4,5])    # 留下列表里的所有奇数

# [1, 3, 5]

filter(lambda x:x%2==1,[1,2,3,4,5])    # 留下列表里的所有奇数

# [1, 3, 5]

zip() 函数

zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。 如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同

# 遍历两个list
for i,j in zip([1,2,3],['x','y','z']):
    print i,j

# 1 x
# 2 y
# 3 z

# 将两个list合成一个dict
t = dict(zip([1,2,3],['x','y','z']))
{1: 'x', 2: 'y', 3: 'z'}

# 遍历两个list
for i,j in zip([1,2,3],['x','y','z']):
    print i,j

# 1 x
# 2 y
# 3 z

# 将两个list合成一个dict
t = dict(zip([1,2,3],['x','y','z']))
{1: 'x', 2: 'y', 3: 'z'}

sort() 函数

排序函数, 基本形式 sorted(iterable[, cmp[, key[, reverse]]])

sorted([1,2,-3,-4,5])    # 默认升序
# [-4, -3, 1, 2, 5]

sorted([1,2,-3,-4,5],key=abs)    # 返回一个以数值的绝对值为排序关键词的升序列表
# [1, 2, -3, -4, 5]

sorted([1,2,-3,-4,5],reverse=True)    # 降序
# [5, 2, 1, -3, -4]

# 对字符串和其他复杂数据结构的排序
sorted(['bob', 'about', 'Zoo', 'Credit']    # 对字符串以asc ii 码为关键词排序
# ['Credit', 'Zoo', 'about', 'bob']

sorted(['bob', 'about', 'Zoo', 'Credit',key = str.lower]    # 忽略大小写以字母顺序为关键词排序
# 'about', 'bob', 'Credit', 'Zoo'

L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]    # 以每个排序对象的第二个元素为关键词,降序排列
sorted(L,key = lambda x:x[1],reverse=True)
# [('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]

sorted([1,2,-3,-4,5])    # 默认升序
# [-4, -3, 1, 2, 5]

sorted([1,2,-3,-4,5],key=abs)    # 返回一个以数值的绝对值为排序关键词的升序列表
# [1, 2, -3, -4, 5]

sorted([1,2,-3,-4,5],reverse=True)    # 降序
# [5, 2, 1, -3, -4]

# 对字符串和其他复杂数据结构的排序
sorted(['bob', 'about', 'Zoo', 'Credit']    # 对字符串以asc ii 码为关键词排序
# ['Credit', 'Zoo', 'about', 'bob']

sorted(['bob', 'about', 'Zoo', 'Credit',key = str.lower]    # 忽略大小写以字母顺序为关键词排序
# 'about', 'bob', 'Credit', 'Zoo'

L = [('Bob', 75), ('Adam', 92), ('Bart', 66), ('Lisa', 88)]    # 以每个排序对象的第二个元素为关键词,降序排列
sorted(L,key = lambda x:x[1],reverse=True)
# [('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)]

yield

带有 yield 的函数在 Python 中被称之为 generator(生成器), 是一个可以迭代的对象。

# 这是一个产生斐波那契数列的迭代器
def fab(max): 
    n, a, b = 0, 0, 1 
    while n < max: 
        yield b 
        # print b 
        a, b = b, a + b 
        n = n + 1 

>>> for n in fab(5): 
...     print n 
... 
1 
1 
2 
3 
5

# 这是一个产生斐波那契数列的迭代器
def fab(max): 
    n, a, b = 0, 0, 1 
    while n < max: 
        yield b 
        # print b 
        a, b = b, a + b 
        n = n + 1 

>>> for n in fab(5): 
...     print n 
... 
1 
1 
2 
3 
5

简单地讲,yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator,调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable 对象!在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield

这里的概念就是协程。有关协程 python 中 greenlet 和 gevent 也可以很好的协程, 这里就不展开了

也可以手动调用 fab(5) 的 next()方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程

装饰器

由于函数也是一个对象,而且函数对象可以被赋值给变量,所以,通过变量也能调用该函数。 于是,你就可以在编写一个控制程序,接收某个特定函数作为参数输入,并在这个特定函数运行的前后在做一些你想做的事情,这就是装饰器。

def log(func):
    def wrapper(*args, **kw):
        print 'call %s():' % func.__name__
        result = func(*args, **kw)
        print 'done'
        return result
    return wrapper

@log
def hello():
    print 'hello, world'

hello()

# call hello():
# hello, world
# done

def log(func):
    def wrapper(*args, **kw):
        print 'call %s():' % func.__name__
        result = func(*args, **kw)
        print 'done'
        return result
    return wrapper

@log
def hello():
    print 'hello, world'

hello()

# call hello():
# hello, world
# done

以上的代码非常清楚的展示了装饰器的作用,值得一提的是 *args, **kw 这是 python 的可变参数的接收方式, 这里一定要将接收到的参数传递给装饰器所装饰的函数,即func(*args, **kw)

类装饰器

还记得元类吗?

元类的主要目的就是为了当创建类时能够自动地改变类

当然,不光是元类能完成这个功能,类装饰器也可以。 装饰器不光能接收函数作为输入,类也是一样 以下是用类实现一个单例模式的方法

    def singleton(cls, *args, **kw):  
        instances = {}  
        def _singleton():  
            if cls not in instances:  
                instances[cls] = cls(*args, **kw)    # 以类名作为实例化的标准,每个类名只能被实例化一次  
            return instances[cls]  
        return _singleton  

    @singleton  
    class MyClass(object):  
        def __init__(self, x=0):  
            self.x = x

    def singleton(cls, *args, **kw):  
        instances = {}  
        def _singleton():  
            if cls not in instances:  
                instances[cls] = cls(*args, **kw)    # 以类名作为实例化的标准,每个类名只能被实例化一次  
            return instances[cls]  
        return _singleton  

    @singleton  
    class MyClass(object):  
        def __init__(self, x=0):  
            self.x = x

装饰器与 AOP(面向切面编程)

装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的有插入日志、性能测试、事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能

AOP 就是写代码的时候 把各个模块中需要重复写的抽取出来,弄成一个切面。例如日志,权限。 切面的具体表现就是实现公共方法的类

函数参数传递

  1. 位置传递实例:
   def fun(a,b,c)
     return a+b+c
     
   print(f(1,2,3))

   def fun(a,b,c)
     return a+b+c
     
   print(f(1,2,3))
  1. 关键字传递 关键字 (keyword) 传递是根据每个参数的名字传递参数。关键字并不用遵守位置的对应关系。
      def fun(a,b,c)
        return a+b+c

print(f(1,c=3,b=2))

  1. 参数默认值 在定义函数的时候,使用形如c=10的方式,可以给参数赋予默认值(default)。如果该参数最终没有被传递值,将使用该默认值。 ​```python def f(a,b,c=10):   return a+b+c

print(f(3,2)) print(f(3,2,1))

  1. 参数默认值 在定义函数的时候,使用形如c=10的方式,可以给参数赋予默认值(default)。如果该参数最终没有被传递值,将使用该默认值。 ​```python def f(a,b,c=10):   return a+b+c

print(f(3,2)) print(f(3,2,1))


在第一次调用函数 f 时, 我们并没有足够的值,c 没有被赋值,c 将使用默认值 10. 第二次调用函数的时候,c 被赋值为 1,不再使用默认值。

1. 包裹传递
   在定义函数时,我们有时候并不知道调用的时候会传递多少个参数。这时候,包裹 (packing) 位置参数,或者包裹关键字参数,来进行参数传递,会非常有用。
   下面是包裹位置传递的例子:

​```python
def func(*name):
  print type(name)
  print name


func(1,4,6)
# <type 'tuple'> 
# (1, 4, 6) 

func(5,6,7,1,2,3)
# <type 'tuple'>
# (5, 6, 7, 1, 2, 3)

def func(*name):
  print type(name)
  print name


func(1,4,6)
# <type 'tuple'> 
# (1, 4, 6) 

func(5,6,7,1,2,3)
# <type 'tuple'>
# (5, 6, 7, 1, 2, 3)

两次调用,尽管参数个数不同,都基于同一个 func 定义。在 func 的参数表中,所有的参数被 name 收集,根据位置合并成一个元组 (tuple),这就是包裹位置传递。

为了提醒 Python 参数,name 是包裹位置传递所用的元组名,在定义 func 时,在 name 前加 * 号。

下面是包裹关键字传递的第二个例子:

def func(**dict):
  print type(dict)
  print dict

func((1,9))
func("a":2,"b":1,"c":11)

def func(**dict):
  print type(dict)
  print dict

func((1,9))
func("a":2,"b":1,"c":11)

与上面一个例子类似,dict 是一个字典,收集所有的关键字,传递给函数 func。为了提醒 Python,参数 dict 是包裹关键字���递所用的字典,在 dict 前加 **。

包裹传递的关键在于定义函数时,在相应元组或字典前加 * 或 **。

  1. 解包裹

* 和 **,也可以在调用的时候使用,即解包裹 (unpacking), 下面为例:

def func(a,b,c):
  print a,b,c


args = (1,3,4)
func(*args)

dict = {'a':1,'b':2,'c':3}
func(**dict)

def func(a,b,c):
  print a,b,c


args = (1,3,4)
func(*args)

dict = {'a':1,'b':2,'c':3}
func(**dict)

在这个例子中,所谓的解包裹,就是在传递 tuple 时,让 tuple 的每一个元素对应一个位置参数。在调用 func 时使用 *,是为了提醒 Python:我想要把 args 拆成分散的三个元素,分别传递给 a,b,c。(设想一下在调用 func 时,args 前面没有 * 会是什么后果?)

相应的,也存在对词典的解包裹,使用相同的 func 定义,然后:在传递词典 dict 时,让词典的每个键值对作为一个关键字传递给 func。

参数默认值的继承性

def extendList(val, list=[]):
    list.append(val)
    return list

list1 = extendList(10)
list2 = extendList(123,[])
list3 = extendList('a')

print "list1 = %s" % list1
print "list2 = %s" % list2
print "list3 = %s" % list3

def extendList(val, list=[]):
    list.append(val)
    return list

list1 = extendList(10)
list2 = extendList(123,[])
list3 = extendList('a')

print "list1 = %s" % list1
print "list2 = %s" % list2
print "list3 = %s" % list3

结果为:

list1 = [10, 'a']
list2 = [123]
list3 = [10, 'a']

list1 = [10, 'a']
list2 = [123]
list3 = [10, 'a']

很多人都会误认为 list1=[10],list3=[‘a’], 因为他们以为每次 extendList 被调用时,列表参数的默认值都将被设置为 []. 但实际上的情况是,新的默认列表只在函数被定义的那一刻创建一次

当 extendList 被没有指定特定参数 list 调用时,这组 list 的值随后将被使用。这是因为带有默认参数的表达式在函数被定义的时候被计算,不是在调用的时候被计算。因此 list1 和 list3 是在同一个默认列表上进行操作(计算)的。而 list2 是在一个分离的列表上进行操作(计算)的。(通过传递一个自有的空列表作为列表参数的数值)

闭包与延迟锁定

Pythonic

交换变量

a,b = b,a

a,b = b,a

翻转

a = 'hello, world!'
a[::-1] # !dlrow ,olleh

a = 'hello, world!'
a[::-1] # !dlrow ,olleh

拼接字符串

a = ['hello', 'world']
" ".join(a)     # hello world

a = ['hello', 'world']
" ".join(a)     # hello world

列表去重

a = [1, 1, 1, 2, 3 ,4 ,4, 5]
a = list(set(a))         # [1, 2, 3, 4, 5]

a = [1, 1, 1, 2, 3 ,4 ,4, 5]
a = list(set(a))         # [1, 2, 3, 4, 5]

复制列表

import copy
a = [1,'a',['x']]

# 浅复制
b = copy.copy(a)
b = a[:]
b = list(a)    # 使用工厂函数

# 深复制
b = copy.deepcopy(a)

import copy
a = [1,'a',['x']]

# 浅复制
b = copy.copy(a)
b = a[:]
b = list(a)    # 使用工厂函数

# 深复制
b = copy.deepcopy(a)

列表推导

ood_list = [i for i in xrange(1,101) if i % 2 == 1]

ood_list = [i for i in xrange(1,101) if i % 2 == 1]

当你把推导式的 [] 替换为 () 时,就变为了一个生成器,即一个可迭代的对象。而且这样做可以解决列表推导与 lambda 结合时产生的延迟锁定问题。

字典推导式

# 快速更换k,v
mcase = {'a': 10, 'b': 34}
mcase_frequency = {v: k for k, v in mcase.items()}

# 快速更换k,v
mcase = {'a': 10, 'b': 34}
mcase_frequency = {v: k for k, v in mcase.items()}

读写文件

with open('/path/to/file', 'r') as f: 
    do something...

with open('/path/to/file', 'r') as f: 
    do something...

Python 的自省

自省就是面向对象的语言所写的程序在运行时, 所能知道对象的类型. 简单一句就是运行时能够获得对象的类型. 比如 type(),dir(),getattr(),hasattr(),isinstance().

练习

8 个面试题

单例模式的几种实现

InterviewKeyOfPython