python面向对象

面向对象是一种编程范式。范式,能够认为是一组方法论,编程范式是一组如何组织代码的方法论。主流的编程范式有:PP、IP、FP、LP、OOP、AOP。java

  • PP:面向过程,表明是 C 语言;
  • IP:面向指令,表明是汇编;
  • FP:函数式编程,把世界抽象成一个个的函数,它要求的是无反作用,即相同的输入会产生相同的输出。所以它是一种无反作用的过程;
  • LP:面向逻辑的编程,把世界抽象成与或非,表明是 prolog;
  • AOP:面向方面,它用来解决一类问题,Python 中的装饰器就是 AOP 的思想。表明是 SQL 语句;

OOP 就是今天的主角,它的世界观为:node

  • 世界由对象组成;
  • 对象具备运动规律和内部状态;
  • 对象之间能够相互做用。

以目前人类的认知来讲,OOP 是最接近真实世界的编程范式。python

设计大型软件时,面向对象比面向过程更容易实现。程序由指令加数据组成,代码能够选择以指令为核心或以数据为核心进行编写:程序员

  • 以指令为核心:围绕“正在发生什么”进行编写。这是面向过程编程,程序具备一系列线性步骤,主体思想是代码做用于数据;
  • 以数据为核心:围绕“将影响谁”进行编写。这就是面向对象编程(OOP),围绕数据及为数据严格定义的接口来组织程序,用数据控制对代码的访问。

类和对象是面向对象中的两个重要概念。算法

  • 类:是对事物的抽象,如人类、球类等。类有静态属性和静态方法,类没法访问动态属性和动态方法;
  • 对象:是类的一个实例,如足球、篮球。对象有动态属性和动态方法,对象能够访问静态属性和静态方法;

面向对象的特性:shell

  1. 惟一性:对象是惟一的,不存在两个相同的对象,除非他们是同一个对象。就像我做为一个对象,世界上只有一个我;
  2. 分类性:对象是可分类的,好比动物、食物。

实例说明:球类能够对球的特征和行为进行抽象,而后能够实例化一个真实的球实体出来。好比咱们对人类进行实例化,能够实例化出张3、李4、王五等。数据库

全部编程语言的最终目的都是提供一种抽象方法。在机器模型("解空间"或“方案空间”)与实际解决的问题模型(“问题空间”)之间,程序员必须创建一种联系。面向对象是将问题空间中的元素以及它们在解空间中的表示物抽象为对象,并容许经过问题来描述问题而不是方案。能够把实例想象成一种新型变量,它保存着数据,但能够对自身的数据执行操做。编程

类型由状态集合(数据)和转换这些状态的操做集合组成。数组

类是抽象的概念,实例才是具体的。可是要先设计类,才能完成实例化。类是定一个多个同一类型对象共享的结构和行为(数据和代码)。就像 list 就是一种类型,使用 list. 而后就可tab补全一堆方法,可是咱们使用 list.pop() 是会报错的,由于它是一个抽象的概念。缓存

类内部包含数据和方法这两个核心,两者都是类成员。其中数据被称为成员变量或实例变量;而方法被称为成员方法,它被用来操纵类中的代码,用于定义如何使用这些成员变量。所以一个类的行为和接口是经过方法来定义的。

方法和变量就是数据和代码,若是是私有的变量和方法,只可以在实例内部使用。若是是公共的,能够在实例外部调用。

在面对对象的程序设计中,全部的东西都是对象,咱们尽量把全部的东西都设计为对象。程序自己也就是一堆对象的集合,若是要有对象,事先要有类,若是没有类,用户就要自定义类,因此用户本身写的类就成为自定义类型。也就是说若是程序里面有类,那咱们就能够直接建立实例,若是没有那咱们就要建立,而后实例化。程序的运行过程就是这些对象彼此之间互相操做的过程,经过消息传递,各对象知道本身该作什么。若是传递消息?每一个对象都有调用接口,也就是方法,咱们向方法传递一个参数就表示咱们调用了该对象的方法,经过参数传递消息。从这个角度来说,消息就是调用请求。

每一个对象都有本身的存储空间,并可容纳其余对象。好比咱们定义列表l1,里面有三个元素,那l1是对象,三个元素也是对象。经过封装现有对象,咱们能够制做新型对象。每一个对象都属于某一个类型,类型即为类,对象是类的实例。类的一个重要特性为“能发什么样的消息给它”,同一个类的全部对象都能接受相同的消息。类的消息接口就是它提供的方法,咱们使用l1.pop(),就至关于给类发送了消息。而不一样的类消息的接口并不相同,就像咱们不能对字串类型使用pop方法同样。

定义一个类后,能够根据须要实例化出多个对象,如何利用对象完成真正有用的工做?必须有一种办法能向对象发出请求,令其作一些事情。这就是所谓的方法,这些方法加起来就表现为该类的接口。所以每一个对象仅能接受特定的请求,对象的“类型”或“类”规定了它的接口类型。

数据保存在变量中,变量就是所谓的属性,方法就是函数。

类间的关系:

  • 依赖("uses-a"):一个类的方法操纵另外一个类的对象;
  • 聚合("has-a"):类 A 的对象包含类 B 的对象;
  • 继承("is-a"):描述特殊与通常关系。

面对对象的特征:

  • 封装(Encapsulation):隐藏实现方案细节,并将代码及其处理的数据绑定在一块儿的一种编程机制,用于保证程序和数据不受外部干扰且不会被误用。类把须要的变量和函数组合在一块儿,这种包含称为封装。好比 list.pop() 实现的细节咱们并不知道,这就是一种封装;
  • 继承(Inheritance):一个对象得到另外一个对象属性的过程,用于实现按层分类的概念。一个深度继承的子类继承了类层次中它的每一个祖先的全部属性,所以便有了超类、基类、父类(都是上级类)以及子类、派生类(继承而来);
  • 多态(Polymorphism):容许一个接口被多个通用的类动做使用的特性,具体使用哪一个动做于应用场合相关。一个接口多种方法。意思是,一样是 x+y,若是 xy 都是数字,那就是从数学运算;若是 xy 是字串,那就是字串链接;若是是列表,则是列表链接,这就是一个接口多种方法。用于为一组相关的动做设计一个通用的接口,以下降程序复杂性。

在几乎全部支持面向对象的语言中,都有 class 关键字,而且这个关键字和面向对象息息相关。而在 Python 中,经过 class 关键字来定义一个类。

咱们定义了一个类以后,只要在程序中执行了class class_name,就会在内存中生成以这个类名被引用的对象。可是类中的代码并不会真正执行,只有在实例化时才会被执行。里面的方法也不会执行,只有对实例执行方法时才会执行。类是对象,类实例化出来的实例也是对象,叫实例对象。所以类包含了类对象和实例对象,类对象是能够调用的对象,而实例对象只能调用实例中的方法。

>>> type(list)
<type 'type'>
>>> l1 = [1, 2, 3]
>>> type(l1)
<type 'list'>
复制代码

list 是类,l1 是类实例化后的对象。

实例化

建立对象的过程称之为实例化。当一个对象被建立后,包含三个方面的特性:对象句柄、属性和方法。句柄用于区分不一样的对象,对象的属性和方法与类中的成员变量和成员函数对应。

定义一个最简单的类:

>>> class TestClass():
...   pass
... 
>>> type(TestClass)
<type 'classobj'> # 类对象
复制代码

调用这个类,让其实例化一个对象:

>>> obj1 = TestClass() # 这就是实例化的过程
>>> type(obj1)
<type 'instance'> # 这是一个实例
复制代码

经过 obj1 = TestClass() 实例化了一个对象,之因此在类名后加上括号表示执行这个类中的构造器,也就是类中的 __init__ 方法。其实就和函数名后面加上括号表示执行这个函数是同样的道理。

从上面能够看出实例初始化是经过调用类来建立实例,语法为:

instance = ClassName(args…)
复制代码

Python 中,class 语句相似 def,是可执行代码,直到运行 class 语句后类才会存在:

>>> class FirstClass: # 类名
        spam = 30 # 类数据属性
        def display(self): # 类方法,属于可调用的属性
            print self.spam

>>> x = FirstClass() # 建立类实例,实例化
>>> x.display() # 方法调用
复制代码

class 语句中,任何赋值语句都会建立类属性,每一个实例对象都会继承类的属性并得到本身的名称空间。

>>> ins1 = FirstClass()
>>> ins1.
ins1.__class__   ins1.__doc__     ins1.__module__  ins1.display(    ins1.spam

# 这个类就出现了全部的方法,能够看到spam是属性
复制代码

封装

封装是面对对象的三大特性之一。在了解封装以前,咱们必须知道什么是 self。

self是啥

经过下面的例子就知道 self 是啥了。

class Foo(object):
    def fun1(self, arg1):
        print(arg1, self)

i1 = Foo()
print(i1)
i1.fun1('hehe')
复制代码

执行结果:

<__main__.Foo object at 0x00000000006C32B0>
hehe <__main__.Foo object at 0x00000000006C32B0>
复制代码

能够看出 i1 和 self 是同一个东西,由此 self 就是实例化对象后的对象自身,也就是 i1。类只有一个,可是实例化的对象能够有无数个,不一样的对象的 self 天然都不相同。

self 是一个形式参数,python 内部自动传递。

在了解了什么是 self 以后,如今就能够聊聊封装了。看下面的例子:

class Foo(object):
    def fetch(self, start):
        print(start)

    def add(self, start):
        print(start)

    def delete(self, start):
        print(start)
复制代码

上面的代码中,一样的参数 start 被传递到了三个函数中,这样就显得很累赘,可否不须要这么麻烦呢?确定是能够的。以下:

class Foo(object):
    def fetch(self):
        print(self.start)

    def add(self):
        print(self.start)

    def delete(self):
        print(self.start)

obj1 = Foo()
obj1.start = 'hehe'
obj1.fetch()
复制代码

修改后三个函数再也不接受参数,这就达到了咱们的需求。因为 self 就是对象自己,所以 self.start 就是咱们传递的“hehe”,这就是类的封装。

经过在对象中封装数据,而后在类中经过 self 进行获取。这是函数式编程没法作到的。这只是类封装的一种方式,也是一种非主流的方式,下面将会提到主流的方式。

构造器

构造器就是所谓 __init__,它是类的内置方法。建立实例时,Python 会自动调用类中的 __init__ 方法。

class Foo(object):
    def __init__(self):
        print('init')

Foo()
复制代码

执行结果:

init
复制代码

能够看到,咱们只要在类名的后面加上括号,就会自动执行类中的 __init__ 函数。经过 __init__ 的这种特性,咱们就能够实现主流的封装方式。

咱们能够看到 __init__ 中并无 return 语句,可是类初始化后的返回值却并不为空,所以,实例化一个对象时,还会执行其余的方法。咱们能够得出结论:__init__ 不是建立对象,它作的只是初始化对象。

实例化一个对象的过程为:

  1. 建立对象;
  2. 对象做为 self 参数传递给 __init__
  3. 返回 self。

以上就是一个对象建立的过程,事实上这个过程咱们是能够手动控制的。

class Foo(object):
    def __init__(self):
        self.start = 'hehe'

    def fetch(self):
        print(self.start)

    def add(self):
        print(self.start)

    def delete(self):
        print(self.start)

obj1 = Foo()
obj1.fetch()
复制代码

这种方式就比较主流了,当咱们要封装多个变量时,能够经过向 __init__ 函数中传递多个参数实现。

class Foo(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def fun1(self):
        print('姓名:{},年龄:{}'.format(self.name, self.age))


obj1 = Foo('小红', 7)
obj2 = Foo('小明', 23)
obj1.fun1()
obj2.fun1()
复制代码

执行结果:

姓名:小红,年龄:7
姓名:小明,年龄:23
复制代码

__init__ 方法被称为构造器,若是类中没有定义 __init__ 方法,实例建立之初仅是一个简单的名称空间。类的 __varname__ 这种方法会被 python 解释器在某些场景下自动调用,就向a+b实际上调用的是 a.__add__(b);l1 = ['abc', 'xyz'] 其实是调用 list.__init__()

构造函数的做用就是不须要咱们手动调用类中的属性或方法,若是想要在实例化成对象的时候执行,就能够将操做写入到 __init__ 方法下。

析构器

析构器又称为解构器,定义的是一个实例销毁时的操做。也就是当使用 del() 函数删除这么一个类时,它会自动调用这个类中的 __del__。可是通常而言,解释器会自动销毁变量的,所以大多状况下,析构函数都无需重载,可是构造器则不一样,它是实现实例变量的一种重要接口。

析构函数就是用于释放对象占用的资源,python 提供的析构函数就是 __del__()__del__() 也是可选的,若是不提供,python 会在后台提供默认析构函数。

析构器会在脚本退出以前执行,咱们能够用它来关闭文件:

class People(object):
    color = 'yellow'
    __age = 30

    def __init__(self,x):
        print "Init..."
        self.fd = open('/etc/passwd')

    def __del__(self):
        print 'Del...'
        self.fd.close()

ren = People('white')
print 'Main end' # 经过这个判断__del__是否在脚本语句执行完毕后执行
复制代码

能够看出是在脚本退出以前执行的:

[root@node1 python]# python c3.py 
Init...
Main end
Del...
复制代码

下面是一个析构器的示例:

class Animal:
  name = 'Someone' # 数据属性(成员变量)
  def __init__(self,voice='hi'): # 重载构造函数
    self.voice = voice # voice有默认值
  def __del__(self): # 这个del就是析构函数,可是它没有起到任何做用,由于pass了
    pass
  def saysomething(self): # 方法属性(成员函数)
    print self.voice

>>> tom = Animal()
>>> tom.saysomething()
hi # 默认值为hi
>>> jerry = Animal('Hello!')
>>> jerry.saysomething()
Hello!
复制代码

例二:

>>> class Person:
...     def __init__(self,name,age): # 定义一个构造器
...         print 'hehe'
...         self.Name = name
...         self.Age = age
...     def test(self):
...         print self.Name,self.Age
...     def __del__(self): # 定义解构器
...         print 'delete'
...
>>> p = Person('Tom',23)
hehe
>>> del(p) # 删除实例时当即调用解构器
delete
复制代码

做用域

函数是做用域的最小单元,那么在类中有何表现呢?

class E:
    NAME = 'E' # 类的直接下级做用域,叫作类变量

    def __init__(self, name):
        self.name = name # 关联到对象的变量,叫作实例变量

>>> e = E('e')
>>> e.NAME
Out[4]: 'E'
>>> E.NAME
Out[5]: 'E'
复制代码

从上面能够看出,类变量对类和实例均可见。

>>> E.name
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-6-b6daf181be33>", line 1, in <module>
    E.name
AttributeError: type object 'E' has no attribute 'name'
复制代码

能够看到,实例变量对实例化后的对象可见,但对类自己并不可见。

>>> e2 = E('e2')
>>> e2.NAME
Out[8]: 'E'
复制代码

能够看到,全部实例共享类变量。可是,当其中一个实例修改了类变量呢?

>>> e2.NAME = 'e2'
>>> e.NAME
Out[10]: 'E'
复制代码

既然共享了,为何其中一个实例修改后不会影响到其余实例呢?实例变量究竟是不是共享的呢?咱们再看一个例子。

>>> e.xxx = 1 # 能够给对象任意增长属性
>>> e2.xxx
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-13-8ca2718f6555>", line 1, in <module>
    e2.xxx
AttributeError: 'E' object has no attribute 'xxx'
复制代码

之因此出现这样的状况,是由于 Python 可动态的给对象增减属性。当给实例的类变量增长属性时,至关于动态的给这个实例增长了一个属性,覆盖了类变量。由于,类变量是共享的这句话并无错。

咱们继续往下看:

>>> E.NAME = 'hehe' # 直接修改类变量
>>> e.NAME
Out[15]: 'hehe'
>>> e2.NAME
Out[16]: 'e2'
复制代码

不要感到慌张和迷茫,这里刚好说明以前的说法都是正确的。由于 e.NAME 并无修改,由于使用的仍然是类变量,当类变量修改了,经过 e 去访问时确定也会发生变化。而 e2 因为以前修改了,由于这个类变量被覆盖了,变成了这个对象的私有属性了,所以不受类变量的影响。

所以,始终都要牢记 Python 中的一大准则,赋值即建立

属性的查找顺序

事实上经过 e.NAME 访问,至关于 e.__class__.NAME。而 e.NAME = 1 至关于 e.__dict__['NAME'] = 1。虽然如此,可是会发生下面这样的状况。

>>> e.NAME
Out[6]: 'E'
>>> e.__dict__['NAME'] = 1
>>> e.NAME
Out[8]: 1
>>> e.__class__.NAME
Out[9]: 'E'
复制代码

经过 e.NAME 和 e.__class__.NAME 访问的结果却不同,这是为何呢?这就涉及到属性的查找顺序了:

__dict__ -> __class__
复制代码

因为在 __dict__ 中能够找到 NAME,因此就直接返回了,而不会在 __class__ 中继续找。

__class__ 是实例对类的一个引用。所以 e.__class__.NAME 这个值就是类中 NAME 的值,它不会改变。而咱们修改实例的属性,则是添加到实例的 __dict__ 中。

装饰器装饰类

给类加装饰器就是动态的给类增长一些属性和方法。

看下面的例子:

def set_name(cls, name):
    cls.NAME = name
    return cls

class F:
    pass

>>> F1 = set_name(F, 'F')
>>> F1.NAME
Out[16]: 'F'
复制代码

事实上 set_name 就至关于一个装饰器。那咱们可使用装饰器的语法将其重写一下:

def set_name(name):
    def wrap(cls):
        cls.NAME = name
        return cls
    return wrap
    
@set_name('G')
class G:
    pass
 
>>> G.NAME
Out[19]: 'G'
复制代码

结果证实是没有问题的,其实使用装饰器语法就至关于:

G = set_name('G')(F) # F 是前面定义的类
复制代码

还能够经过装饰器给类添加方法:

def print_name(cls):
    def get_name(sel): # 必须传递一个参数给它,否则不能经过实例来调用
        return cls.__name__
    cls.__get_name__ = get_name
    return cls

@print_name
class H:
    pass

>>> h = H()
>>> h.__get_name__()
Out[24]: 'H'
复制代码

只不过类装饰器一般用于给类增长属性的,而增长方法则有更好的方式。

属性和方法

类中的变量称为属性、函数称为方法。它们又有静态属性、静态方法、动态属性、动态方法、类方法等之分。

方法的定义都是类级的,可是有的方法使用实例调用,用的方法经过类调用。

实例方法和属性

实例方法和属性都是与 self 相关的,所以只能经过实例进行访问。实例方法的第一个参数是实例名,默认便是如此。因为类根本不知道实例(self)是什么(由于尚未实例化),所以不能经过类直接实例方法和实例属性。

class Foo:
    def __init__(self, name):
        self.name = name # 实例属性

    def f1(self): # 实例方法
        print('f1')
复制代码

类方法和属性

类属性前面提到过了,定义在类做用域下的变量就是类属性。它能够经过类和实例直接访问。

类方法相似于静态方法,它能够经过类直接访问。与静态方法的区别在于,它能够获取当前类名。第一个参数为类自己的方法叫作类方法。类方法能够经过实例进行调用,可是第一个参数依然是类自己。

class Foo:
 @classmethod # 修饰为类方法
    def f2(cls): # 必须接受一个参数
复制代码

类方法必须接受一个参数,它是由类自动传递的,它的值为当前类名。也就是说,经过 classmethod 装饰器会将自动传递给方法的第一个参数(以前为实例名)改成类名。而被装饰的方法的参数名和 self 同样,不强制要求为 cls,只是习惯这么写而已。

类方法的最大的用处就是无需实例化便可使用。

静态方法

不一样于实例方法和类方法的必须拥有一个参数,静态方法不须要任何参数。

class Foo:
 @staticmethod # 装饰为静态方法
    def f1(): # 没有任何参数
        print('static method')
复制代码

被 staticmethod 装饰器装饰后,访问的时候不会自动传递第一个参数。静态方法和类方法同样,能够同时被类和实例访问。

class Foo:
 @staticmethod
    def f1():
        print('static method')

    def f2(): # 能够被类访问
        print('hehe')
复制代码

f1 和 f2 的区别在于,f2 没法经过实例访问。

私有方法和属性

以双下划线开头,且非双下划线结尾的函数/变量就是私有方法/属性,在类的外部没法访问。咱们能够得出结论:全部以双下划线开头,且非双下划线结尾的成员都是私有成员。

经过下面的例子能够看到它的用处。

class Door:
    def __init__(self, number, status):
        self.number = number
        self.__status = status

    def open(self):
        self.__status = 'opening'
    
    def close(self):
        self.__status = 'closed'

>>> door = Door(1, 'closed')
>>> door.__status # 直接访问会报错
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-35-d55234f04e7f>", line 1, in <module>
    door.__status
AttributeError: 'Door' object has no attribute '__status'
复制代码

可是能够直接修改它的属性:

>>> door.__status
Out[37]: 'hehe'
>>> door.open()
>>> door.__status
Out[39]: 'hehe'
复制代码

虽然赋值即定义,可是仍是有些没法接受。在外面应该不能修改它才对。

私有属性虽然没法直接访问,可是并不绝对,Python 提供了访问它的方法。

_类名 + 私有属性
复制代码

好比这么访问:

door._Door__status
复制代码

所以,严格的说,Python 中没有真正的私有成员。

咱们能够经过这个方式修改私有属性:

door._Door__status = 'hehe'
复制代码

但除非真的有必要,而且清楚的知道会有什么后果,不然不要这么干。

类中还有以单下划线开头的属性,这是一种惯用法,标记它为私有属性,可是解释器并非将其当作私有属性处理。

property

property 装饰器会把一个仅有 self 参数的函数变成一个属性,属性的值为方法的返回值。

class Foo:
 @property
    def f1(self):
        print('f1')


obj = Foo()
obj.f1 # 不须要加括号了
复制代码

结合以前的装饰器,理解下面的例子:

def f(fn):
 @property
    def abc(self):
        print('abc')
        fn(self)
    return abc

class A:
 @f
    def t1(self):
        print('t1')

a = A()
a.t1
复制代码

经过 property 能够将方法修饰为字段,可是属性的值能够修改,而使用 property 修饰的函数的返回值却没法修改,由于它没法接受参数。从这里看,property 好像就只能在调用的时候少些两个括号而已,可是它并无这么简单。

class Foo:
    def __init__(self, name):
        self.name = name

    @property
    def f1(self):
        return ('f1')

>>> obj = Foo('hello')
>>> obj.name
hello
>>> obj.name = 'hehe' # 能够修改
>>> obj.name
Out[6]: 'hehe'
>>> obj.f1 = 'xxx' # 这么确定报错
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-7-6e871c456103>", line 1, in <module>
    obj.f1 = 'xxx'
AttributeError: can't set attribute
复制代码

因为 property 装饰器限制了函数不能接收参数,所以不能给它传参,也就难以修改里面的值了。可是,若是想要修改 property 函数中的值也是能够的,这就用到了它的第二个功能了。

class Foo:
    def __init__(self, name):
        self.name = name

 @property
    def f1(self):
        return self.name

 @f1.setter # f1 是函数名,必须和 property 修饰的函数一致
    def f1(self, value):
        self.name = value

obj = Foo('hello')
print(obj.f1) # 结果是 hello
obj.f1 = 'xxx'
print(obj.f1) # 结果是 xxx
复制代码

所以,property setter 装饰器能够把一个方法转化为对此赋值,但此方法有必定要求:

  1. 同名;
  2. 必须接收 self 和 value 两个参数,value 为所赋的值。

有了 property setter 装饰器以后,被 property 装饰的函数就能够接收参数了。相应的,咱们能够经过这个参数来达到咱们的一些目的。

除了 setter 以外,还有一个 deleter 的装饰器,这也是 property 的第三个功能。当删除 property 装饰器装饰的函数(因为被 property 装饰,所以函数变成属性)时,会调用 deleter 装饰的函数。

class Foo:
    def __init__(self, name):
        self.name = name
    
 @property
    def f1(self):
        return self.name
    
 @f1.deleter
    def f1(self):
        print('hehe')

>>> obj = Foo('f1')
>>> del obj.f1
hehe
复制代码

事实上 del 是不能删除方法的,可是因为函数被 property 装饰后会变成属性,所以能够被删除。

能够看出 property 很强大,它不只能够做为装饰器,还能够引伸出来两个装饰器,只不过须要定义三个函数。其实它彻底能够定义的更简单,而达到相同的效果。下面就是将以前定义的三个函数经过一行代码取代。

f1 = property(lambda self: self.name, lambda self, value: self.name = value, lambda: self: print('hehe'))
复制代码

f1 这个函数接收三个函数做为参数,第一个函数是必须的,后面两个能够省略。正好对应 property, f1.setter, f1.deleter 装饰的三个函数。

下面定义了两个类,第二个类比第一个类中多了一个继承对象 object,其余的都同样,而且调用方式也相同。咱们看看结果,而后进行对比:

class test1:
    def __init__(self,flag):
        self.__pravite = flag
    
 @property
    def show(self):
        return self.__pravite

class test2(object):
    def __init__(self,flag):
        self.__pravite = flag
    
 @property
    def show(self):
        return self.__pravite

t1 = test1('t1')
print t1.show
t1.show = 'x1'
print t1.show

t2 = test2('t2')
print t2.show
t2.show = 'x2'
print t2.show
复制代码

一样进行修改,而后进行访问。如下是执行结果:

t1
x1
t2
Traceback (most recent call last):
  File "E:\workspace\test\main\t1.py", line 48, in <module>
    t2.show = 'x2'
AttributeError: can't set attribute
复制代码

能够看出,若是不继承 object,不使用 @xxx.setter 装饰器,私有属性是能够直接修改的。可是若是继承了 object,那就必须使用不使用 @xxx.setter 装饰器了,否则没法修改。

继承

继承是面对对象的重要特性之一,前面提到的都属于“封装”。继承是相对两个类而言的父子关系,子类继承了父类的全部公有属性和方法,继承实现了代码的重用。Python 容许多继承,也就是说一个类能够继承多个父类,这是其余面向对象编程语言(C#, Java 等)所不具有的。多继承时,哪一个类放在前面,哪一个类就最终继承。也就是说两个类中都有相同的属性或方法时,写在前面的类是子类继承的类。

子类也称为派生类,父类也称为基类。

派生类能够继承父类中的全部内容,当派生类和父类中同时存在相同的内容时,优先使用本身的。也就是说当实例化子类时,若是执行父类中 self.abc 代码,首先会在当前类(也就是子类中)查找 abc,由于实例化的是子类而非父类,只要找不到才会去父类中找。若是是多继承,也是一级一级的往上找。

若是子类中没有定义初始化方法,实例化时会执行父类中的初始化方法。而若是子类中存在,就不会执行父类的。若是想要执行父类中的初始化方法,可使用 super 函数,下面会讲到。

继承描述了基类的属性如何“遗传”给派生类。

  • 子类能够继承它的基类的任何属性,包括数据和方法;
  • 一个未指定基类的类,其默认有一个名为 object 的基类;
  • Python 容许多重继承,也就是说能够有多个并行的父类。

建立子类时,只须要在类名后跟一个或从其中派生的父类:

class SubClassName(ParentClass1[,ParentClass2,…])
复制代码

凡是公有的都能继承,凡是私有的都不能继承。由于私有的继承后会更名,这就会致使找不到(改下名后就能访问到了)。原来是什么,继承过来仍是什么。类变量继承过来仍是类变量。

当咱们对一个子类进行实例化时,这个实例化对象也是父类的实例化对象,好比:

class A:
    pass
class B(A):
    pass

>>> a = B()
>>> isinstance(a, A)
Out[6]: True
复制代码

方法重写

当子类和父类中都拥有相同的成员(包括属性和方法)时,子类会使用本身的而不是父类的,这是继承的规则。可是这样会致使子类没法使用父类中的同名方法。为了解决这一问题,Python 中引入了 super 类。

也就是说只有要进行方法重写的时候才会使用 super。

super 的使用方法:

super() -> same as super(__class__, <first argument>)
super(type) -> unbound super object
super(type, obj) -> bound super object; requires isinstance(obj, type)
super(type, type2) -> bound super object; requires issubclass(type2, type)
复制代码

能够看出 super 能够不接受参数,也能够最多接收两个参数。当 super 不带参数时,它的意义和 super(子类名, self) 相同。

# 定义一个父类
class Base:
    def print(self):
        print('Base.print')

# 子类中也有 print 方法,很显然会覆盖父类的 print 方法
class Sub(Base):
    def print(self):
        print('Sub.print')
    def foo(self):
        # 经过 super 调用父类的同名方法,如下两种写法做用相同
        super(Sub, self).print()
        super().print()
        
>>> Sub().foo() # 明显两种写法做用相同
Base.print
Base.print
复制代码

这是针对实例方法的,它一样能够针对类方法:

class Base:
 @classmethod
    def cls_print(cls):
        print('Base.cls_print')

class Sub(Base):
 @classmethod
    def cls_print(cls):
        print('Sub.cls_print')

 @classmethod
    def cls_foo(cls):
        # 因为是类方法,所以能够直接经过父类进行访问
        Base.cls_print()
        # 能够针对类方法
        super().cls_print()
        # 这种会报错
        #super(Base, cls).cls_print()
        
>>> Sub().cls_foo()
Base.cls_print
Base.cls_print
复制代码

咱们能够得出结论,90% 的状况下 super 不须要带参数。下面介绍的就是那 10%。

class Base:
    def print(self):
        print('Base.print')

class Sub(Base):
    def print(self):
        print('Sub.print')

# 咱们定义了子类,可是继承的是 Sub,父子孙中都拥有同名的方法
class Subsub(Sub):
    def print(self):
        print('Subsub.print')

    def foo(self):
        # 当要调用父类的父类中的同名方法时,super 就要带参数了
        super(Sub, self).print()

>>> Subsub().foo()
Base.print
复制代码

回到一开始列出的 super 的使用方法,能够得出结论:super 代理 TYPE 的父类方法,而且使用 obj 绑定。第一个参数是指调用谁的直接父类,第二个参数是指调用时,传递给方法的第一个参数。

带参数的__init__

看一个示例:

class Base:
    def __init__(self, a, b):
        # 定义两个私有属性
        self.__a = a
        self.__b = b

    def sum(self):
        return self.__a + self.__b

class Sub(Base):
    def __init__(self, a, b, c):
        self.c = c
        self.__a = a
        self.__b = b

>>> Sub(1, 2, 3).sum()
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-3-e982c4a04055>", line 1, in <module>
    Sub(1, 2, 3).sum()
  File "<ipython-input-2-7fde2b10dc1f>", line 7, in sum
    return self.__a + self.__b
AttributeError: 'Sub' object has no attribute '_Base__a'
复制代码

报错了。若是初始化函数中的属性不是私有的话,是不会报错的。可是私有属性必定会报错,由于私有属性是没法继承的。为了让它不报错,就能够用到 super 了。

class Base:
    def __init__(self, a, b):
        self.__a = a
        self.__b = b

    def sum(self):
        return self.__a + self.__b

class Sub(Base):
    def __init__(self, a, b, c):
        self.c = c
        # 直接调用父类的初始化方法
        super().__init__(a, b)
     
>>> Sub(1, 2, 3).sum()
Out[6]: 3
复制代码

若是继承父类,那么定义在父类 __init__ 中的相同的属性会覆盖子类中的。

若是父类含有一个带参数的初始化方法的时候,子类必定须要一个初始化方法,而且在初始化方法中调用父类的初始化方法。

Python2 中还能经过下面的方法继承父类中同名的方法,可是很显然 super 彻底能够替代它,由于 super 能够指定继承哪个父类中同名的成员。

父类.__init__(self[, arg1, arg2...])
复制代码

super获取类变量

前面经过 super 获取的是方法,此次获取的是变量:

class Base:
    NAME = 'BASE'


class Sub(Base):
    NAME = 'SUB'

    def print(self):
        print(self.NAME)
        print(super(Sub, Sub).NAME)
        
>>> Sub().print()
SUB
BASE
复制代码

实例变量是没法获取的,由于父类并无实例化,实例变量是不存在的,所以确定是没法继承的。

还有一种状况:

class Base:
    NAME = 'BASE'

class Sub(Base):
    NAME = 'SUB'

    def print(self):
        print(self.NAME)
        print(super(Sub, Sub).NAME)
        print(Base.NAME)
复制代码

最后两行在单继承环境下没有区别,可是在多级继承时存在区别。

多继承

python 支持多继承,而 Python3 中的全部类都会继承 object 这个类,所以下面这三种写法意义相同:

class A:
    pass
    
class A(object):
    pass

class A():
    pass
复制代码

咱们能够看到 object 中包含的全部方法和属性:

In [4]: dir(A())
Out[4]: 
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']
复制代码

因此,咱们只要定义一个类,天生就拥有这么多的属性和方法。

多继承的写法:

class Sub(Base, Base2) # 继承列表中有多个类就表示多继承
复制代码

多继承会把继承列表中的全部公有成员都继承过来,当有同名成员时,就会有一个继承顺序了。

多继承的查找顺序

在讨论类继承顺序以前,咱们首先要了解 MRO,它的本意就是方法查找顺序。它要知足几个条件:

  • 本地优先:本身定义或重写的方法优先。本地没有的,按照继承列表,从左往右查找;
  • 单调性:全部子类,也要知足查找顺序。也就是说 A 继承 B C,A 会先找 B 再找 C。可是在 A 查找以前,B 若是有多个继承,那么它先得按查找顺序查找。

若是定义一个多继承的类,若是不能知足 MRO 的话,会抛出 MRO 的异常。

class A:
    pass

class E(A):
    pass

class F(A, E):
    pass

>>> F()
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-9-491a467e42f0>", line 7, in <module>
    class F(A, E):
TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, E # 抛出 MRO 异常,缘由下面讲
复制代码

MRO 是能够看到的,由于类中存在这个属性。

>>> A.__mro__
Out[10]: (__main__.A, object)
>>> E.__mro__
Out[11]: (__main__.E, __main__.A, object)

# 再定义一个 G
class G(E, A):
    pass

>>> G.__mro__
Out[17]: (__main__.G, __main__.E, __main__.A, object)
复制代码

能够看到 G 的查找顺序是 G -> E -> A -> object,既能知足 A 的顺序,也能知足 E 的顺序,因此咱们说 G 的定义知足了它们 MRO 的单调性。可是若是 F 可以定义的话,它的查找顺序是 F -> A -> E -> object,很显然既知足不了 A,也不能知足 E,所以就会抛出 MRO 异常。

Python 和其余语言经过 c3 算法检测类是否知足了 MRO 的两个原则,算法的解释是:

class B(O) -> [B, O] # 最终查找顺序要是这样 class B(A1, A2, ..., An) -> [B] + merge(mro(A1), mro(A2), ..., mro(An), [A1, A2, ..., An, O]) 复制代码

它实际上会使用递归,结果相似于第一行的列表(正确的 MRO)为退出条件。

merge 有四个步骤:

  1. 遍历列表。它会先求出 A1 到 An 全部类的 MRO,它会造成一个列表,merge 的参数也就是这个列表;
  2. 看一个列表的首元素,它存在两种状况:
    • 它在其余列表中也是首元素;
    • 它在其余列表中不存在;
  3. 若是首元素知足上面两种状况中的一种,那么会将其从列表中移除,合并到 MRO;
  4. 不知足的话,抛出异常。不然不断循环,直到列表为空。

大体过程是这样的(其中 G, E, A 都是一个类,O 表示 object,最后的 [E, A, O] 表示继承的顺序):

mro(G) -> [G] + merge(mro[E], mro[A], [E, A, O])
	-> [G] + merge([E, A, O], [A, O], [E, A, O]) # E 在全部列表的首部,所以拿出来
	-> [G, E] + merge([A, O], [A, O], [A, O]) 
	-> [G, E, A] + merge([O], [O], [O])
	-> [G, E, A, O] # 最终就只剩下这个列表
复制代码

当定义一个类的时候,解释器会指定 c3 算法来确认 MRO,若是 c3 算法抛出异常,此类不能定义。

咱们应该尽可能避免多继承。继承多了很容易就搞懵了,而且 python 边解释便运行,若是不调用类,它也不会知道类的定义有没有错误。当咱们觉得继承是对的时候,可是某天忽然报错了,可能都不知道是什么缘由形成的。

如下就是 MRO 的图形展现。

image_1b0jrmk4ocdh1j8nlem40gjqp9.png-14.7kB

如上图所示,A 同时继承 B 和 C,B 继承 D,C 继承 E。当 A 中没有对应的方法时,会先在 B 找,找不到会找 D 而不会找 C,最后找 E。

image_1b0jruq3t14j77rd13db1hujmjam.png-16kB

和上图基本相同,就多了个 D 和 E 同时继承 F。这里 D 找不到时不会找F而是会找 C,F 是最后一个找的。

也就是说若是没有共同继承的基类,会一直往上找。而共同继承的基类最后一个找。

可是还有下面这种状况:

image_1b0k16v3k20fgch1v0k49o5q52n.png-14.4kB

class D:
    def xxx(self):
        print('xxx')
        self.f()


class C:
    def f(self):
        print('C')


class B(D):
    def f(self):
        print('B')


class A(B, C):
    pass


obj = A()
obj.xxx()
复制代码

继承关系和前面同样,当执行 XXX 方法时,因为 B 里面没有,因此找到了 D。可是 D 中会执行 self.f(),那么 f 函数会在哪一个函数中找呢?答案是B。

执行 self.f() 时,self 是 obj,而 obj 又是从 A 中实例化而来。所以执行 self.f() 会如今 A 中找 f 这个函数,若是没有,确定在 B 中找。所以答案是 B。

Mixin

要给一个类动态的增长方法,有多种方式:

  • 能够经过继承的方式,可是若是继承的类是标准库中的,因为没法修改,因此行不通。
  • 经过类装饰器,惟一的问题是装饰器没法继承。
class Document:
    def __init__(self, content):
        self.content = content

class Word(Document):
    def __init__(self, content):
        super().__init__('word: {}'.format(content))

def printable(cls):
    def _print(self): # 给类加了这个方法
        print('P: {}'.format(self.content))
    cls.print = _print
    return cls

@printable
class PrintableWord(Word):
    def __init__(self, content):
        super().__init__(content)

>>> PrintableWord('abc').print()
P: word: abc
复制代码
  • Mixin 的方式。它就是继承一个类,在这个类中增长方法,就能达到给目标类增长功能的目的。
class Document:
    def __init__(self, content):
        self.content = content


class Word(Document):
    def __init__(self, content):
        super().__init__('word: {}'.format(content))


class PrintableMixin:
    def print(self):
        print('P: {}'.format(self.content))


class PrintableWord(PrintableMixin, Word):
    def __init__(self, content):
        super().__init__(content)


>>> PrintableWord('abc').print()
P: word: abc
复制代码

使用 Minix 的好处在于,经过定义类的方式添加的动态方法是能够被其余类继承的。

class Document:
    def __init__(self, content):
        self.content = content


class Word(Document):
    def __init__(self, content):
        super().__init__('word: {}'.format(content))


class PrintableMixin:
    def print(self):
        result = 'P: {}'.format(self.content)
        print(result)
        return result


class PrintableWord(PrintableMixin, Word):
    def __init__(self, content):
        super().__init__(content)

# 再次被继承
class PrintToMonitorMixin(PrintableMixin):
    def print(self):
        print('Monitor: {}'.format(super().print()))


class PrintToMonitorWord(PrintToMonitorMixin, Word):
    pass


>>> PrintToMonitorWord('abc').print()
P: word: abc
Monitor: P: word: abc
复制代码

Mixin 是经过多继承实现的组合方式。一般来讲,组合优于继承。socketserver 就用到了 Mixin。

多态

多态也是面对对象的特性之一,意思是多种形态。可是不一样于 C# 和 java,python 中的多态是原生的,这应该也是弱类型语言的特性,所以 python 中的多态不多有提到。

class Foo:
    def f1(self):
        print('Foo')


class Bar:
    def f1(self):
        print('Bar')


def fun(arg):
    arg.f1()


fun(Foo())
fun(Bar())
复制代码

执行没有任何问题,由于python中什么参数都能接收,你给我什么我就接收什么。可是在 Java 中函数 fun 不能这么写,只能写成 fun(Foo arg)fun(Bar arg)。这就限定死了它只能接受一种类建立的对象,所以它们才有实现多态的需求。

其余语言的多态是这样实现的:

class Father:
    pass


class Foo(Father):
    def f1(self):
        print('Foo')


class Bar(Father):
    def f1(self):
        print('Bar')


def fun(Father arg):
    arg.f1()


fun(Foo())
fun(Bar())
复制代码

给它一个父类便可,这样父类和子类均可以传递。经过相同的父类实现多态。Python 中的方法重写、运算符重载都是多态的体现。

特有方法

前面咱们就已经看到了,定义一个类时,这个类会从 object 中继承不少以双下划线开头和双下划线结尾的成员,这些成员中有的是属性,有的是方法。

class A:
     pass
 
>>> dir(A)
Out[4]: 
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__le__',
 '__lt__',
 '__module__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__']
复制代码

__name__

得到类的名字。

>>> A.__name__
Out[9]: 'A'
复制代码

注意,实例是没有这个属性的。

__module__

获取模块名。ipython 并不知道它的模块名,由于结果为 main:

>>> A.__module__
Out[11]: '__main__'
复制代码

__doc__

显示文档字符串。

>>> A.__doc__
复制代码

__class__

python 一切皆对象,类也是对象,全部类都是 type 的对象。

>>> A.__class__
Out[13]: type
复制代码

而实例的 class 则是它的类:

>>> A().__class__
Out[14]: __main__.A
复制代码

所以咱们能够获取实例的类名:

>>> A().__class__.__name__
Out[15]: 'A'
复制代码

__dict__

针对实例的,它持有全部实例拥有的属性。咱们给实例增长属性就是给这个字典增长 key,这也是实例能够动态增长属性的缘由。

__dir__

它会获得实例的全部成员,类并无这个方法。dir() 底层就是调用它。

>>> A().__dir__()
Out[19]: 
['__module__',
 '__dict__',
 '__weakref__',
 '__doc__',
 '__repr__',
 '__hash__',
 '__str__',
 '__getattribute__',
 '__setattr__',
 '__delattr__',
 '__lt__',
 '__le__',
 '__eq__',
 '__ne__',
 '__gt__',
 '__ge__',
 '__init__',
 '__new__',
 '__reduce_ex__',
 '__reduce__',
 '__subclasshook__',
 '__init_subclass__',
 '__format__',
 '__sizeof__',
 '__dir__',
 '__class__']
复制代码

__hash__

当咱们传递一个对象给内置方法 hash() 时,它可以返回一个整数。

>>> hash('abc')
Out[2]: 1751202505306800636
复制代码

事实上它是调用类的 __hash__ 方法。

class Point:
    def __hash__(self):
        return 1
    
>>> hash(Point())
Out[3]: 1
复制代码

可是 __hash__ 的返回值必须是一个整数,不然会报错。

class Point:
    def __hash__(self):
        return 'a'
    
>>> hash(Point())
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-5-a919dcea3eae>", line 1, in <module>
    hash(Point())
TypeError: __hash__ method should return an integer
复制代码

__hash__ 有什么用呢?一个类中有这个方法,而且返回一个整数,那么它的实例对象就是可哈希对象。而字典和集合中只能添加可哈希对象,也就说它们在添加以前会调用 hash 方法。

class Point:
    # 将 __hash__ 直接干掉
    __hash__ = None
    
 >>> set([Point()]) # 添加时直接抛出异常
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-7-b7932f1140b9>", line 1, in <module>
    set([Point()])
TypeError: unhashable type: 'Point' 
复制代码

一个类,若是没有重写 __hash__ 方法,这个类的每一个对象,将会具备不一样的 hash 值。这会形成什么问题呢?咱们定义一个类:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        
>>> a = Point(2, 3)
>>> b = Point(2, 3)
>>> set([a, b])
Out[11]: {<__main__.Point at 0x7f410bc5e0b8>, <__main__.Point at 0x7f410bc5e358>}
复制代码

事实上咱们认为 a 和 b 是彻底相同的,可是因为它们的哈希值不一样,set 并不会将其当成一个对象,由于没法经过 set 进行去重。那么应该怎么办呢?咱们须要对 __hash__ 进行重写。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __hash__(self):
        return hash('{}:{}'.format(self.x, self.y))

>>> a = Point(2, 3)
>>> b = Point(2, 3)
>>> hash(a) == hash(b)
Out[15]: True
>>> set([a, b])
Out[16]: {<__main__.Point at 0x7f410b399908>, <__main__.Point at 0x7f410b399d68>}
复制代码

它们 hash 是相等了,可是仍是不能经过 set 进行去重。这是由于 set 不光要检查元素 hash 值,还会检查类自己是否相同。很显然,a 和 b 并不相同:

>>> a == b
Out[17]: False
复制代码

若是想让它们相等,那咱们得重写 __eq__ 方法:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __hash__(self):
        return hash('{}:{}'.format(self.x, self.y))

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

>>> a = Point(2, 3)
>>> b = Point(2, 3)
>>> set([a, b]) # 去重成功
Out[21]: {<__main__.Point at 0x7f410b376588>}
复制代码

一般 __hash__ 会和 __eq__ 同时使用,由于解释器同时判断 hash 和实例是否相等。所以当咱们的实例要放在字典或者集合中时,咱们就要在类中实现它们。

__len__

内置函数 len 就是调用类自己的 __len__ 方法。

>>> class Sized:
...     def __len__(self):
...         return 10
...     
>>> len(Sized())
Out[23]: 10
复制代码

因为 object 中并无 __len__,所以须要咱们手动实现。须要注意的是,__len__ 必须返回整数,且必须大于 0。

自定义数据结构的时候会用到它。

__bool__

经过 bool 这个内置方法能够判断一个对象的真假,事实上就是调用对象自己的 __bool__ 方法。

class O:
    pass

>>> bool(O()) # 默认为真
Out[24]: True

class O:
    # 定义它为假
    def __bool__(self):
        return False
    
>>> bool(O()) # 那就为假
Out[25]: False
复制代码

事实上并无这么简单,由于列表并无实现 __bool__,可是空列表会返回假,列表有元素就返回真。判断依据是什么?空列表的 __len__ 是 0。

class Sized:
    def __init__(self, size):
        self.size = size
    
    def __len__(self):
        return self.size
        
>>> bool(Sized(0))
Out[31]: False
>>> bool(Sized(1))
Out[32]: True
复制代码

当对象没有实现 __bool__,而实现了 __len__ 时,__len__ 等于 0 返回假,不然为真;若是这两种方法都没有实现,返回真;而当这两种方法同时出现时,__bool__ 优先级更高。

__bool__ 必须返回 True 和 False。

__str__

当咱们 print 一个对象时,就会调用这个方法。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __str__(self):
        return 'Point<{}, {}>'.format(self.x, self.y)
    
>>> print(Point(1, 3))
Point<1, 3>
复制代码

'{!s}'.format() 也是调用类的 __str__ 方法。它在调试程序、打日志的时候颇有用。

__repr__

内置方法 repr'{!r}'.format() 就是调用这个方法。str 一般给人读,而 repr 则是给机器读的。所以咱们会重写 str,可是不多会重写 repr。好比下面这种就适合给机器读而不适合给人读:

<__main__.A at 0x7fb30cd64860>
复制代码

__repr__ 到底实现了什么呢?当咱们在交互式模式下实例化一个类时:

>>> class A: pass
... 
>>> a = A()
>>> a
Out[11]: <__main__.A at 0x7f9cc1fd1898>
复制代码

a 的结果就是调用了 __repr__ 方法,好比:

class A:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return 'A({0.x}, {0.y})'.format(self)

>>> a = A(2, 5)
>>> a
Out[16]: A(2, 5)
复制代码

上面的 format() 方法的使用看上去颇有趣,格式化代码 {0.x} 对应的是第一个参数的 x 属性。 所以,0 实际上指的就是 self 自己。做为这种实现的一个替代,你也可使用 % 操做符,就像下面这样:

def __repr__(self):
    return 'A(%r, %r)' % (self.x, self.y)
复制代码

__repr__() 生成的文本字符串标准作法是须要让 eval(repr(x)) == x 为真。 若是实在不能这样子作,应该建立一个有用的文本表示,并使用 <> 括起来。好比:

>>> f = open('file.dat')
>>> f
<_io.TextIOWrapper name='file.dat' mode='r' encoding='UTF-8'>
复制代码

若是 __str__() 没有被定义,那么就会使用 __repr__() 来代替输出。

__format__

为了自定义字符串的格式化,咱们须要在类上面定义 __format__() 方法。例如:

_formats = {
    'ymd' : '{d.year}-{d.month}-{d.day}',
    'mdy' : '{d.month}/{d.day}/{d.year}',
    'dmy' : '{d.day}/{d.month}/{d.year}'
    }

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    def __format__(self, code):
        if code == '':
            code = 'ymd'
        fmt = _formats[code]
        return fmt.format(d=self)
复制代码

如今 Date 类的实例能够支持格式化操做了,如同下面这样:

>>> d = Date(2012, 12, 21)
>>> format(d)
'2012-12-21'
>>> format(d, 'mdy')
'12/21/2012'
>>> 'The date is {:ymd}'.format(d)
'The date is 2012-12-21'
>>> 'The date is {:mdy}'.format(d)
'The date is 12/21/2012'
复制代码

__format__() 方法给 Python 的字符串格式化功能提供了一个钩子,这里须要着重强调的是格式化代码的解析工做彻底由类本身决定。所以,格式化代码能够是任何值。例如,参考下面来自 datetime 模块中的代码:

>>> from datetime import date
>>> d = date(2012, 12, 21)
>>> format(d)
'2012-12-21'
>>> format(d, '%A, %B %d, %Y')
'Friday, December 21, 2012'
>>> 'The end is {:%d %b %Y}. Goodbye'.format(d)
'The end is 21 Dec 2012. Goodbye'
>>>
复制代码

__call__

一个对象中只要存在该方法,那么就能够在它的后面加上小括号执行。函数在 Python 也是对象,全部函数都是 function 的实例:

>>> def fn():
...     pass
... 
>>> fn.__class__
Out[4]: function
复制代码

函数之因此能够被调用执行,就是由于其内部存在 __call__ 方法,所以咱们只要在类的内部定义这个方法,那么该类的实例就能够被调用。而这种对象咱们称之为可调用对象

>>> class Fn:
...     def __call__(self):
...         print('called')
...         
>>> Fn()()
called
复制代码

内置方法 callable 能够用来判断一个对象是否可被调用。

它能够用类来写装饰器,让你和使用函数写的装饰器同样用。之因此使用类来写装饰器是由于很是复杂的装饰器,使用类来写的话,能够方便拆分逻辑。而且以前咱们要为函数保存一些变量须要经过闭包来实现,如今彻底可使用类的 __call__ 方法。用 __call__ 实现可调用对象,和闭包是异曲同工的,一般都是为了封装一些内部状态。

__enter__

详见上下文管理。

__exit__

详见上下文管理。

__getattr__

直接看例子:

>>> class A:
...     def __init__(self):
...         self.x = 3
... 
...     def __getattr__(self, item):
...         return 'missing property {}'.format(item)
...     
>>> a = A()
>>> a.x
Out[20]: 3
>>> a.y
Out[21]: 'missing property y'
>>> a.z
Out[22]: 'missing property z'
复制代码

当一个类定义了 __getattr__ 方法时,若是访问不存在的成员,会调用该方法。所以一个对象的属性查找顺序为:__dict__ -> class -> __getattr__。当 dict 和 class 中都不存在时,就会执行 getattr。好比字典的 setdefault 方法。

__setattr__

当一个类实现了 __setattr__ 时,任何地方对这个类增长属性,或者对现有属性赋值时,都会调用该方法。

>>> class A:
...     def __init__(self):
...         self.x = 3
... 
...     def __setattr__(self, name, value):
...         print('set {} to {}'.format(name, value))
...         
>>> a = A()
set x to 3
>>> a.y = 5
set y to 5
复制代码

实例化的时候,因为实例化方法中存在赋值的行为,所以触发 __setattr__。此时的 self.x 没有赋值,可是咱们是能够进行赋值的。

>>> class A:
...     def __init__(self):
...         self.x = 3
... 
...     def __setattr__(self, name, value):
...         print('set {} to {}'.format(name, value))
...         self.__dict__[name] = value # 增长这一行便可
...         
>>> a = A()
set x to 3
>>> a.y = 5
set y to 5
>>> a.__dict__
Out[9]: {'x': 3, 'y': 5}
复制代码

所以 __setattr__ 用在须要对实例属性进行修改的同时,作一些额外的操做时。

可是事实上这个方法并非那么好用,它有不少坑,好比:

class A:
    def __init__(self):
        self.x = 3

    def __setattr__(self, name, value):
        print('set {} to {}'.format(name, value))
        setattr(self, name, value) # 换成这个
复制代码

这个类只要实例化就会将解释器干掉。由于 setattr 至关于执行了 self.name = value,可是这一赋值操做就又会触发 __setattr__,这就造成递归了。因为没有退出条件,递归达到极限后,解释器退出。

谨慎使用吧。

__delattr__

当删除实例的属性时,会调用该方法。

class A:
    def __init__(self):
        self.x = 3
    def __delattr__(self, name):
        print('u cannot delete property'.format(name))
        
a = A()
del a.x
u cannot delete property
复制代码

它用在保护实例属性不被删除。

__getattribute__

只要类中定义了它,那么访问实例化对象中的任何属性和方法都会调用该方法,杀伤力巨大。

>>> class A:
...     NAME = 'A'
... 
...     def __init__(self):
...         self.x = 3
... 
...     def __getattribute__(self, item):
...         return 'hehe'
... 
...     def method(self):
...         print('method')
...         
>>> a = A()
>>> a.x
Out[18]: 'hehe'
>>> a.method
Out[19]: 'hehe'
>>> a.NAME
Out[20]: 'hehe'
复制代码

所以实例化对象成员查找顺序为:

__getattribute__ -> __dict__ -> __class__.__dict__ -> __getattr__
复制代码

这玩意基本不会用到。

__get__

详见描述器。

__set__

详见描述器。

__getitem__

这个方法是用来在对象后面使用中括号的。咱们之因此可以经过在字典后面加中括号获取字典里面 key 对应的值,就是由于 dict 这个类中使用了 __getitem__ 方法。

class Foo:
    def __getitem__(self, item): # 接收中括号中的参数
        print(item)


obj = Foo()
obj['hehe']
复制代码

执行后输出hehe。中括号中如今能够输入内容了,可是若是使用序列的切片操做呢?python2 中会调用 __getslice__,可是 python3 中仍然调用 __getitem__

>>> class Foo:
    def __getitem__(self, item):
        print(item, type(item))

obj = Foo()
obj[1:4:2]
slice(1, 4, 2) <class 'slice'>
复制代码

当咱们往中括号中传递切片的语法时,它会先调用slice这个类,而后将这个类传递给 __getitem__

可是却没法跟字典同样进行赋值,若是想赋值,可使用下面的方法。

__setitem__

class Foo:
    def __setitem__(self, key, value):
        print(key, value)


obj = Foo()
obj['name'] = 'lisi'
复制代码

这就能够赋值了。当咱们使用切片赋值时,python2 中会调用 __setslice__ 方法,可是 python3 中仍是调用 __setitem__

>>> class Foo:
    def __setitem__(self, key, value):
        print(key, value, type(key), type(value))

obj = Foo()
obj[1:4:2] = [11, 22, 33]
slice(1, 4, 2) [11, 22, 33] <class 'slice'> <class 'list'>
复制代码

可是不能使用 del obj['xxx'] 进行删除。若是想删除,使用下面的方法。

__delitem__

class Foo:
    def __delitem__(self, key):
        print(key)


obj = Foo()
del obj['name']
复制代码

删除分片和上面是同样的。

__iter__

当 for 循环一个对象时,实际上就是执行类中的 __iter__ 方法。也就是说若是一个对象能够被 for 进行循环,那么类中就必须存在 __iter__ 方法。不存在时会报错。

>>> class Foo:
    def __iter__(self):
        yield 1
        yield 2
        yield 3

obj = Foo()
for i in obj:
    print(i)

1
2
3
复制代码

__metaclass__

对象默认都是由 type 建立的,咱们却能够经过 __metaclass__ 指定该对象有什么建立。

class Foo:
    __metaclass__ = xxx
复制代码

表示指定该类由 xxx 建立。

__missing__

须要传递进来一个 key,可是没有传递时触发。

__reversed__

反向迭代,当对对象使用 reversed() 内置方法时触发。

class Countdown:
    def __init__(self, start):
        self.start = start

    # Forward iterator
    def __iter__(self):
        n = self.start
        while n > 0:
            yield n
            n -= 1

    # Reverse iterator
    def __reversed__(self):
        n = 1
        while n <= self.start:
            yield n
            n += 1

for rr in reversed(Countdown(30)):
    print(rr)
for rr in Countdown(30):
    print(rr)
复制代码

运算符重载

+, -, *, / 这样的用于数学计算的字符就属于运算符,而这些运算符实际上是对应类中的以双下滑先开头双下划线结尾的方法。

Python 中的运算符有不少,它们分为:

  • 算术运算符
  • 比较(关系)运算符
  • 赋值运算符
  • 逻辑运算符
  • 位运算符
  • 成员运算符
  • 身份运算符
  • 运算符优先级

Python 中“身份运算符”、“逻辑运算符”和“赋值运算符”以外的全部运算符均可以重载,那运算符对应的类方法是什么呢?咱们知道 int 类型支持全部的算术运算,所以咱们 help 它一下就知道大多数的运算符对应哪些方法了。而成员运算符就能够找 list 类。

算术运算:

+ -> __add__
- -> __sub__
* -> __mul__
/ -> __truediv__
% -> __mod__
复制代码

位运算:

& -> __and__
| -> __or__
复制代码

比较:

> -> __gt__
< -> __lt__
<= -> __le__
!= -> __ne__
== -> __eq__
复制代码

成员:

in -> __contains__
索引取值 -> __getitem__
复制代码

运算符重载就是咱们重写了一个类的运算符方法。运算符方法在类建立的那一刻就从 object 中继承了,根据类继承的原则,咱们在类中从新定义运算符方法,天然就会覆盖父类中的方法。

重载加法

先定义一个类:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

>>> a = Point(2, 4)
>>> b = Point(3, 5)
>>> a + b
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-7-f96fb8f649b6>", line 1, in <module>
    a + b
TypeError: unsupported operand type(s) for +: 'Point' and 'Point'
复制代码

很显然 a 和 b 并不能相加,可是咱们能够定义一个方法让它们实现相加。

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    # 定义一个 add 方法
    def add(self, other):
        return Point(self.x + other.x, self.y + other.y)

>>> a = Point(2, 4)
>>> b = Point(3, 5)
>>> c = a.add(b)
>>> c.x
Out[6]: 5
复制代码

经过一个 add 方法,咱们实现了它们的相加功能。可是,咱们仍是习惯使用加号,事实上,咱们只要改下函数名就可使用 + 进行运算了。

def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)
复制代码

很显然 + 就是调用类的 __add__ 方法,由于咱们只要加入这个方法就可以实现加法操做。

修改运算符

咱们先重载减法的运算符:

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __add__(self, other):
        return Point(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Point(self.x - other.x, self.y - other.y)
复制代码

而后将加法的默认操做改成减法的:

Point.__add__ = lambda self, value: self - value
复制代码

这样一来,咱们执行加法操做,实际上执行的倒是减法:

>>> (Point(8, 9) + Point(2, 4)).x
Out[34]: 6
复制代码

可是 Python 限制不能对默认类型这么作。

>>> int.__add__ = lambda self, value: self - value
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-35-77f6fd9e3d43>", line 1, in <module>
    int.__add__ = lambda self, value: self - value
TypeError: can't set attributes of built-in/extension type 'int'
复制代码

所以不要过分使用运算符重载。

上下文管理

在打开文件的时候,咱们可使用 with 语法,只要出了这个代码块,那么文件会自动关闭,它实际上就是使用了上下文管理。open 方法里面实现了 __enter____exit__ 这两个方法,而存在这两个方法的对象就是支持上下文管理的对象。

咱们定义一个类:

>>> class Context:
...     def __enter__(self):
...         print('enter context')
... 
...     def __exit__(self, *args, **kwargs):
...         print('exit context')
...         
>>> with Context():
...     print('do somethings')
...     
enter context
do somethings
exit context
复制代码

一个支持上下文管理的对象就能够经过 with 语句进行管理,在执行 with 代码块中的内容以前(__enter__)和以后(__exit__)会作些事情。

即便 with 语句块中抛出异常,__enter____exit__ 仍然会执行,所以上下文管理是安全的。

>>> with Context():
...     raise Exception # 直接抛出异常
... 
enter context
exit context
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-4-63ba5aff5acc>", line 2, in <module>
    raise Exception
Exception
复制代码

即便在解释器退出的状况下,__exit__ 仍然执行:

import sys
with Context():
    sys.exit()
复制代码

with 语法还支持 as 子句,它会获取 __enter__ 的返回值,并将其赋值给 as 后面的变量:

>>> class Context:
...     def __enter__(self):
...         print('enter, context')
...         return 'hehe'
... 
...     def __exit__(self, *args, **kwargs):
...         print('exit context')
... 
... with Context() as c:
...     print(c)
... 
enter, context
hehe
exit context
复制代码

__enter__ 除了 self 以外,不接收任何参数,或者说它接受参数没有意义。__exit__ 的返回值没有办法获取到,可是若是 with 语句块中抛出异常,__exit__ 返回 False 时,会向上抛出异常;返回 True 则会屏蔽异常。

__exit__ 能够接受参数,不过它的参数都是和异常相关的。当 with 代码块中抛出异常时,该异常的信息就会被 __exit__ 所获取。其中第一个参数是异常的类型、第二个就是这个异常的实例、第三个则是 traceback 对象。对于 with 代码块中的异常,咱们只能获取异常信息,而没法捕获。事实上当咱们定义 __exit__ 方法时,IDE 自动会补全为:

def __exit__(self, exc_type, exc_val, exc_tb):
复制代码

上下文管理的使用场景:凡是要在代码块先后插入代码的场景,这点和装饰器相似。

  • 资源管理类:申请和回收,包括打开文件、网络链接、数据库链接等;
  • 权限验证。

contextlib

若是只想实现上下文管理而不想定义一个类的话,Python 提供了现成的东西:

import contextlib

@contextlib.contextmanager
def context():
    print('enter context') # 初始化部分
    try:
        yield 'hehe' # 至关于 __enter 的返回值
    finally:
        print('exit context') # 清理部分

with context as c:
    print(c)
复制代码

若是业务逻辑简单的话,直接使用这种方式就能够了;可是若是业务复杂的话,仍是使用类来的直接。

with嵌套

首先定义一个支持上下文管理的类:

from socket import socket, AF_INET, SOCK_STREAM

class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = family
        self.type = type
        self.sock = None

    def __enter__(self):
        if self.sock is not None:
            raise RuntimeError('Already connected')
        self.sock = socket(self.family, self.type)
        self.sock.connect(self.address)
        return self.sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.sock.close()
        self.sock = None
复制代码

有一个细节问题就是 LazyConnection 类是否容许多个 with 语句来嵌套使用链接。 很显然,上面的定义中一次只能容许一个 socket 链接,若是正在使用一个 socket 的时候又重复使用 with 语句, 就会产生一个异常了。不过你能够像下面这样修改下上面的实现来解决这个问题:

from socket import socket, AF_INET, SOCK_STREAM

class LazyConnection:
    def __init__(self, address, family=AF_INET, type=SOCK_STREAM):
        self.address = address
        self.family = family
        self.type = type
        self.connections = []

    def __enter__(self):
        sock = socket(self.family, self.type)
        sock.connect(self.address)
        self.connections.append(sock)
        return sock

    def __exit__(self, exc_ty, exc_val, tb):
        self.connections.pop().close()

# Example use
from functools import partial

conn = LazyConnection(('www.python.org', 80))
with conn as s1:
    pass
    with conn as s2:
        pass
        # s1 and s2 are independent sockets
复制代码

在第二个版本中,LazyConnection 类能够被看作是某个链接工厂。在内部,一个列表被用来构造一个栈。每次 __enter__() 方法执行的时候,它复制建立一个新的链接并将其加入到栈里面。__exit__() 方法简单的从栈中弹出最后一个链接并关闭它。这里稍微有点难理解,不过它能容许嵌套使用 with 语句建立多个链接,就如上面演示的那样。

在须要管理一些资源好比文件、网络链接和锁的编程环境中,使用上下文管理器是很广泛的。这些资源的一个主要特征是它们必须被手动的关闭或释放来确保程序的正确运行。例如,若是你请求了一个锁,那么你必须确保以后释放了它,不然就可能产生死锁。经过实现 __enter__()__exit__() 方法并使用 with 语句能够很容易的避免这些问题,由于 __exit__() 方法可让你无需担忧这些了。

用类来写装饰器

前面都是使用函数写装饰器,可是因为类中存在 __call__ 方法,所以让经过类写装饰器成为了现实。前面也调到过,使用类来写装饰器的好处在于,咱们能够在类中定义不少的方法,这样就容易的实现逻辑拆分了,这在定义复杂的装饰器的时候很好用。

这里使用一个简单的类装饰器做为例子,用来统计一个函数的执行时间。

import datetime
from functools import wraps

class Timeit:
 # 当类要做为装饰器的时候,init 只能接受被装饰的函数这一个参数
    def __init__(self, fn=None): 
        wraps(fn)(self)

    # 做为装饰器还得有一个 call 方法,让其对象可调用
    def __call__(self, *args, **kwargs):
        start = datetime.datetime.now()
        ret = self.__wrapped__(*args, **kwargs)
        cost = datetime.datetime.now() - start
        print(cost)
        return ret

    def __enter__(self):
        self.start = datetime.datetime.now()

    def __exit__(self, exc_type, exc_val, exc_tb):
        cost = datetime.datetime.now()
        print(cost)

@Timeit
def add(x, y):
    x + y

add(2, 4)
复制代码

下面的两个方法是上下文管理用的,做用是上这个类不只能够做为装饰器来计算函数的执行时间,还可以经过 with 语句统计代码块的执行时间。

wraps 是 functools 模块提供的功能,它是一个柯里化函数。它的一个参数是包装的函数,第二个参数是被包装的函数。在这里包装的函数就是被装饰的函数,而被包装的函数就是存在 __call__ 方法的对象自己了。wraps 会给被包装函数增长一个 __wrapped__ 的属性,实际上就是包装的函数 fn。事实上咱们直接调用 fn 也是同样的。

反射

所谓的反射指的是运行时获取类的信息。事实上,咱们已经接触了一些反射相关的东西了,好比实例对象的 __dict__ 就是反射的一种体现。

前面讲到了,咱们能够经过对象的 __dict__ 根据属性名称来得到属性值:

>>> class Point:
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
... 
...     def print(self):
...         print(self.x, self.y)
...         
>>> a = Point(2, 5)
>>> a.__dict__['x']
Out[13]: 2
复制代码

可是因为 __dict__ 中没有方法,所以咱们是没法这种方式来获取方法的。这时就轮到 getattr 登场了,它接收三个参数,分别为对象、成员名称和默认值。

>>> getattr(a, 'print')()
2 5
复制代码

它不光能够获取方法,也能获取属性:

>>> getattr(a, 'x')
Out[15]: 2
复制代码

由此,咱们也能知道 setattr 和 hasattr 的用法了。

>>> setattr(a, 'z', 'hehe')
>>> a.z
Out[17]: 'hehe'
复制代码

setattr 的对象是实例,若是想给实例动态的增长方法首先要将函数转换成方法,转化的方法是 type.MethodType。

import types

class Point:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def print(self):
        print(self.x, self.y)

def mm(self):
    print(self.x)

p = Point(2, 4)
setattr(p, 'mm', types.MethodType(mm, p))
p.mm()
复制代码

在描述器中会提到 types.MethodType 是如何实现的。可是,基本上没有动态给实例增长方法的需求。setattr 通常用来修改实例中已经存在的属性,不到万不得已,是不会给实例增减属性的。

getattr 和 setattr 是相互对立的,一个是获取一个是设置。hasattr 则用来判断对象中是否存在特定的成员,它返回一个布尔值。

这三个 *attr 就构成了反射,如下是它应用的一个小示例:

class Command:
    def cmd1(self):
        print('cmd1')

    def cmd2(self):
        print('cmd2')

    def run(self):
        while True:
            cmd = input('>>> ')
            if cmd == 'quit':
                return
            getattr(self, cmd, lambda :print('not found cmd {}'.format(cmd)))()

cmd = Command()
cmd.run()
复制代码

run 这个函数咱们能够始终不变,若是想添加功能就定义函数,而后外部就能够经过字符串的方式直接调用这个方法了。

这只是一个小而简单的例子,在 RPC 的使用场景中,基本都会用到反射。

描述器

当一个类成员实现了 __get____set__ 方法以后,访问这个类成员会调用 __get__ 方法,对这个类变量赋值会调用 __set__ 方法。对于实现了这两种方法的类变量,咱们称之为描述器。

描述器是一个类,实现了 __get____set____delete__ 中一个或多个方法。

示例:

class Int:
    def __init__(self, name):
        self.name = name
        self.data = {}

    def __get__(self, instance, owner):
        print('get {}'.format(self.name))
        if instance is not None:
            return self.data[instance]
        return self

    def __set__(self, instance, value):
        self.data[instance] = value

    def __str__(self):
        return 'Int'

    def __repr__(self):
        return 'Int'

class A:
    val = Int('val') # 很显然 val 是类变量
    def __init__(self):
        self.val = 3

>>> a = A() 
>>> a.val
get val
Out[5]: 3
>>> a.__dict__
Out[6]: {'val': 3}
复制代码

当对 A 进行实例化的时候,首先会执行 A 中的初始化方法,因为初始化中有赋值操做,所以里面的赋值操做不会执行,而是调用 Int 类中的 __set__ 方法(它接收的 instance 是 A 的实例化对象,value 是赋的值),而且在 __set__ 方法执行完毕后,继续执行初始化操做。而当咱们对 a 中的 val 进行访问时,会调用 Int 中的 __get__ 方法,它会接收两个参数,instance 是 A 的实例化对象,cls 为 A 这个类自己。它是自动传递的。

前面提到过实例成员的查找顺序,最早查找的是类自己的 __dict__。可是下面却不是这样的:

>>> a.__dict__['val'] = 5
>>> a.val
get val
Out[15]: 3 # 结果仍是 3
复制代码

这是由于带 __set____delete__ 方法的描述器会提高优先级到 __dict__ 以前。

>>> class Desc:
...     def __get__(self, instance, owner):
...         pass
... 
...     def __delete__(self, instance):
...         pass
... 
... 
... class A:
...     x = Desc()
...     
>>> a = A()
>>> a.x = 3
Traceback (most recent call last):
  File "/usr/local/python3/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 2862, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-7-0a024de0ab56>", line 1, in <module>
    a.x = 3
AttributeError: __set__
复制代码

这就是由于 delete 提高了描述器的优先级,所以赋值的时候会先找 set,找不到的话就报错了。

描述器事实上是一个代理机制,当一个类变量被定义为描述器,对这个类变量的操做,将由描述器代理。其中:

  • 访问对应 __get__
  • 赋值对应 __set__
  • 删除对应 __delete__

注意,没有增长方法。

即便 __set__ 会提高优先级,可是依然逊于 __getattribute__

以前能够看到描述器中会接收参数。其中:

__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)
复制代码

instance 表示访问这个类的实例,owner 表示访问这个类的类自己。当经过类来访问时,instance 为 None;value 表示咱们所赋的值。

实现classmethod

from functools import partial

class Classmethod:
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        return partial(self.fn, owner) # 执行 self.fn,并将 owner 做为参数传递给 self.fn

class A:
 @Classmethod # 由于 __get__ 返回的是函数,且函数可调用,所以它能够做为装饰器
    def cls_method(cls):
        print(cls)

A.cls_method() # 经过类调用的话,__get__ 方法的 instance 为 None
A().cls_method()
复制代码

partial 做用是将 self.fn 的第一个参数固定为 owner。cls_method 整个函数包括参数都传递给 Classmethod 的构造函数,等到下面经过类访问类成员(cls_method)的时候,调用 __get__ 方法。经过 partial 执行 self.fn(也就是下面的 cls_method 方法),而且将第一个参数固定为 owner,而很显然 owner 就是 A 的实例,因而 cls = A 的实例,因而最后将这个实例打印了出来。

若是装饰器的写法看的有些费劲,那么能够将之转换为:

from functools import partial

class Classmethod:
    def __init__(self, fn):
        self.fn = fn

    def __get__(self, instance, owner):
        return partial(self.fn, owner)

class A:
    cls_method = Classmethod(lambda x: print(x))
复制代码

实现staticmethod

class Staticmethod:
    def __init__(self, fn):
        self.fn = fn
        
    def __get__(self, instance, owner):
        return self.fn
复制代码

实现property

class Property:
    def __init__(self, fget, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel

    def __get__(self, instance, owner):
        if instance is not None:
            return self.fget(instance)
        return self

    def __set__(self, instance, value):
        if callable(self.fset):
            self.fset(instance, value)
        else:
            raise AttributeError('{} cannot assignable'.format(self.fget.__name__))

    def __delete__(self, instance):
        if callable(self.fdel):
            self.fdel(instance)
        else:
            raise AttributeError('{} cannot deletable'.format(self.fget.__name__))

    def setter(self, fn):
        self.fset = fn
        return self

    def deletter(self, fn):
        self.fdel = fn
        return self


class A:
    def __init__(self):
        self.__x = 1

 @Property
    def x(self):
        return self.__x

 @x.setter
    def x(self, value):
        self.__x = value

 @x.deletter
    def x(self):
        print('cannot delete')
复制代码

缓存

class lazyproperty:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            value = self.func(instance)
            setattr(instance, self.func.__name__, value) # 直接将类方法变成了实例方法,下次访问就不会触发 __get__ 方法了,这就实现了缓存
            return value

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

 @lazyproperty # 经过装饰器将 area 变成了类方法,area = lazyproperty(area)
    def area(self):
        print('Computing area')
        return math.pi * self.radius ** 2

 @lazyproperty
    def perimeter(self):
        print('Computing perimeter')
        return 2 * math.pi * self.radius
复制代码

下面在一个交互环境中演示它的使用:

>>> c = Circle(4.0)
>>> c.radius
4.0
>>> c.area
Computing area
50.26548245743669
>>> c.area
50.26548245743669
>>> c.perimeter
Computing perimeter
25.132741228718345
>>> c.perimeter
25.132741228718345
复制代码

仔细观察你会发现消息 Computing area 和 Computing perimeter 仅仅出现一次。

因为经过装饰器将类的动态方法变成了类方法,所以访问 area 时就触发了 lazyproperty 类的 __get__ 方法,只是在执行 __get__ 方法的过程当中又将这个类方法变成了实例方法,所以下次再访问这个实例方法时,就不会再触发 __get__,也就实现了缓存的效果。能够经过下面的代码来观察它的执行:

>>> c = Circle(4.0)
>>> # Get instance variables
>>> vars(c)
{'radius': 4.0}

>>> # Compute area and observe variables afterward
>>> c.area
Computing area
50.26548245743669
>>> vars(c)
{'area': 50.26548245743669, 'radius': 4.0}

>>> # Notice access doesn't invoke property anymore
>>> c.area
50.26548245743669

>>> # Delete the variable and see property trigger again
>>> del c.area
>>> vars(c)
{'radius': 4.0}
>>> c.area
Computing area
50.26548245743669
复制代码

这种方案有一个小缺陷就是计算出的值被建立后是能够被修改的。例如:

>>> c.area
Computing area
50.26548245743669
>>> c.area = 25
>>> c.area
25****
复制代码

若是你担忧这个问题,那么可使用一种稍微没那么高效的实现,就像下面这样:

def lazyproperty(func):
    name = '_lazy_' + func.__name__
 @property
    def lazy(self):
        if hasattr(self, name):
            return getattr(self, name)
        else:
            value = func(self)
            setattr(self, name, value)
            return value
    return lazy

class Circle:
    def __init__(self, radius):
        self.radius = radius

 @lazyproperty
    def area(self): # 结果就是 area = lazyproperty(area),它的结果是一个属性而不是方法,由于 lazy 被使用 property 了
        print('Computing area')
        return math.pi * self.radius ** 2

 @lazyproperty
    def perimeter(self):
        print('Computing perimeter')
        return 2 * math.pi * self.radius

>>> c = Circle(4.0)
>>> c.area
Computing area
50.26548245743669
复制代码

经过装饰器将动态方法 area 变成了类方法 area = lazyproperty(area),并将 area 的参数 self 传递给了 lazy。lazyproperty 函数返回的是一个函数,一个被 property 装饰的函数,也就是能够经过访问属性的方式访问一个函数。当下面经过访问属性的方式执行 c.area 时,开始执行 lazy 函数。

self 中确定没有 name 属性存在的,所以执行 else 子句,而后在 else 子句中在 self 中设置了 name 属性,所以下次访问就不会执行 else 子句了。因为 area 是一个 property 对象,所以没法对其进行赋值。

这种方案有一个缺点就是全部 get 操做都必须被定向到属性的 getter 函数上去。这个跟以前简单的在实例字典中查找值的方案相比效率要低一点。

描述器总结

描述器的使用场景为:用于接管实例变量的操做。好比数据校验,如下是验证使用类型注解以后,输入的类型必须是类型注解的类型。

import inspect

class Typed:
    def __init__(self, name, type):
        self.name = name
        self.type = type

    def __get__(self, instance, owner):
        if instance is not None:
            return instance.__dict__[self.name]
        return self

    def __set__(self, instance, value):
        if not isinstance(value, self.type):
            raise TypeError()
        instance.__dict__[self.name] = value

def typeassert(cls):
    params = inspect.signature(cls).parameters
    for name, param in params.items():
        if param.annotation != inspect._empty:
            setattr(cls, name, Typed(name, param.annotation))
    return cls

@typeassert
class Person:
    def __init__(self, name: str, age: int, desc):
        self.name = name
        self.age = age
        self.desc = desc

Person(11, 'tom', {})
复制代码

当咱们须要在装饰器中注入当前类的实例时:

import datetime
from functools import wraps
from types import MethodType

class Timeit:
    def __init__(self, fn):
        self.fn = fn
    
    def __get__(self, instance, owner):
 @wraps(self.fn)
        def wrap(*args, **kwargs):
            start = datetime.datetime.now()
            ret = self.fn(*args, **kwargs)
            cost = datetime.datetime.now() - start
            instance.send(cost)
            return ret
        
        if instance is not None:
            return MethodType(wrap, instance)
        return self

class Sender:
    def send(self, cost):
        print(cost)
    
 @Timeit
    def other(self):
        pass
复制代码

类的建立与销毁

前面提到了 __init__ 方法,可是它并不会建立类,它只是执行了初始化。那类是如何建立的,self 又是哪里来的呢?它来自于 __new__ 方法。

new 方法的定义有必定格式上的要求:

>>> class A:
...     def __new__(cls, *args, **kwargs):
...         print('new')
...         return object.__new__(cls)
... 
... A()
new
Out[2]: <__main__.A at 0x7f2247eeb9b0>
复制代码

object.__new__() 方法用于给类建立一个对象:

>>> object.__new__(A)
Out[3]: <__main__.A at 0x7f2246b7c240>
复制代码

__del__ 在对象的生命周期的结束会被调用:

>>> class A:
...     def __new__(cls, *args, **kwargs):
...         print('new')
...         return object.__new__(cls)
... 
...     def __init__(self):
...         print('init')
... 
...     def __del__(self):
...         print('del')
... 

>>> a = A()
new
init
>>> del a
del
复制代码

元编程

在元组的时候提到过命名元祖:

from collections import namedtuple
Person = namedtuple('Person', ['name', 'age'])
复制代码

它的神奇之处在于用代码建立了一个新的数据类型,也就是说代码具备写代码的能力,这种能力叫作元编程。

咱们都知道使用内置的 type 方法能够检测一个对象的数据类型,可是它还能够用来建立一个对象。

>>> class A:
...     pass
... 
>>> type(A) # A 既是类也是对象,它的类型为 type
Out[9]: type
>>> type('Group', (), {}) # 这就动态的建立了一个对象。
Out[10]: __main__.Group
复制代码

经过元编程,咱们能够控制类(注意是类而不是对象)的建立过程。而类的建立过程无非就是:

  • 成员
  • 继承列表

改变类的建立过程无非就是更改为员以及继承列表,这种改变可分为静态的方式和动态的方式。首先看静态的继承:

>>> class DisplayMixin:
...     def display(self):
...         print(self)
... 
... class A(DisplayMixin):
...     pass
... 
... 
>>> A().display()
<__main__.A object at 0x7f2246b7c160>
复制代码

而元编程能够实现动态的方式:

>>> B = type('B', (DisplayMixin, object), {})
>>> B().display() # B 的对象也具备了 display 的方法
<__main__.B object at 0x7f2246b800f0>
复制代码

咱们能够将之用在 if 判断中:

if debug:
    B = type('B', (DisplayMixin, object), {})
else:
    B = type('B', (), {})
复制代码

若是是 debug 模式,就让类继承 display 方法,不然就不继承。

固然若是以为这么写麻烦的话,还能够这样:

class DisplayMixin:
    def display(self):
        print(self)

# 这个类能够随意的修改
class Meta(type):
    def __new__(cls, name, bases, clsdict):
        new_bases = [DisplayMixin]
        new_bases.extend(bases)
        return super().__new__(cls, name, tuple(new_bases), clsdict)

    def __init__(self, name, bases, clsdict):
        super().__init__(name, bases, clsdict)

# 而后定义类的时候这么用便可
class C(metaclass=Meta): # metaclass 用来指定使用哪一个 type 的子类来建立该类
    pass

C().display()
复制代码

除非你明确知道本身在干什么,不然不要使用元编程。

新式类和经典类

经典类就是 class Foo: 这种的,什么都不继承的。新式类就是以前老是使用的 object,从 object 类中继承。新式类做为新的,确定会比老的多出一些功能。那既然有新式类和经典类了,应该使用哪种呢?用新式类。

新式类是 python 2.2 后出现的,新式类彻底兼容经典类,就是在经典类上面增长了新的功能。

class A:
              ^ ^  def save(self): ...
             /   \
            /     \
           /       \
          /         \
      class B     class C:
          ^         ^  def save(self): ...
           \       /
            \     /
             \   /
              \ /
            class D
复制代码

类B类C都是从类A继承的,类D则是从类BC中继承。按理来讲D会继承C中的save方法,可是经典类中会先找B,B找不到会找A,就不会找C了。示例以下:

class A:
    def __init__(self):
        print 'This is A'
    def save(self):
        print 'save method from A'

class B(A):
    def __init__(self):
        print 'This is B'


class C(A):
    def __init__(self):
        print 'This is C'
    def save(self):
        print 'save method from C'

class D(B,C):
    def __init__(self):
        print 'This is D'

c = D()
c.save()
复制代码

执行结果为:

This is D
save method from A
复制代码

很显然没有继承到C的,而是继承了A。这是经典类中的BUG,所谓的深度优先。

可是一旦A继承了新式类结果就是咱们想要的了:

class A(object): # 只加了这点内容
    def __init__(self):
        print 'This is A'
    def save(self):
        print 'save method from A'

class B(A):
    def __init__(self):
        print 'This is B'


class C(A):
    def __init__(self):
        print 'This is C'
    def save(self):
        print 'save method from C'

class D(B,C):
    def __init__(self):
        print 'This is D'

c = D()
c.save()
复制代码

执行结果:

This is D
save method from C
复制代码

这就从C中继承了。

补充

实现contextlib.contextmanager

from functools import wraps

class ContextManager:
    def __init__(self, fn, *args, **kwargs):
        self.gen = fn(*args, **kwargs)

    def __enter__(self):
        return next(self.gen)

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            return next(self.gen)
        except StopIteration as e:
            return False

# 使用这个函数是为了让使用help查看的时候能显示正确的文档信息
# 包括正确的显示函数名,而由于有了这个函数,因此类中不须要call方法了
def contextmanager(fn):
 @wraps(fn)
    def wrap(*args, **kwargs):
        return ContextManager(fn, *args, **kwargs)
    return wrap
复制代码

实现super

最简单的一种形式:

from types import MethodType

class Super:
    def __init__(self, obj):
        self.type = type
        self.obj = obj

    def __getattr__(self, name):
        is_super = False
        for cls in self.type.__mro__:
            if is_super and hasattr(cls, name):
                return MethodType(getattr(cls, name), self.obj)
            if cls == self.type:
                is_super = True
        raise AttributeError()
复制代码

建立独一无二的对象

object 是全部类的基类,所以咱们能够调用它来建立一个对象,这个对象没什么实际用处,由于它并无任何有用的方法,也没有任何实例数据。由于它没有任何的实例字典,你甚至都不能设置任何属性值,它惟一的做用就是来标识一个独一无二的对象。

_no_value = object()

def spam(a, b=_no_value):
    if b is _no_value:
        print('No b value supplied')
复制代码

经过这个对象来判断是否有参数传递进来。

实践

cookbook 书中内容。

简化数据结构的初始化

若是多个类都要进行初始化,且初始化的内容相同的话,就能够将这个初始化函数独立出来,造成一个基类。

import math

class Structure1:
    # Class variable that specifies expected fields
    _fields = []

    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))
        # Set the arguments
        for name, value in zip(self._fields, args): # zip 绝对是很妙的用法
            setattr(self, name, value) # 经过 setattr 设置实例变量,很显然这个变量是给子类设置的,由于初始化是在子类上完成的
复制代码

而后使你的类继承自这个基类:

# Example class definitions
class Stock(Structure1):
    _fields = ['name', 'shares', 'price']

class Point(Structure1):
    _fields = ['x', 'y']

class Circle(Structure1):
    _fields = ['radius']

    def area(self):
        return math.pi * self.radius ** 2
复制代码

使用这些类的示例:

>>> s = Stock('ACME', 50, 91.1)
>>> p = Point(2, 3)
>>> c = Circle(4.5)
>>> s2 = Stock('ACME', 50)
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
    File "structure.py", line 6, in __init__
        raise TypeError('Expected {} arguments'.format(len(self._fields)))
TypeError: Expected 3 arguments
复制代码

若是还想支持关键字参数,能够将关键字参数设置为实例属性:

class Structure2:
    _fields = []

    def __init__(self, *args, **kwargs):
        if len(args) > len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))

        # Set all of the positional arguments
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

        # Set the remaining keyword arguments
        for name in self._fields[len(args):]:
            setattr(self, name, kwargs.pop(name))

        # Check for any remaining unknown arguments
        if kwargs:
            raise TypeError('Invalid argument(s): {}'.format(','.join(kwargs))) # join 至关于对 kwargs 进行循环,全部值为 key
# Example use
if __name__ == '__main__':
    class Stock(Structure2):
        _fields = ['name', 'shares', 'price']

    s1 = Stock('ACME', 50, 91.1)
    s2 = Stock('ACME', 50, price=91.1)
    s3 = Stock('ACME', shares=50, price=91.1)
    # s3 = Stock('ACME', shares=50, price=91.1, aa=1)
复制代码

你还能将不在 _fields 中的名称加入到属性中去:

class Structure3:
    # Class variable that specifies expected fields
    _fields = []

    def __init__(self, *args, **kwargs):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))

        # Set the arguments
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

        # Set the additional arguments (if any)
        extra_args = kwargs.keys() - self._fields # 计算差集,返回值为集合
        for name in extra_args:
            setattr(self, name, kwargs.pop(name))

        if kwargs:
            raise TypeError('Duplicate values for {}'.format(','.join(kwargs)))

# Example use
if __name__ == '__main__':
    class Stock(Structure3):
        _fields = ['name', 'shares', 'price']

    s1 = Stock('ACME', 50, 91.1)
    s2 = Stock('ACME', 50, 91.1, date='8/2/2012')
复制代码

当你须要使用大量很小的数据结构类的时候,相比手工一个个定义 __init__() 方法而已,使用这种方式能够大大简化代码。在上面的实现中咱们使用了 setattr() 函数类设置属性值,你可能不想用这种方式,而是想直接更新实例字典,就像下面这样:

class Structure:
    # Class variable that specifies expected fields
    _fields= []
    def __init__(self, *args):
        if len(args) != len(self._fields):
            raise TypeError('Expected {} arguments'.format(len(self._fields)))

        # Set the arguments (alternate)
        self.__dict__.update(zip(self._fields,args))
复制代码

尽管这也能够正常工做,可是当定义子类的时候问题就来了。当一个子类定义了 __slots__ 或者经过 property(或描述器)来包装某个属性,那么直接访问实例字典就不起做用了。咱们上面使用 setattr() 会显得更通用些,由于它也适用于子类状况。

这种方法惟一很差的地方就是对某些IDE而言,在显示帮助函数时可能不太友好。好比:

>>> help(Stock)
Help on class Stock in module __main__:
class Stock(Structure)
...
| Methods inherited from Structure:
|
| __init__(self, *args, **kwargs)
|
...
复制代码
相关文章
相关标签/搜索