Python随学随记(4)

来源:互联网 发布:传奇霸业翅膀升级数据 编辑:程序博客网 时间:2024/05/01 15:30

面向对象编程(Object Oriented Programming )

简称OOP,是一种程序设计思想,OPP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数

面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行
为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度

而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接收其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递

在Python中,所有数据类型都可以视为对象,当然也可以自定义对象
自定义的对象数据类型就是面向对象中的类(Class)的概念

举个栗子:

处理一个成绩表,为了表示一个学生的成绩,面向过程的程序可以用一个dict表示:

std1 = { 'name': 'Michael', 'score': 98 }std2 = { 'name': 'Bob', 'score': 81 }

而处理学生成绩可以通过函数实现,比如打印学生的成绩:

def print_score(std):    print '%s: %s' % (std['name'], std['score'])

如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,而是Student这种数据类型应该被视为一个对象,这个对象拥有namescore这两个属性(Property)

如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给对象发一个print_score消息,让对象自己把自己的数据打印出来:

class Student(object):    def __init__(self, name, score):        self.name = name        self.score = score    def print_score(self):        print '%s: %s' % (self.name, self.score)

给对象发消息实际上就是调用对象对应的关联函数,我们称之为对象的方法(Method)
面向对象的程序写出来就像这样:

bart = Student('Bart Simpson', 59)lisa = Student('Lisa Simpson', 87)bart.print_score()lisa.print_score()

类(Class)和实例(Instance)的概念是很自然的。
Class是一种抽象概念,比如我们定义的Class——Student,是指学生这个概念,而实例(Instance)则是一个个具体的Student,比如,Bart Simpson和Lisa Simpson是两个具体的Student

所以,面向对象的设计思想是抽象出Class,根据Class创建Instance
面向对象的抽象程度又比函数要高,因为一个Class既包含数据,又包含操作数据的方法

  • 数据封装、继承和多态是面向对象的三大特点

类和实例

类是抽象的板,比如Student类,而实例是根据类创建出来的一个个具体的“对象”,每个对象都拥有相同的方法,但各自的数据可能不同

定义类是通过class关键字:

class Student(object):    pass

class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的
通常,如果没有合适的继承类,就使用object类,这是所有类最终都会继承的类

根据Student类创建出Student的实例

>>> bart = Student()>>> bart<__main__.Student object at 0x10a67a590>>>> Student<class '__main__.Student'>

可以看到,变量bart指向的就是一个Studentobject,后面的0x10a67a590内存地址,每个object的地址都不一样,而Student本身则是一个类。

可以自由地给一个实例变量绑定属性:

>>> bart.name = 'Bart Simpson'>>> bart.name'Bart Simpson'

由于类可以起到模板的作用,因此,可以在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。
通过定义一个特殊的__init__方法,在创建实例的时候,就把namescore等属性绑上去:

class Student(object):    def __init__(self, name, score):        self.name = name        self.score = score

__init__方法的第一个参数永远是self,表示创建的实例本身,因此,在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身

>>> bart = Student('Bart Simpson', 59)>>> bart.name'Bart Simpson'>>> bart.score59

和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且调用时,不用传递该参数,
除此之外,类的方法和普通函数没有什么区别,仍然可以用默认参数、可变参数和关键字参数

数据封装

在上面的Student类中,每个实例就拥有各自的namescore这些数据
我们可以通过函数来访问这些数据,比如打印一个学生的成绩:

>>> def print_score(std):...     print '%s: %s' % (std.name, std.score)...>>> print_score(bart)Bart Simpson: 59

既然Student实例本身就拥有这些数据,要访问这些数据,就没有必要从外面的函数去访问,可以直接在Student类的内部定义访问数据的函数,这样,就把“数据”给封装起来了

这些封装数据的函数是和Student类本身是关联起来的,我们称之为类的方法

class Student(object):    def __init__(self, name, score):        self.name = name        self.score = score    def print_score(self):        print '%s: %s' % (self.name, self.score)

这样一来,我们从外部看Student类,就只需要知道,创建实例需要给出namescore,而如何打印,都是在Student类的内部定义的
这些数据和逻辑被“封装”起来了,调用很容易,但却不用知道内部实现的细节

封装的另一个好处是可以给Student类增加新的方法,比如get_grade

class Student(object):...    def get_grade(self):        if self.score >= 90:            return 'A'        elif self.score >= 60:            return 'B'        else:            return 'C'

直接在实例变量上调用:

>>> bart.get_grade()'C'

类是创建实例的模板,而实例则是一个一个具体的对象,各个实例拥有的数据都互相独立,互不影响
方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据
通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。

和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同:

>>> bart = Student('Bart Simpson', 59)>>> lisa = Student('Lisa Simpson', 87)>>> bart.age = 8>>> bart.age8>>> lisa.ageTraceback (most recent call last):  File "<stdin>", line 1, in <module>AttributeError: 'Student' object has no attribute 'age'

访问限制

从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的namescore属性

如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:

class Student(object):    def __init__(self, name, score):        self.__name = name        self.__score = score    def print_score(self):        print '%s: %s' % (self.__name, self.__score)

改完后,对于外部代码来说,没什么变动,但是已经无法从外部访问实例变量.__name实例变量.__score

但是如果外部代码要获取namescore怎么办?可以给Student类增加get_nameget_score这样的方法:

class Student(object):    ...    def get_name(self):        return self.__name    def get_score(self):        return self.__score

如果又要允许外部代码修改score怎么办?可以给Student类增加set_score方法:

class Student(object):    ...    def set_score(self, score):        self.__score = score

为什么要定义一个方法大费周折?
因为在方法中,可以对参数做检查避免传入无效的参数

class Student(object):    ...    def set_score(self, score):        if 0 <= score <= 100:            self.__score = score        else:            raise ValueError('bad score')

双下划线开头的实例变量是不是一定不能从外部访问呢?其实也不是
不能直接访问__name是因为Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:

>>> bart._Student__name'Bart Simpson'

但是强烈不建议这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名

总的来说就是,Python本身没有任何机制阻止你干坏事,一切全靠自觉

继承和多态

当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass)
而被继承的class称为基类父类超类(Base class、Super class)

继承有什么好处?最大的好处是子类获得了父类的全部功能
当然,也可以对子类增加一些方法

class Animal(object):    def run(self):        print 'Animal is running...'class Dog(Animal):    def run(self):        print 'Dog is running...'class Cat(Animal):    def run(self):        print 'Cat is running...'

当子类和父类都存在相同的run()方法时,我们说,子类的run()覆盖了父类的run(),在代码运行的时候,总是会调用子类的run()
这样,我们就获得了继承的另一个好处:多态

要理解什么是多态,我们首先要对数据类型再作一点说明
当我们定义一个class的时候,我们实际上就定义了一种数据类型
我们定义的数据类型和Python自带的数据类型,比如str、list、dict没什么两样:

a = list() # a是list类型b = Animal() # b是Animal类型c = Dog() # c是Dog类型>>> isinstance(a, list)True>>> isinstance(b, Animal)True>>> isinstance(c, Dog)True>>> isinstance(c, Animal)True

看来c不仅仅是Dogc还是Animal
当我们创建了一个Dog的实例c时,我们认为c的数据类型是Dog没错,但c同时也是Animal也没错,Dog本来就是Animal的一种

在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类,但是,反过来就不行

要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:

def run_twice(animal):    animal.run()    animal.run()

当我们传入Animal的实例时,run_twice()就打印出:

>>> run_twice(Animal())Animal is running...Animal is running...

当我们传入Dog的实例时,run_twice()就打印出:

>>> run_twice(Dog())Dog is running...Dog is running...

现在,如果我们再定义一个Tortoise类型,也从Animal派生:

class Tortoise(Animal):    def run(self):        print 'Tortoise is running slowly...'

当我们调用run_twice()时,传入Tortoise的实例:

>>> run_twice(Tortoise())Tortoise is running slowly...Tortoise is running slowly...

实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态

多态的好处就是,当我们需要传入DogCatTortoise……时,我们只需要接收Animal类型就可以了
因为DogCatTortoise……都是Animal类型,然后,按照Animal类型进行操作即可

由于Animal类型有run()方法,因此,传入的任意类型,只要是Animal类或者子类,就会自动调用实际类型的run()方法

对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在AnimalDogCat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:

调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的
这就是著名的:

“开闭”原则

扩展开放:允许新增Animal子类

修改封闭:不需要修改依赖Animal类型的run_twice()等函数

继承还可以一级一级地继承下来,就好比从爷爷到爸爸、再到儿子这样的关系
而任何类,最终都可以追溯到根类object,这些继承关系看上去就像一颗倒着的树, 比如如下的继承树:
这里写图片描述

继承可以把父类的所有功能都直接拿过来,这样就不必重零做起,子类只需要新增自己特有的方法,也可以把父类不适合的方法覆盖重写

有了继承,才能有多态
在调用类实例方法的时候,尽量把变量视作父类类型,这样,所有子类类型都可以正常被接收

旧的方式定义Python类允许不从object类继承,但这种编程方式已经严重不推荐使用,任何时候如果没有合适的类可以继承,就继承自object

获取对象信息的方法

type( )type()函数返回的是type类型, 如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:

>>> type(123)==type(456)True>>> type('abc')==type('123')True>>> type('abc')==type(123)False

Python把每种type类型都定义好了常量,放在types模块里,使用之前,需要先导入

>>> import types>>> type('abc')==types.StringTypeTrue>>> type(u'abc')==types.UnicodeTypeTrue>>> type([])==types.ListTypeTrue>>> type(str)==types.TypeTypeTrue

最后注意到有一种类型就叫TypeType所有类型本身的类型就是TypeType,比如:

>>> type(int)==type(str)==types.TypeTypeTrue

isinstance( ): 作用是判断class的类型, 能用type()判断的基本类型也可以用isinstance()判断:

>>> isinstance('a', str)True>>> isinstance(u'a', unicode)True>>> isinstance('a', unicode)False

isinstance()判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上

dir( ):获得一个对象的所有属性和方法,可以使用dir()函数,它返回一个包含字符串的list,比如,获得一个str对象的所有属性和方法:

>>> dir('ABC')['__add__', '__class__', '__contains__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__getslice__', '__gt__', '__hash__', '__init__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_formatter_field_name_split', '_formatter_parser', 'capitalize', 'center', 'count', 'decode', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'index', 'isalnum', 'isalpha', 'isdigit', 'islower', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

在Python中,如果你调用len()函数试图获取一个对象的长度
实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以,下面的代码是等价的:

>>> len('ABC')3>>> 'ABC'.__len__()3

仅仅把属性和方法列出来是不够的,配合getattr()setattr()以及hasattr(),我们可以直接操作一个对象的状态:

>>> class MyObject(object):...     def __init__(self):...         self.x = 9...     def power(self):...         return self.x * self.x...>>> obj = MyObject()

接着可以测试该对象的属性

>>> hasattr(obj, 'x') # 有属性'x'吗?True>>> obj.x9>>> hasattr(obj, 'y') # 有属性'y'吗?False>>> setattr(obj, 'y', 19) # 设置一个属性'y'>>> hasattr(obj, 'y') # 有属性'y'吗?True>>> getattr(obj, 'y') # 获取属性'y'19>>> obj.y # 获取属性'y'19

如果试图获取不存在的属性,会抛出AttributeError的错误
也可以传入一个default参数,如果属性不存在,就返回默认值

>>> getattr(obj, 'z', 404) # 获取属性'z',如果不存在,返回默认值404404>>> hasattr(obj, 'power') # 有属性'power'吗?True>>> getattr(obj, 'power') # 获取属性'power'<bound method MyObject.power of <__main__.MyObject object at 0x108ca35d0>>>>> fn = getattr(obj, 'power') # 获取属性'power'并赋值到变量fn>>> fn # fn指向obj.power<bound method MyObject.power of <__main__.MyObject object at 0x108ca35d0>>>>> fn() # 调用fn()与调用obj.power()是一样的81
0 0