第19课:面向对象编程进阶

前面我们讲解了 Python 面向对象编程的一些基础知识,本节我们继续讨论面向对象编程相关的内容。

可见性和属性装饰器

在很多面向对象编程语言中,对象的属性通常会被设置为私有(private)或受保护(protected)的成员,简单的说就是不允许直接访问这些属性;对象的方法通常都是公开的(public),因为公开的方法是对象能够接受的消息,也是对象暴露给外界的调用接口,这就是所谓的访问可见性。在 Python 中,可以通过给对象属性名添加前缀下划线的方式来说明属性的访问可见性,例如,可以用__name表示一个私有属性,_name 表示一个受保护属性,代码如下所示。

1class Student:
2
3    def __init__(self, name, age):
4        self.__name = name
5        self.__age = age
6
7    def study(self, course_name):
8        print(f'{self.__name}正在学习{course_name}.')
9
10
11stu = Student('王大锤', 20)
12stu.study('Python程序设计')
13print(stu.__name)  # AttributeError: 'Student' object has no attribute '__name'

上面代码的最后一行会引发AttributeError(属性错误)异常,异常消息为:'Student' object has no attribute '__name' 。由此可见,以__开头的属性__name相当于是私有的,在类的外面无法直接访问,但是类里面的study方法中可以通过self.__name 访问该属性。需要说明的是,大多数使用 Python 语言的人在定义类时,通常不会选择让对象的属性私有或受保护,正如有一句名言说的:“* We are all consenting adults here*”(大家都是成年人),成年人可以为自己的行为负责,而不需要通过 Python 语言本身来限制访问可见性。事实上,大多数的程序员都认为开放比封闭要好,把对象的属性私有化并非必不可少的东西,所以 Python 语言并没有从语义上做出最严格的限定,也就是说上面的代码如果你愿意,用stu._Student__name 的方式仍然可以访问到私有属性__name,有兴趣的读者可以自己试一试。

动态属性

Python 语言属于动态语言,维基百科对动态语言的解释是:“在运行时可以改变其结构的语言,例如新的函数、对象、甚至代码可以被引进,已有的函数可以被删除或是其他结构上的变化”。动态语言非常灵活,目前流行的 Python 和 JavaScript 都是动态语言,除此之外,诸如 PHP、Ruby 等也都属于动态语言,而 C、C++ 等语言则不属于动态语言。

在 Python 中,我们可以动态为对象添加属性,这是 Python 作为动态类型语言的一项特权,代码如下所示。需要提醒大家的是,对象的方法其实本质上也是对象的属性,如果给对象发送一个无法接收的消息,引发的异常仍然是AttributeError

1class Student:
2
3    def __init__(self, name, age):
4        self.name = name
5        self.age = age
6
7
8stu = Student('王大锤', 20)
9stu.sex = '男'  # 给学生对象动态添加sex属性

如果不希望在使用对象时动态的为对象添加属性,可以使用 Python 语言中的__slots__魔法。对于Student 类来说,可以在类中指定__slots__ = ('name', 'age'),这样Student类的对象只能有nameage属性,如果想动态添加其他属性将会引发异常,代码如下所示。

1class Student:
2    __slots__ = ('name', 'age')
3
4    def __init__(self, name, age):
5        self.name = name
6        self.age = age
7
8
9stu = Student('王大锤', 20)
10# AttributeError: 'Student' object has no attribute 'sex'
11stu.sex = '男'

静态方法和类方法

之前我们在类中定义的方法都是对象方法,换句话说这些方法都是对象可以接收的消息。除了对象方法之外,类中还可以有静态方法和类方法,这两类方法是发给类的消息,二者并没有实质性的区别。在面向对象的世界里,一切皆为对象,我们定义的每一个类其实也是一个对象,而静态方法和类方法就是发送给类对象的消息。那么,什么样的消息会直接发送给类对象呢?

举一个例子,定义一个三角形类,通过传入三条边的长度来构造三角形,并提供计算周长和面积的方法。计算周长和面积肯定是三角形对象的方法,这一点毫无疑问。但是在创建三角形对象时,传入的三条边长未必能构造出三角形,为此我们可以先写一个方法来验证给定的三条边长是否可以构成三角形,这种方法很显然就不是对象方法,因为在调用这个方法时三角形对象还没有创建出来。我们可以把这类方法设计为静态方法或类方法,也就是说这类方法不是发送给三角形对象的消息,而是发送给三角形类的消息,代码如下所示。

1class Triangle(object):
2    """三角形"""
3
4    def __init__(self, a, b, c):
5        """初始化方法"""
6        self.a = a
7        self.b = b
8        self.c = c
9
10    @staticmethod
11    def is_valid(a, b, c):
12        """判断三条边长能否构成三角形(静态方法)"""
13        return a + b > c and b + c > a and a + c > b
14
15    # @classmethod
16    # def is_valid(cls, a, b, c):
17    #     """判断三条边长能否构成三角形(类方法)"""
18    #     return a + b > c and b + c > a and a + c > b
19
20    def perimeter(self):
21        """计算周长"""
22        return self.a + self.b + self.c
23
24    def area(self):
25        """计算面积"""
26        p = self.perimeter() / 2
27        return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5

上面的代码使用staticmethod装饰器声明了is_valid方法是Triangle类的静态方法,如果要声明类方法,可以使用classmethod 装饰器(如上面的代码15~18行所示)。可以直接使用类名.方法名的方式来调用静态方法和类方法,二者的区别在于,类方法的第一个参数是类对象本身,而静态方法则没有这个参数。简单的总结一下, **对象方法、类方法、静态方法都可以通过“类名.方法名”的方式来调用,区别在于方法的第一个参数到底是普通对象还是类对象,还是没有接受消息的对象 **。静态方法通常也可以直接写成一个独立的函数,因为它并没有跟特定的对象绑定。

这里做一个补充说明,我们可以给上面计算三角形周长和面积的方法添加一个property装饰器(Python 内置类型),这样三角形类的perimeterarea就变成了两个属性,不再通过调用方法的方式来访问,而是用对象访问属性的方式直接获得,修改后的代码如下所示。

1class Triangle(object):
2    """三角形"""
3
4    def __init__(self, a, b, c):
5        """初始化方法"""
6        self.a = a
7        self.b = b
8        self.c = c
9
10    @staticmethod
11    def is_valid(a, b, c):
12        """判断三条边长能否构成三角形(静态方法)"""
13        return a + b > c and b + c > a and a + c > b
14
15    @property
16    def perimeter(self):
17        """计算周长"""
18        return self.a + self.b + self.c
19
20    @property
21    def area(self):
22        """计算面积"""
23        p = self.perimeter / 2
24        return (p * (p - self.a) * (p - self.b) * (p - self.c)) ** 0.5
25
26
27t = Triangle(3, 4, 5)
28print(f'周长: {t.perimeter}')
29print(f'面积: {t.area}')

继承和多态

面向对象的编程语言支持在已有类的基础上创建新类,从而减少重复代码的编写。提供继承信息的类叫做父类(超类、基类),得到继承信息的类叫做子类(派生类、衍生类)。例如,我们定义一个学生类和一个老师类,我们会发现他们有大量的重复代码,而这些重复代码都是老师和学生作为人的公共属性和行为,所以在这种情况下,我们应该先定义人类,再通过继承,从人类派生出老师类和学生类,代码如下所示。

1class Person:
2    """人"""
3
4    def __init__(self, name, age):
5        self.name = name
6        self.age = age
7    
8    def eat(self):
9        print(f'{self.name}正在吃饭.')
10    
11    def sleep(self):
12        print(f'{self.name}正在睡觉.')
13
14
15class Student(Person):
16    """学生"""
17    
18    def __init__(self, name, age):
19        super().__init__(name, age)
20    
21    def study(self, course_name):
22        print(f'{self.name}正在学习{course_name}.')
23
24
25class Teacher(Person):
26    """老师"""
27
28    def __init__(self, name, age, title):
29        super().__init__(name, age)
30        self.title = title
31    
32    def teach(self, course_name):
33        print(f'{self.name}{self.title}正在讲授{course_name}.')
34
35
36
37stu1 = Student('白元芳', 21)
38stu2 = Student('狄仁杰', 22)
39tea1 = Teacher('武则天', 35, '副教授')
40stu1.eat()
41stu2.sleep()
42tea1.eat()
43stu1.study('Python程序设计')
44tea1.teach('Python程序设计')
45stu2.study('数据科学导论')

继承的语法是在定义类的时候,在类名后的圆括号中指定当前类的父类。如果定义一个类的时候没有指定它的父类是谁,那么默认的父类是object 类。object类是 Python 中的顶级类,这也就意味着所有的类都是它的子类,要么直接继承它,要么间接继承它。Python 语言允许多重继承,也就是说一个类可以有一个或多个父类,关于多重继承的问题我们在后面会有更为详细的讨论。在子类的初始化方法中,我们可以通过super().__init__() 来调用父类初始化方法,super函数是 Python 内置函数中专门为获取当前对象的父类对象而设计的。从上面的代码可以看出,子类除了可以通过继承得到父类提供的属性和方法外,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力。在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,也叫做“里氏替换原则”(Liskov Substitution Principle)。

子类继承父类的方法后,还可以对方法进行重写(重新实现该方法),不同的子类可以对父类的同一个方法给出不同的实现版本,这样的方法在程序运行时就会表现出多态行为(调用相同的方法,做了不同的事情)。多态是面向对象编程中最精髓的部分,当然也是对初学者来说最难以理解和灵活运用的部分,我们会在下一个章节用专门的例子来讲解这个知识点。

总结

Python 是动态类型语言,Python 中的对象可以动态的添加属性,对象的方法其实也是属性,只不过和该属性对应的是一个可以调用的函数。在面向对象的世界中, 一切皆为对象,我们定义的类也是对象,所以类也可以接收消息,对应的方法是类方法或静态方法。通过继承,我们**可以从已有的类创建新类 **,实现对已有类代码的复用。