Python系列之面对对象基础

本文尝试探讨Python面向对象的基础概念

从定义说起

面向过程(Procedure-oriented)

可以使用在函数中调用其他函数的方式设计我们的
程序。这叫做面向过程的编程方式。它的特点是把
程序分成多个步骤,用函数把这些步骤一步一步实
现,使用的时候串行依次调用。

面向对象编程(Object Oriented Programming - OOP)

面向对象编程是一种程序设计思想,OOP把对象作
为程序的基本单元,一个对象可能包含了数据、属
性和操作数据的方法。

Python支持面对对象,也支持面向过程,在Python中一切皆对象。

类(Class)与对象(Object)

类(Class)

是用来描述具有相同属性(Attribute)和方法(Method)对象的集合。是抽象的模版。

对象或实例(Object)

是类(Class)的具体实例,即根据类创建过来一个个具体的“对象”。每个对象都拥有相同方法,但各自数据可能不尽相同。

示例

比如学生都有名字和分数,他们有着共同的属性。这时我们就可以设计一个学生类, 用于记录学生的名字和分数,并自定义方法打印出他们的名字和方法。

优势

  • 继承(inheritance)。子类可以继承父类通用类型的属性和方
    法。也就是在父类或者说基类里面实现一次就能被子类重用
  • 封装(Encapsulation)。对外部隐藏有关对象工作原理的细节
  • 多态(polymorphism)。也就是同一个方法,不同的行为,指
    由继承而产生的相关但不同的类,其对象对同一消息会做出不同的
    响应

属性(Attribute)和方法(Method)

介绍

  • 属性

对象可以使用属于它的普通变
量来存储数据,这种从属于对象或类的变量就是属性,它用于描述所有对象共同特征。比如学生的名字和分数。

  • 方法

类里面的函数,用来区别类外面的函数, 用来实现某些功能,实现对对象的操作。比如打印出学生的名字和分数。

示例

要创建一个类我们需要使用关键词class. 这个学生类Student看上去应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建一个学生类
class Student:

# 定义学生属性,初始化方法
def __init__(self, name, score):
self.name = name
self.score = score

# 定义打印学生信息的方法
def show_score(self):
print("Name: {}. Score: {}".format(self.name, self.score))
# 修改分数的方法
def update_score(self, score):
self.score = score

在这个案例中,我们只定义了一个抽象的类,程序并没有创建什么存储空间。只有当我们完成类的实例化(Instance)时,程序才会创建一个具体的对象(Object),并为之分配存储空间。所以这是就是上文说的对象(Object)是类(Class)的一个实例。

要创建一个具体的学生对象(Object),我们还需要输入:

1
2
student1 = Student("John", 100)
student2 = Student("Hex", 99)

在这个案例中,Student是类,student1和student2是我们创建的具体的学生对象。当我们输入上述代码时,Python会自动调用默认的init初始构造函数来生成具体的对象。关键字self是个非常重要的参数,代表创建的对象本身。

特殊方法init,可以把我们认为必须绑定的属性传进来,将属性绑定到self,即创建的实例的身上,表示该实例拥有的特征。

1
2
3
4
5
6
7
8
9
>>> student1.name
John
>>> student2.score
99
>> student1.show_score()
Name: John. Score: 100
>> student2.update_score(88)
>> student2.show_score()
Name: Hex. Score: 88

当你创建具体的对象后,你可以直接通过student1.name和student1.score来分别获取学生的名字和分数,也可以通过student1.show_score()来直接打印学生的名字和分数。当然我们重新通过方法更新了分数,也可先看到分数确实被我们修改了。这些方法我们看到和普通函数是一样的,定义的时候传入self,调用的时候,直接在实例上面调用,不用传递self,其他参数正常传入。

我们从外部看,只有一个Student类,创建实例需要给出name和score,而如何打印,都是在Student类的内部定义的,这些数据和逻辑被“封装”起来了,调用很容易,不用知道内部实现的细节。这样封装另一个好处就是,对上层调用保留接口即调用方法,具体的内部实现细节,在对象里面定义,软件架构的分层,隔离的概念就是类似思想。

类变量(class variables)与实例变量(instance variables)

介绍

假设我们需要在Student类里增加一个计数器number,每当一个新的学生对象(Object)被创建时,这个计数器就自动加1。由于这个计数器不属于某个具体学生,而属于Student类的,所以被称为类变量(class variables)。而姓名和分数属于每个学生对象的,所以属于实例变量(instance variables),也被称为对象变量(object variables)。

示例

这个新Student类看上去应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 创建一个学生类
class Student:

# number属于类变量,不属于某个具体的学生实例
number = 0

# 定义学生属性,初始化方法
# name和score属于实例变量
def __init__(self, name, score):
self.name = name
self.score = score
Student.number = Student.number + 1

# 定义打印学生信息的方法
def show_score(self):
print("Name: {}. Score: {}".format(self.name, self.score))

# 修改分数的方法
def update_score(self, score):
self.score = score

# 实例化,创建对象
student1 = Student("John", 100)
student2 = Student("Lucy", 99)

print(Student.number) # 打印2
print(student1.__class__.number) # 打印2

类变量和实例变量的区别很大,访问方式也不一样。

区别

  • 类变量

类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外。访问或调用类变量的正确方式是类名.变量名或者self.class.变量名。self.class自动返回每个对象的类名。

  • 实例变量
    定义在方法中的变量,属于某个具体的对象。访问或调用实例变量的正确方式是对象名.变量名或者self.变量名。

实例变量即实例属性,为各个实例所有,互不干扰的;类变量即类属性,为类所有,所有实例可共享一个属性;若类变量与实例变量同名,尝试访问时,会先从实例变量里面找,没有再从类变量里面找(实例属性优先级比类属性高),所以结果可能有错误,这是要避免的

静态方法(static method)和类方法(Class method)

介绍

正如同有些变量只属于类,有些方法也只属于类,不属于具体的对象。你有没有注意到属于对象的方法里面都有一个self参数, 比如init(self), show_score(self) self是指对象本身。属于类的方法不使用self参数, 而使用参数cls,代表类本身。另外对类方法我们会加上@classmethod的修饰符做说明。另外一种是静态方法,它既没有self,也没有cls,用@staticmethod的修饰符做表示,方法里面的逻辑与该对象完全无关。

示例

同样拿Student为例子,我们不用print函数打印出已创建学生对象的数量,而是自定义一个类方法来打印,同时也定义了静态方法,say_hello, 来打个招呼我们可以这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Student:

# number属于类变量,不属于某个具体的学生实例
number = 0

# 定义学生属性,初始化方法
# name和score属于实例变量
def __init__(self, name, score):
self.name = name
self.score = score
Student.number = Student.number + 1

# 定义打印学生信息的方法
def show_score(self):
print("Name: {}. Score: {}".format(self.name, self.score))

# 修改分数的方法
def update_score(self, score):
self.score = score

# 定义类方法,打印学生的数量
@classmethod
def total(cls):
print("Total: {0}".format(cls.number))

@staticmethod
def say_hello():
print('Hello!')

# 实例化,创建对象
student1 = Student("John", 100)
student2 = Student("Lucy", 99)
Student.show_score(student1)
# Name: John. Score: 100
Student.total() # 打印 Total: 2

注意一下当我们使用类调用实例方法的方法时候,只有绑定实例才可以正常访问,正确调用。

类的私有属性(private attribute)和私有方法(private method)

介绍

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

1
2
3
4
5
6
>> student1 = Student('hex', 60)
>> student1.score
60
>> student1.score = 99
student1.score
99

我们看到内部属性被外部访问,直接修改了。这是有些过度自由了,我只想该类通过自己的方法修改属性,也就是说设置为私有怎么办呢?

在Python里面,类里面的私有属性和私有方法以双下划线__开头。私有属性或方法不能在类的外部被使用或直接访问。只有在内部可以访问。

示例

我们同样看看学生类这个例子,把分数score变为私有属性,看看会发生什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建一个学生类
class Student:

# 定义学生属性,初始化方法
# name和score属于实例变量, 其中__score属于私有变量
def __init__(self, name, score):
self.name = name
self.__score = score

# 定义打印学生信息的方法
def show_score(self):
print("Name: {}. Score: {}".format(self.name, self.__score))

# 修改分数的方法
def update_score(self, score):
self.__score = score

# 实例化,创建对象
student1 = Student("John", 100)

student1.show_score() # 打印 Name: John, Score: 100
student1.__score # 打印出错,该属性不能从外部访问。

如果你将score变成score, 你将不能直接通过student1.score获取该学生的分数。show_score()可以正常显示分数,是因为它是类里面的函数,可以访问私有变量。通过update_score()可以修改我们分数,
student1.score = 99也可以修改啊,为什么要定义一个方法大费周折?因为在方法中,可以对参数做检查,避免传入无效的参数:
1
2
3
4
5
6
7
8
class Student(object):
...

def update_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('bad score')

这样通过访问限制保护,我们让代码更健壮了。

双下划线的实例变量也不是真的不能从外部访问,因为解释器对把score变量改成_Student_score,所以仍然可以通过_Studentname来访问__score变量:

1
2
>>> student1._Student__score
99

但是Python这么做实际上只是给你建了一个围栏,你要不要翻过去完全个人的自觉性。

私有方法是同样的道理。show_score()变成,show_score()你将不能再通过student1.show_score()打印出学生的名字和分数。值得注意的是私有方法必需含有self这个参数,且把它作为第一个参数。

@property的用法

介绍

在上述案例中用户不能用student1.score方式访问学生分数,然而用户也就知道了score是个私有变量。根据上文的功能我分别调用了两个方法来实现,有限制的访问属性来访问学生分数,有没有一种简单方法我们让用户通过student1.score而继续保持__score私有变量的属性呢?这时我们就可以借助Python的@property装饰器了。我们知道装饰器就是动态非函数增加功能的。

示例

我们可以先定义一个方法score(), 利用@property把这个函数伪装成属性。见下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 创建一个学生类
class Student:

# 定义学生属性,初始化方法
# name和score属于实例变量, 其中score属于私有变量
def __init__(self, name, score):
self.name = name
self.__score = score

# 利用property装饰器把函数伪装成属性
@property
def score(self):
return self.__score

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

def get_score(self):
return self.__score

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

这是第二种方法
# score = property(get_score, set_score)

# 实例化,创建对象
>>> student1 = Student("John", 100)
>>> student1.score
100
>>> student1.score = 60
>>> student1.score
60
>>> student1.score = 999
Traceback (most recent call last):
...
ValueError: score must between 0 ~ 100!

注意: 一旦给函数加上一个装饰器@property,调用函数的时候不用加括号就可以直接调用函数了。把一个getter方法变成属性,只需要加上@property,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作。

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

类的继承(Inheritance)

介绍

面向对象的编程带来的最大好处之一就是代码的重用,实现这种重用的方法之一是通过继承(Inheritance)。你可以先定义一个基类(Base class)或父类(Parent class),再按通过class 子类名(父类名)来创建子类(Child class)。这样子类就可以从父类那里获得其已有的属性与方法,这种现象叫做类的继承。

示例

我们再看另一个例子,老师和学生同属学校成员,都有姓名和年龄的属性,然而老师有工资这个专有属性,学生有分数这个专有属性。这时我们就可以定义一个学校成员父类,2个子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 创建父类学校成员SchoolMember
class SchoolMember:

def __init__(self, name, age):
self.name = name
self.age = age

def tell(self):
# 打印个人信息
print('Name:"{}" Age:"{}"'.format(self.name, self.age), end=" ")


# 创建子类老师 Teacher
class Teacher(SchoolMember):

def __init__(self, name, age, salary):
SchoolMember.__init__(self, name, age) # 利用父类进行初始化
self.salary = salary

# 方法重写(或覆盖)
def tell(self):
SchoolMember.tell(self)
print('Salary: {}'.format(self.salary))


# 创建子类学生Student
class Student(SchoolMember):

def __init__(self, name, age, score):
SchoolMember.__init__(self, name, age)
self.score = score

def tell(self):
SchoolMember.tell(self)
print('score: {}'.format(self.score))


teacher1 = Teacher("John", 44, "$60000")
student1 = Student("Mary", 12, 99)

teacher1.tell() # 打印 Name:"John" Age:"44" Salary: $60000
student1.tell() # Name:"Mary" Age:"12" score: 99


上述代码中,你注意到以下几点了吗?

在创建子类的过程中,你需要手动调用父类的构造函数init来完成子类的构造。

在子类中调用父类的方法时,需要加上父类的类名前缀,且需要带上self参数变量。比如SchoolMember.tell(self), 这个可以通过使用super关键词简化代码。

如果子类调用了某个方法(如tell())或属性,Python会先在子类中找,如果找到了会直接调用。如果找不到才会去父类找。这为方法重写带来了便利。

实际Python编程过程中,一个子类可以继承多个父类,原理是一样的。第一步总是要手动调用init构造函数。

super()

super()关键字调用父类方法

在子类当中可以通过使用super关键字来直接调用父类的中相应的方法,简化代码。在下面例子中,学生子类调用了父类的tell()方法。super().tell()等同于SchoolMember.tell(self)。当你使用Python super()关键字调用父类方法时时,注意去掉括号里self这个参数。

1
2
3
4
5
6
7
8
9
10
11
# 创建子类学生Student
class Student(SchoolMember):

def __init__(self, name, age, score):
SchoolMember.__init__(self, name, age)
self.score = score

def tell(self):
super().tell() # 等同于 SchoolMember.tell(self)
# pyhotn2的需要显示的写明,super(Student, self).tell()
print('score: {}'.format(self.score))

方法解析顺序(Method Resolution Order- MRO)

介绍

其方法(属性)可能定义在当前类,也可能来自于基类,所以在方法调用时就需要对当前类和基类进行搜索以确定方法所在的位置。其搜索顺序就是方法解析顺序。这里我们来一个测试验证一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
class A:
def run(self):
print('A.run')

class B(A):
pass

class C(A):
def run(self):
print('C.run')

class D(B, C):
pass

继承图示

1
2
3
4
5
6
7
   A
/ \
/ \
B C
\ /
\ /
D

经典类的顺序

Python 2中可用的形式,一种简单的MRO方法,从左到右的深度优先遍历。
对于菱形继承,调用不是我们希望的结果。我们知道C.run()是A.run()更具体的版本,但实际会调用A.run()。

1
2
3
4
5
6
7
8
9
>>> import inspect
>>> inspect.getmro(D)
(<class classic_mro.D at 0x1080b67a0>,
<class classic_mro.B at 0x1080b6668>,
<class classic_mro.A at 0x1080b6738>,
<class classic_mro.C at 0x1080b66d0>)
>>> d = D()
>>> d.run()
A.run()

查找结果是[D, B, A, C]

新式类顺序

Python3中的新式类,都继承自ojbect,采用C3广度优先算法来做搜索MRO,多重继承更符合实际情况。如下例子:我们看到查找结果为[D, B, A, C]

1
2
3
4
5
6
7
8
9
10
>>> import inspect
>>> inspect.getmro(D)
(<class '__main__.D'>,
<class '__main__.B'>,
<class '__main__.C'>,
<class '__main__.A'>,
<class 'object'>)
>>> d = D()
>>> d.run()
C.run()

参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!