16. 类和函数
现在我们已经知道如何创建新的类型 ,
下一步是编写接收用户定义的对象作为参数或者将其当作结果用户定义的函数 .
本章我会展示 '函数式编程风格' , 以及两个新的程序开发计划 .
本章的代码示例可以从↓下载 .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Time1 . py
练习的解答在↓ .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Time1_soln . py
16.1 时间
作为用户定义类型的另一个例子 , 我们定义一个叫Time的的类 , 用于记录一天里的时间 . 类定义如下 :
class Time : """Represents the time of day.attributes: hour, minute, second"""
我们可以创建一个Time对象并给其属性小时数 , 分钟数和秒钟数赋值 :
time = Time( )
time. hour = 11
time. minute = 59
time. second = 30
Time对象的状态图参见图 16 - 1.
作为练习 , 编写一个叫作print_time的函数 , 接收一个Time对象作为形参
并以 'hour:minute:second' 的格式打印它 .
提示 : 格式序列 '%.2d' 可以以最少两个字符打印一个整数 , 如果需要 , 它会在前面添加前缀 0.
class Time : """Represents the time of day.attributes: hour, minute, second""" def print_time ( time_obj) : print ( '%.2d:%.2d:%.2d' % ( time_obj. hour, time_obj. minute, time_obj. second) ) def main ( ) : time = Time( ) time. hour = 11 time. minute = 59 time. second = 30 print_time( time) if __name__ == '__main__' : main( )
编写一个布尔函数is_after , 接收两个Time对象 , t1和t2 ,
并若t1在t2时间之后 , 则返回True , 否则返回False .
挑战不许使用if表达式 .
class Time : """Represents the time of day.attributes: hour, minute, second""" def print_time ( time_obj) : print ( '%.2d:%.2d:%.2d' % ( time_obj. hour, time_obj. minute, time_obj. second) ) def main ( ) : time = Time( ) time. hour = 11 time. minute = 59 time. second = 30 print_time( time) time2 = Time( ) time2. hour = 11 time2. minute = 59 time2. second = 32 res = is_after( time, time2) print ( res) def is_after ( t1, t2) : t1_count = t1. hour * 60 * 60 + t1. minute * 60 + t1. secondt2_count = t2. hour * 60 * 60 + t2. minute * 60 + t2. secondprint ( t1_count, t2_count) return t1_count > t2_count or False if __name__ == '__main__' : main( )
16.2 纯函数
在下面几节中 , 我们会编写两个用来增加时间值的函数 .
它们展示了两种不同类型的函数 : 纯函数和修改器 .
它们也展示了我会称为 '原型和补丁' ( prototype and patch ) 的开发计划 .
这是一种对应复杂问题的方法 , 从一个简单的原型开始 , 并逐渐解决更多的复杂情况 . 下面是add_time的一个简单原型 :
def add_time ( t1, t2) : sum = Time( ) sum . hour = t1. hour + t2. hoursum . minute = t1. minute + t2. minutesum . second = t1. second + t2. secondreturn sum
这个函数创建一个新的Time对象 , 初始化它的属性 , 并返回这个新对象的一个引用 .
这个被称为一个 '纯函数' , 因为它除了返回一个值之外 , 并不修改作为实参传入的任何对象 ,
也没有任何如显示值或获得用户输入之类的副作用 . 为了测试这个函数 , 我将创建两个Time对象 :
start , 存放一个电影 ( 如 Monty Python and the Holy Grail ) 的开始时间 ;
duration , 存放电影的播放时间 , 在这里是 1 小时 35 分钟 . add_time计算出电影何时结束 .
>> > start = Time( )
>> > start. hour = 9
>> > start. minute = 45
>> > start. second = 0 >> > duration = Time( )
>> > duration. hour = 1
>> > duration. minute = 35
>> > duration. second = 0 >> > done = add_time( start, duration)
>> > print_time( done)
10 : 80 : 00
结果 10 : 80 : 00 可能并不是你所期望的 .
问题在于这个函数并没有处理好秒数或者分钟数超过 60 的情况 .
当发生时 , 我们需要将多余的秒数 '进位' 到分钟数 , 将多余的分钟数 '进位' 到小时数 . 下一个是改进的版本 :
def add_time ( t1, t2) : sum = Time( ) sum . hour = t1. hour + t2. hoursum . minute = t1. minute + t2. minutesum . second = t1. second + t2. secondif sum . second >= 60 : sum . second -= 60 sum . minute += 1 if sum . minute >= 60 : sum . minute -= 60 sum . hour += 1 return sum
虽然这个函数是正确的 , 它已经开始变大了 .
我们会在后面看到一个更短的版本 .
16.3 修改器
有时候用函数修改传入的参数是很有用的 . 在这个情况下 , 修改对调用者是可见的 .
这样的工作的函数称为修改器 ( modifile ) .
函数increment给一个Time对象增加指定的秒数 , 可以自然地写为一个修改器 .
下面是一个初稿 :
def increment ( time, second) : time. second += secondsif time. second >= 60 : time. second -= 60 time. minute += 1 if time. minute >= 60 : time. minute -= 60 time. hour += 1
第一行进行基础操作 ; 后面的代码处理我们前面看到的特殊情况 .
这个函数正确吗? 如果seconds比 60 大很多 , 会发生什么? ( 时间不是所期望的 . ) 在那种情况下 , 只进位一次是不够的 ; 我们需要重复进位 , 知道time . second比 60 小 .
一个办法是使用while语句替代if语句 . 那样会让韩素变正确 , 但并不很高效 . 作为练习 , 编写正确的increment版本 , 并不包含任何循环 .
class Time : """""" def print_time ( time_obj) : print ( '%.2d:%.2d:%.2d' % ( time_obj. hour, time_obj. minute, time_obj. second) ) def increment ( time, seconds) : time. second += secondsquotient, remainder = divmod ( time. second, 60 ) time. second = remaindertime. minute += quotientquotient, remainder = divmod ( time. minute, 60 ) time. hour += quotienttime. minute = remainderreturn timedef main ( seconds) : time_obj = Time( ) time_obj. hour = 11 time_obj. minute = 59 time_obj. second = 30 res = increment( time_obj, seconds) print_time( res) if __name__ == '__main__' : main( 9999 )
任何可以使用修改器做到的功能都可以使用纯函数实现 .
事实上 , 有的编程语言只允许使用纯函数 .
有证据表明使用纯函数的程序比使用修改器的程序开发更快 , 错误更少 .
但有时候修改器还是很方便的 , 并且函数式程序的运行效率不那么高 , 总的来说 , 我推荐你只要合理的时候 , 都尽量编写纯函数 , 而只在有绝对说服力的原因时才使用修改器 .
这种方法可以称为 '函数式编程风格' .
作为练习 , 写一个incrment的纯函数版本 , 创建并返回一个新的Time对象而不是修改参数 .
( 纯函数不修改传入参数的值 , 修改器修改传入参数的值 . )
提示 : 在函数中新建Time对象 , 为这个对象赋值属性 , 最后返回这个新对象 .
class Time : """""" def print_time ( time_obj) : print ( '%.2d:%.2d:%.2d' % ( time_obj. hour, time_obj. minute, time_obj. second) ) def increment ( time, seconds) : new_time = Time( ) sum_second = time. second + secondsminute, second = divmod ( sum_second, 60 ) sum_minute = time. minute + minutehour, minute = divmod ( sum_minute, 60 ) new_time. second = secondnew_time. minute = minutenew_time. hour = time. hour + hourreturn new_timedef main ( seconds) : time_obj = Time( ) time_obj. hour = 11 time_obj. minute = 59 time_obj. second = 30 res = increment( time_obj, seconds) print_time( res) if __name__ == '__main__' : main( 9999 )
16.4 原型和计划
刚才我展示的开发计划为 '原型和补丁' .
对每个函数 , 我编写一个可以进行基本计算的原型 , 再测试它 , 从中发现错误并打补丁 . 这种方法在对问题的理解并不深入时尤其有效 .
但增量地修改可能会导致代码过度复杂 ( 因为它们需要处理很多特殊情况 ) ,
并且也不够可靠 ( 因为很难知道你是否已经找到了所有的错误 ) .
另一种方法是 '有规划开发' ( designed devwlopment ) .
对问题有更高阶的理解能够让编程简单得多 .
在上面的问题中 , 如果更深入地理解 , 可以发现Time对象实际上是六十进制数里的 3 位数
( 参见http : / / en . wikipedia . org / wiki / Sexagesimal ) !
second属性是 '个位数' , minute属性是 '60位数' , 而hour属性是 '360位数' .
在编写add_time和increment时 , 我们实际上是在六十进制上进行加减 , 因此才需要从一位到另一位 .
这个观察让我们可以考虑整个问题的另一个中解决方法-我们将Time对象转换为整数 ,
并利用计算机知道如何做整数运行的事实 .
def time_to_int ( time) : minutes = time. hour * 60 + time. minteseconds = mintes * 60 + time. secondreturn seconds
而下面是一个将整数转换回Time对象的函
( 记着divmod函数将第一个参数除以第二个参数 , 并以元组的形式返回商和余数 ) :
def int_to_time ( seconds) : time = Time( ) minutes, time. second = divmod ( seconds, 60 ) time. hour, time. minute = divmod ( minutes, 60 ) return time
你可能会思考一下 , 并运行一些测试 , 来说服自己这些函数是正确的 .
一种测试它们的方法是对很多x值检查 time_to_int ( int_to_time ( x ) ) = = x .
这是一致性检验的一个例子 .
一旦确认他们是正确的 , 就可以使用他们重写add_time :
def add_time ( t1, t2) : seconds = timr_to_int( t1) + time_to_int( t2) return int_to_time( seconds)
这个版本比最初版本短的多 , 并且也很容易检验 .
作为练习 , 使用time_to_int和int_to_time重写increment函数 .
class Time : """""" def print_time ( time_obj) : print ( '%.2d:%.2d:%.2d' % ( time_obj. hour, time_obj. minute, time_obj. second) ) def time_to_int ( time) : minutes = time. hour * 60 + time. minuteseconds = minutes * 60 + time. secondreturn secondsdef int_to_time ( seconds) : time = Time( ) minutes, time. second = divmod ( seconds, 60 ) time. hour, time. minute = divmod ( minutes, 60 ) return timedef increment ( time, seconds) : seconds = time_to_int( time) + secondsreturn int_to_time( seconds) def main ( seconds) : time_obj = Time( ) time_obj. hour = 11 time_obj. minute = 59 time_obj. second = 30 res = increment( time_obj, seconds) print_time( res) if __name__ == '__main__' : main( 9999 )
从某个角度看 , 在六十进制和十进制之间来会转换比只处理时间更难 .
进制转换更加抽象 ; 我们对时间值的直觉更好 . 但如果我们将时间看作六十进制数 , 并做好编写转化函数 ( time_to_int和int_to_time ) 的先期投入 ,
就能得到一个更短 , 更可读 , 也更可靠的函数 . 它也让我们今后更容易添加功能 .
( 将时间表示为六十进制数并实现转换函数可以为今后添加更多功能提供方便 ,
因为将时间表示为六十进制数可以使得时间的处理更加统一化 , 简化了程序实现的复杂性 . ) 例如 , 假设将两个Time对象相减来获取它们之间的时间间隔 .
简单得做法是使用借位实现减法 . 而使用转换函数则更简单且更容易正确 . 讽刺的是 , 有时候把一个问题弄的更难 ( 或者更通用 )
反而会让他更简单 ( 因为会有更少的特殊情况以及更少的出错机会 ) .
16.5 调试
一个Time对象但minute当second的值在 0 到 60 之间 ( 包好 0 但不包含 60 ) 以及hour是正值时 , 是合法的 .
hour和minute应当是整数值 , 但我们也许需要允许second拥有小数值 . 这些需求称为 '不变式' , 因为他们应当总为真 .
换句话说 , 如果它们不为真 , 则一定有什么地方出错了 . 编写代码来检查不变式可以帮你探测错误并找寻它们的根源 .
例如 , 你可以写一个像valid_time这样的函数 , 接收Time对象 , 并在它违反了一个不变式时 , 返回False :
def valid_time ( time) : if time. hour < 0 or time. minute < 0 or time. second < 0 : return Faalseif time. minute >= 60 time. second >= 60 : return False return True
接着在每个函数的开头 , 可以检查参数 , 确保它们是有效的 :
def add_time ( t1, t2) : if not valid_time( t1) or not valid_time( t2) : raise ValueRrror( 'invalid Time object in add_time' ) seconds = time_to_int( t1) + time_to_int( t2) return int_to_time( seconds)
或者可以使用一个assert语句 .
它会检查一个给定的不变式 , 并但检查失败时抛出异常 :
def add_time ( t1, t2) : assert valid_time( t1) and valid_time( t2) seconds = time_to_int( t1) + time_to_int( t2) return int_to_time( seconds)
assert语句很有用 , 因为它们区分了处理普通条件的代码和检查错误的代码 .
16.6 术语表
原型和补丁 ( prototype and patch ) : 一种开发计划模式 , 先编写程序的粗略原型 , 并测试 , 则找到错误时更正 . 有规划开发 ( planned development ) : 一种开发计划模式 , 先对问题有了高阶的深入理解 , 并且比增量开发或者原型开发有更多的规划 . 纯函数 ( pure function ) : 不修改任何形参对象的函数 . 大部分纯函数都有返回值 . 修改器 ( modifier ) : 修改一个或多个形参对象的函数 . 大部分修改器都不返回值 , 也就是返回None . 函数式编程风格 ( functional proframming style ) : 一种编程风格 , 其中大部分函数都是存函数 . 不变式 ( invariant ) : 在程序的执行过程中应当总是为真的条件 . assert语句 ( assert statement ) : 一种检查某个条件 , 如果检查失败则抛出异常的语句 .
16.7 练习
本章中的代码示例可以从↓下载 .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Time1 . py
这些练习的解答可以从↓下载 .
https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / Time1_soln . py
1.练习1
编写一个函数mul_time接收一个Time对象以及一个整数 , 返回一个新的Time对象 ,
包含原始时间和整数的乘积 . ( 时间对象转为秒 * 整数 ) 然后使用mul_time来编写一个函数 , 接收一个Time对象表示一场赛车的结束时间 ,
以及一个表示距离的数字 , 并返回一个Time对象表达平均节奏 ( 每英里花费的时间 ) . ( 正赛的距离不能少于 305 公里 , 不能大于 320 公里 , 时间不能少于 1 个小时 10 分钟 , 不能大于 2 个小时 .
1 千米 ( 公里 ) = 0.621371192237 英里 )
import copyclass Time : """""" def print_time ( time_obj) : print ( '%.2d:%.2d:%.2d' % ( time_obj. hour, time_obj. minute, time_obj. second) ) def time_to_int ( time) : minutes = time. hour * 60 + time. minuteseconds = minutes * 60 + time. secondreturn secondsdef mul_time ( t1, num) : t2 = copy. copy( t1) seconds = time_to_int( t2) t2. product = seconds * numreturn t2def main ( num) : time_obj = Time( ) time_obj. hour = 11 time_obj. minute = 59 time_obj. second = 30 t2 = mul_time( time_obj, num) print_time( t2) print ( t2. product) if __name__ == '__main__' : main( 9 )
import copyclass Time : """""" def print_time ( time_obj) : print ( '%.2d:%.2d:%.2d' % ( time_obj. hour, time_obj. minute, time_obj. second) ) def time_to_int ( time) : minutes = time. hour * 60 + time. minuteseconds = minutes * 60 + time. secondreturn secondsdef mul_time ( t1, mile) : t2 = copy. copy( t1) seconds = time_to_int( t2) distance = mile / secondst2. average_time = 1 / distancereturn t2def main ( num) : time_obj = Time( ) time_obj. hour = 1 time_obj. minute = 59 time_obj. second = 30 mile = num * 0.621371192237 t2 = mul_time( time_obj, mile) print ( '每英里花费%.3f秒!' % t2. average_time) if __name__ == '__main__' : main( 320 )
2. 练习2
datetime模块提供了time对象 , 和本章中的Time对象类似 , 但它们提供了更丰富的方法和操作符 .
在https : / / docs . python . org / 3 /library/datetime.html阅读相关文档. 1. 使用datetime模块来编写一个程序获取当前日期并打印出今天是周几 . 2. 编写一个程序接收生日作为输入 , 并打印出用户的年龄 , 以及到他们下一次生日还需要的天数 , 小时数 , 分钟数和秒数 . 3. 对于生于不同天的两个人 , 总有一天 , 一个人的年龄是另一个人的两倍 . 我们称这是他们的 '双倍日' . 编写一个程序接收两个生日 , 并计算出它们的 '双倍日' . ( 数学不好不写了 . . . )
4. 在增加一点挑战 , 编写一个更通用的版本 , 计算一个人比另一个人大n倍的日子 ( n倍日 ) .
解答 : https : / / github . com / AllenDowney / ThinkPython2 / blob / master / code / double . py
import datetime
today = datetime. date. today( )
print ( today)
weekday = today. weekday( )
print ( '今天是星期%s.' % weekday + 1 )
from datetime import datetimedef main ( ) : print ( "今天是星期" , end= '' ) today = datetime. today( ) print ( today. weekday( ) + 1 ) s = '1997-03-26' bday = datetime. strptime( s, '%Y-%m-%d' ) next_bday = bday. replace( year= today. year) if next_bday < today: next_bday = next_bday. replace( year= today. year + 1 ) print ( "你的下一个生日为: %s." % next_bday. date( ) ) until_next_bday = next_bday - todayprint ( '离生日还有%s天.' % until_next_bday. days) age = today. year - bday. yearprint ( "你现在的年龄:%s岁." % age) if __name__ == '__main__' : main( )
from datetime import datetimedef main ( ) : print ( "在这些日期出生的人:" ) bday1 = datetime( day= 11 , month= 5 , year= 1967 ) bday2 = datetime( day= 11 , month= 10 , year= 2003 ) print ( bday1. date( ) ) print ( bday2. date( ) ) print ( "双倍日是:" , end= '' ) d1 = min ( bday1, bday2) d2 = max ( bday1, bday2) dd = d2 + ( d2 - d1) print ( dd. date( ) ) print ( '年纪大的活了%s天.' % ( dd - d1) . days) print ( '年纪小的活了%s天.' % ( dd - d2) . days) if __name__ == '__main__' : main( )