17. 类和方法
虽然我们以及使用了Python的一些面向特性, 但前两章的程序还算不上真正的面向对象,
因为它们没有体现用户自定义类型之间的关联, 以及操作它们的函数.
下一步是将那些函数转换成方法, 让这种关联更加明显.
本章的代码示例可以从↓下载,
Time2.py
而本章练习的解答参见↓.
Point2_solb.py
17.1 面向对象特性
Python是一门'面向对象编程语言', 它提供了一些支持面向编程的语言特性, 这些特性有如下明确的特征.
* 程序包括类定义和方法定义.
* 大部分计算都通过对象的操作来表达.
* 每个对象定义对应真实世界的某些对象或概念, 而方法则对应真实世界中对象之间交互的方式.
例如, 第16章中定义的Time类对应与人们记录一天中的时间的方式,
而其中我们定义的函数对应人们平时处理时间所做的事情.
类似地, Point和Rectangle类对应于数学中点和矩形的概念.
目前为止, 我们还没有利用上Python所提供的面向对象编程特性.
严格地说, 这些特性并不是必须的; 它们中大部分都是我们已经做过的事情的另一种选择方案.
但在很多情况下, 这种方案更简洁, 更能准确地表达程序的结构.
例如, 在Time1.py程序中, 类定义和接着的函数定义并没有明显的关联.
稍加观察, 很明显每个函数都至少接收一个Time对象作为参数.
这种现象就是方法的由来. 一个方法即是和某个特定类相关联的函数.
我们已经见过字符串, 列表, 字典个元组的方法. 本章中, 我们会为用户定义类型定义方法.
方法和函数在语义上是一样的, 但在语法上有两个区别.
* 方法定义写在类定义之中, 更明确的表示类和方法的关联.
* 调用方法和调用函数的语法形式不同.
在接下来几节中, 我们会将前两章中定义的函数转换为方法.
这种转换时纯机械式的; 你可以依照一系列步骤完成它.
如果你能够轻松地在方法和函数之间转换, 也就能够在任何情况下选择最合适的形式了.
17.2 打印对象
在第16章中, 我们在练习16-1中定义了一个名为Time的类, 你写过一个名为print_time的函数:
class Time:
"""Represents the time of day."""
def print_time(time):
print("%.2d:%.2d:.2d" % (time.hour, time.minute, time.second))
要调用这个函数, 需要传入一个Time对象作为实参:
>>> start = Time()
>>> start.hour = 9
>>> start.minute = 45
>>> start.second = 00
>>> print_time(start)
09:45:00
要把print_time转化为方法, 我们只需要将函数定义移动到类定义中即可. 注意缩进的改变.
class Time:
def print_time(time):
print("%.2d:%.2d:.2d" % (time.hour, time.minute, time.second))
现在有两种方法可以调用print_time.
第一种(更少见的)方式是使用函数调用语法:
>>> Time.print(start)
09:45:00
在这里的点表示法中, Time是类的名称, 而print_time是方法的名称. start是作为参数传入的.
另一种(更简洁的)方式是使用方法调用语句:
>>> start.print_timr()
09:45:00
在这里的点表示法中print_time(又一次)是方法的名称, 而start是调用这个方法的对象, 也成为'主体'(subject).
和一句话中主语用来表示这句话是关于什么东西的一样, 方法调用的主体表示这个方法是关于哪个对象的.
在方法中, 主体会被赋值给第一个形参, 所以本例中start被赋值给time.
依惯例来, 方法的第一个形参通常被叫做self, 所以print_time通常写成这样的形式:
def Time:
def print_time(self):
print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second))
这种惯例的原因是一个隐喻.
* 函数调用的语法print_time(start)暗示函数是活动主体.
它仿佛在说'喂, print_time! 这里是一个让你打印的对象.'
* 在面对对象编程中, 对象是活动主体.
类似start.print_time()的方法调用相当于说: '喂, start! 请打印你自己.'
这种视角的改变可能变得更礼貌, 但是否也更有用这一点却不那么明显.
在我们已经见过的例子中, 它也行并没有更有用.
但有时候将函数的责任转到对象上, 使我们能够编写功能丰富的函数(或方法), 也行代码的维护和复用更容易.
作为练习, 将16.4节中的函数time_to_int重新为方法.
你大概也会想将int_to_time重写为方法, 但这么做事实上没有意义, 因为你找不到可以调用它的对象.
class Time:
def print_time(self):
print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second))
def time_to_int(self):
minutes = self.hour * 60 + self.minute
seconds = minutes * 60 + self.second
return seconds
def increment(self, seconds):
# 将时间对象转换为秒
seconds = self.time_to_int() + seconds
# 将秒转换为时间对象
return int_to_time(seconds)
def int_to_time(seconds):
time = Time()
minutes, time.second = divmod(seconds, 60)
time.hour, time.minute = divmod(minutes, 60)
return time
def main(seconds):
time_obj = Time()
# 为time对象赋值属性
time_obj.hour = 11
time_obj.minute = 59
time_obj.second = 30
res = time_obj.increment(seconds)
res.print_time()
if __name__ == '__main__':
main(9999) # 14:46:09
17.3 另一个示例
下面是一个函数incrment(参见16.3节)的另一个重新成了方法的版本:
# 完整代码↑上面的练习.
# inside class Time:
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
这个版本假设time_to_int已经写成了方法.
另外, 注意它是一个纯函数, 而不是一个修改器.
下面是调用increment的方式:
>>> start.print_time()
09:45:00
>>> end = start.increment(1337)
>>> end.print_time()
10:07:17
主体start赋值给第一个形参self, 实参1337, 赋值给第二个形参seconds.
这种机制有时也会带来困惑, 尤其在程序出错的时候.
例如, 如果使用两个实参调研increment, 则会得到:
>>> end = start.increment(1337, 460)
...
TypeReeor: increment() takes 2 positional arguments but 3 were given
错误信息初看起来似乎很领人困惑, 因为括号里只有两个实参.
但调用的主体也被看作一个实参, 所有其实总共有3个.
另外, 按位实参(positional argument)指的是没有指定名称的实参, 也就是说, 它不是一个关键词实参.
在下面这个函数调用中, parrot和cage是按位实参, 而dead是一个关键词实参.
sketch(parrot, cage, dead=True)
17.4 一个更复杂的示例
重写函数is_after(见16.1节)稍微更复杂一些, 因为它接收两个Time对象作为形参.
这种情况下, 依惯例, 第一个形参命名为self, 而第二个形参命名为other:
# 将代码写在时间对象内.
# inside class Time:
def is_after(self, other):
# 两个时间对象转为十进制数做比较
retun self.time_to_int() > other.time_to_int()
要使用这个方法, 需要在一个对象上调用它, 并传入另一个对象作为实参.
(这个答案好像错误: 两个时间对象转为十进制数做比较 09:45:00 不大于 10:07:17)
>>> end.is_after(start)
True
这种语法的一个好处是, 阅读起来几乎和英语一样:'end is after start?'.
# 完整代码
class Time:
# 打印时间
def print_time(self):
print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second))
# 时间转为十进制
def time_to_int(self):
minutes = self.hour * 60 + self.minute
seconds = minutes * 60 + self.second
return seconds
# 时间增量
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
# 两个时间对象转为十进制数做比较
def is_after(self, other):
return self.time_to_int() > other.time_to_int()
# 十进制转为时间对象
def int_to_time(seconds):
time = Time()
minutes, time.second = divmod(seconds, 60)
time.hour, time.minute = divmod(minutes, 60)
return time
def main():
start = Time()
start.hour = 9
start.minute = 45
start.second = 00
start.print_time() # 09:45:00
# 增加1337秒
end = start.increment(1337)
end.print_time() # 10:07:17
print(start.is_after(end)) # False
if __name__ == '__main__':
main()
17.5 init方法
init方法(即'initialization'的简写, 意思是初始化)是一个特殊的方法, 当对象初始化时会被调用.
它的全名是__init__(两个下划线, 接着是init, 再接着两个下划线).
Time类的init方法可能如下所示:
# inside class Time:
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
__init__的形参和类的属性名称常常是相同的. 语句:
self.hour = hour
将形参hour的值存储为self的一个属性.
形参是可选的, 所以当你不使用任何实参调研Time时 , 会得到默认值:
>>> time = Time()
>>> time.print_time()
00:00:00
如果提供1个实参, 它会覆盖hour:
>>> time = Time(9)
>>> time,print_time()
如果提供2个实参, 它会覆盖hour和minute:
>>> time = Time(9, 45)
>>> time.print_time()
09:45:00
如果提供3个实参, 它们会覆盖全部3个默认值.
# 完整代码
class Time:
# 初始化方法
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
# 打印时间
def print_time(self):
print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second))
def main():
time = Time()
time.print_time() # 00:00:00
time = Time(9)
time.print_time() # 09:00:00
time = Time(9, 45)
time.print_time() # 09:45:00
time = Time(9, 45, 15)
time.print_time() # 09:45:15
if __name__ == '__main__':
main()
作为练习, 为Point类编写一个init方法, 接收x和y作为可选形参, 并将它们的值赋值到对应的属性上.
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def print_point(self):
print('(%g, %g)' % (self.x, self.y))
p = Point()
p.print_point() # (0, 0)
p = Point(1)
p.print_point() # (1, 0)
p = Point(1, 1)
p.print_point() # (1, 1)
17.6 str方法
__str__和__init__类似, 是一个特殊方法, 它用来放回对象的字符串表达形式.
例如, 下面是一个Time对象的str方法.
# inside class Time:
class __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
当你打印对象是, Python会调用str方法.
(打印对象显示的是对象的信息和内存地址<__main__.Time object at 0x0000024253786790>)
>>> time = Time(9, 45)
>>> print(time)
09:45:00
# 完整代码
class Time:
# 初始化方法
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
# 字符串方法
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
time = Time(9, 45)
print(time) # 09:45:00
当我编写一个新类时, 我总是开始先写__init__, 以便初始化对象, 然后写__str__以便调试.
作为练习, 为Point类编写一个str方法. 创建一个Point对象并打印它.
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def __str__(self):
return '(%g, %g)' % (self.x, self.y)
p = Point()
print(p) # (0, 0)
17.7 操作符重载
通过定义其他的特殊方法, 你可以问用户定义类型的更重操作符指定行为.
例如, 如果你为Time类定义一个__add__方法, 则可以在Time对象上使用+操作符.
下面是这个方法的定义:
# inside class Time:
def __add__(self, other):
seconds = self.time_to_int() + other.timr_to_time()
return int_to_time(seconds)
而下面是如何用它:
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print(start + duration)
11:20:00
当你对Time对象应用+操作符时, Python会调用__add__.
当你打印结果时, Python会调用__str__.
幕后其实发生了很多事情!
修改操作符的行为以便它能够作用于用户定义类型, 这个过程称为操作符重载.
对每一个操作符, Python都提供了一个对应的特殊方法, 如__add__.
更多细节, 可以参见http://docs.python.prg/3/reference/datamodel.html#specialnames
作为练习, 我Point类编写一个add方法.
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
# 新建一个对象保存计算的结果
def __add__(self, other):
p = Point()
p.x = self.x + other.x
p.y = self.y + other.y
return p
def __str__(self):
return '(%g, %g)' % (self.x, self.y)
p1 = Point(1, 3) # (1, 3)
p2 = Point(3, 1) # (3, 1)
add_p = p1 + p2
print(add_p) # (4, 4)
17.8 基于类型的分支
在前面一节中我们将两个Time对象相加, 但你也可能会想要将一个Time对象加上一个整数.
接下来是__add__的一个版本, 检查other的类型, 并调用add_timr或increment:
# inside class Time:
# 传递一个对象和一个值.
def __add__(self, other):
# 如果值是Time的实例, 则是两个时间对象相加.
if isinstance(other, Time):
return self.add_time(other)
# 如果值不是Time的实例, 则是认定它一个整数, 将时间对象转为整数相加, 最后转为时间对象.
else:
return self.increment(other)
def add_time(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
内置函数isinstance接收一个值值与一个类对象, 并当值时此类的一个实例时返回True.
如果other是一个Time对象, __add__会调用add_time.
否则它认为实参是整数, 并调用increment.
这个操作成为'基于类型的分发(type-based dispatch)', 因为它根据形参的类型, 将计算分发到不同的方法上.
下面是使用不同类型的实参调研+操作符的示例:
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print(start + duration)
11:20:00
print(satrt + 1337)
10:07:17
遗憾的是这个加法的实现并不满足交换律.
(交换律是一种计算定律, 两个数计算, 交换操作数位置, 它们的计算结果不变.)
如果整数时第一个操作数, 则会得到:
>>> print(1337 + start)
...
TypeError: unsupported operand type(s) for +: 'int' and 'instance'
类型错误: 不支持+的操作数类型: 'int'和'实例'
问题在于, 这里和之前询问一个Time对象上加上一个整数不同,
Python在询问一个整数去加上一个Time对象, 而它并不知道如何去做到.
但这个问题也有一个聪明的解决方案: 特别方法__radd__意即'右加法(right-side add)'.
当Time对象出现在-好的右侧是, 会调用这个方法.
下面是它的定义:
# inside class Time:
def __radd__(self, other):
return self.__add__(other)
而下面是如何使用:
>>> print(1337 + start)
10:07:17
# 完整代码
class Time:
# 初始化方法
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
# 字符串方法
def __str__(self):
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
# 传递一个对象和一个值.
def __add__(self, other):
# 如果值是Time的实例, 则是两个时间对象相加.
if isinstance(other, Time):
return self.add_time(other)
# 如果值不是Time的实例, 则是认定它一个整数, 将时间对象转为整数相加, 最后转为时间对象.
else:
return self.increment(other)
# 右加法(时间对象被+时触发)
def __radd__(self, other):
# 调用add方法.
return self.__add__(other)
# 时间转为十进制
def time_to_int(self):
minutes = self.hour * 60 + self.minute
seconds = minutes * 60 + self.second
return seconds
# 时间增量
def increment(self, seconds):
seconds += self.time_to_int()
return int_to_time(seconds)
def add_time(self, other):
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
# 十进制转为时间对象
def int_to_time(seconds):
time = Time()
minutes, time.second = divmod(seconds, 60)
time.hour, time.minute = divmod(minutes, 60)
return time
def main():
start = Time(9, 45)
print(1337 + start) # 10:07:17
if __name__ == '__main__':
main()
作为练习, 为Point类编写一个add方法, 可以接受一个Point对象或者一个元组.
* 如果第二个操作对象是一个Point对象, 则方法应该返回一个新的Point对象,
其x坐标是两个坐标的和, y坐标也是类似.
* 如果第二个操作对象是一个元组, 方法则将第一个元素和x坐标相加, 将第二个元素和y坐标相加,
并返回一个包含相加结果的新Point对象.
class Point:
# 初始化方法
def __init__(self, x=0, y=0):
self.x = x
self.y = y
# 坐标加法
def __add__(self, other):
if isinstance(other, Point):
return self.add_point(other)
else:
p = Point(self.x + other[0], self.y + other[1])
return p
# 右加法
def __radd__(self, other):
return self.__add__(other)
# 打印字符串方法
def __str__(self):
return '(%g, %g)' % (self.x, self.y)
def add_point(self, other):
p = Point(self.x + other.x, self.y + other.y)
return p
p1 = Point(1, 3)
p2 = Point(3, 1)
print(p1 + p2) # (4, 4)
p3 = (3, 1)
print(p1 + p3) # (4, 4)
print(p3 + p1) # (4, 4)
17.9 多态
当需要时, 基于类型的分发很有用, 但(幸运的是)我们并不总是需要它.
我们编写很多处理字符串的函数, 实际上对其他序列类型也可以用.
例如, 在11.1节中, 我们使用histogram来记录单词中每个字母出现的次数:
# 直方图
def histogram(s):
d = dict()
for c in s:
if c not in d:
d[c] = 1
else:
d[c] = d[c] + 1
return d
这个函数对列表, 元组甚至是字典都可用, 只要s的元素是可散列的, 因而可以用作d的键即可:
>>> t = ['spam', 'agg', 'spam', 'spam', 'bacon', 'spam']
>>> histogram(t)
{'bacon': 1, 'agg': 1, 'spam': 4}
处理多个类型的函数称为多态(polymorphic). 多态可以促进代码复用.
例如, 用来计算一个序列所有元素的和的内置函数sum, 对所有其元素支持加法的序列都可用.
由于Time对象提供了add方法, 它们也可以使用sum:
>>> t1 = Time(7, 43)
>>> t2 = Time(7, 41)
>>> t3 = Time(7, 37)
>>> total = sum([t1, t2, t3])
>>> print(total)
23:01:00
总的来说, 如果函数内部所有的操作都支持某种类型, 那么这个函数就可以用于那种类型.
当你法相一个写好的函数, 尽然有出人意料的效果, 可以用于没有计划过的类型时, 这才是最好的多态.
17.10 接口和实现
面向对象设计的目的之一是提高软件的可维护性,
也就是说, 当系统的其他部分改变时, 程序还能够保持真确运行, 并且能够修改程序来适应新的需求.
将接口和实现分离的设计理念, 可以帮我们更容易达到这个目标.
对于对象来说, 那意味着类所提供的方法不改依赖其属性的表达方式.
例如, 在本章中我们来发了一个类来表达一天中的时间.
这个类提供的方法包括time_to_int, is_after和add_time.
我们可以使用几种不同的方式来实现这些方法.
实现的细节依赖于我们表达的时间概念的方式.
在本章中, Time对象的属性hour, minute和second.
用另一种方案, 我们可以将这些属性替换成一个整数, 表示从凌晨开始到现在的秒数.
这个实现可能会让一些方法, 如is_after, 更容易实现, 但也会让另一些方法更难实现.
在部署一个新类时, 你可能会发现更好的实现.
如果程序中其他部分用到到你的类, 则修改接口会非常消耗时间, 并且容易产生错误.
但是, 如果很谨慎小心地设计接口, 则可以在不修改接口的情况下修改实现,
这样程序的其他部分就不需要跟着修改.
在计算机科学中, 接口(Interface)是指程序中的一个抽象的规范,
它定义了一个模块或一个类所提供的服务, 但并不涉及这些服务的具体实现细节.
接口通常包含一组方法签名, 这些方法描述了模块或类所提供的服务以及它们的参数和返回类型.
接口的实现(Implementation)是指程序中的一个具体的实现, 它实现了一个或多个接口中定义的方法.
接口的实现通常包含了实现方法的代码, 以及实现所需的数据结构和算法.
在面向对象编程中, 接口通常用于描述类的行为和能力, 而实现则是具体实现类的实现细节.
通过接口的定义, 我们可以定义一个类所需实现的方法, 而不用关心这些方法的具体实现.
这样, 在实现类中, 我们只需要关注具体的实现, 而不用担心接口的规范.
这样可以提高代码的可维护性和可扩展性.
17.11 调试
在程序运行的任时刻, 往对象上添加属性都是合法的, 但如果遵守更严格的类型理论,
让对象拥有相同的类型却有不同的属性组, 会很容易导致错误.
通常来说, 在init方法中初始化对象的全部属性是个好习惯.
如果并不清楚一个对象时候拥有某个属性, 可以使用内置函数hasattr(参见15.7节).
另一种访问一个对象的属性的方法是使用内置函数vars,
它接收一个对象, 并返回一个将属性名称(字符串形式)映射到属性值的字典对象:
>>> p = Point(3, 4)
>>> vars(9)
{'y': 4, 'x': 3}
为了调试, 你可能会发现这个函数放在手边是很有用的:
def print_attributes(obj):
for attr in vars(obj):
print(attr, getattr(obj, attr))
print_attributes遍历对象的属性字典, 并打印出每个属性的名称和相应的值.
内置函数getattr接收一个对象以及一个属性名称(字符串形式)并返回属性的值.
# 完整代码
class Point:
# 初始化方法
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def print_attributes(obj):
for attr in vars(obj):
print(attr, getattr(obj, attr))
p = Point(1, 2)
print(vars(p)) # {'x': 1, 'y': 2}
print_attributes(p)
17.12 术语表
面向对象语言(object-oriented language): 一种提供诸如用户定义类型和方法这类的语言特性,
以便面向对象编程的语言.
面向对象编程(object-oriented programming): 一种编程风格, 数据和修改数据的操作组织成类和方法的形式.
方法(method): 在类定义之内定义的函数, 在类的实例上调用.
主体(subiect): 调用方法所在的对象.
按位实参(positional argument): 一个不包含参数名字的实参, 所以它不是一个关键字实参.
操作符重载(operator overloading): 修改一个类似+号这样的操作符的行为, 使之可以用于用户定义类型.
基于类型的分发(type-based dispatch): 一种编程模式, 检查操作对象的类型, 并对不同类型调用不同的函数.
多态(polymorphic): 函数的一种属性, 可以处理多种类型的参数.
信息隐藏(information hiding): 对象提供的接口不应当依赖于其实现, 特别是其属性的表达形式的原则.
17.13 练习
1. 练习1
从↓下载本章的代码.
https://github.com/AllenDowney/ThinkPython2/blob/master/code/Time2.py
将Time的属性改为从凌晨开始到现在的秒数.
接着修改方法(以及函数int_to_time), 以使用新的属性实现.
你应该不需要修改main里面的属性实现.
当你做完之后, 输出应该和以前一样.
解答: https://github.com/AllenDowney/ThinkPython2/blob/master/code/Time2_soln.py
(意思是不修改main中的程序, 修改Time初始化方法中的代码, 只初始化一个属性second,
后续需要second属性的方法, 都改变, 使其输出的之前的一样...)
# Time2.py
class Time:
def __init__(self, hour=0, minute=0, second=0):
"""
初始化时间对象
hour: int
minute: int
second: int or float
"""
self.hour = hour
self.minute = minute
self.second = second
def __str__(self):
"""返回时间的字符串表示形式."""
return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
def print_time(self):
"""打印时间的字符串表示形式."""
print(str(self))
def time_to_int(self):
"""计算自午夜以来的秒数."""
minutes = self.hour * 60 + self.minute
seconds = minutes * 60 + self.second
return seconds
def is_after(self, other):
"""如果t1在t2之后, 则返回True; 否则为false."""
return self.time_to_int() > other.time_to_int()
def __add__(self, other):
"""添加两个时间对象或一个时间对象和一个数字. 其他: 时间对象或秒数."""
if isinstance(other, Time):
return self.add_time(other)
else:
return self.increment(other)
def __radd__(self, other):
"""添加两个时间对象或一个时间对象和一个数字."""
return self.__add__(other)
def add_time(self, other):
"""添加两个时间对象."""
assert self.is_valid() and other.is_valid()
seconds = self.time_to_int() + other.time_to_int()
return int_to_time(seconds)
def increment(self, seconds):
"""返回一个新的时间, 该时间是此时间和秒的总和."""
seconds += self.time_to_int()
return int_to_time(seconds)
def is_valid(self):
"""检查Time对象是否满足不变量."""
if self.hour < 0 or self.minute < 0 or self.second < 0:
return False
if self.minute >= 60 or self.second >= 60:
return False
return True
def int_to_time(seconds):
"""创建一个新的'时间'对象. 秒: 午夜后的int秒."""
minutes, second = divmod(seconds, 60)
hour, minute = divmod(minutes, 60)
time = Time(hour, minute, second)
return time
def main():
start = Time(9, 45, 00)
print('开始时间', end=' ')
start.print_time()
print('结束时间', end=' ')
end = start.increment(1337)
# end = start.increment(1337, 460)
end.print_time()
print('在开始(对象) 在 结束(对象)之后?', end=' ')
print(end.is_after(start))
print('使用 __str__方法')
print(start, end)
start = Time(9, 45)
duration = Time(1, 35)
print(start + duration)
print(start + 1337)
print(1337 + start)
print('多态性示例')
t1 = Time(7, 43)
t2 = Time(7, 41)
t3 = Time(7, 37)
print(t1, t2, t3)
total = sum([t1, t2, t3])
print('时间总和为:', total)
if __name__ == '__main__':
main()
结果如下:
开始时间 09:45:00
结束时间 10:07:17
在开始(对象) 在 结束(对象)之后? True
使用 __str__方法
09:45:00 10:07:17
11:20:00
10:07:17
10:07:17
多态性示例
07:43:00 07:41:00 07:37:00
时间总和为: 23:01:00
class Time:
def __init__(self, hour=0, minute=0, second=0):
"""
初始化方法
:param hour: int
:param minute: int
:param second: int and float
"""
# 计算总分钟
minutes = hour * 60 + minute
# 计算总秒数
self.second = minutes * 60 + second
# 时间对象相加
def __add__(self, other):
# 判断值的类型
if isinstance(other, Time):
# 秒数总和
seconds = self.time_to_int() + other.time_to_int()
# 秒转为时间对象
return int_to_time(seconds)
else:
return self.increment(other)
# 右相加
def __radd__(self, other):
return self.__add__(other)
def __str__(self):
# 将秒转为时分秒
minute, second = divmod(self.second, 60)
hour, minute = divmod(minute, 60)
return '%.2d:%.2d:%.2d' % (hour, minute, second)
# 将时间对象转为十进制
def time_to_int(self):
# 总秒数
return self.second
def print_time(self):
# 将秒转为时分秒
minute, second = divmod(self.second, 60)
hour, minute = divmod(minute, 60)
print('%.2d:%.2d:%.2d' % (hour, minute, second))
def increment(self, other):
# 总秒数
seconds = self.second + other
return int_to_time(seconds)
def is_after(self, other):
# 两个时间对象转为十进制数
return self.time_to_int() > other.time_to_int()
def int_to_time(seconds):
minute, second = divmod(seconds, 60)
hour, minute = divmod(minute, 60)
# 返回一个新的时间对象
time = Time(hour, minute, second)
return time
def main():
start = Time(9, 45, 00)
print('开始时间', end=' ')
start.print_time()
print('结束时间', end=' ')
end = start.increment(1337)
# end = start.increment(1337, 460)
end.print_time()
print('在开始(对象) 在 结束(对象)之后?', end=' ')
print(end.is_after(start))
print('使用 __str__方法')
print(start, end)
start = Time(9, 45)
duration = Time(1, 35)
print(start + duration)
print(start + 1337)
print(1337 + start)
print('多态性示例')
t1 = Time(7, 43)
t2 = Time(7, 41)
t3 = Time(7, 37)
print(t1, t2, t3)
total = sum([t1, t2, t3])
print('时间总和为:', total)
if __name__ == '__main__':
main()
2. 练习2
这个练习提醒你关于Python的一种最常见且最难查找的错误的故事.
编写一个叫作kangroo(袋鼠)的类, 有如下方法.
1. 一个__init__方法, 将属性pouch_contents(口袋中的东西)初始化为一个空列表.
2. 一个put_in_pouch方法, 接收任何类型的对象, 并将它添加到piuch_contents中.
3. 一个__str__方法, 返回Kangaroo对象以及口袋中的内容的字符串表达形式.
创建两个Kangaroo对象, 将它们复制到变量kango和roo, 并将roo添加到Kanga的口袋中.
下载↓, 它包含了前面问题的解答, 但里面有一个很大很丑陋的bug. 找打并修改这个bug.
https://github.com/AllenDowney/ThinkPython2/blob/master/code/BadKangaroo.py
如果你遇到阻碍, 可以下载↓它解释了问题的原因, 并提供了一个解决方案.
https://github.com/AllenDowney/ThinkPython2/blob/master/code/GoodKangaroo.py
# BadKangaroo.py
"""
警告:这个项目包含了一个严重的错误.
我把它放在那里作为调试练习.
但是你不想效仿这个例子!
"""
class Kangaroo:
"""袋鼠是一种有袋类动物,"""
def __init__(self, name, contents=[]):
# 袋鼠的名称
self.name = name
# 设置口袋属性
self.pouch_contents = contents
def __str__(self):
"""返回一个字符串的袋鼠对象."""
# 设置口袋的名字
t = [self.name + ' has pouch contents:']
# 获取口袋的值
for obj in self.pouch_contents:
# object.__str__(obj) 返回obj的字符串格式.
s = ' ' + object.__str__(obj)
t.append(s)
# 口袋中的对个值分行展示.
return '\n'.join(t)
def put_in_pouch(self, item):
"""将一个新项添加到袋内容.
item: 对象添加
"""
self.pouch_contents.append(item)
# 实例袋鼠对象
kanga = Kangaroo('Kanga')
# 实例袋鼠对象
roo = Kangaroo('Roo')
# 往袋鼠的口袋放一个钱包.
kanga.put_in_pouch('wallet')
# 往袋鼠的口袋放一个车钥匙.
kanga.put_in_pouch('car keys')
# 往袋鼠的口袋放一个袋鼠对象.
kanga.put_in_pouch(roo)
# 打印袋鼠对象1的字符串形式.
print(kanga)
# 打印袋鼠对象1的字符串形式, 这个时你会发现,
# 自己并没有往第二个袋鼠的口袋里装过东西可现在它有东西存在.
# 第二次实例对象时, 并没有重新创建新的列表, 而是引用了已经创建的列表.
# 两个对象共有一个列表了.
print(roo)
# GoodKangaroo.py
class Kangaroo:
def __init__(self, name, contents=None):
# 在这个版本中,默认值是None.
# 当 __init__ 运行, 它检查的内容和价值,
# 如果有必要的话,创建一个新的空列表.
# 这种方式每一个袋鼠会获得一个默认值, 引用到另一个列表.
# 作为一般规则,你应该避免使用一个可变的对象作为默认值,除非你真的知道你在做什么.
self.name = name
if contents is None:
contents = []
self.pouch_contents = contents
def __str__(self):
t = [self.name + ' has pouch contents:']
for obj in self.pouch_contents:
s = ' ' + object.__str__(obj)
t.append(s)
return '\n'.join(t)
def put_in_pouch(self, item):
self.pouch_contents.append(item)
kanga = Kangaroo('Kanga')
roo = Kangaroo('Roo')
kanga.put_in_pouch('wallet')
kanga.put_in_pouch('car keys')
kanga.put_in_pouch(roo)
print(kanga)
print(roo)