Python3入门到精通——面向对象

作者: Daniel Meng

GitHub: LibertyDream

博客:明月轩

本系列教程采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议

编程就是和计算机对话,告诉计算机做什么、怎么做。而当代计算机的外貌或许不同,但其实质都差不多,因为他们都是按照冯诺依曼架构设计的,冯诺依曼架构设计计算机讲究存储程序,即程序和数据都存放在一起

而对于怎么放,一开始人们按照机器运行的思路,需要数据就先准备好数据,处理数据所需的函数与算法也准备好,然后运行程序,按照一定顺序执行语句,以完成任务。这就是面向过程的编程

当程序越来越复杂,简单的“顺序”堆放数据和程序给项目开发带来了巨大的效率瓶颈。于是人们开始按照人类认识世界的方式重新将程序和数据围绕着某个意义组织起来,这就是面向对象的编程

面向对象编程有两个核心概念,一是,一个是对象。类就是人类认识世界时所相信的那些意义,是对世界的抽象,比如教师,学生,国家,公司。而对象是类的实例化,比如孔子,颜回,中国,腾讯。

使用类和对象进行编程不代表就是面向对象,尽量编写有意义的面向对象代码

outline

类的定义

Python中使用关键字class定义类,类的命名推荐每个单词首字母大写。类内可以像模块一样定义变量和方法。

In [ ]:
class Student():  # 简单的类定义,但有错误
    age = 0
    name = ''
    
    def print_student_details():
        print("name: %s, age: %s":(name, age))

使用类要先将类实例化,通过ClassName()的方式进行,可以将实例化的结果赋值给一个变量。Python中类的实例化不必像 C++ 或者 Java 一样借助new关键字

In [ ]:
student = Student()

实例化后,使用.操作符访问类中的变量和方法

In [ ]:
student.print_student_details()

类中的方法当然能像函数定义一样不接收参数,但即使如此,类中方法也需要在定义时最少传入self作为参数

In [10]:
class Student():
    age = 0
    name = ''
    
    def print_student_details():  # 最少要有self作为参数
        print('name: %s, age: %s' % (name, age))
        
student = Student()
student.print_student_details()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-10-060aad832b66> in <module>()
      7 
      8 student = Student()
----> 9 student.print_student_details()

TypeError: print_student_details() takes 0 positional arguments but 1 was given

其次,模块中的变量使用时按照var_name形式定义即可通过var_name调用,但类里的变量在使用时必须通过self.var_name的方式调用

In [11]:
class Student():
    age = 0
    name = ''
    
    def print_student_details(self):
        print('name: %s, age: %s' % (name, age))  # 调用类里的变量必须使用 self.var_name方式
        
student = Student()
student.print_student_details()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-11-ebf2d7303933> in <module>()
      7 
      8 student = Student()
----> 9 student.print_student_details()

<ipython-input-11-ebf2d7303933> in print_student_details(self)
      4 
      5     def print_student_details(self):
----> 6         print('name: %s, age: %s' % (name, age))
      7 
      8 student = Student()

NameError: name 'name' is not defined
In [12]:
class Student():  # 正确的类定义
    age = 0
    name = ''
    
    def print_student_details(self):
        print('name: %s, age: %s' % (self.name, self.age))
        
student = Student()
student.print_student_details()
name: , age: 0

如此我们就将变量nameage和方法print_student_details装在了Student这个类中。这就体现了类最基本的功能——封装,每个类都有自己的一些变量和方法。

注意,类中只能定义方法而不能使用

In [13]:
class Student():
    age = 0
    name = ''
    
    def print_student_details(self):
        print('name: %s, age: %s' % (self.name, self.age))
    
    print_student_details()  # 类中不能调用方法
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-218f236d950c> in <module>()
----> 1 class Student():
      2     age = 0
      3     name = ''
      4 
      5     def print_student_details(self):

<ipython-input-13-218f236d950c> in Student()
      6         print('name: %s, age: %s' % (self.name, self.age))
      7 
----> 8     print_student_details()  # 类中不能调用方法

TypeError: print_student_details() missing 1 required positional argument: 'self'

在编程实践中,类的定义和类的调用应该分属不同模块,避免在同一模块中即定义又使用

在一个模块定义好类后,可以在其他模块内通过from module_name import class_name的方式导入类,再实例化类进行使用

方法和函数形式和功用都很类似,但方法的概念更偏向设计层面,讲究作为意义的一部分,而函数的概念更像是程序运行时的一种过程式的称谓。

类里面的变量也叫做数据成员、属性,和方法一起构成了类。二者对应于意义的属性和行为,比如老师这一抽象具有教师编号等属性,还有留作业等行为

类和对象

通过上面的介绍和样例,应该已经知道怎么定义和使用类,但怎么理解类和对象呢?

专业的定义中,类是现实世界或思维世界中的实体在计算机中的反应,它将数据及这些数据上的操作封装在一起。现实或认知里有的东西(概念、意义等)映射到计算机世界中就化身为一个类,比如学生,猫,狗。这些概念本身包含了一些独有的特征和行为,比如学生有学号、学历特征,要会做作业和考试,猫有叫声、花纹特征,要能捉老鼠、爬树。

这些概念所蕴含的独有特征就是类里的数据,也叫做数据成员或者属性,概念本身所蕴含的独有行为就是类里的方法

方法、属性和类所代表的概念(意义)要对应,这是类的设计原则,也是编程艺术的体现。这时我们再来看一下上面Student类的设计:

In [ ]:
class Student():
    age = 0
    name = ''
    
    def print_student_details(self):
        print('name: %s, age: %s' % (self.name, self.age))

学生当然有年龄、姓名的属性,但是print_student_details这个打印学生档案的方法并不是学生自有的行为,学生的行为应该是做作业、考试、听课等。打印学生信息应该交给另一个类,比如打印类Printer

In [ ]:
class Student():
    age = 0
    name = ''
    
    def do_homework(self):
        pass
    
class Printer():
    
    def print_info(self):
        pass

类既然是一类事物的总称,那么这一类事物中的每一个独特的个体就是对象,所以也说对象是类的实例化。类告诉我们有什么属性和方法, 对象告诉我们属性具体是什么,也是具体行为的执行者,当想要使用类的时候请实例化一个对象去做吧,正如生活中老师下达任务给学生做,最后总要指明是给李蕾、韩梅梅或是其他人,一定落实在人上,而不是学生这样一个概念上。

生活中学生可能拥有相同的年龄、名字,但每个学生都是独一无二的个体。计算机中也一样,对象属性可以相同,但每个对象都是独一无二的,可以通过id查看

In [2]:
class Student():
    name = 'Li Lei'
    age = 18
    
    def do_homework(self):
        print('I am doing homework')
In [3]:
student1 = Student()
student2 = Student()

print('student1   name: %s, age: %s' % (student1.name, student1.age))  # 两名学生姓名、年龄相同
print('student2   name: %s, age: %s' % (student2.name, student2.age))
student1   name: Li Lei, age: 18
student2   name: Li Lei, age: 18
In [5]:
print('student1 id:%s, student2 id: %s' % (id(student1), id(student2)))  # 内存位置并不相同
student1 id:2510290950128, student2 id: 2510290950464

构造函数

上面我们创建学生对象的时候都是使用Student类下数据成员的默认值,并不能自由“定制”属性。我们希望能像函数那样,自行决定参数值,来定制对象属性,类里面通过一个名为构造函数的方法实现这个功能——__init__,注意是双下划线,这也是构造函数唯一的名字。构造函数也和其他方法一样最少需要self作为参数。

In [8]:
class Student():
    
    name = ''
    age = 18
    
    def __init__(self):
        print('object is created.')
    
    def do_homework(self):
        print('I am doing homework!')

构造函数的功能就是建构对象,按照我们希望的方式初始化对象属性,在实例化类的时候会自动运行。虽然能通过.操作符调用构造函数,但强烈建议不要这样做。

In [9]:
some_student = Student()  # 自动执行构造函数,打印一遍
some_student.__init__()  # 手动调用,打印一遍。  强烈建议不要这样做
object is created.
object is created.

构造函数有且仅有唯一默认返回值None 。不能自定义返回其他值。

In [11]:
rst = some_student.__init__()

print(rst)
type(rst)
object is created.
None
Out[11]:
NoneType
In [13]:
class Student():
    
    name = ''
    age = 18
    
    def __init__(self):
        print('object is created.')
        return 'create a object'  # 构造函数返回值只能是 None
    
    def do_homework(self):
        print('I am doing homework!')

some_student = Student()
object is created.
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-c68d4c58d7b4> in <module>()
     11         print('I am doing homework!')
     12 
---> 13 some_student = Student()

TypeError: __init__() should return None, not 'str'

类变量和实例变量

可以在self之后给出构建对象时外部要传入的参数列表,通常都是必须参数。但类中的赋值规则和模块里函数中的赋值规则不同,因为接收值的变量可能不是同一个。

为作解释我们重新定义一下Student

In [21]:
class Student():
    
    name = 'Wu Ming'
    age = 18
    
    def __init__(self, name, age):
        name = name  # Python能识别出这是为左边的 name 变量赋值, 形参是右边的 name
        age = age
    
    def do_homework(self):
        print('I am doing homework!')

新的Student类在实例化的时候要指明姓名和年龄,类中属性的默认值是Wu Ming18。我们构造两个对象并输出各自的姓名、年龄

In [16]:
li_lei = Student('Li Lei', 21)
han_mei_mei = Student('Han Meimei', 20)

print('Student1   name: %s, age: %s' % (li_lei.name, li_lei.age))
print('Student2   name: %s, age: %s' % (han_mei_mei.name, han_mei_mei.age))
Student1   name: Wu Ming, age: 18
Student2   name: Wu Ming, age: 18

打印结果似乎有些奇怪,我们给出了两个对象的姓名与年纪,为什么打印值仍旧是默认值呢?这里涉及到了两个概念——类变量和实例变量

顾名思义,类变量是指属于类的变量,实例变量是属于对象的变量。

类变量就是在定义类后,定义方法前定义的变量,注意,在 C, Java 等其他语言中这个位置定义的变量是实例变量,与 Python 不同。实例变量是在构造函数里借助self,通过形如self.var_name的方式确定的。具体到这个例子中:

In [17]:
class Student():
    
    name = 'Wu Ming'  # 类变量
    age = 18  # 类变量
    
    def __init__(self, name, age):
        
        # name = name  # 类似普通方法中定义的变量
        # age = age  # 类似普通方法中定义的变量
        
        self.name = name  # 实例变量
        self.age = age  # 实例变量
    
    def do_homework(self):
        print('I am doing homework!')

有了类变量和实例变量的概念后,再来看一下类的设计。既然类变量是类本身所拥有的,应该和具体对象无关,相应的,实例变量所代表的属性也是紧紧的和一个个具体对象绑定,和类代表的概念无关。那么本例中,学生姓名、年龄自然是学生个体所拥有的属性,和“学生”这一概念无关,应当作为实例变量,而学生在政治网络中的位置是“学生”这一身份本身确定的,并不因学生不同而不同。

所以理想中的Student的类设计现阶段应该如下:

In [1]:
class Student():
    
    political_class = 0
    
    def __init__(self, name, age):
        self.name = name  
        self.age = age
    
    def do_homework(self):
        print('I am doing homework!')

话虽如此,我们依旧没有解释清楚为什么li_leihan_mei_mei两个对象没有输出自身信息,却输出了默认值。除了类变量和实例变量的概念,还要知道 Python 中变量的访问顺序,面向对象编程中,解释器会先找对象的实例变量,后找同名类变量

为了确认对象所拥有的变量与变量值,可以使用__dict__这一 Python 内置变量来进行查看,类也有

In [19]:
li_lei.__dict__
Out[19]:
{}
In [20]:
han_mei_mei.__dict__
Out[20]:
{}
In [22]:
Student.__dict__
Out[22]:
mappingproxy({'__dict__': <attribute '__dict__' of 'Student' objects>,
              '__doc__': None,
              '__init__': <function __main__.Student.__init__>,
              '__module__': '__main__',
              '__weakref__': <attribute '__weakref__' of 'Student' objects>,
              'age': 18,
              'do_homework': <function __main__.Student.do_homework>,
              'name': 'Wu Ming'})

可以看到,li_leihan_mei_mei两个对象中没有存储任何变量,而Student类中存有nameage两个字段。所以打印时自然输出的是默认值了。

self与实例方法

有实例变量自然就有实例方法,即属于对象的方法,特点是必须传入一个self,目前我们所接触到的方法都是实例方法。self定义时必须给出,对象调用方法的时候自动隐去

self实际上代表着调用方法的对象本身,比如,对于Student类中的实例方法来说,构造li_lei时,构造函数中的self就代表li_lei这个对象,构造han_mei_mei时就代表han_mei_mei

Python 中的self等价于 C,Java 中的 this 关键字,但self本身不是 Python 保留的关键字,可以是任意形式,只是必须第一个给出。

In [23]:
class Student():
    
    political_class = 0
    
    def __init__(this, name, age):  # 不必拘泥于 self,但 Python 建议是 self
        this.name = name  
        this.age = age
    
    def do_homework(self):
        print('%s is doing homework!' % self.name)
In [24]:
li_lei = Student('Li Lei', 21)
li_lei.do_homework()
Li Lei is doing homework!

实例方法对类变量的访问

在设计类的时候,实例方法内通过self.var_name的方式访问实例变量,如果不加self访问的是同名形参

In [2]:
class Student():
    
    political_class = 0
    
    def __init__(self, name, age):
        self.name = name  
        self.age = age
        print(self.name)
        print(name)  # 访问的是形参 name,不会访问到实例变量
    
    def do_homework(self):
        print('I am doing homework!')
        
some_student = Student('奔波灞', 18)
奔波灞
奔波灞

如果实例方法要访问类变量,可以通过ClassName.var_nameself.__class__.var_name两种方式。__class__也是Python内置变量之一

In [3]:
class Student():
    
    political_class = 0
    
    def __init__(self, name, age):
        self.name = name  
        self.age = age
        print(Student.political_class)
        print(self.__class__.political_class)
    
    def do_homework(self):
        print('I am doing homework!')
        
some_student = Student('奔波灞', 18)
0
0

类方法与静态方法

有类变量相应的有类方法,类似于实例方法,类方法是专门用于操作类变量的函数,代表着类本身具有且和个体无关的行为。类方法的定义

@classmethod
    def menthod_name(cls,...):
        pass

@classmethod一种特殊的标识,确切的名字是装饰器,日后会讲到。cls类似于self,是一个约定俗称的可变名字,代表着调用类方法的类本身。我们对Student类内添加一个表示学生数量的nums类变量和一个修改nums值的类方法

In [7]:
class Student():
    
    political_class = 0
    nums = 0  # 代表学生数量
    
    def __init__(self, name, age):
        self.name = name  
        self.age = age
    
    def do_homework(self):
        print('I am doing homework!')
        
    @classmethod
    def plus_one(cls):
        cls.nums += 1

类方法通过ClassName.method_name的方式使用。Python支持对象访问类方法,这点与 C#, Java 不同,但是不建议这么做,因为逻辑不同。

In [8]:
some_student = Student('霸波奔', 18)
In [9]:
Student.plus_one()
print(Student.nums)
1
In [10]:
some_student.plus_one()  # 不会报错,但不建议这么调用

print(Student.nums)
2

C#, Java 等语言中的静态方法更类似于 Python 中的类方法,但 Python 中也有静态方法的实现方式,其定义为:

@staticmethod
    def func_name(...):
        pass

@staticmethod同样是装饰器,用于指明函数类型并赋予一定功能。注意静态方法不需要像类方法和实例方法一样传入指代参数self,cls

从静态方法的定义方式可以看出其本身和类与对象的关联较弱,所以不推荐使用。我们在Student里面实现一个名为complain的静态方法

In [18]:
class Student():
    
    political_class = 0
    nums = 0  # 代表学生数量
    
    def __init__(self, name, age):
        self.name = name  
        self.age = age
    
    def do_homework(self):
        print('I am doing homework!')
        
    @classmethod
    def plus_one(cls):
        cls.nums += 1
        
    @staticmethod
    def complain(mood):
        print(Student.political_class)
        print('Complain! Being %s' % (mood))

类和对象都能访问静态方法,静态方法内部可以通过ClassName.var_name的方式调用类变量,但是和类方法一样不能访问实例变量

In [19]:
some_student = Student('王也', 20)

some_student.complain('angry')
Student.complain('nauty')
0
Complain! Being angry
0
Complain! Being nauty

成员可见性

类有内外之分,定义类所在的区块属于内部,通过self.cls.的访问与调用叫做内部调用。类定义区块之外属于外部,通过对象、类名进行访问和调用叫做外部调用

类设计好后,进行外部调用时,常常希望一些敏感数据不为外界随意修改。这就涉及到成员可见性的概念,这也不是 Python 独有的概念。对于外部和内部都能随意访问修改的变量、方法我们称其是公开的,而只有内部能调用、访问而外部不能的,我们说它是私有的。

数据成员都应是私有的,只应通过方法来访问、修改是公共提倡的设计准则。因为方法可以对值进行边界、安全检查以及其他一些操作。

Python中通过__name(双下划线)的方式确认可见性。这里注意__只在前面,前后都有双下划线的通常代表 Python 内置方法、变量,虽然自己也能这么命名但不建议这么做。

In [29]:
class Student():
    
    __political_class = 0
    
    def __init__(self, name, age):
        self.__name = name  # 数据成员应该私有
        self.__age = age
        self.__score = 0
    
    def do_exercise(self):
        self.__plus_score()  # 内部调用私有方法
        print('I am doing exercise! Score is going up!')
        
    def __plus_score(self):
        self.__score += 1
        
    def get_name(self):  # 通过方法访问成员
        return self.__name
    
    def set_name(self, name):  # 通过方法修改成员
        self.__name = name
In [30]:
some_student = Student('汤姆', 20)

some_student.do_exercise()
I am doing exercise! Score is going up!
In [31]:
some_student.__plus_score()  # 私有方法外部不可见
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-31-0e4c3c575fb6> in <module>()
----> 1 some_student.__plus_score()

AttributeError: 'Student' object has no attribute '__plus_score'
In [32]:
some_student.__score  # 私有成员外部不可见
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-32-eeba3fa11884> in <module>()
----> 1 some_student.__score

AttributeError: 'Student' object has no attribute '__score'

私有成员在外部是不能访问和修改的,但是可能会发生下面的事,既修改了,也访问到了

In [33]:
some_student.__score = -1
print(some_student.__score)
-1

难道是 Python 的防护措施失效了吗?当然不是,因为 Python 是解释型语言,所以some_student.__score = -1实际上是给some_student添加了一个新的名为__score的实例变量。可以通过__dict__验证

In [34]:
some_student.__dict__
Out[34]:
{'_Student__age': 20,
 '_Student__name': '汤姆',
 '_Student__score': 1,
 '__score': -1}

可以看到some_student对象下的私有成员中真的多了一个,而更奇怪的是其他三个成员和我们代码中似乎没定义过。这就是 Python 私有保护机制的特点了,对于私有成员,Python 会通过_ClassName__private_var_name的形式进行存储,我们定义的变量实际变了样子。

变相地说明,Python中没有什么是不能访问的,如果我们通过相同方式组装一个变量名进行访问,是能够访问的到私有成员的,当然强烈建议不要这么做

In [35]:
print(some_student._Student__score)  # 没有什么不能访问
1

类的继承

类有两大组成部分——特征和行为,而在工程实践当中,难免会遇到有些特征、行为高度一致,为了避免重复开发相同的变量、方法就可以使用类的继承。

上面是从语法和开发上讲,从实际意义上来看,因为类代表的是现实、认知世界实体在计算机中的映射,那么类的继承其实是对一些实体间存在的上下游、超集和子集等关联信息在计算机进行表达。比如说我们定义了学生类Student,但学生是人类,所以学生的特征、行为一定继承了人类类Human的一些内容。

Python中类继承的语法是:

class SampleClass(ParentClass):
        pass

一个良好的编码规范是,如果一个类不继承自其它类, 就显式的从object继承

In [14]:
class Human(object):  # 没有继承其他类,就显式的从object继承
    
    __sums = 0
    
    def __init__(self, name, age):
        self.__name = name
        self.__age = age
        
    def get_name(self):
        return self.__name
    
    def get_age(self):
        return self.__age
    
    @classmethod
    def get_nums(cls):
        return cls.__sums
In [15]:
class Student(Human):  # 继承自 Human 类
    
    def do_homeword(self):
        print('I am doing homeword')

这里我们展示了StudentHuman的继承,如果是在工程中不同类可能处于不同包、模块内,可以使用from ... import ...语句导入后再继承使用。

继承后Student虽然没有定义构造函数和一些方法,但实际有和Human一致的一套同名方法

In [7]:
li_lei = Student()  # 不符合构造函数,报错
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-7-a10a162dcd76> in <module>()
----> 1 li_lei = Student()  # 不符合构造函数,报错

TypeError: __init__() missing 2 required positional arguments: 'name' and 'age'
In [11]:
li_lei = Student('Li Lei', 20)

print('I am %s and %s years old' % (li_lei.get_name(), li_lei.get_age()))
I am Li Lei and 20 years old
In [16]:
Student.get_nums()  # 类变量也继承
Out[16]:
0

有了父类后就可以自由扩展成自己需要的子类,比如可以在Human类上继承出教师类、厨师类等等,有多少子类完全由个人决定。

继承

上图展示了最常见也是推荐的继承方式,每个子类都只有一个父类,也叫做单继承,Python 实际上支持多继承,即一个类可以同时继承自多个父类。但是为了保证继承关系清晰简洁,避免不必要的冲突,不建议使用多继承。

Student作为子类总有自己独特的一些特征和方法,我们添加成绩score属性和其访问方法get_score。注意这时Student类的构造函数实际要接收的是三个参数,分别是父类的nameage,以及独有的score。构造函数的实现也要体现所有关系

In [17]:
class Student(Human):
    
    def __init__(self, name, age, score):  # 要接收三个参数
        Human.__init__(self, name, age)  # 注意 self 也要传入
        self.__score = score
    
    def do_homework(self):
        print('I am doing homework!')
        
    def get_score(self):
        return self.__score
In [19]:
li_lei = Student('Li Lei', 20, 30)

print('I am %s and %s years old. I scored %s points.' % (li_lei.get_name(), li_lei.get_age(), li_lei.get_score()))
I am Li Lei and 20 years old. I scored 30 points.

在类的继承中,子类中的方法会覆盖父类中的同名方法,而为了获取足够的实例变量往往需要在子类的构造函数内显式的调用父类构造函数,注意这里self这一指代参数也要被传入,这和原来我们展示的在外部显式调用构造函数不同,注意辨别。

这时我们思考两个问题,如果我们的子类设计很复杂,每个方法中都要使用父类,如果我们都使用Human这样通过名字来显式指定父类的方式,每当继承关系改变,我们都要逐行去寻找哪些地方的Human样名称需要改变,这不仅繁琐且容易出错,同时触犯了一个很重要的设计原则——开闭原则

开闭原则讲究类、模块和函数应该对扩展开放,对修改关闭,简单理解就是可以添加新代码,但拒绝修改旧代码。我们实现变化的时候应该扩展实体行为,而不是修改旧有代码进行实现,对旧有代码能不动就不动。

第二个问题是我们调用方法多是通过对象调用,例子中的构造函数实际也被我们当做一个普通的实例方法使用。那么我们通过类调用实例方法在逻辑上就不太讲的通了,毕竟类和对象有别。

为了解决上述问题,Python 设计了super专门指代父类,且通过super关键字可以实例化一个父类对象来调用父类中的方法。

In [20]:
class Student(Human):
    
    def __init__(self, name, age, score):
        super(Student, self).__init__(name, age)  # 通过 super 实例化一个父类对象,调用父类的构造函数
        self.__score = score
    
    def do_homework(self):
        print('I am doing homework!')
        
    def get_score(self):
        return self.__score
In [21]:
li_lei = Student('Li Lei', 20, 30)

print('I am %s and %s years old. I scored %s points.' % (li_lei.get_name(), li_lei.get_age(), li_lei.get_score()))
I am Li Lei and 20 years old. I scored 30 points.