python面向对象

2,026 阅读1小时+

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

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

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

  • 世界由对象组成;
  • 对象具有运动规律和内部状态;
  • 对象之间可以相互作用。

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

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

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

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

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

面向对象的特性:

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

实例说明:球类可以对球的特征和行为进行抽象,然后可以实例化一个真实的球实体出来。比如我们对人类进行实例化,可以实例化出张三、李四、王五等。

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

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

类是抽象的概念,实例才是具体的。但是要先设计类,才能完成实例化。类是定一个多个同一类型对象共享的结构和行为(数据和代码)。就像 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)
|
...