面向对象编程

面向对象编程

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.score
59
>>> 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.age
8
>>> kevin.age
Traceback (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.weight
126
>>> kevin.weight = 130
>>> kevin.weight
130

如果要让内部属性不被外部访问,可以在属性的名称前加上两个下划线。在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.__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-6-1abc5119b3fc>", line 1, in <module>
    zhengning.__height
AttributeError: '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__height
163

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

class 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)  int
True
>>>type('123')  type('abc')  # 注意123加了引号
True
>>>type(123)  type('abc')
False

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

>>>import types
>>>def fcn():
       pass
>>>type(fcn)  types.FunctionType
True
>>>type(abs)  types.BuiltinFunctionType
True
>>>type(lambda x: x)  types.LambdaType
True
>>>type(x for x in range(10))  types.GeneratorType
True

使用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.age
25

如果试图获取不存在的属性,会抛出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有方法'love'
True
>>>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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容