面向对象编程应用
面向对象编程对初学者来说不难理解但很难应用,虽然我们为大家总结过面向对象的三步走方法(定义类、创建对象、给对象发消息),但是说起来容易做起来难。大量的编程练习和阅读优质的代码可能是这个阶段最能够帮助到大家的两件事情。接下来我们还是通过经典的案例来剖析面向对象编程的知识,同时也通过这些案例把我们之前学过的 Python 知识都串联起来。
例子1:扑克游戏。
说明:简单起见,我们的扑克只有52张牌(没有大小王),游戏需要将 52 张牌发到 4 个玩家的手上,每个玩家手上有 13 张牌,按照黑桃、红心、草花、方块的顺序和点数从小到大排列,暂时不实现其他的功能。
使用面向对象编程方法,首先需要从问题的需求中找到对象并抽象出对应的类,此外还要找到对象的属性和行为。当然,这件事情并不是特别困难,我们可以从需求的描述中找出名词和动词,名词通常就是对象或者是对象的属性,而动词通常是对象的行为。扑克游戏中至少应该有三类对象,分别是牌、扑克和玩家,牌、扑克、玩家三个类也并不是孤立的。类和类之间的关系可以粗略的分为 is-a关系(继承)、has-a关系(关联)和 use-a关系(依赖)。很显然扑克和牌是 has-a 关系,因为一副扑克有(has-a)52 张牌;玩家和牌之间不仅有关联关系还有依赖关系,因为玩家手上有(has-a)牌而且玩家使用了(use-a)牌。
牌的属性显而易见,有花色和点数。我们可以用 0 到 3 的四个数字来代表四种不同的花色,但是这样的代码可读性会非常糟糕,因为我们并不知道黑桃、红心、草花、方块跟 0 到 3 的数字的对应关系。如果一个变量的取值只有有限多个选项,我们可以使用枚举。与 C、Java 等语言不同的是,Python 中没有声明枚举类型的关键字,但是可以通过继承enum
模块的Enum
类来创建枚举类型,代码如下所示。
1 | from enum import Enum |
通过上面的代码可以看出,定义枚举类型其实就是定义符号常量,如SPADE
、HEART
等。每个符号常量都有与之对应的值,这样表示黑桃就可以不用数字 0,而是用Suite.SPADE
;同理,表示方块可以不用数字 3, 而是用Suite.DIAMOND
。注意,使用符号常量肯定是优于使用字面常量的,因为能够读懂英文就能理解符号常量的含义,代码的可读性会提升很多。Python 中的枚举类型是可迭代类型,简单的说就是可以将枚举类型放到for-in
循环中,依次取出每一个符号常量及其对应的值,如下所示。
1 | for suite in Suite: |
接下来我们可以定义牌类。
1 | class Card: |
可以通过下面的代码来测试下Card
类。
1 | card1 = Card(Suite.SPADE, 5) |
接下来我们定义扑克类。
1 | import random |
可以通过下面的代码来测试下Poker
类。
1 | poker = Poker() |
定义玩家类。
1 | class Player: |
创建四个玩家并将牌发到玩家的手上。
1 | poker = Poker() |
执行上面的代码会在player.arrange()
那里出现异常,因为Player
的arrange
方法使用了列表的sort
对玩家手上的牌进行排序,排序需要比较两个Card
对象的大小,而<
运算符又不能直接作用于Card
类型,所以就出现了TypeError
异常,异常消息为:'<' not supported between instances of 'Card' and 'Card'
。
为了解决这个问题,我们可以对Card
类的代码稍作修改,使得两个Card
对象可以直接用<
进行大小的比较。这里用到技术叫运算符重载,Python 中要实现对<
运算符的重载,需要在类中添加一个名为__lt__
的魔术方法。很显然,魔术方法__lt__
中的lt
是英文单词“less than”的缩写,以此类推,魔术方法__gt__
对应>
运算符,魔术方法__le__
对应<=
运算符,__ge__
对应>=
运算符,__eq__
对应==
运算符,__ne__
对应!=
运算符。
修改后的Card
类代码如下所示。
1 | class Card: |
说明: 大家可以尝试在上面代码的基础上写一个简单的扑克游戏,如 21 点游戏(Black Jack),游戏的规则可以自己在网上找一找。
例子2:工资结算系统。
要求:某公司有三种类型的员工,分别是部门经理、程序员和销售员。需要设计一个工资结算系统,根据提供的员工信息来计算员工的月薪。其中,部门经理的月薪是固定 15000 元;程序员按工作时间(以小时为单位)支付月薪,每小时 200 元;销售员的月薪由 1800 元底薪加上销售额 5% 的提成两部分构成。
通过对上述需求的分析,可以看出部门经理、程序员、销售员都是员工,有相同的属性和行为,那么我们可以先设计一个名为Employee
的父类,再通过继承的方式从这个父类派生出部门经理、程序员和销售员三个子类。很显然,后续的代码不会创建Employee
类的对象,因为我们需要的是具体的员工对象,所以这个类可以设计成专门用于继承的抽象类。Python 语言中没有定义抽象类的关键字,但是可以通过abc
模块中名为ABCMeta
的元类来定义抽象类。关于元类的概念此处不展开讲解,当然大家不用纠结,照做即可。
1 | from abc import ABCMeta, abstractmethod |
在上面的员工类中,有一个名为get_salary
的方法用于结算月薪,但是由于还没有确定是哪一类员工,所以结算月薪虽然是员工的公共行为但这里却没有办法实现。对于暂时无法实现的方法,我们可以使用abstractmethod
装饰器将其声明为抽象方法,所谓抽象方法就是只有声明没有实现的方法,声明这个方法是为了让子类去重写这个方法。接下来的代码展示了如何从员工类派生出部门经理、程序员、销售员这三个子类以及子类如何重写父类的抽象方法。
1 | class Manager(Employee): |
上面的Manager
、Programmer
、Salesman
三个类都继承自Employee
,三个类都分别重写了get_salary
方法。重写就是子类对父类已有的方法重新做出实现。相信大家已经注意到了,三个子类中的get_salary
各不相同,所以这个方法在程序运行时会产生多态行为,多态简单的说就是调用相同的方法,不同的子类对象做不同的事情。
我们通过下面的代码来完成这个工资结算系统,由于程序员和销售员需要分别录入本月的工作时间和销售额,所以在下面的代码中我们使用了 Python 内置的isinstance
函数来判断员工对象的类型。我们之前讲过的type
函数也能识别对象的类型,但是isinstance
函数更加强大,因为它可以判断出一个对象是不是某个继承结构下的子类型,你可以简单的理解为type
函数是对对象类型的精准匹配,而isinstance
函数是对对象类型的模糊匹配。
1 | emps = [Manager('刘备'), Programmer('诸葛亮'), Manager('曹操'), Programmer('荀彧'), Salesman('张辽')] |
总结
面向对象的编程思想非常的好,也符合人类的正常思维习惯,但是要想灵活运用面向对象编程中的抽象、封装、继承、多态需要长时间的积累和沉淀,这件事情无法一蹴而就,因为知识的积累本就是涓滴成河的过程。