Python 进阶学习笔记之八:面向对象高级编程

Python 进阶系列笔记文章链接:
Python 进阶学习笔记之一:内置常用类型及方法
Python 进阶学习笔记之二:常用数据类型(上)
Python 进阶学习笔记之三:常用数据类型(下)
Python 进阶学习笔记之四:高效迭代器工具
Python 进阶学习笔记之五:异步 IO
Python 进阶学习笔记之六:多线程编程
Python 进阶学习笔记之七:互联网支持
Python 进阶学习笔记之八:面向对象高级编程
Python 进阶学习笔记之九:IO 编程
Python 进阶学习笔记之十:一般加密支持
Python 进阶学习笔记之十一:日志支持
Python 进阶学习笔记之十二:数据压缩与归档

面向对象编程——Object Oriented Programming,简称OOP,是一种程序设计思想,编码之前先要进行进行数据的抽象设计,把数据的特点特性进行抽象分类,进而抽象成一个一个的类,而对象就是一个个具体化的 “数据-行为” 个体。

1 面向对象基本概念

1.1 一个简单的类实现

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

在一个类中以两个下滑线开头和结尾的都有特殊含义,上面哪个类中的方法__init__ 方法就是类的构造方法(初始化方法),而其内的 name 和 score 就是实例属性,需要在实例化一个对象时进行赋值(初始化)。类中方法和普通函数主要区别是,类中普通方法的第一个参数是self,指向了调用对象本身,因此对象中的属性在这个方法中都能直接引用。

1.2 访问限制

在 Class 内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑,这称为数据封装。但是,从前面 Student 类的定义来看,外部代码还是可以自由地修改一个实例的name、score属性:

>>> bart = Student('Bart Simpson', 98)
>>> bart.score
98
>>> bart.score = 59
>>> bart.score
59

一般我们做类设计时,目的之一就是为了隐藏数据操作或者统一数据操作,一般对外提供一个修改属性的方法,而不是直接对外开放属性,比如添加一个修改 score 的方法:

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

这种方法的好处是,类提供者可以控制赋值操作的有效性。但要怎样对外隐藏属性?如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线 __,在 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 了。如果外部需要获取属性值,我们也像上面提供修改属性方法那样提供一个获取方法即可:

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

    def get_score(self):
        return self.__score

需要注意的是,在Python中,变量名类似 __xxx__ 的,也就是以双下划线开头,并且以双下划线结尾的,是特殊变量,特殊变量是可以直接访问的,不是private变量,所以,不能用 __name__、__score__ 这样的变量名。

有些时候,你会看到以一个下划线开头的实例变量名,比如_name,这样的实例变量外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的变量时,意思就是,“虽然我可以被访问,但是,请把我视为私有变量,不要随意访问”。

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

>>> bart._Student__name
'Bart Simpson'

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

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

1.3 继承和多态

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

继承 顾名思义就是从子从父哪里获取一些东西,具体来讲就是子类从父类哪里获取其属性和方法,前面说过,类是一种抽象的概念,抽象程度越高,其所表示的范围也就越大,而子类就是继承其特性然后缩小抽象范围并对不合适的部分进行覆盖重写。代码示例:

class Animal(object):
    def run(self):
        print('Animal can run...')

class Dog(Animal):
    pass

class Cat(Animal):
    pass

dog = Dog()
dog.run()    # 输出 Animal can run...

cat = Cat()
cat.run()    # 输出 Animal can run...

类 Dog 和 Cat 继承了 Animal,自然获取了父类中的 run 方法,除此之外,这两个类就是普通的两个 Python 类,可以在定义自己的属性和方法。如果 Dog 或者 Cat 对继承得到的 run 方法不满意,可以重写:

class Dog(Animal):
    def run(self):
        print('Dog can run...')

dog = Dog()
dog.run()          # 输出 Dog can run...

注意:在 Python2 中每个类需要显示的指定其父类,而在 Python3 中如果不写,默认继承 object 类

1.4 多态

多态 就是多种状态,大概意思只一个对象可能处于不同的状态。什么意思?要理解什么是多态,我们首先要对数据类型再作一点说明。当我们定义一个 class 的时候,我们实际上就定义了一种数据类型。我们定义的数据类型和 Python 自带的数据类型,比如 str、list、dict 没什么两样,它们也是普通的 class。判断一个变量是否是某个类型可以用 isinstance() 判断:

>> 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 不仅仅是 Dog,还是Animal。在继承关系中,如果一个实例的数据类型是某个子类,那它的数据类型也可以被看做是父类。但是,反过来就不行。

多态最大的好处就是可以运行是确定一个变量的类型。要理解多态的好处,我们还需要再编写一个函数,这个函数接受一个Animal类型的变量:

>>> def animal_run(animal):
>>>     animal.run()
>>> animal_run(Animal())
Animal can run...
>>> animal_run(Dog())
Dog can run...

可以看到,就算在从 Animal 派生多少子类都可以适用这个方法,而且这个方法不用做什么任何改变。

多态的好处就是,当我们需要传入 Dog、Cat……时,我们只需要接收 Animal 类型就可以了,因为 Dog、Cat……都是 Animal 类型,然后,按照 Animal 类型进行操作即可。由于 Animal 类型有 run() 方法,因此,传入的任意类型,只要是 Animal 类或者子类,就会自动调用实际类型的run()方法,这就是多态的意思。

Python 中继承关系可以一直持续下去,即 Dog 也可以派生它的子类,也就是抽象的范围再一次缩小,原理的形式和上面没上面区别。

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

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

这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
Python 的 “file-like object“ 就是一种鸭子类型。对真正的文件对象,它有一个 read() 方法,返回其内容。但是,许多对象,只要有 read() 方法,都被视为 “file-like object“。许多函数接收的参数就是 “file-like object“ ,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。

1.5 日常小技巧

适用type() 可以查看和判断类型:

>>> type(123)
<class 'int'>
>>> type(123)==type(456)
True
>>> type(123)==int
True

判断函数需要模块 types 支持:

>>> import types
>>> def fn():
...     pass
...
>>> type(fn)==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

适用isinstance()判断 class 的类型,上面已经用过,不多说。

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

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

类似 __xxx__ 的属性和方法在 Python 中都是有特殊用途的,比如 len 方法返回长度。在 Python 中,如果你调用 len() 函数试图获取一个对象的长度,实际上,在 len() 函数内部,它自动去调用该对象的 len() 方法。我们自己写的类,如果也想用len(myObj)的话,就自己写一个__len__()方法:

>>> class MyDog(object):
...     def __len__(self):
...         return 100
...
>>> dog = MyDog()
>>> len(dog)
100

2 面向对象的高级特性

数据封装、继承和多态只是面向对象程序设计中最基础的3个概念。在Python中,面向对象还有很多高级特性,允许我们写出非常强大的功能。

2.1 动态绑定以及 __slots__ 应用

一般情况下,我们定义完一个 class,里面已经包含了一些属性和方法,就等着实例化对象对属性进行赋值,在 Java 这种静态语言中,一个类在运行过程中被加入到内存后,它的结构就是固定的(后续JVM可能会作出改变),但在 Python 中,从一个类实例化得到一个对象后,我们确可以动态的对这个对象进行额外的属性绑定和方法绑定,这就是动态语言的灵活性。先定义 class:

from types import MethodType

class Student(object):
    pass
    
s = Student()
s.name = 'Michael'   # 动态给实例绑定一个属性
print(s.name)        # 输出 Michael

def set_age(self, age): # 定义一个函数作为实例方法
    self.age = age

s.set_age = MethodType(set_age, s) # 给实例绑定一个方法
s.set_age(25)                  # 调用实例方法
print(s.age)                   # 输出 25

但要注意,这样给一个实例绑定的方法,对另一个实例是不起作用的,如果必要可以给类绑定属性,这样其所有示例对象就都可以用了:

Student.set_score = set_score
s1 = Student()
s2 = Student()
s1.set_score(12)
s2.set_score(13)

通常情况下,上面的 set_score 方法应该直接定义在 class 中,但动态绑定虽然允许我们在程序运行的过程中动态给 class 加上功能,但会在调试和其他代码维护人员带来不便。

为了限制动态绑定被滥用,Python 提供但一个限制机制,那就是在类声明指定 __slots__。比如,只允许对Student实例添加name和age属性:

class Student(object):
    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称

我们测试一下:

>>> s = Student() # 创建新的实例
>>> s.name = 'Michael' # 绑定属性'name'
>>> s.age = 25 # 绑定属性'age'
>>> s.score = 99 # 绑定属性'score'
Traceback (most recent call last):
  File "", line 1, in <module>
AttributeError: 'Student' object has no attribute 'score'

由于 ‘score’ 没有被放到 __slots__ 中,所以不能绑定 score 属性,试图绑定score将得到 AttributeError 的错误。

**注意: **
使用 __slots__ 要注意,__slots__ 定义的属性仅对当前类实例起作用,对继承的子类是不起作用的:

>>> class GraduateStudent(Student):
...     pass
...
>>> g = GraduateStudent()
>>> g.score = 9999

除非在子类中也定义__slots__,这样,子类实例允许定义的属性就是自身的__slots__加上父类的__slots__。

2.2 使用@property

前面说过,类的一大特性是数据封装,也提到通过暴露方法的方式间接暴露属性的读写,实际上 Python 有更优雅的方式来完成这些常规操作,那就是其内置装饰器 @property,这个装饰器就是用来把方法变成属性来访问的:

class Student(object):

    @property
    def score(self):
        return self._score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an integer!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100!')
        self._score = value

使用 @property 来修饰的方法一般我称之为 getter 方法,而用 @score.setter 修饰的方法称之为 setter方法。应用来看看:

>>> s = Student()
>>> s.score = 60       # OK,实际转化为s.set_score(60)
>>> s.score            # OK,实际转化为s.get_score()
60
>>> s.score = 9999
Traceback (most recent call last):
  ...
ValueError: score must between 0 ~ 100!

还可以定义只读属性,只定义 getter 方法,不定义 setter 方法就是一个只读属性。

@property 广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

2.3 多重继承

我们已经指定来一个类可以从一个类中继承得到一些特性,但如果还想得到另一个类的特性怎么办?答案是,同时继承它。这就是多重继承。

多重继承没有特别的,和单一继承也没有任何特别注意之处:

class Bat(Mammal, Flyable):
    pass

——————
继续阅读请点击:Python 进阶学习笔记之九:IO 编程

你可能感兴趣的:(Python,面向对象,继承,多态,__slots__)