朋也是大学班上的文娱委员,他留意到周围同学经常表达出希望“课余生活能多看书”的意愿,但苦于不知道要选什么书看。
思前想后,朋也决定启动一个流浪图书计划:鼓励同学们将自己中意的闲置图书贡献出来,形成一个小型的精品图书库,以供同学们免费借阅、流转。
为此他需要一个图书管理系统,来帮助他管理书籍的借阅情况。他希望这个简易的程序可以做到:
在没接触类之前,如果碰到这样的需求,我们会想到通过定义不同的函数来封装不同的功能。这样当然是可行的,只是可能会多费些功夫。
今天的主题是类,我们就只用面向对象编程来完成这个程序。
思考:需要定义多少个类?每个类有怎样的属性和方法?
第一种用法是使用类生成实例对象。类作为实例对象的模版,每个实例创建后,都将拥有类的所有属性和方法。
第二种用法是用类将多个函数(方法)打包封装在一起,让类中的方法相互配合。
回到项目:我们的处理对象是每本具体的书,而每本书都有自己的属性信息,所以我们可以定义一个Book类,利用Book类创建一个个书的实例,绑定属性(对应用法1)。
而这个管理系统的运行主体,是多个可供选择的功能的叠加,所以我们可以创建一个系统运行类BookManager,将查询书籍、添加书籍等功能封装成类中的方法以供调用(对应用法2)。
我们的预期效果是当实例化这个系统运行类的时候,会出现一个菜单,能让用户选择不同的功能,如下图所示:
为了让类的结构更清晰,我们可以将这个选择菜单也封装成一个方法menu(),方便调用其他方法。
那么,将上述要编写的两个类整理一下,这个程序的骨架就是这样:
【定义Book类】
根据需求,每本书的基本属性都要有四个:书名、作家、推荐语和借阅状态。所以,我们可以利用初始化方法__init__,让实例被创建时自动获得这些属性。
为了后续方便参数传递,借阅状态state采用默认参数,用0来表示'未借出',1来表示'已借出'。
初始化方法定义完成后,一旦实例对象被创建,这几个属性就存在且可随时调用。
接下来,我们来看看Book类还需不需要其他方法。
我们注意到系统里有一个功能是显示所有书籍信息,所以我们可以在Book类定义一个方法,当调用这个方法时,就能够打印出这本书的信息,加上循环,就能打印所有书籍的信息。
我们希望的格式是这样的:
那么,我们可以在初始化方法的基础上定义一个show_info()方法,打印出每本书的信息:
不过这里要介绍一个更符合编程习惯的方法__str__(self)。
在Python中,如果方法名形式是左右带双下划线的,那么就属于特殊方法(如初始化方法),有着特殊的功能。
只要在类中定义了__str__(self)方法,那么当使用print打印实例对象的时候,就会直接打印出在这个方法中return的数据。
直接把上述代码里的方法名show_info(self)替换成__str__(self),留意最后一行调用的代码。
__str__打印对象即可打印出该方法中的返回值,而无须再调用方法。
【类BookManager的编写】
menu()是与用户互动的界面。
那么我们就完成了menu()的定义,但目前还不能运行,是因为代码里的许多方法我们还没有定义,接下来我们就来一个个攻克。
show_all_book()方法,它的功能是打印出系统里所有书籍的信息。
为了方便调试,验证代码是否写对了,我们可以先往书籍系统里添加几本书籍,也就是创建Book类的实例对象。
上面的代码,在BookManager类的初始化方法中创建了3个Book类的实例对象。换言之,当创建实例manager时,book1、book2、book3就会生成。
当有多个对象的时候,就要考虑数据存储的方式。由于每个Book实例是并列平行的关系,所以可以用列表来存储。
于是可以在类的开头定义一个空列表books,方便其他方法调用,然后把刚刚创建的Book实例添加到这个列表里。
如此一来,列表books里的每个元素都是基于Book类创建的实例对象,所以每个元素会自动拥有Book类的方法__str__。我们可以验证一下:
验证成功。book1,book2,book3,都是Book类的实例对象。又因为对象本身有__str__方法,所以当打印对象时,就会打印出该方法中的返回值。
发现了吗?这个结果和我们想要定义的显示书籍信息的方法show_all_book()是一样的,所以我们可以把最后几行代码封装成方法。
我们将两个类的代码组合在一起,直接运行,遇到输入请输入数字1:
完美!那么打印书籍信息的方法我们就讲到这。接下来我们来看第二个功能:添加书籍add_book()。
代码结构应该是这样的:当输入数字2的时候,就会跳转到对应的方法。
尝试录入一本你喜欢的书,再跳回到查询功能。
接下来,来看第三个功能:借阅书籍lend_book()。这是最关键的一环,也是最容易出错的一环。
这里有两个要点:1. 怎么判断这本书在不在系统里;2.怎么判断这本书有没有被借走。
首先,判断在不在系统里,我们可以采用遍历书籍列表books的方式,一旦输入的书籍名称和列表元素中的书籍名称出现匹配,就证明系统里有这本书。
其次,如果书在系统里,有没有被借走可以根据实例属性state来判断,0表示'未借出',1表示'已借出'。
注意这里有几个层级的else语句,我们可以看到最外层结构是for...else,表示的是当for循环里的对象都遍历完毕后,才执行else子句的内容。
也就是说,当列表里没有对象能够满足if book.name == borrow_name条件时,才会打印else子句的那一句话。(如果for循环内部有break子句,也不会执行)
代码是完成了,但归还书籍的时候,也会碰到类似的逻辑。为了不写重复的代码,我们可以额外在类中定义一个方法,专门检查输入的书名是否在书籍列表里。
换句话说:将上面lend_book()方法中检测书名的代码抽出来,封装成一个函数。这样就可以在借书和还书的代码里直接调用,不用两处重复写同样的代码。
为什么要分别返回书籍名称和None值呢?
现在只剩下归还图书return_book这一功能了,它和lend_book有着异曲同工之妙,也是调用check_book方法进行判断。
完整代码:
当然啦,这个程序还有完善的空间,比如将书籍进行分类、录入借书人的姓名,设置归还期限等等……
另外,你会发现,目前我们的代码只能在终端窗口里运行,对终端以外的世界并没有产生什么影响、留下什么痕迹。
这是因为我们还没有学习Python中的文件读写,所以还不能用Python在硬盘上创建、读取和保存文件。
当我们学习到模块相关知识的时候,我们还可以将代码封装成一个有图形界面和数据库管理且可在终端之外运行的程序,就像你电脑中各种各样的软件。
【课后练习】
练习目标
利用类的继承,创建一个Book类的子类。
练习要求
在Book类的基础上,创建一个子类FictionBook类代表虚构类图书,并改造初始化方法,增加一个默认参数type = '虚构类'。
再创建一个子类的实例,利用__str__()方法打印出FictionBook类实例的相关信息。