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面向对象的开发工作中,我们应该尽量避免采用多重继承。除此之外,还要注意不要混用经典类和新式类,调用父类的时候要注意检查类层次。