17.类和方法

17. 类和方法

handlebars 复制代码
虽然我们以及使用了Python的一些面向特性, 但前两章的程序还算不上真正的面向对象, 
因为它们没有体现用户自定义类型之间的关联, 以及操作它们的函数.
下一步是将那些函数转换成方法, 让这种关联更加明显.
handlebars 复制代码
本章的代码示例可以从↓下载,
Time2.py
而本章练习的解答参见↓.
Point2_solb.py
17.1 面向对象特性
handlebars 复制代码
Python是一门'面向对象编程语言', 它提供了一些支持面向编程的语言特性, 这些特性有如下明确的特征.
* 程序包括类定义和方法定义.
* 大部分计算都通过对象的操作来表达.
* 每个对象定义对应真实世界的某些对象或概念, 而方法则对应真实世界中对象之间交互的方式.

例如, 第16章中定义的Time类对应与人们记录一天中的时间的方式,
而其中我们定义的函数对应人们平时处理时间所做的事情.
类似地, Point和Rectangle类对应于数学中点和矩形的概念.
handlebars 复制代码
目前为止, 我们还没有利用上Python所提供的面向对象编程特性.
严格地说, 这些特性并不是必须的; 它们中大部分都是我们已经做过的事情的另一种选择方案.
但在很多情况下, 这种方案更简洁, 更能准确地表达程序的结构.

例如, 在Time1.py程序中, 类定义和接着的函数定义并没有明显的关联.
稍加观察, 很明显每个函数都至少接收一个Time对象作为参数.
这种现象就是方法的由来. 一个方法即是和某个特定类相关联的函数.
我们已经见过字符串, 列表, 字典个元组的方法. 本章中, 我们会为用户定义类型定义方法.
handlebars 复制代码
方法和函数在语义上是一样的, 但在语法上有两个区别.
* 方法定义写在类定义之中, 更明确的表示类和方法的关联.
* 调用方法和调用函数的语法形式不同.

在接下来几节中, 我们会将前两章中定义的函数转换为方法. 
这种转换时纯机械式的; 你可以依照一系列步骤完成它.
如果你能够轻松地在方法和函数之间转换, 也就能够在任何情况下选择最合适的形式了.
17.2 打印对象
handlebars 复制代码
在第16章中, 我们在练习16-1中定义了一个名为Time的类, 你写过一个名为print_time的函数:
python 复制代码
class Time:
    """Represents the time of day."""
    

def print_time(time):
    print("%.2d:%.2d:.2d" % (time.hour, time.minute, time.second))
    
handlebars 复制代码
要调用这个函数, 需要传入一个Time对象作为实参:
python 复制代码
>>> start = Time()
>>> start.hour = 9
>>> start.minute = 45
>>> start.second = 00
>>> print_time(start)
09:45:00
handlebars 复制代码
要把print_time转化为方法, 我们只需要将函数定义移动到类定义中即可. 注意缩进的改变.
python 复制代码
class Time:
	def print_time(time):
    	print("%.2d:%.2d:.2d" % (time.hour, time.minute, time.second))
        
handlebars 复制代码
现在有两种方法可以调用print_time.
第一种(更少见的)方式是使用函数调用语法:
python 复制代码
>>> Time.print(start)
09:45:00
handlebars 复制代码
在这里的点表示法中, Time是类的名称, 而print_time是方法的名称. start是作为参数传入的.
另一种(更简洁的)方式是使用方法调用语句:
python 复制代码
>>> start.print_timr()
09:45:00
handlebars 复制代码
在这里的点表示法中print_time(又一次)是方法的名称, 而start是调用这个方法的对象, 也成为'主体'(subject).
和一句话中主语用来表示这句话是关于什么东西的一样, 方法调用的主体表示这个方法是关于哪个对象的.
handlebars 复制代码
在方法中, 主体会被赋值给第一个形参, 所以本例中start被赋值给time.
依惯例来, 方法的第一个形参通常被叫做self, 所以print_time通常写成这样的形式:
python 复制代码
def Time:
	def print_time(self):
		print('%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second))
        
handlebars 复制代码
这种惯例的原因是一个隐喻.
* 函数调用的语法print_time(start)暗示函数是活动主体. 
  它仿佛在说'喂, print_time! 这里是一个让你打印的对象.'
* 在面对对象编程中, 对象是活动主体. 
  类似start.print_time()的方法调用相当于说: '喂, start! 请打印你自己.'
  
这种视角的改变可能变得更礼貌, 但是否也更有用这一点却不那么明显.
在我们已经见过的例子中, 它也行并没有更有用.
但有时候将函数的责任转到对象上, 使我们能够编写功能丰富的函数(或方法), 也行代码的维护和复用更容易.
handlebars 复制代码
作为练习, 将16.4节中的函数time_to_int重新为方法.
你大概也会想将int_to_time重写为方法, 但这么做事实上没有意义, 因为你找不到可以调用它的对象.
python 复制代码
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 另一个示例
handlebars 复制代码
下面是一个函数incrment(参见16.3节)的另一个重新成了方法的版本:
python 复制代码
# 完整代码↑上面的练习.
# inside class Time: 
	def increment(self, seconds):
		seconds += self.time_to_int()
		return int_to_time(seconds)
		
handlebars 复制代码
这个版本假设time_to_int已经写成了方法.
另外, 注意它是一个纯函数, 而不是一个修改器.
handlebars 复制代码
下面是调用increment的方式:
python 复制代码
>>> start.print_time()
09:45:00
>>> end = start.increment(1337)
>>> end.print_time()
10:07:17
handlebars 复制代码
主体start赋值给第一个形参self, 实参1337, 赋值给第二个形参seconds.
这种机制有时也会带来困惑, 尤其在程序出错的时候.
例如, 如果使用两个实参调研increment, 则会得到:
python 复制代码
>>> end = start.increment(1337, 460)
...
TypeReeor: increment() takes 2 positional arguments but 3 were given
handlebars 复制代码
错误信息初看起来似乎很领人困惑, 因为括号里只有两个实参.
但调用的主体也被看作一个实参, 所有其实总共有3个.

另外, 按位实参(positional argument)指的是没有指定名称的实参, 也就是说, 它不是一个关键词实参.
在下面这个函数调用中, parrot和cage是按位实参, 而dead是一个关键词实参.
python 复制代码
sketch(parrot, cage, dead=True)
17.4 一个更复杂的示例
handlebars 复制代码
重写函数is_after(见16.1节)稍微更复杂一些, 因为它接收两个Time对象作为形参.
这种情况下, 依惯例, 第一个形参命名为self, 而第二个形参命名为other:
python 复制代码
# 将代码写在时间对象内.
# inside class Time:
	def is_after(self, other):
         # 两个时间对象转为十进制数做比较
		retun self.time_to_int() > other.time_to_int()
        
handlebars 复制代码
要使用这个方法, 需要在一个对象上调用它, 并传入另一个对象作为实参.
(这个答案好像错误: 两个时间对象转为十进制数做比较 09:45:00 不大于 10:07:17)
python 复制代码
>>> end.is_after(start)
True
handlebars 复制代码
这种语法的一个好处是, 阅读起来几乎和英语一样:'end is after start?'.
python 复制代码
# 完整代码
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方法
handlebars 复制代码
init方法(即'initialization'的简写, 意思是初始化)是一个特殊的方法, 当对象初始化时会被调用.
它的全名是__init__(两个下划线, 接着是init, 再接着两个下划线).
Time类的init方法可能如下所示:
python 复制代码
# inside class Time:
	def __init__(self, hour=0, minute=0, second=0):
		self.hour = hour
		self.minute = minute
		self.second = second
        
handlebars 复制代码
__init__的形参和类的属性名称常常是相同的. 语句:
python 复制代码
self.hour = hour
handlebars 复制代码
将形参hour的值存储为self的一个属性.
形参是可选的, 所以当你不使用任何实参调研Time时 , 会得到默认值:
python 复制代码
>>> time = Time()
>>> time.print_time()
00:00:00
handlebars 复制代码
如果提供1个实参, 它会覆盖hour:
python 复制代码
>>> time = Time(9)
>>> time,print_time()
handlebars 复制代码
如果提供2个实参, 它会覆盖hour和minute:
python 复制代码
>>> time = Time(9, 45)
>>> time.print_time()
09:45:00
handlebars 复制代码
如果提供3个实参, 它们会覆盖全部3个默认值.
python 复制代码
# 完整代码
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()
handlebars 复制代码
作为练习, 为Point类编写一个init方法, 接收x和y作为可选形参, 并将它们的值赋值到对应的属性上.
python 复制代码
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方法
handlebars 复制代码
__str__和__init__类似, 是一个特殊方法, 它用来放回对象的字符串表达形式.
例如, 下面是一个Time对象的str方法.
python 复制代码
# inside class Time:
	class __str__(self):
		return '%.2d:%.2d:%.2d' % (self.hour, self.minute, self.second)
    
handlebars 复制代码
当你打印对象是, Python会调用str方法.
(打印对象显示的是对象的信息和内存地址<__main__.Time object at 0x0000024253786790>)
python 复制代码
>>> time = Time(9, 45)
>>> print(time)
09:45:00
python 复制代码
# 完整代码
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
handlebars 复制代码
当我编写一个新类时, 我总是开始先写__init__, 以便初始化对象, 然后写__str__以便调试.
handlebars 复制代码
作为练习, 为Point类编写一个str方法. 创建一个Point对象并打印它.
python 复制代码
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 操作符重载
handlebars 复制代码
通过定义其他的特殊方法, 你可以问用户定义类型的更重操作符指定行为.
例如, 如果你为Time类定义一个__add__方法, 则可以在Time对象上使用+操作符.
下面是这个方法的定义:
python 复制代码
# inside class Time:
	def __add__(self, other):
		seconds = self.time_to_int() + other.timr_to_time()
       	
         return int_to_time(seconds)
        
handlebars 复制代码
而下面是如何用它:
python 复制代码
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print(start + duration)
11:20:00
handlebars 复制代码
当你对Time对象应用+操作符时, Python会调用__add__.
当你打印结果时, Python会调用__str__.
幕后其实发生了很多事情!

修改操作符的行为以便它能够作用于用户定义类型, 这个过程称为操作符重载.
对每一个操作符, Python都提供了一个对应的特殊方法, 如__add__.
更多细节, 可以参见http://docs.python.prg/3/reference/datamodel.html#specialnames
handlebars 复制代码
作为练习, 我Point类编写一个add方法.
python 复制代码
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 基于类型的分支
handlebars 复制代码
在前面一节中我们将两个Time对象相加, 但你也可能会想要将一个Time对象加上一个整数.
接下来是__add__的一个版本, 检查other的类型, 并调用add_timr或increment:
python 复制代码
# 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)
	
handlebars 复制代码
内置函数isinstance接收一个值值与一个类对象, 并当值时此类的一个实例时返回True.
如果other是一个Time对象, __add__会调用add_time.
否则它认为实参是整数, 并调用increment.
这个操作成为'基于类型的分发(type-based dispatch)', 因为它根据形参的类型, 将计算分发到不同的方法上.

下面是使用不同类型的实参调研+操作符的示例:
python 复制代码
>>> start = Time(9, 45)
>>> duration = Time(1, 35)
>>> print(start + duration)
11:20:00
print(satrt + 1337)
10:07:17
handlebars 复制代码
遗憾的是这个加法的实现并不满足交换律.
(交换律是一种计算定律, 两个数计算, 交换操作数位置, 它们的计算结果不变.)
如果整数时第一个操作数, 则会得到:
python 复制代码
>>> print(1337 + start)
...
TypeError: unsupported operand type(s) for +: 'int' and 'instance' 
类型错误: 不支持+的操作数类型: 'int'和'实例'
handlebars 复制代码
问题在于, 这里和之前询问一个Time对象上加上一个整数不同, 
Python在询问一个整数去加上一个Time对象, 而它并不知道如何去做到.
但这个问题也有一个聪明的解决方案: 特别方法__radd__意即'右加法(right-side add)'.
当Time对象出现在-好的右侧是, 会调用这个方法.
下面是它的定义:
python 复制代码
# inside class Time:
	def __radd__(self, other):
		return self.__add__(other)
handlebars 复制代码
而下面是如何使用:
python 复制代码
>>> print(1337 + start)
10:07:17
python 复制代码
# 完整代码
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()
handlebars 复制代码
作为练习, 为Point类编写一个add方法, 可以接受一个Point对象或者一个元组.
* 如果第二个操作对象是一个Point对象, 则方法应该返回一个新的Point对象, 
  其x坐标是两个坐标的和, y坐标也是类似.
* 如果第二个操作对象是一个元组, 方法则将第一个元素和x坐标相加, 将第二个元素和y坐标相加,
  并返回一个包含相加结果的新Point对象.
python 复制代码
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 多态
handlebars 复制代码
当需要时, 基于类型的分发很有用, 但(幸运的是)我们并不总是需要它.
我们编写很多处理字符串的函数, 实际上对其他序列类型也可以用.
例如, 在11.1节中, 我们使用histogram来记录单词中每个字母出现的次数:
python 复制代码
# 直方图
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
handlebars 复制代码
这个函数对列表, 元组甚至是字典都可用, 只要s的元素是可散列的, 因而可以用作d的键即可:
python 复制代码
>>> t = ['spam', 'agg', 'spam', 'spam', 'bacon', 'spam']
>>> histogram(t)
{'bacon': 1, 'agg': 1, 'spam': 4}
handlebars 复制代码
处理多个类型的函数称为多态(polymorphic). 多态可以促进代码复用.
例如, 用来计算一个序列所有元素的和的内置函数sum, 对所有其元素支持加法的序列都可用.

由于Time对象提供了add方法, 它们也可以使用sum:
python 复制代码
>>> t1 = Time(7, 43)
>>> t2 = Time(7, 41)
>>> t3 = Time(7, 37)
>>> total = sum([t1, t2, t3])
>>> print(total)
23:01:00
handlebars 复制代码
总的来说, 如果函数内部所有的操作都支持某种类型, 那么这个函数就可以用于那种类型.
当你法相一个写好的函数, 尽然有出人意料的效果, 可以用于没有计划过的类型时, 这才是最好的多态.
17.10 接口和实现
handlebars 复制代码
面向对象设计的目的之一是提高软件的可维护性, 
也就是说, 当系统的其他部分改变时, 程序还能够保持真确运行, 并且能够修改程序来适应新的需求.

将接口和实现分离的设计理念, 可以帮我们更容易达到这个目标.
对于对象来说, 那意味着类所提供的方法不改依赖其属性的表达方式.

例如, 在本章中我们来发了一个类来表达一天中的时间.
这个类提供的方法包括time_to_int, is_after和add_time.

我们可以使用几种不同的方式来实现这些方法.
实现的细节依赖于我们表达的时间概念的方式.
在本章中, Time对象的属性hour, minute和second.

用另一种方案, 我们可以将这些属性替换成一个整数, 表示从凌晨开始到现在的秒数.
这个实现可能会让一些方法, 如is_after, 更容易实现, 但也会让另一些方法更难实现.

在部署一个新类时, 你可能会发现更好的实现.
如果程序中其他部分用到到你的类, 则修改接口会非常消耗时间, 并且容易产生错误.

但是, 如果很谨慎小心地设计接口, 则可以在不修改接口的情况下修改实现,
这样程序的其他部分就不需要跟着修改.
handlebars 复制代码
在计算机科学中, 接口(Interface)是指程序中的一个抽象的规范,
它定义了一个模块或一个类所提供的服务, 但并不涉及这些服务的具体实现细节.
接口通常包含一组方法签名, 这些方法描述了模块或类所提供的服务以及它们的参数和返回类型.

接口的实现(Implementation)是指程序中的一个具体的实现, 它实现了一个或多个接口中定义的方法.
接口的实现通常包含了实现方法的代码, 以及实现所需的数据结构和算法.

在面向对象编程中, 接口通常用于描述类的行为和能力, 而实现则是具体实现类的实现细节.
通过接口的定义, 我们可以定义一个类所需实现的方法, 而不用关心这些方法的具体实现.
这样, 在实现类中, 我们只需要关注具体的实现, 而不用担心接口的规范.
这样可以提高代码的可维护性和可扩展性.
17.11 调试
handlebars 复制代码
在程序运行的任时刻, 往对象上添加属性都是合法的, 但如果遵守更严格的类型理论, 
让对象拥有相同的类型却有不同的属性组, 会很容易导致错误.
通常来说, 在init方法中初始化对象的全部属性是个好习惯.
 
如果并不清楚一个对象时候拥有某个属性, 可以使用内置函数hasattr(参见15.7节).

另一种访问一个对象的属性的方法是使用内置函数vars, 
它接收一个对象, 并返回一个将属性名称(字符串形式)映射到属性值的字典对象:
python 复制代码
>>> p = Point(3, 4)
>>> vars(9)
{'y': 4, 'x': 3}
handlebars 复制代码
为了调试, 你可能会发现这个函数放在手边是很有用的:
python 复制代码
def print_attributes(obj):
	for attr in vars(obj):
		print(attr, getattr(obj, attr))
        
handlebars 复制代码
print_attributes遍历对象的属性字典, 并打印出每个属性的名称和相应的值.
内置函数getattr接收一个对象以及一个属性名称(字符串形式)并返回属性的值.
python 复制代码
# 完整代码
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 术语表
handlebars 复制代码
面向对象语言(object-oriented language): 一种提供诸如用户定义类型和方法这类的语言特性, 
	以便面向对象编程的语言.

面向对象编程(object-oriented programming): 一种编程风格, 数据和修改数据的操作组织成类和方法的形式.

方法(method): 在类定义之内定义的函数, 在类的实例上调用.

主体(subiect): 调用方法所在的对象.

按位实参(positional argument): 一个不包含参数名字的实参, 所以它不是一个关键字实参.

操作符重载(operator overloading): 修改一个类似+号这样的操作符的行为, 使之可以用于用户定义类型.

基于类型的分发(type-based dispatch): 一种编程模式, 检查操作对象的类型, 并对不同类型调用不同的函数.

多态(polymorphic): 函数的一种属性, 可以处理多种类型的参数.

信息隐藏(information hiding): 对象提供的接口不应当依赖于其实现, 特别是其属性的表达形式的原则.
17.13 练习
1. 练习1
handlebars 复制代码
从↓下载本章的代码.
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属性的方法, 都改变, 使其输出的之前的一样...)
python 复制代码
# 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()
python 复制代码
结果如下:
开始时间 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
python 复制代码
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
handlebars 复制代码
这个练习提醒你关于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
python 复制代码
# 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)
python 复制代码
# 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)
相关推荐
_.Switch11 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一个闪现必杀技29 分钟前
Python入门--函数
开发语言·python·青少年编程·pycharm
小鹿( ﹡ˆoˆ﹡ )1 小时前
探索IP协议的神秘面纱:Python中的网络通信
python·tcp/ip·php
卷心菜小温1 小时前
【BUG】P-tuningv2微调ChatGLM2-6B时所踩的坑
python·深度学习·语言模型·nlp·bug
陈苏同学1 小时前
4. 将pycharm本地项目同步到(Linux)服务器上——深度学习·科研实践·从0到1
linux·服务器·ide·人工智能·python·深度学习·pycharm
唐家小妹2 小时前
介绍一款开源的 Modern GUI PySide6 / PyQt6的使用
python·pyqt
羊小猪~~2 小时前
深度学习项目----用LSTM模型预测股价(包含LSTM网络简介,代码数据均可下载)
pytorch·python·rnn·深度学习·机器学习·数据分析·lstm
Marst Code2 小时前
(Django)初步使用
后端·python·django
985小水博一枚呀3 小时前
【对于Python爬虫的理解】数据挖掘、信息聚合、价格监控、新闻爬取等,附代码。
爬虫·python·深度学习·数据挖掘
立秋67893 小时前
Python的defaultdict详解
服务器·windows·python