Pyhton学习笔记七:面向对象编程

  • Object Oriented Programming(面向对象编程),简称OOP,OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数;
  • 面向过程的程序设计把计算机程序视为一系列的命令集合,即一组函数的顺序执行,为了简化程序设计,面向过程把函数继续切分为子函数,即把大块函数通过切割成小块函数来降低系统的复杂度;
  • 而面向对象的程序设计把计算机程序视为一组对象的集合,而每个对象都可以接受其他对象发过来的消息,并处理这些消息,计算机程序的执行就是一系列消息在各个对象之间传递;
  • 在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既包含数据,又包含操作数据的方法。
  • 数据封装继承多态是面向对象的三大特点;

1. 类(class)和实例(instance)

  • 类和实例是面向对象中最重要的概念,类是抽象出来的模板,比如Student类,而实例是根据类创建出来的一个个具体的’对象’,每个对象都拥有相同的方法,但各自的数据可能不同;
  • 在Python中,定义类是通过class关键字,class后面紧接的是类名,即Student,类名通常以大写字母开头,紧接着是(object),表示该类是从那个类继承下来的,通常没有合适的继承类,就使用object类,这是所有类最终都会继承的类
class Student(object):
    pass
  • 定义好了Student类,就可以根据Student类创建出Student的实例,创建实例是通过类名+()实现的;
  • 变量bart指向的就是一个Student的实例,后面的0x000002A5BF8D7E80是内存地址,每个object的地址都不一样,而Student本身就是一个类;
>>> bart = Student()
>>> bart
<Student object at 0x000002A5BF8D7E80>
>>> Student
<class 'Student'>
  • 也可以自由的给一个实例变量绑定属性,如,给实例bart绑定一个name属性:
# 
>>> bart.name = 'ABC'
>>> bart.name
'ABC'
# 通过定义一个特殊的__init__方法,在创建实例时,就把name, score等属性绑上去
class Student(object):
    def __init__(self, name, score):  # __init__方法的第一个参数永远是self,表示创建的实例本身
        self.name = name  # 因此在__init__方法内部,就可以把各种属性绑定到self,因为self就指向创建的实例本身
        self.score = score
# 有了__init__方法,在创建实例的时候,必须传入与__init__方法匹配的参数,但self不用传,Python解释器会自己把实例变量传进去
>>> bart = Student('ABC', 59)
>>> bart.name
'ABC'
>>> bart.score
59
  • 和普通的函数相比,在类中定义的函数只有一点不同,就是第一个参数永远是实例变量self,并且,调用时,不用传递该参数。除此之外,类的方法和普通函数没有什么区别,所以,你仍然可以用默认参数、可变参数、关键字参数和命名关键字参数。

1.1 数据封装

  • 在上面的Student类中,每个实例拥有各自的namescore这些数据,可以通过函数来访问这些数据:
# 打印一个学生的成绩:
>>> def print_score(std):
...     print('%s: %s' % (std.name, std.score))
...
>>> print_score(bart)
ABC: 59
  • 既然Student实例本身就拥有这些数据,就没必要在外面的函数访问,可以直接在Student类内部定义访问数据的函数,这样就把数据给封装起来了,这些封装数据的函数和Student类本身是关联的,我们称之为类的方法:
# 在pycharm中将此程序打包为class_instance.py
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))

# 先将类导入
>>> from class_instance import Student

# 要调用一个方法,只需要在实例上直接调用,除了self不用传递,其他参数正常传入:
>>> bart = Student('ABC', 59)  # 创建一个实例
>>> bart.print_score()
ABC: 59
  • 从外部看Student类,就只需知道,创建实例需要给出namescore,而如何打印,都是Student类内部定义的,这样被封装起来的,数据和逻辑,调用就非常简单;
  • 同时另外一个好处就是可以给Student类增加新的方法,比如get_grade
# 在pycharm中将程序存为class_instance.py
class Student(object):
    def __init__(self, name, score):
        self.name = name
        self.score = score

    def get_grade(self):
        if self.score >= 90:
            return 'A'
        elif self.score >= 60:
            return 'B'
        else:
            return 'C'
# 在命令行中导入Student类
>>> from class_instance import Student
>>> lisa = Student('Lisa', 90)
>>> bart = Student('Bart', 59)
>>> print(lisa.name, lisa.get_grade())
>>> print(bart.name, bart.get_grade())
Lisa A
Bart C
  • 和静态语言不同,Python允许对实例变量绑定任何数据,也就是说,对于两个实例变量,虽然他们都是同一个类的不同实例,但拥有的变量名称都可能不同:
>>> bart = Student('Bart Simpson', 59)
>>> lisa = Student('Lisa Simpson', 87)
>>> bart.age = 8
>>> bart.age
8
>>> lisa.age
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute 'age'

2. 访问限制

  • 从前面Student类的定义来看,外部代码依然可以自由的修改一个实例的namescore属性:
from class_instance import Student
bart = Student('ABC', 59)
bart.score
59
bart.score = 99
bart.score
99
  • 在属性名称前加两个下划线__,此实例变量就变成了私有变量(private),只有内部可以访问,外部不可以访问:
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
>>> bart = Student('ABC', 59)
>>> bart.__name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__name'
  • 如果外部代码要获取name和score,可以给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方法:
calss Student(object):
    ...
    def set_score(self, score):
    	self.score = score
  • 那原先那种bart.score = 99也可以修改,为什么要这样操作,这是因为在方法中,可以对参数做检查,避免传入无效的参数:
class Student(object):
	...
	def set_score(self, score):
		if 0 <= score <= 100:
			self.__score = score
		else:
			raise ValueError('bad score')
  • 注意在Python中,变量名类似__xxx__的,也就是以双下划线开头,结尾的,是特殊变量,不是private变量,可以直接访问,所以,不能用这种命名变量名;
  • 有些时候,看到以一个下划线开头的实例变量名,如_name,这种的实例变量虽然是可以外部访问,但是一般是视为私有变量,不要随意访问;
  • 双下划綫开头的实例变量是不是一定不能在外部访问呢?其实也不是,不能直接访问是因为python解释器对外把__name变量变改成了_Student__name,所以,可以通过_Student__name来访问__name变量;
>>> bart._Student__name
'ABC'
  • 但是强烈建议你不要这么干,因为不同版本的Python解释器可能会把__name改成不同的变量名;

3. 继承和多态

3.1 继承

  • 在OOP中,当我们定义一个class的时候,可以从某个现有的class继承,新的class成为子类(Subclass),而被继承的class成为基类,父类或超类(Base class、Super class):
# 如:编写一个名为Animal的class,有一个run()方法可以直接打印:
class Animal(object):
	def run(self):
		print('Animal is running...')

# 当需要编写Dog和Cat类时,就可以直接在Animal类继承:
class Dog(Animal):
	pass
class Cat(Animal):
	pass

# 对于`Dog`来说,Animal就是他的父类,对于`Animal`来说,`Dog`就是他的子类;
  • 继承最大的好处就是子类获得了父类的全部功能,由于Animal实现了run()方法,因此,作为子类的DogCat就自动拥有了run()方法:
dog = Dog()
dog.run()

cat = Cat()
cat.run()

# 运行结果:
Animal is running...
Animal is running...
  • 同时,可以对子类增加一些方法,如:
class Dog(Animal):
	def run(self):
		print('Dog is running...')

	def eat(self):
		print('Eating meat...')
  • 继承的另外一个好处需要我们对代码做一下改进:
# 当run()的时候,分别显示Dog is running...和Cat is running...

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

class Cat(Animal):
	def run(self):
		print('Cat is running')
# 运行

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

3.2 多态

  • 首先对数据类型做一点说明:当我们定义一个class时,实际上就定义了一种数据类型,我们自定义的数据类型和Python自带的数据类型没什么两样:
a = list() # a 是list类型
b = Animal() # b 是Animal类型
c = Dog() # c 是Dog类型

# 用isinstance()判断变量是某个类型:

>>> isinstance(a, list)
True
>>> isinstance(b, Animal)
True
>>> isinstance(c, Dog)
True

# 试试这个

>>> isinstance(c, Animal)
True
  • 这样看来,c不仅是Dog,还是Animal,这是因为Dog是从Animal继承下来的。所以在继承关系中,如果一个实例的数据类型是某个子类,那他的数据类型也可以被看做父类,但是反过来不行:
>>> b = Animal()
>>> isinstance(b, Dog)
False
  • 再通过一个函数来理解多态的好处:
# 编写一个函数,这个函数接受一个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...

# 当传入Cat的实例时,run_twice()就打印出:
>>> run_twice(Cat())
Cat is running...
Cat 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子类,不必对run_twice()做任何修改,实际上,任何依赖Animal作为参数的函数或者方法都可以不加修改的正常运行,原因就在多态;

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

  • 对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在AnimalDogCat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:

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

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

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

在这里插入图片描述

3.3 静态语言VS动态语言

  • 对于静态语言(java)来说,如果传入Animal类型,则传入的对象必须是Animal类型或者他的子类,否则,无法调用run()方法;
  • 对于像python这种的动态语言,则不一定需要传入Animal类型,我们只需要保证传入的对象有一个run()方法就可以了:
class Timer(object):
	def run(self):
		print('Start...')
  • Python的“file-like object“就是一种鸭子类型。对真正的文件对象,它有一个read()方法,返回其内容。但是,许多对象,只要有read()方法,都被视为“file-like object“。许多函数接收的参数就是“file-like object“,你不一定要传入真正的文件对象,完全可以传入任何实现了read()方法的对象。

4. 获取对象信息

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

4.1 使用type()

# 使用type()函数判断基本类型
type(123)
<class 'int'>
type('str')
<class 'str'>
type(None)
<class 'NoneType'>

# 如果一个变量指向函数或者类,同样可以判断
>>> type(abs)
<class 'builtin_function_or_method'>
>>> type(a)
<class '__main__.Animal'>

# type()函数返回对应的class类型,如:在if语句中判断,比较两个变量的type类型是否相同:
>>> type(123)==type(456)
True
>>> type(123)==int
True
>>> type('abc')==type('123')
True
>>> type('abc')==str
True
>>> type('abc')==type(123)
False

# 判断基本数据类型可以直接写int,str等,但如果判断一个对象是否是函数时,就需要用到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

4.2 使用isinstance()

# 对class的继承关系来说,使用type()很不方便,所以我们可以使用isinstance()函数:
# 回顾上次的例子,如果继承关系是:
object -> Animal -> Dog -> Husky

>>> a = Animal()  # 先创建3种类型的对象
>>> b = Dog()
>>> c = Husky()

>>> isinstance(c, Husky)
True

>>> isinstance(c, Dog)  # c自身虽是Husky类型,但是Husky是从Dog继承下来的,所以c也是Dog类型,
True

>>> isinstance(c, Animal)   # 换句话说,isinstance判断的是一个对象是否是该类型本身,或者位于该类型的父继承链上
True

# 能用type()判断的基本类型也可用isinstance()判断:
>>> isinstance('a', str)
True


# 同时可以判断一个变量是否是某些类型中的一种:
>>> isinstance([1, 2, 3], (list, tuple))
True
>>> isinstance([1, 2, 3], list)
True
>>> isinstance((1, 2, 3), (list, tuple))
True
>>> isinstance((1, 2, 3), tuple)
True
  • 总是优先使用isinstance()判断类型,可以将指定类型及其子类‘一网打尽’

4.3 使用dir()

# 获得一个对象的所有属性,使用dir()函数,返回一个包含字符串的list:

>>> dir('ABC')
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__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', 'isascii', '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__的属性和方法都是有特殊用途的,比如__len__方法返回长度;还有lower()返回小写的字符串;

# 在python中你调用len()函数去获取一个对象的长度,实际上,在len()函数内部,它自动去调用该对象的__len__()方法:

>>> len('ABC')   # 这两种方法是等价的
3
>>> 'ABC'.__len__()
3
# 自己写的类,如果想用len(myObj),就自己写个__len__()方法:

>>> class MyDog(object):
...     def __len__(self):
...         return 100
...     
>>> dog = MyDog()
>>> len(dog)
100
# 仅仅把属性和方法列出来是不够的,配合`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.x
9
>>> hasattr(obj, 'y')  # 有属性‘y’吗
False
>>> setattr(obj, 'y', 19)   # 设置一个属性‘y’
>>> hasattr(obj, 'y')   # 有属性‘y’吗
True
>>> obj.y
19
>>> getattr(obj, 'y')   # 获取属性‘y’
19
>>> getattr(obj, 'z')   # 获取不存在的属性,会抛出错误:
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AttributeError: 'MyObject' object has no attribute 'z'
>>> getattr(obj, 'z', 404)   # 传入一个default参数,若属性不存在,返回默认值
404
>>> hasattr(obj, 'power')   # 获得对象的方法,有属性‘power’吗
True
>>> getattr(obj, 'power')   # 获取属性‘power’
<bound method MyObject.power of <__main__.MyObject object at 0x00000281D577BB70>>
>>> fn = getattr(obj, 'power')   # 获取属性‘power’并赋值到变量fn
>>> fn   # fn指向obj.power
<bound method MyObject.power of <__main__.MyObject object at 0x00000281D577BB70>>
>>> fn()   # 调用fn()与调用obj.power()是一样的
81

4.4 小结

  • 注意只有在不知道对象信息的时候,我们才回去获得对象信息:
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对象是否有read方法,如果存在,则该对象是一个流,如果不存在,则无法读取。hasattr()就派上了用场。
  • 请注意,在Python这类动态语言中,根据鸭子类型,有read()方法,不代表该fp对象就是一个文件流,它也可能是网络流,也可能是内存中的一个字节流,但只要read()方法返回的是有效的图像数据,就不影响读取图像的功能。

5. 实例属性和类属性

  • 因为python是动态语言,所以根据类创建的实例可以任意绑定属性
# 给实例绑定属性的方法时通过实例变量,或通过self变量:

>>> class Student(object):
...     def __init__(self, name):
...         self.name = name
...         
>>> s = Student('Bob')
>>> s.score = 90

# 如果Student类本身需要绑定一个属性,可以直接在class中定义属性,这种属性是类属性,归Student类所有;这个属性虽然归类所有,但类的所有实例都可以访问:

>>> class Student(object):
...     name = 'Student'
...
>>> s = Student()   # 创建实例s
>>> print(s.name)   # 打印name属性,因为实例并没有name属性,所以会继续查找class的name属性
Student
>>> print(Student.name)   # 打印类的name属性
Student
>>> s.name = 'Michael'   # 给实例绑定name属性
>>> print(s.name)   # 由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性
Michael
>>> print(Student.name)   # 但是类属性并未消失,用Student.name仍然可以访问
Student
>>> del s.name   # 如果删除实例的name属性
>>> print(s.name)   # 再次调用s.name,由于实例的name属性没有找到,类的name属性就显示出来了
Student
  • 从这个例子可以看出,在编写程序的时候,千万不要对实例属性和类属性使用相同的名字,因为相同名称的实例属性将屏蔽掉类属性,但是当你删除实例属性后,再使用相同名字的名称,访问到的将是类属性;

5.1 练习

  • 为了统计学生人数,可以给Student类增加一个类属性,每创建一个实例,该属性自动增加:

class Student(object):
    count = 0

    def __init__(self, name):
        self.name = name
        Student.count = Student.count + 1

if Student.count != 0:
    print('测试失败!')
else:
    bart = Student('Bart')
    if Student.count != 1:
        print('测试失败!')
    else:
        lisa = Student('Bart')
        if Student.count != 2:
            print('测试失败!')
        else:
            print('Students:', Student.count)
            print('测试通过!')
 Run

Students: 2 
测试通过! 

猜你喜欢

转载自blog.csdn.net/zhao416129/article/details/83999024