面向对象编程

面向对象编程

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值