跳转到内容

Python 编程/类和函数

来自维基教科书,开放的书籍,开放的世界

作为用户定义类型的另一个示例,我们将定义一个名为时间的类,它记录一天中的时间。类定义如下

class Time(object):
  """represents the time of day.
    attributes: hour, minute, second"""

我们可以创建一个新的时间对象并为小时、分钟和秒分配属性

time = Time()
time.hour = 11
time.minute = 59
time.second = 30

时间对象的状态图如下所示


<IMG SRC="book025.png">

编写一个名为 print_time 的函数,它接收一个 Time 对象并以 hour:minute:second 的格式打印它。

提示:格式序列 %.2d 使用至少两位数字打印整数,如果需要,包括前导零。

编写一个名为 is_after 的布尔函数,它接收两个 Time 对象 t1t2,如果 t1 在时间上跟随 t2,则返回 True,否则返回 False

挑战:不要使用 if 语句。

纯函数

[编辑 | 编辑源代码]

在接下来的几节中,我们将编写两个添加时间值的函数。它们演示了两种类型的函数:纯函数和修饰符。它们还演示了我会称为 **原型和修补** 的开发计划,这是一种通过从简单的原型开始并逐步处理复杂问题来解决复杂问题的方法。

以下是 add_time 的简单原型

def add_time(t1, t2):
  sum = Time()
  sum.hour = t1.hour + t2.hour
  sum.minute = t1.minute + t2.minute
  sum.second = t1.second + t2.second
  return sum

该函数创建一个新的时间对象,初始化其属性,并返回对新对象的引用。这被称为 **纯函数**,因为它不会修改作为参数传递给它的任何对象,并且除了返回值之外,没有任何效果,例如显示值或获取用户输入。

为了测试此函数,我们将创建两个 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可能不是你所希望的。问题是此函数没有处理秒数或分钟数加起来超过六十的情况。发生这种情况时,我们必须将额外的秒数“进位”到分钟列,或将额外的分钟数“进位”到小时列。

这是一个改进的版本

def add_time(t1, t2):
  sum = Time()
  sum.hour = t1.hour + t2.hour
  sum.minute = t1.minute + t2.minute
  sum.second = t1.second + t2.second

  if sum.second >= 60:
    sum.second -= 60
    sum.minute += 1

  if sum.minute >= 60:
    sum.minute -= 60
    sum.hour += 1

  return sum

尽管此函数是正确的,但它开始变得很大。稍后我们将看到一个更短的替代方案。

修饰符

[编辑 | 编辑源代码]

有时,函数修改它获取的参数对象很有用。在这种情况下,调用者可以看到更改。以这种方式工作的函数被称为 **修饰符**。

increment,它将给定的秒数添加到一个时间对象中,可以很自然地写成一个修饰符。这是一个粗略的草稿

def increment(time, seconds):
  time.second += seconds

  if time.second >= 60:
    time.second -= 60
    time.minute += 1

  if time.minute >= 60:
    time.minute -= 60
    time.hour += 1

第一行执行基本操作;其余部分处理我们之前见过的特殊情况。

此函数是否正确?如果参数seconds远大于六十会发生什么?

在这种情况下,进位一次是不够的;我们必须不断地进行下去,直到time.second小于六十。一个解决方案是用if语句替换while语句。这将使函数正确,但不高效。

练习 3 编写increment 的一个正确版本,它不包含任何循环。

任何可以用修饰符完成的事情也可以用纯函数完成。事实上,一些编程语言只允许使用纯函数。有一些证据表明,使用纯函数的程序比使用修饰符的程序开发速度更快且错误更少。但修饰符有时很方便,函数式程序往往效率较低。

总的来说,我建议你在合理的情况下编写纯函数,只有在有明显的优势时才使用修饰符。这种方法可能被称为 **函数式编程风格**。

练习 4  编写increment 的一个“纯”版本,它创建一个新的 Time 对象并返回它,而不是修改参数。

原型设计与规划

[编辑 | 编辑源代码]

我正在演示的开发计划称为“原型和修补”。对于每个函数,我都编写了一个执行基本计算的原型,然后对其进行了测试,并一路修补错误。

这种方法可能很有效,特别是如果你还没有深入了解问题。但增量校正可能会生成不必要的复杂代码(因为它处理了许多特殊情况)并且不可靠(因为很难知道你是否已经找到了所有错误)。

另一种方法是 **计划开发**,其中对问题的深入洞察可以使编程变得容易得多。在这种情况下,洞察力在于 Time 对象实际上是一个 60 进制的三位数(见wikipedia.org/wiki/Sexagesimal)!该second属性是“个位数列”,该minute属性是“六十位数列”,以及该hour属性是“三千六百位数列”。

当我们编写 add_timeincrement时,我们实际上是在做 60 进制加法,这就是我们必须从一列进位到下一列的原因。

这种观察表明了另一种解决整个问题的方法——我们可以将 Time 对象转换为整数,并利用计算机知道如何进行整数运算这一事实。

以下是一个将 Times 转换为整数的函数

def time_to_int(time):
  minutes = time.hour * 60 + time.minute
  seconds = minutes * 60 + time.second
  return seconds

以下是将整数转换为 Times 的函数(回想一下,divmod将第一个参数除以第二个参数,并返回商和余数作为元组)。

def int_to_time(seconds):
  time = Time()
  minutes, time.second = divmod(seconds, 60)
  time.hour, time.minute = divmod(minutes, 60)
  return time

你可能需要思考一下,并进行一些测试,才能说服自己这些函数是正确的。测试它们的一种方法是检查 time_to_int(int_to_time(x)) == x 是否对许多值的x都成立。这是一个一致性检查的示例。

一旦你确信它们是正确的,你就可以使用它们来重写 add_time

def add_time(t1, t2):
  seconds = time_to_int(t1) + time_to_int(t2)
  return int_to_time(seconds)

此版本比原始版本更短,并且更容易验证。

练习 5  

使用time_to_intint_to_time 重写 'increment'

在某种程度上,从 60 进制转换为 10 进制然后再转换回来比处理时间更难。进制转换更抽象;我们处理时间值的直觉更好。

但如果我们有将时间视为 60 进制数的洞察力,并对编写转换函数(time_to_intint_to_time)进行投资,那么我们就可以得到一个更短、更易于阅读和调试以及更可靠的程序。

它也更容易在以后添加功能。例如,想象一下减去两个 Times 来找到它们之间的持续时间。幼稚的方法是使用借位来实现减法。使用转换函数会更容易并且更有可能正确。

具有讽刺意味的是,有时让问题变得更难(或更通用)会更容易(因为特殊情况和出错的机会更少)。

如果分钟seconds的值介于 0 到 60 之间(包括 0 但不包括 60),并且如果小时为正数,则时间对象是格式良好的。小时分钟应该是整数,但我们可能允许seconds有小数部分。

这些类型的要求称为不变式,因为它们应该始终为真。换句话说,如果它们不为真,则说明出现了错误。

编写代码来检查您的不变式可以帮助您检测错误并找到其原因。例如,您可能有一个名为 valid_time 的函数,它接受一个时间对象并返回False如果它违反了不变式

def valid_time(time):
  if time.hours &lt; 0 or time.minutes &lt; 0 or time.seconds &lt; 0:
    return False
  if time.minutes >= 60 or time.seconds >= 60:
    return False
  return True

然后,您可以在每个函数的开头检查参数以确保它们有效


def add_time(t1, t2):
  if not valid_time(t1) or not valid_time(t2):
    raise ValueError, '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语句很有用,因为它们区分了处理正常情况的代码和检查错误的代码。

词汇表

[编辑 | 编辑源代码]
原型和补丁
一种开发计划,涉及编写程序的粗略草稿、测试以及在发现错误时进行修正。
计划开发
一种开发计划,涉及对问题的高级洞察力和比增量开发或原型开发更多的计划。
纯函数
一个不修改它作为参数接收的任何对象的函数。大多数纯函数是有结果的。
修饰符
一个改变它作为参数接收的一个或多个对象的函数。大多数修饰符是无结果的。
函数式编程风格
一种程序设计风格,其中大多数函数是纯函数。
不变式
一个在程序执行期间始终为真的条件。

编写一个名为 mul_time 的函数,它接受一个时间对象和一个数字,并返回一个新的时间对象,该对象包含原始时间和数字的乘积。 然后使用 mul_time 编写一个函数,它接受一个表示比赛结束时间的时间对象和一个表示距离的数字,并返回一个表示平均配速(每英里时间)的时间对象。

为一个日期对象编写一个类定义,该对象具有属性 day month year 。编写一个名为 increment_date 的函数,它接受一个日期对象 date 和一个整数 n ,并返回一个新的日期对象,该对象表示 date 之后的第 n 天。提示:“九月有三十天……”挑战:您的函数是否正确处理闰年?参见 wikipedia.org/wiki/Leap_year

datetime 模块提供 date time ,它们类似于本章中的 Date 和 Time 对象,但它们提供了一套丰富的函数和运算符。阅读 docs.python.org/lib/datetime-date.html 中的文档。

  • 使用 datetime 模块编写一个程序,该程序获取当前日期并打印星期几。
  • 编写一个程序,它以生日作为输入并打印用户的年龄以及到下一个生日的天数、小时、分钟和秒数。
华夏公益教科书