Linux集群之美
上QQ阅读APP看书,第一时间看更新

2.5.7 Python的面向对象

Python从设计之初就已经是一门面向对象的语言,正因为如此,在Python中创建一个类和对象是很容易的。本章将详细介绍Python的面向对象编程。

Python是支持面向对象、面向过程、函数式编程等多种编程范式的,它不强制我们使用任何一种编程范式,我们可以使用过程式编程编写任何程序。对于中等和大型项目来说,面向对象将给我们带来许多优势。如果你以前没有接触过面向对象的编程语言,那可能需要先了解一些面向对象语言的基本特征,在头脑里形成一个基本的面向对象的概念,这样有助于你更容易地学习Python的面向对象编程。

下面来简单了解一下面向对象的基本特征。

1.面向对象的基本定义

Python面向对象的基本定义如下。

·类(Class):用来描述具有相同属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。

·类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。类变量通常不作为实例变量使用。

·方法:类中定义的函数。

·数据成员:类变量或者实例变量用于处理类及其实例对象的相关数据。

·方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个过程叫方法的覆盖(override),也称为方法的重写。

·实例变量:定义在方法中的变量,只作用于当前实例的类。

·继承:即一个派生类(derived class)继承基类(base class)的字段和方法。继承也允许把一个派生类的对象作为一个基类对象对待。

·实例化:创建一个类的实例、类的具体对象。

·对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和一个方法。

类和对象是面向对象的两个重要概念,类是客观世界中事物的抽象,而对象则是类实例化后的实体。大家可以将类想象成图纸或模型,对象则是通过图纸或模型设计出来的实物。例如,同样的汽车模型可以造出不同的汽车,不同的汽车有不同的颜色、价格和车牌,如图2-7所示。

图2-7 以汽车类型类比类和实例化

汽车模型是对汽车特征和行为的抽象,而汽车则是实际存在的事物,是客观世界中实实在在的物体。

我们在描述一个真实对象(物体)时包括以下两个方面:

·它可以做什么(行为)。

·它是什么样的(属性或特征)。

在Python中,一个对象的特征也称为属性,它所具有的行为则称为方法,对象=属性+方法。另外在Python中,我们会把具有相同属性和方法的对象归为一个类。

这里举个简单的例子:


#-*- encoding:utf8 -*-  
class Turtle(object):
    #属性
    color = "green"
    weight = "10"
    #方法
    def run(self):
        print "我正在跑..."
    def sleep(self):
        print "我正在睡觉..."

tur = Turtle()
print tur.weight
#打印实例tur的weight属性
tur.sleep()
#调用实例tur的sleep方法

执行后的结果如下:


10
我正在睡觉...

Python会自动给每个对象添加特殊变量self,它相当于C++的指针,这个变量指向对象本身,让类中的函数能够明确地引用对象的数据和函数(self不能被忽略),示例如下:


#-*- encoding:utf-8 -*-

class NewClass(object):
    def __init__(self,name):
        print self
        self.name = name 
        print "我的名字是:{}".format(self.name)

cc = NewClass('yhc')

打印结果如下:


<__main__.NewClass instance at 0x020D4440>
我的名字是:yhc

format函数是Python新增的一种格式化字符串的函数,它增强了字符串格式化的功能。其基本语法是通过“{}”和“:”来代替以前的“%”,可以接受无限个参数,位置可以不按顺序排列。

在这段代码中,self是NewClass类在内存地址0x020D4440处的实例。因此,self在这里与C++中的this一样,代表的都是当前对象的地址,可以用来调用当前类中的属性和方法。在这段代码中,有一个特殊的函数,即__init__()方法,它是Python中的构造函数,构造函数用于初始化类的内部状态,为类的属性设置默认值。

如果我们想看一下cc的属性,可以在Python命令行模式下输入如下命令:


dir(cc)

打印结果如下:


['__class__',
 '__delattr__',
 '__dict__',
 '__doc__',
 '__format__',
 '__getattribute__',
 '__hash__',
 '__init__',
 '__module__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'name']

内建函数dir()可以显示类属性,同样还可以打印所有的实例属性。

与类相似,实例其实也有一个__dict__的特殊属性,它是由实例属性构成的一个字典,同样在Python命令行模式下输入如下命令:


cc.__dict__

输出结果如下:


{'name': 'yhc'}

事实上,Python中定义了很多内置类属性,用于管理类的内部关系。

·__dict__:类的属性(包含一个字典,由类的数据属性组成)。

·__doc__:类的文档字符串。

·__name__:类名。

·__module__:类定义所在的模块(类的全名是'__main__.className',如果类位于一个导入模块mymod中,那么className.__module__等于mymod)。

·__bases__:类的所有父类构成元素(包含了一个由所有父类组成的元组)。

我们如果执行print NewClass.__bases__这段代码,则会输出如下结果:


(<type 'object'>,)

另外,在上面的代码中,如果想打印出cc的值,可用如下命令:


print cc

打印结果如下:


<__main__.NewClass instance at 0x020D4440>

在这里,cc跟上面的self的效果是一样的,它也是NewClass类在内存地址0x020D4440处的实例,显然这种不是我们想要的效果,所以需要一个方法来打印出适合我们人类阅读的方式,这里采用__str__,将上面的代码精简并加入新的内容,整个代码变成:


# -*- coding: UTF-8 -*-

class NewClass(object):
    def __init__(self,name):
        # print self
        self.name = name 
        print "我的名字是:{}".format(self.name)
    def __str__(self):
        print "NewClass:{}".format(self.name)

cc = NewClass('yhc')

注意,对于这里采用的__str__方法,它的输出结果为我们预先定义好的格式:


我的名字是:yhc

__repr__具有跟__str__类似的效果,这里就不重复演示了。事实上,我们在创建自己的类和对象时,编写__str__和__repr__方法是有必要的。它们对于显示对象的内容很有帮助,而显示对象内容有助于调试程序。

注意

__str__()必须使用return语句返回,如果__str__()不返回任何值,则执行print语句会出错。

另外,请注意这段代码中的object,即class NewClass(object),专业的说法叫定义基类。很多资料上面都将此object略过了,这里写段代码对比一下带上object和不带object的区别:


class NewClass():
    pass
class NewClass1(object):
    pass

a1 = NewClass()
print dir(a1)
a2 = NewClass1()
print dir(a2)

执行这段代码,发现区别还是很明显的:


['__doc__', '__module__']
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

还可以用__bases__类属性来看一下NewClass和NewClass1的区别,代码如下:


print NewClass.__bases__
print NewClass1.__bases__

结果如下:


()
(<type 'object'>,)

NewClass和NewClass1类的区别很明显,NewClass不继承object对象,只拥有了doc和module,也就是说这个类的命名空间只有两个对象可以操作;而NewClass1类继承了object对象,拥有好多可操作对象,这些都是类中的高级特性。另外,此处如果不加object,有时候还会影响代码的执行结果,所以结合以上种种因素考虑,建议此处带上object。

注意

Python 2.7中默认都是经典类,只有显式继承了object才是新式类,即类名后面括号中需要带上object;Python 3.7(Python3.x)中默认都是新式类,不必显式地继承object。由于本书采用的版本是Python 2.7.10,因此建议此处都带上object。

2.Python装饰器

Python面向对象的开发工作中经常会涉及Python装饰器,它究竟有什么用途呢?

装饰器本质上是一个Python函数,它可以让其他函数不需要做任何代码变动即可增加额外的功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。概括地讲,装饰器的作用就是为已经存在的对象添加额外的功能。

这里先来看一个简单的例子:


def foo():
    print('i am foo')

现在有一个新的需求,即记录下函数的执行日志,于是在代码中添加了日志代码:


import logging
def foo():
    print "i am foo"
    logging.info "foo is running"

那么问题来了,foo1()、foo2()也有类似的需求,怎么做?再写一个logging在foo1和foo2函数里?这样就会造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个函数专门处理日志,日志处理完之后再执行真正的业务代码,示例如下:


import logging
def use_logging(func):
    logging.warn("{} is running".format(func.__name__))
    func()
def bar():
    print "i am bar"
use_logging(bar)

逻辑上不难理解,但是这样做的话,我们每次都要将一个函数作为参数传递给use_logging函数,而且这种方式已经破坏了原有的代码逻辑结构,之前执行业务逻辑时,运行bar()即可,但是现在不得不改成use_logging(bar)。那么有没有更好的处理方式呢?当然有,答案就是使用Python装饰器,示例代码如下:


import logging
def use_logging(func):
    def wrapper(*args, **kwargs):
        logging.warn("{} is running".format(func.__name__))
        return func(*args, **kwargs)
    return wrapper

def bar():
    print "i am bar"

bar = use_logging(bar)
bar()

函数use_logging就是装饰器,它把执行真正业务方法的func包裹在函数里面,看起来像bar被use_logging装饰了。

下面来介绍一下Python程序中出现的@符号。

@符号是装饰器的语法糖(也是Python独有的语法糖,其他语言中没有),在定义函数的时候使用,可避免再一次的赋值操作。也就是说,可以省去bar=use_logging(bar)这一句,直接调用bar()即可得到想要的结果。如果我们有其他的类似函数,可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样就提高了程序的可重复利用性,并增加了程序的可读性。程序如下:


def use_logging(func):
    def wrapper(*args, **kwargs):
        logging.warn("{} is running".format(func.__name__))
        return func(*args)
    return wrapper
@use_logging
def foo1():
    print "i am foo"
@use_logging
def foo2():
    print "i am bar"
foo1()
foo2()

在Python中使用装饰器如此方便,这要归因于Python的函数能像普通的对象一样作为参数传递给其他函数,它可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。@staticmethod和@classmethod这些装饰器在面向对象的开发工作中会经常用到,希望大家都能熟练掌握其用法。

3.面向对象的特性介绍

面向对象的编程带来的主要好处之一是代码的复用,实现这种复用的方法之一是使用继承机制。继承完全可以理解成类之间类型和子类型的关系。

注意,继承的语法为class派生类名(基类名)…,基类名要写在括号里,基本类是在类定义的时候,在元组中指明的。

在Python的面向对象中,继承机制具有如下特点:

·在继承中基类的构造方法__init__()方法不会被自动调用,它需要在其派生类的构造中亲自调用。

·在调用基类的方法时,需要加上基类的类名前缀,且需要带上self参数变量。但在类中调用普通函数时并不需要带上self参数。

·Python总是会首先查找对应类型的方法,如果不能在派生类中找到,才会到基类中逐个查找(先在本类中查找调用的方法,找不到才去基类中找)。

·如果在继承元组中列了一个以上的类,那么它就被称作“多重继承”,也称为“Mixin”。

类的继承语法为:


classname(parent_class1,parent_class2,prant_class3...)

这里举个简单的例子说明其用法,GoldFish类继承自Fish父类,其继承关系如图2-8所示。

图2-8  Python类继承关系图示

完整的代码如下:


#-*- coding:utf-8 -*-

class Fish(object):
    def __init__(self,name):
        self.name = name
        print "我是一条鱼"

class GoldFish(Fish):
    def __init__(self,name):
        Fish.__init__(self,name) #显式调用父类的构造函数
        print "我不仅是条鱼,还是条金鱼"

if __name__ == "__main__":
    aa = Fish('fish')
    bb = GoldFish('goldfish')

输出结果如下:


我是一条鱼
我是一条鱼
我不仅是条鱼,还是条金鱼

可以看到,GoldFish类成功地继承了Fish父类。

在工作中常会遇到在子类里访问父类的同名属性,而又不想直接引用父类名字的情况,因为说不定什么时候会去修改它,所以数据还是只保留一份的好。这时可以采用super()的方式,其语法为:


super(type,object)

type一般接的是父类的名称,object接的是self,示例如下:


#-*- coding:utf-8 -*-
class Fruit(object):
    def __init__(self,name):
        self.name = name
    def greet(self):
        print "我的种类是:{}".format(self.name)

class Banana(Fruit):
    def greet(self):
        super(Banana,self).greet()
        print "我是香蕉,在使用super函数"

if __name__ == "__main__":
    aa = Fruit('fruit')
    aa.greet()


    cc = Banana('banana')
    cc.greet()

输出结果如下:


我的种类是:fruit
我的种类是:banana
我是香蕉,在使用super函数

Banana类在这里也继承了父类Fruit类。此外,在继承父类的同时,子类也可以重写父类的方法,这叫方法重写,示例如下:


class Fruit(object):
    def __init__(self,color):
        self.color = color 
        print "fruit's color %s:" % self.color

    def grow(self):
        print "grow ..."

class Apple(Fruit):
    def __init__(self,color):
        Fruit.__init__(self,color)
        print "apple's clolr {}:".format(self.color)

    def grow(self):
        print "sleep ..."

if __name__ == "__main__":
    apple = Apple('red')
    apple.grow()

程序执行结果如下:


fruit's color red:
apple's clolr red:
sleep ...

另外,通过继承,我们可以获得另一个好处:多态。

多态的好处就是,当我们需要传入更多的子类,例如新增Teenagers、Grownups等时,只需要继承Person类型就可以了,而print_title()方法既可以不重写(即使用Person的),也可以重写一个特有的。调用方只管调用,不管细节,而当我们新增一种Person的子类时,只要确保新方法编写正确即可,不用管原来的代码,这就是著名的“开闭”原则。

·对扩展开放(Open for extension):允许子类重写方法函数。

·对修改封闭(Closed for modification):不重写,直接继承父类方法函数。

来看个示例:


#!/usr/bin/env python
# -*- encoding:utf-8 -*-

class Fruit(object):
    def __init__(self,color = None):
        self.color = color

class Apple(Fruit):
    def __init__(self,color = 'red'):
        Fruit.__init__(self,color)

class Banana(Fruit):
    def __init__(self,color = "yellow"):
        Fruit.__init__(self,color)

class FruitShop:
    def sellFruit(self,fruit):
        if isinstance(fruit,Apple):
            print "sell apple"
        if isinstance(fruit,Banana):
            print "sell banana"
        if isinstance(fruit,Fruit):
            print "sell fruit"

if __name__ == "__main__":
    shop = FruitShop()
    apple = Apple("red")
    banana = Banana('yellow')
    shop.sellFruit(apple)
    #Python的多态性,传递apple
    shop.sellFruit(banana)
    #Python的多态性,传递banana

代码执行结果如下:


sell apple
sell fruit
sell banana
sell fruit

多重继承(也称为Mixin)跟其他主流语言一样,Python也支持多重继承,多重继承虽然有不少好处,但是问题其实也很多,比如存在属性继承等问题,所以我们设计Python多重继承的时候,应尽可能地让代码逻辑简单明了,这里简单说明Python多重继承的用法,示例如下:


class A(object):
    def foo(self):
        print('called A.foo()')

class B(A):
    pass

class C(A):
    def foo(self):
        print('called C.foo()')

class D(B, C): 
    pass

if __name__ == '__main__':
    d = D() 
    d.foo()
    print D.__bases__

在上述代码中,B、C是A的子类,D继承了B和C两个类,其中C重写了A中的foo()方法。

输出结果如下:


called C.foo()
(<class '__main__.B'>, <class '__main__.C'>)

请注意最后一行,这说明D隶属于父类B和C,事实上我们还可以用issubclass()函数来判断,其语法为:


issubclass(sub,sup)

issubclass()返回True的情况为给出的子类属于父类(父类这里也可以是一个元组)的一个子类(反之,则为False),命令如下:


issubclass(D,(B,C))

在命令行下输入上面命令,则返回结果为:


True

另外我们还可以用isinstance()函数来判断对象是否是类的实例,语法如下:


isinstance(obj,class)

如果对象obj是class类的一个实例或其子类的一个实例,会返回True;反之,则返回False。

最后还得提一下多重继承的MRO(方法解释顺序),我们在写类继承时都会带上object类,它采用的是C3算法(类似于广度优先,如果不带上object,它采取的就是深度优先算法,所以为了避免程序的差异性,这里所有基于class类的写法均带上了object类),下面用示例来分析其用法:


class A(object):
    def getValue(self):
        print 'return value of A'
    def show(self):
        print 'I can show the information of A'

class B(A):
    def getValue(self):
        print 'return value of B'

class C(A):
    def getValue(self):
        print 'return value of B'
    def show(self):
        print 'I can show the information of C'
class D(B,C):
    pass


d = D()
d.show()
d.getValue()

输出结果如下:


I can show the information of C
return value of B

用下面的命令打印D类的__mro__属性:


print D.__mro__

结果如下:


(<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>)

从结果可以看出,其继承顺序为D→B→C→A。

注意

事实上,在Python面向对象的开发工作中,我们应该尽量避免采用多重继承。除此之外,还要注意不要混用经典类和新式类,调用父类的时候要注意检查类层次。