面向对象编程

来源:互联网 发布:程序员培训全日制吗 编辑:程序博客网 时间:2024/04/30 05:39

面向对象编程

6.1 类和实例

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

class Student(object):    pass

可以自由地给一个实例变量绑定属性,比如,给实例bart绑定一个name属性:

>>> class Student(object):...     pass>>> bart = Student()>>> bart.name = 'Bart.Simpson'>>> bart.name'Bart.Simpson'

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

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

注意到_init_方法的第一个参数永远是self,表示创建的实例本身。因此,在_init_方法内部,就可以把各种属性绑定到self,因为self就指向创建实例的本身。
有了_init_方法就,在创建实例的方法匹配的参数时候就不能传入空的参数了,必须传入与_init_方法匹配的参数,但self不需要传,Python解释器自己会把实例变量传进去。

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

数据封装

可以直接在类的内部定义访问数据的函数,这样,就把“数据”封装起来了。这些封装数据的函数是和类本身是关联起来的,称之为类的方法

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))

要定义一个方法,除了第一个参数是self外,其他和普通函数一样。要调用一个方法,只需要在实例变量上直接调用,除了self不用传递,其他参数正常传人。

>>> bart = Student('Kevin', 99)>>> bart.print_score()Kevin: 99

这样的话,我们从外部看Student类,就只需要知道,创建实例需要给出类的属性name和score,而如何打印,则是在Student类的内部定义的。
封装的另一个优点在于,可以给类增加新的方法。比如下例给Person类增加新的判别身材的方法:

class Person(object):    def __init__(self, name, sex, height,weight):        self.sex = sex        self.height = height        self.name = name        self.weight = weight    def print_sex_height(self):        print('%s: %s, %s' %(self.name, self.sex, self.height))    def figure(self):        if self.weight > 140:            print('Too fat, you should take more exsrcise.')        elif self.weight <110:            print('Too thin, you should be extra mindful of getting the right nourishment.')        else:            print('You have a good figure, keep it.')

同样的,新增的figure方法可以直接在实例变量上调用,不需要知道内部实现细节。

>>> baby = Person('zhengning', 'female', 162, 98)>>> baby.print_sex_height()zhengning: female, 162>>> baby.figure()Too thin, you should be extra mindful of getting the right nourishment.

小结

  1. 类是创建实例的模板,而实例则是一个一个具体的对象,每个实例拥有的数据都互相独立,互不影响。
  2. 方法就是与实例绑定的函数,和普通函数不同,方法可以直接访问实例的数据。
  3. 通过在实例上调用方法,我们就直接操作了对象内部的数据,但无需知道方法内部的实现细节。
  4. 和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然它们都是同一个类的不同实例,但拥有的变量名称都可能不同。
>>> baby = Person('zhengning', 'female', 162, 98)>>> kevin = Person('wukaiwen', 'male', 165, 126)>>> baby.age = 8>>> baby.age8>>> kevin.ageTraceback (most recent call last):  File "<stdin>", line 1, in <module>AttributeError: 'Person' object has no attribute 'age'

6.2 访问限制

在class内部,可以通过定义属性和方法,而外部代码则可以通过直接实例变量的方法来操作数据。这样,就隐藏了内部的复杂逻辑。但是这样,则存在一个内部属性易被修改的问题,即外部代码能通过实例的方法来修改类的属性。

>>> kevin = Person('wukaiwen', 'male', 165, 126)>>> kevin.weight126>>> kevin.weight = 130>>> kevin.weight130

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

class Person(object):    def __init__(self, name, sex, height, weight):        self.__sex = sex        self.__name = name        self.__height = height        self.__weight = weight

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

>>>zhengning = Person('zhengning', 'female', 162, 98)>>>zhengning.__heightTraceback (most recent call last):  File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code    exec(code_obj, self.user_global_ns, self.user_ns)  File "<ipython-input-6-1abc5119b3fc>", line 1, in <module>    zhengning.__heightAttributeError: 'Person' object has no attribute '__height'

这样保证了外部代码不能随意修改对象内部的状态,即通过访问限制的保护,代码更加健壮。
如果要获取类内部的私有属性的话,可以给Person类增加诸如get_name和get_sex这样的方法:

class Person(object):    ...    def get_name(self):        return self.__name    def get_sex(self):        return self.__sex

此时,调用方法获取实例的姓名和性别及输出如下:

>>>baby = Person('zhengning', 'female', 162, 98)>>>baby.get_name()'zhengning'>>>baby.get_sex()'female'

如果又要允许外部代码修改height和weight的话,可以再给Person类增加set_height和set_weight方法:

class Person(object):    ...    def set_name(self):        self.__name = name    def set_sex(self):        self.__sex = sex

此时,调用方法获取实例的姓名和性别及输出如下:

>>>from test1 import *>>>baby = Person('zhengning', 'female', 162, 98)>>>baby.get_height()162>>>baby.set_height(165)>>>baby.get_height()165

这里我们就需要考虑一个问题了,即最开始直接通过修改实例的属性kevin.weight = 130即可修改属性值,那么为什么要单独定义一个方法来修改属性呢?这是因为在方法中,可以对参数做检查,避免传入无效的参数。

class Person(object):    ...    def set_weight(self, weight):        if 0 <= weight <= 200:            self.__weight = weight        else:            raise ValueError('Invalid weight.')

使用情况如下:

>>>from test1 import *>>>baby = Person('zhengning', 'female', 162, 210)>>>baby.get_weight()210>>> baby.set_weight(102)>>>baby.get_weight()102>>>baby.set_weight(220)Traceback (most recent call last):  File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code    exec(code_obj, self.user_global_ns, self.user_ns)  File "<ipython-input-7-038c5fd42666>", line 1, in <module>    baby.set_weight(220)  File "G:/PyCharm/PycharmProjects/Python_Liaoxuefeng/chapter06\test1.py", line 25, in set_weight    raise ValueError('Invalid weight.')ValueError: Invalid weight.

上面说了这么多,我们再来考虑这么一个问题:双下划线开头的实例是不是一定不能从外部访问呢?其实也不是。不能直接访问的原因在于Python解释器对外把__name变量改成了_Student__name,所以,仍然可以通过_Student__name来访问__name变量:

>>>zhengning._Person__height = 163>>>zhengning._Person__height163

6.3 继承和多态

在面向对象的程序设计(OPP)中,当我们定义一个类class时,可以从某个现有的已定义的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类(Base class、Super class)。
比如,我们已经编写了一个名为Fruit的class,有一个run()方法可以直接打印:

class Fruit(object):    def taste(self):        print('Fruit is delicious.')

当我们需要编写Apple类和Pear类时,就可以直接从Fruit类继承:

class Apple(Fruit):    passclass Pear(Fruit):    pass

对于Apple类和Pear类而言,Fruit类就是它们的父类,而它们就是Fruit类的子类。
继承有什么好处呢?最大的好处是子类获得了父类的全部功能。上述中,由于Fruit类定义了taste方法,因此,Apple和Pear作为它的子类,什么事也没干,就自动获得了taste()方法:

>>>from test2 import *>>>apple = Apple()>>>apple.taste()Fruit is delicious.>>>pear = Pear()>>>pear.taste()Fruit is delicious.

当然,也可以直接对子类增加一些方法,比如在子类Apple中:

class Apple(Fruit):    def color(self):        print('The apple is red.')

继承的第二个好处是:多态即当子类和父类都存在相同的方法时,子类的方法会覆盖父类的方法

class Apple(Fruit):    def taste(self):        print('Fruit is delicious.')class Pear(Fruit):    def taste(self):        print('Fruit is delicious.')

再次运行,结果如下:

>>>from test2 import *>>>apple = Apple()>>>apple.taste()The apple is delicious.>>>pear = Pear()>>>pear.taste()The pear is delicious.

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

aList = list()  # a是list类型fruit = Fruit()  # b是Fruit类型apple = Apple()  # c是Apple类型

判断一个变量是否是某个类型可以用isinstance()判断:

>>>isinstance(apple, Apple)True

现在,我们再思考一个关于继承的问题:顾名思义,如果一个实例的数据类型是某个子类例如Aplle类型,那么它是否也属于该类的父类Fruit类型呢?答案是肯定的:

>>>isinstance(apple, Fruit)True

那么多态的好处在哪里呢?我们需要再编写一个函数,这个函数接受一个Fruit类型的变量:

def taste_twice(fruit):    fruit.taste()    fruit.taste()

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

>>>taste_twice(Fruit())Fruit is delicious.Fruit is delicious.

当我们传入Apple实例时,taste_twice()就打印出:

>>>taste_twice(Apple())The apple is delicious.The apple is delicious.

在这种情况下,如果我们再定义一个Orange类型,也从Fruit派生出来:

class Orange(Fruit):    def taste(self):        print('The orange is delicious.')

当我们调用taste_twice()时,传入Orange的实例:

>>>taste_twice(Orange())The orange is delicious.The orange is delicious.

这时,我们发现,新增一个Fruit的子类Orange,而不必对taste_twice()做任何修改。实际上,任何以Fruit作为参数的函数或者方法都可以不加修改地正常运行,原因就在于多态。


静态语言 vs 动态语言

对于静态语言而言,如果需要传入Fruit类型,则传入的对象必须是Fruit类型或者它的子类,否则,将无法调用run()方法。
对于Python这样的动态语言来说,则不一定需要传入Fruit类型,我们只需要保证传入的对象有一个run()就可以了:

class Timer(object):    def taste(self):        print('Start...')

调用结果如下:

>>>taste_twice(Timer())Start...Start...

小结

  1. 继承可以把父类的所有功能都直接拿过来,而不必直接定义属性和方法。
  2. 多态则使得子类可以新增自己特有的方法,也可以把父类不合适的方法覆盖重写,只需从新定义和父类相同的方法。

6.4 获取对象信息

当我们拿到一个对象的引用时,可以用哪些方法来知道这个对象是什么类型呢?

使用type()

首先,我们使用type()函数来判断对象类型,基本类型都可以通过type()来判断:

>>>type(123)int>>>type('str')str

如果一个变量指向函数或者类,也可以用type()判断:

>>>type(abs)builtin_function_or_method

通过上述几个例子,可以看出type()函数返回的类型是对象所应的class类型。如果我们要在if语句中判断,就需要比较两个变量的type类型是否相同:

>>>type(123)  type(456)True>>>type(123)  intTrue>>>type('123')  type('abc')  # 注意123加了引号True>>>type(123)  type('abc')False

判断基本类型可以直接写int,str等,但如果要判断一个对象是否是函数怎么办?可以使用types模块中定义的常量:

>>>import types>>>def fcn():       pass>>>type(fcn)  types.FunctionTypeTrue>>>type(abs)  types.BuiltinFunctionTypeTrue>>>type(lambda x: x)  types.LambdaTypeTrue>>>type(x for x in range(10))  types.GeneratorTypeTrue

使用instance()

对于class的继承关系来说,使用type()就不太方便。我们要判断class类型的时候,可以使用instance()函数,这在上节使用过。
此外,还可以判断一个变量是否是某些类型中的一种:

>>>isinstance([1, 2, 3], (list, tuple))True>>>isinstance((1, 2, 3), (list, tuple))True

这里需要注意几个问题:
* 能用type()判断的基本类型也可以使用isinstance()判断
* 可以在if判断语句中,同时使用多个type()和isinstance()来进行逻辑运算


使用dir()

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

>>>dir('zhengning')Out[22]: ['__add__', '__class__', '__contains__', '__len__', ... 'translate', 'upper', 'zfill']

类似__xxx__的属性和方法在Python中都是有特殊用途的,比如__len__方法返回长度。在Python中,如果你试图调用len()函数试图获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法,所以下面的代码是等价的:

>>>len('zhengning')9>>>'zhengning'.__len__()9

我们自己写的类,也想用len(myObj)的话,就自己写一个__len__方法:

class MyObj(object):    def __len__(self):        return 10>>>baby = MyObj()>>>len(baby)100

剩下的就都是普通属性或方法,比如upper()返回大写的字符串:

>>>'zhengning'.upper()'ZHENGNING'

上述只说明了如何把属性和方法列出来,其实配合getattr()、setattr()、hasattr(),我们可以直接操作一个对象的状态:

class MyObj(object):    def __init__(self):        self.name = 'zhengning'    def love(self):        print('I love u.')>>>from test3 import *>>>baby = MyObj()>>>hasattr(baby, name)  # 属性name需要加引号,否则会报错Traceback (most recent call last):  File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code    exec(code_obj, self.user_global_ns, self.user_ns)  File "<ipython-input-4-e82eb9214bf5>", line 1, in <module>    hasattr(baby, name)NameError: name 'name' is not defined>>>hasattr(baby, 'name')  # 检查是否有属性'name'True>>>baby.name  # 获取属性'name''zhengning'>>>hasattr(baby, 'age')  # 检查是否有属性'age'False>>>setattr(baby, 'age', 25)  # 设置一个属性'age'>>>hasattr(baby, 'age')True>>>getattr(baby, 'age')  # 获取属性'age'25>>>baby.age25

如果试图获取不存在的属性,会抛出AttributeError的错误:

>>>getattr(baby, 'height')  # 获取baby的属性'height',没有该属性则报错Traceback (most recent call last):  File "G:\Anaconda\Anaconda3\lib\site-packages\IPython\core\interactiveshell.py", line 2881, in run_code    exec(code_obj, self.user_global_ns, self.user_ns)  File "<ipython-input-14-15ec7e1fc4a1>", line 1, in <module>    getattr(baby, 'height')AttributeError: 'MyObj' object has no attribute 'height'

也可以获得对象的方法:

>>>hasattr(baby, 'love')  # 检查对象baby有方法'loveTrue>>>getattr(baby, 'love')  # 获取对象baby的方法'love'<bound method MyObj.love of <test3.MyObj object at 0x000002074210F6A0>>>>>myLove = getattr(baby, 'love')  # 获取方法'love'并赋值给变量myLove>>>myLove  # myLove指向baby.love<bound method MyObj.love of <test3.MyObj object at 0x000002074210F6A0>>>>>myLove()  # 调用myLove()与调用baby.love()是一样的I love u.

小结

通过内置的一系列函数,我们可以对任意一个Python对象进行剖析,拿到其内部的数据。但需要注意的是,只有在不知道对象信息的时候,我们才会取获取信息。比如,如果可以直接写:

sum = obj.x + obj.y

就不要写:

sum = getattr(obj, 'x') + getattr(obj, 'y')

一个正确的用法的例子如下:

def readImage(fp):    if hasattr(fp, 'read'):        return readData(fp)    return None

假设我们希望从文件流fp中读取图像,我们首先要判断fp对象中是否存在read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()就派上了用场。


6.5 实例属性和类属性

由于Python是动态语言,根据类创建的实例可以任意绑定属性。给实例绑定属性有两种方法,即通过实例变量或者self变量:

class Person(object):    def __init__(self, name):  # 通过self变量创建属性        self.name = name>>>baby = Person('zhengning')>>>baby.name  'zhengning'>>>baby.age = 25  # 通过实例变量创建属性

那么,Person类本身怎么绑定属性呢?可以直接在class中定义属性,这种属性是类属性,归Person类所有:

class Person(object):    race = 'Asian'    def __init__(self, name):        self.name = name

当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到:

>>>from test3 import *  # 程序保存在test3.py文件中>>>baby = Person('zhengning')  # 创建实例baby>>>baby.race  'Asian'>>>print(baby.race)  # 打印race属性,因为实例并没有race属性,所以会基础查找class的name属性。注意和上一条语句进行对比Asian>>>print(Person.race)  # 打印类的race属性Asian>>>baby.race = 'Han'  # 给实例绑定race属性>>>print(baby.race)  # 由于实例属性的优先级比类属性高,因此,它会屏蔽掉类的race属性Han>>>print(Person.race)  # 但是类属性并未消失,仍然可以用左边这种方式访问Asian>>>del baby.race  # 如果删除实例的race属性>>>print(baby.race)  # 再次调用baby.race,由于实例的race属性没有找到,类的race属性就显示出来了Asian

从上面的例子可以看出,在编写程序的时候,千万不能把实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性。但是当你删除实例属性后,再使用相同的名称,访问到的将是类属性。

0 0
原创粉丝点击