1 class Student(object):
2
3 # __init__是一个特殊方法用于在创建对象时进行初始化操作
4 # 通过这个方法我们可以为学生对象绑定name和age两个属性
5 # 在变量或函数前加两个下划线表示变量私有 private
6 # 一般在变量前加单下划线表示保护 protect (这种做法并不是语法上的规则)
7 def __init__(self, name, age):
8 self.name = name
9 self.__age = age
10
11 def study(self, course_name):
12 print('%s正在学习%s.' % (self.name, course_name))
13
14 # PEP 8要求标识符的名字用全小写多个单词用下划线连接
15 # 但是部分程序员和公司更倾向于使用驼峰命名法(驼峰标识)
16 def watch_movie(self):
17 if self.__age < 18:
18 print('%s只能观看《熊出没》.' % self.name)
19 else:
20 print('%s正在观看《喜羊羊》.' % self.name)
21
22 def main():
23 # 创建学生对象并指定姓名和年龄
24 stu1 = Student('骆昊', 38)
25 # 给对象发study消息
26 stu1.study('Python程序设计')
27 # 给对象发watch_movie消息
28 stu1.watch_movie()
29 stu2 = Student('王大锤', 15)
30 stu2.study('思想品德')
31 stu2.watch_movie()
32 print(stu2.name)
33 # 访问私有变量,报错
34 print(stu2.age)
35
36 if __name__ == '__main__':
37 main()
@property和__slots__
之前我们讨论过Python中属性和方法访问权限的问题,虽然我们不建议将属性设置为私有的,但是如果直接将属性暴露给外界也是有问题的,比如我们没有办法检查赋给属性的值是否有效。我们之前的建议是将属性命名以单下划线开头,通过这种方式来暗示属性是受保护的,不建议外界直接访问,那么如果想访问属性可以通过属性的getter(访问器)和setter(修改器)方法进行对应的操作。如果要做到这点,就可以考虑使用@property包装器来包装getter和setter方法,使得对属性的访问既安全又方便,代码如下所示。
我们讲到这里,不知道大家是否已经意识到,Python是一门动态语言。通常,动态语言允许我们在程序运行时给对象绑定新的属性或方法,当然也可以对已经绑定的属性和方法进行解绑定。但是如果我们需要限定自定义类型的对象只能绑定某些属性,可以通过在类中定义__slots__变量来进行限定。需要注意的是__slots__的限定只对当前类的对象生效,对子类并不起任何作用。
1 class Person(object):
2
3 # 限定Person对象只能绑定_name, _age和_gender属性
4 __slots__ = ('_name', '_age', '_gender')
5
6 def __init__(self, name, age):
7 self._name = name
8 self._age = age
9
10 # 访问器 - getter方法
11 @property
12 def name(self):
13 return self._name
14
15 # 访问器 - getter方法
16 @property
17 def age(self):
18 return self._age
19
20 # 修改器 - setter方法
21 @age.setter
22 def age(self, age):
23 self._age = age
24
25 def play(self):
26 if self._age <= 16:
27 print('%s正在玩飞行棋.' % self._name)
28 else:
29 print('%s正在玩斗地主.' % self._name)
30
31 def main():
32 person = Person('王大锤', 12)
33 person.play()
34 person._gender = '男'
35 person.age = 22
36 person.play()
37 # person.name = '白元芳' # AttributeError: can't set attribute
38
39
40 if __name__ == '__main__':
41 main()
静态方法和类方法
之前,我们在类中定义的方法都是对象方法,也就是说这些方法都是发送给对象的消息。实际上,我们写在类中的方法并不需要都是对象方法,例如我们定义一个“三角形”类,通过传入三条边长来构造三角形,并提供计算周长和面积的方法,但是传入的三条边长未必能构造出三角形对象,因此我们可以先写一个方法来验证三条边长是否可以构成三角形,这个方法很显然就不是对象方法,因为在调用这个方法时三角形对象尚未创建出来(因为都不知道三条边能不能构成三角形),所以这个方法是属于三角形类而并不属于三角形对象的。我们可以使用静态方法来解决这类问题,代码如下所示。
1 from math import sqrt
2
3 class Triangle(object):
4
5 def __init__(self, a, b, c):
6 self._a = a
7 self._b = b
8 self._c = c
9
10 # 静态方法用@staticmethod标记
11 @staticmethod
12 def is_valid(a, b, c):
13 return a + b > c and b + c > a and a + c > b
14
15 def perimeter(self):
16 return self._a + self._b + self._c
17
18 def area(self):
19 half = self.perimeter() / 2
20 return sqrt(half * (half - self._a) *
21 (half - self._b) * (half - self._c))
22
23 def main():
24 a, b, c = 3, 4, 5
25 # 静态方法和类方法都是通过给类发消息来调用的
26 if Triangle.is_valid(a, b, c):
27 t = Triangle(a, b, c)
28 print(t.perimeter())
29 # 也可以通过给类发消息来调用对象方法但是要传入接收消息的对象作为参数
30 # print(Triangle.perimeter(t))
31 print(t.area())
32 # print(Triangle.area(t))
33 else:
34 print('无法构成三角形.')
35
36 if __name__ == '__main__':
37 main()
和静态方法比较类似,Python还可以在类中定义类方法,类方法的第一个参数约定名为cls,它代表的是当前类相关的信息的对象(类本身也是一个对象,有的地方也称之为类的元数据对象),通过这个参数我们可以获取和类相关的信息并且可以创建出类的对象,代码如下所示。
类之间的关系
简单的说,类和类之间的关系有三种:is-a、has-a和use-a关系。
- is-a关系也叫继承或泛化,比如学生和人的关系、手机和电子产品的关系都属于继承关系。
- has-a关系通常称之为关联,比如部门和员工的关系,汽车和引擎的关系都属于关联关系;关联关系如果是整体和部分的关联,那么我们称之为聚合关系;如果整体进一步负责了部分的生命周期(整体和部分是不可分割的,同时同在也同时消亡),那么这种就是最强的关联关系,我们称之为合成关系。
- use-a关系通常称之为依赖,比如司机有一个驾驶的行为(方法),其中(的参数)使用到了汽车,那么司机和汽车的关系就是依赖关系。
我们可以使用一种叫做UML(统一建模语言)的东西来进行面向对象建模,其中一项重要的工作就是把类和类之间的关系用标准化的图形符号描述出来。关于UML我们在这里不做详细的介绍,有兴趣的读者可以自行阅读《UML面向对象设计基础》一书。
利用类之间的这些关系,我们可以在已有类的基础上来完成某些操作,也可以在已有类的基础上创建新的类,这些都是实现代码复用的重要手段。复用现有的代码不仅可以减少开发的工作量,也有利于代码的管理和维护,这是我们在日常工作中都会使用到的技术手段。
继承和多态
刚才我们提到了,可以在已有类的基础上创建新类,这其中的一种做法就是让一个类从另一个类那里将属性和方法直接继承下来,从而减少重复代码的编写。提供继承信息的我们称之为父类,也叫超类或基类;得到继承信息的我们称之为子类,也叫派生类或衍生类。子类除了继承父类提供的属性和方法,还可以定义自己特有的属性和方法,所以子类比父类拥有的更多的能力,在实际开发中,我们经常会用子类对象去替换掉一个父类对象,这是面向对象编程中一个常见的行为,对应的原则称之为里氏替换原则。下面我们先看一个继承的例子。
1 class Student(Person):
2 """学生"""
3
4 def __init__(self, name, age, grade):
5 super().__init__(name, age)
6 self._grade = grade
7
8 @property
9 def grade(self):
10 return self._grade
11
12 @grade.setter
13 def grade(self, grade):
14 self._grade = grade
15
16 def study(self, course):
17 print('%s的%s正在学习%s.' % (self._grade, self._name, course))
18
19
20 class Teacher(Person):
21 """老师"""
22
23 def __init__(self, name, age, title):
24 super().__init__(name, age)
25 self._title = title
26
27 @property
28 def title(self):
29 return self._title
30
31 @title.setter
32 def title(self, title):
33 self._title = title
34
35 def teach(self, course):
36 print('%s%s正在讲%s.' % (self._name, self._title, course))
37
38
39 def main():
40 stu = Student('王大锤', 15, '初三')
41 stu.study('数学')
42 stu.watch_av()
43 t = Teacher('骆昊', 38, '砖家')
44 t.teach('Python程序设计')
45 t.watch_av()
46
47
48 if __name__ == '__main__':
49 main()
子类在继承了父类的方法后,可以对父类已有的方法给出新的实现版本,这个动作称之为方法重写(override)。通过方法重写我们可以让父类的同一个行为在子类中拥有不同的实现版本,当我们调用这个经过子类重写的方法时,不同的子类对象会表现出不同的行为,这个就是多态(poly-morphism),要import ABCMeta。
1 from abc import ABCMeta, abstractmethod
2
3 # 多态性是指具有不同功能的函数可以使用相同的函数名,这样就可以用一个函数名调用不同内容的函数。
4 # 在面向对象方法中一般是这样表述多态性:向不同的对象发送同一条消息,不同的对象在接收时会产生不同的行为(即方法)
5 # 也就是说,每个对象可以用自己的方式去响应共同的消息。所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。
6 # @abc.abstractmethod是严格控制子类必须实现这个方法
7
8 class Pet(object, metaclass=ABCMeta):
9 """宠物"""
10
11 def __init__(self, nickname):
12 self._nickname = nickname
13
14 # 抽象方法用@abstractmethod标注
15 @abstractmethod
16 def make_voice(self):
17 """发出声音"""
18 # pass没有意义,只是为了保持方法结构完整性
19 pass
20
21 class Dog(Pet):
22 """狗"""
23
24 def make_voice(self):
25 print('%s: 汪汪汪...' % self._nickname)
26
27 class Cat(Pet):
28 """猫"""
29
30 def make_voice(self):
31 print('%s: 喵...喵...' % self._nickname)
32
33 def main():
34 pets = [Dog('旺财'), Cat('凯蒂'), Dog('大黄')]
35 for pet in pets:
36 pet.make_voice()
37
38 if __name__ == '__main__':
39 main()