优秀程序的良好习惯-二-

175 阅读1小时+

优秀程序的良好习惯(二)

原文:Good Habits for Great Coding

协议:CC BY-NC-SA 4.0

九、停止编程

  • 当我编程时,我有时会写一些行,希望它们能工作,但并不真正理解它们在做什么。—一名高中生正在上他的第四节编程课(2011 年 12 月)。

当你开始感到困惑的时候,停止编程。当我第一次把旅行推销员问题分配给我的学生时,我附加了以下建议:

  • 你程序中的数据将会是一个 xy 坐标列表。让它成为列表的列表,而不是元组。并在第零个位置附加一个id-例如,

  • 为什么要做列表的列表而不是元组的列表?因为你不知道以后会不会需要修改组件,而且元组是不可变的。为什么要附加一个id?因为您不知道以后是否需要为您的xy-坐标提供一个属性——例如,已访问和未访问——或者一个属性元组。

city = [[id, x-value, y-value], [id, x-value, y-value], ..., [id, x-value, y-value]]

我刚来写旅行推销员问题的时候,是用没有id的元组工作的。最终我开始失去对程序的控制。这些函数变得如此复杂(三个下标的括号),以至于修改起来很痛苦。是重新开始的时候了。基于我的失败,我知道可变列表和属性会简化程序。为什么当初我没有意识到这一点?因为我太专注于概念上的细节,所以我不能很好地考虑实现。只有当我的编程变得困难时,我才意识到我的设计错误。

如果你的程序开始变得如此复杂以至于你不能理解它,那么你必须重构或者完全重新开始。当你不得不重新开始的时候,好消息是你变得更聪明了,你的一些代码是可以挽救的。

在开始之前,你需要概述整个程序或算法吗?如果程序/算法异常复杂,那么你至少需要一些大纲。你会知道这一点,因为你会立即对这项任务感到不舒服。让我明确地说明这一点:你必须在编程前花时间思考你觉得复杂的项目。对于大多数学校的问题,我通常会同时设计和键入代码。任何人都可以用简单的程序做到这一点,但是有一定的难度,超过这个难度,动态规划就不能很好地工作。你需要找到自己的水平,知道什么时候可以,什么时候不能用快速肮脏的风格逃脱。 2 这并不容易,因为习惯很难打破,我们的自我卷入其中,我们想要跟上我们的同学。键入不能很好地一起工作的代码行不是编程,除了在名称上。也许我们已经吸取了教训。注意编程的心理学。

在键盘上勾画轮廓的一种方法是只写函数名(里面没有代码的存根,或者返回虚假数据的模拟)。 3

def doIt(x): # <--STUB
    pass

def doIt(x): # <--MOCK
    return 0

在计算机编程中有两种相互竞争的设计哲学:做正确的事情,越差越好。“做正确的事情”哲学同样关注软件设计的完整性、一致性、正确性、易用性和简单性。这是构建完美程序的一种尝试。而我们为什么不应该做这种尝试呢?这是专业软件设计师的哲学。为什么有人会说越差越好?原因如下:

  1. 完整性是指程序中不被忽略的特殊情况。如果用户对这些特例不感兴趣,为什么要花大价钱编写代码呢?有时候完整是浪费时间。
  2. 一致性对于团队来说是有用和必要的,但是对于孤独的程序员来说,半一致性已经足够好了。我们有限的时间可以用在更好的地方。
  3. 如果其他人会使用你写的程序,易用性是很重要的。但是学校项目通常只由设计者来执行。除非易用性是任务的目标,否则它可能会妨碍其他目标。
  4. 甚至希望程序正确的愿望也可能因为好的理由而被牺牲。我曾经看到过一些代码,为了加快程序速度,把距离公式\sqrt{x²+{y}²}换成了x+y 4 如果我的近似程序运行 10 秒,而你的精确程序运行 3 分钟,你的精确程序会是人们想要使用的程序吗?有时我们会为正确付出过高的代价。

在这一点上做正确的事情的人会想打断。他们会指出我只是从例外中推理。既然每一种哲学都有例外,我的反对就毫无价值。更糟糕的是,这些例外正试图摧毁一种积极的哲学。置换哲学在哪里?很公平。越差越好学校确实有一套替代哲学。这就是:越差越好的哲学主张简单的设计是首要的。它表明,一致性、完整性、易用性、正确性和其他积极的属性更有可能从保持简单的简单设计中演化而来,而不是从追求完美的第一次尝试中演化而来。如果这些特征不是自己出现的,那么我们可以通过修改一个简单可行的程序来插入它们。“做正确的事情”的理念可能适用于花费数月时间设计程序的团队,但这不是一个孤独的程序员应该写程序的方式。

我对这场小辩论的看法是,学校课程完美的目标可能是迂腐的。优秀编程的标准也必须根据资源(主要是时间)和任务背后的动机来衡量。现在举个例子。

编程时间足够长的学生会注意到,他们反复编写调试代码来打印矩阵和其他数据结构。那么为什么不写一个通用的矩阵打印机,放在个人图书馆里呢?我的版本在下面。给它一个整数、浮点、字符串、布尔、None都混在一起的矩阵(这是 Python,记住),代码会整齐地打印出所有垂直对齐的数据。

def printMatrix(Lst, decimalAccuracy = 2):
    print('---MATRIX:')
    if type (Lst) != list or type (Lst[0]) != list:
        print('*' * 45)
        print(' WARNING: The received parameter is NOT a \n',
               'matrix type. No printing was done.        ')
        print('*' * 45)
        return
    maxLength = 0
    for row in Lst:
        for x in row:
            if type(x) == float: x = round(x, decimalAccuracy)
            maxLength = max(len(str(x)), maxLength)
            if type(x) == float:
                 print('%11.2f'%x,      end='')
            elif type(x) == int:
                 print('%8d   '%x,      end='')
            elif type(x) == str:
                 print('%8s   '%x,      end='')
            elif type(x) == bool:
                 print('%8s   '%str(x), end='')
            elif x == None:
                 print('%8s   '%str(x), end='')
            else:
                 print(x, ' ')
        print()
    print('==============================')
    print('cell maxlength =', maxLength, '(8 is limit)')

大问题:曾经需要一个通用打印机(这个代码)吗?我打印过的唯一矩阵包含浮点数和整数。现在我似乎被这个小项目的酷冲昏了头脑:一台通用矩阵打印机。它被过度设计了。我违反了 YAGNI 原则(如果你不需要,就不要写代码)。在这里,越差肯定越好。

顺便说一下,这里有一个 Python 技巧来漂亮地打印一个列表:

    Lst = ['A', 2, [1,2,3], 4000, 0.123]
    print('', *Lst, sep='\n....')
"""
Output:
....A
....2
....[1, 2, 3]
....4000
....0.123
"""

下面的建议实际上包含了一些智慧:没有计划就是计划失败。三思而后行,编程一次。匆忙编程,永远调试。 5 记住一周的调试可以节省整整一个小时的规划。

Footnotes 1

2003 年,在 ARML 举行的 H.S .数学竞赛中,我们学校的数学队队长在决赛前给队友们打了打气。他说他在实践中注意到他的许多队友忽略了他们有能力解决的问题。为什么呢?他们没有足够仔细地阅读问题,以发现给定信息中的微妙关系。他的建议是“在开始解决问题之前仔细阅读每个问题。”那年我们学校赢得了 ARML 奖。

  2

在互联网上搜索 BDUF(前期大设计)、RDUF(前期粗略设计)和“紧急设计”在没有编写过相同程序的原型(缩小版)的情况下,设计一个复杂的程序会有很大的问题。

  3

存根和模拟的定义各不相同。更安全的说法是使用“假货”

  4

错误会有多严重?设z=x+y,其中 x 和 y 均为非负,w=\sqrt{x²+{y}²} 。那么最大的 \raisebox{1ex}{z}\!\left/ \!\raisebox{-1ex}{w}\right. 会变成什么样?答案是\sqrt{2}

  5

罗伯特 l .克鲁斯,数据结构与编程,第二版。(普伦蒂斯-霍尔,1987),第 55 页。

十、测试

据说“永远保持警惕是自由的代价。” 1 是的,因为永远的警惕是一切品质的代价。当编写代码时,这意味着边写边测试,在编写完代码后立即测试每个关键功能,而不是等到整个程序都写完了。CABTAB:编程一点;测试一下。早期测试可能是减少编程错误的最好方法。如果我们在编写代码时没有发现每个代码块中的错误,那么以后当我们试图调试代码时,我们对代码就不太熟悉了。

我们测试已知输入的预期输出。我们测试超出范围的值、偏离 1 的值、无意义/互换的数据、空集、零长度步骤、糟糕的游戏移动(非法移动或不合法移动)、除以零、前置条件、后置条件、不变量、适当的关系,尤其是边界条件。你进行的测试被称为系统测试和增量原型。

回想一下,二维视频屏幕在内部由一维列表表示。因此,如果我们希望通过像素戳在矩形(WIDTH × HEIGTH)屏幕上画一个圆,那么二维图像必须转换成一维表示。我试图直接做到这一点。看看下面的代码。一维列表被称为image。三条线(A、B 和 C)中只有一条是正确的,然而它们看起来都是正确的。哪个是正确的?

def frange(start, stop, step = 1):
    i = start
    while i < stop:
        yield i # <-- not return i
        i += step

def drawCircle(cx, cy, radius, image):
    from math import cos, sin
    for t in frange(0, 6.28, 0.01): # range will not allow float steps.
        x = cx + radius*cos(t)
        y = cy + radius*sin(t)
        image[int(y)*WIDTH  + int(x)] = 255 # <--A
        image[int(y *WIDTH) + int(x)] = 255 # <--B
        image[int(y *WIDTH  +     x)] = 255 # <--C
    return image

唯一正确的线是 b。我花了几分钟看这段代码(和线 A ),试图发现为什么圆被展开成波浪。表达式y*WIDTH first 必须向下舍入。这个例子的要点是,如果不测试代码,错误是不可能避免的。

下面的陷阱抓住了我的一个聪明的学生:

   v = [0]*2
   print('v =', v)  # v = [0, 0]
   m = [v]*2 #
   print('m =', m)  # m = [[0, 0], [0, 0]]
   m[0][0] = 8
   print('m =',m)   # m = [[8, 0], [8, 0]]
#--Surprise: m[0][0] and m[1][0] share the same memory address.

几年前,我设计了一个简单的问题来确定我的高中三年级和四年级学生中谁在数学方面比较弱。随便找个高中生试试。

根据其他字母求解 y:x-a=\frac{by-c}{d-y}。[3 分钟]

后来我意识到,编写这个问题的解决方案提供了一个有启发性的例子,说明消除逻辑错误是多么困难。

测验 4。用你最喜欢的语言,写下面这个简短的函数,然后把你的代码和我的进行比较。

def solveEquation(a,b,c,d,x):
#   +---------------------------------------------------------+
#   | Given: (x-a) = (b*y-c)/(d-y)                            |
#   | Return the unique value for y, if it exists.            |
#   |        if no value for y exists, then print an          |
#   |           error message and exit the program.           |
#   |        if multiple values for y exist, then print       |
#   |           a warning and return a valid value for y.     |
#   +---------------------------------------------------------+
#... Finish writing this function.

祝你好运。

#                     QUIZ 4 (My Solution)
def solveEquation(a,b,c,d,x):
#   +---------------------------------------------------------+
#   | Given: (x-a) = (by-c)/(d-y)                             |
#   | Return the unique value for y, if it exists.            |
#   |           [y = (x*d - a*d + c)/(x-a+b).]                |
#   |        If no value for y exists, then print an          |
#   |           error message and exit the program.           |
#   |        If multiple values for y exist, then print       |
#   |           a warning and return a valid value for y.     |
#   +---------------------------------------------------------+

    if (x == (a-b) and (c != b*d)):
       exit('ERROR: No solution. The expression reduces to c = b*d.')

    if (x == (a-b) and (c == b*d)):
       print('WARNING: y is NOT unique: y may take ANY value, except d.')
       return int(not d) # y = 0 or 1

    if (x != (a-b) and (c == b*d)):
       exit('ERROR: No solution. The expression reduces to y = d.')

#---Note: x != (a-b) and c != b*d).
    y = (x*d - a*d + c)/(x-a+b)   # <-- No division by zero and no y = d.
    return y

不幸的是,这个问题太难了,以至于它的教育用途受到了限制。不过,这是一种培养解决问题技能的练习。

你可能会认为A += B只是A = A + B的简写符号。这在 Python 中是不正确的,也许在其他一些语言中也是如此。

def append1(A): #
    A += [3]    #
#------------------------
def append2(A):
    A = A + [3] # The two As are different objects.
#------------------------
def main():
    A = [1,2]
    append1(A)
    print(A) # output: [1, 2, 3]

    A = [1,2]
    append2(A)
    print(A) # output: [1, 2] ß Surprise!

那么,我们如何保护自己免受这种句法毒害呢?答案是让我们的代码保持简单,并且边走边测试。

为了修正一个错误,每个人尝试的第一个工具是猜测,因为这不需要努力,而且很多时候是成功的。只有当我们无法通过猜测来修复错误时,我们才必须停下来思考。但有时我们不会停下来——我们只是不断改变,希望我们的问题会消失。在这一点上,我们的效率实际上可能会降到零以下。我们可能开始改变不应该改变的代码。

发现学校程序中错误的主要方法是通过程序运行测试数据并检查结果(跟踪)。有时,对于复杂的算法,编写一个通过函数运行随机数据并检查答案的测试程序是有帮助的。

另一种发现错误的方法是放置错误陷阱,只有在发现错误时才会打印。这在某种程度上给了我们一个自调试程序。这样做的一个原因是,程序某一部分的错误修正可能会导致另一部分的失败。

Misko Hevery 在 YouTube 视频中提出了一个有趣的问题:你能从测试中重建源代码吗?我最初的反应是“当然不是。”然后他建议用一组测试来讲述一个故事。假设测试看起来像这样:

Test1_ItShouldDoThis()
Test2_ItShouldDoThat()
Test3_ItShouldDoSomethingElse()
Test4_ItShouldDoThisToo()
Test5_ItShouldExitLikeThis()

所以,也许他是对的,也许我们的测试应该是为了讲述一个故事。

测验 5。(重要)回想一下,两个向量的点积是它们成对乘积的和。例如,x = [1,2,3,4]y = [2, -3, 0, 5]的点积为

1×2 + 2×(-3) + 3×0 + 4×5 = 16.

下面的四个函数都正确地计算并返回两个向量(又名列表,又名数组)xy的点积。唯一的区别是错误陷阱。哪一个是首选的错误陷阱:A、B、C 还是 D?

#---Method A.
def dotProd(x,y):
    return sum(x[n]*y[n] for n in range(len(x)))

#---Method B.
def dotprod(x,y):
    assert type(x) == type(y) == list
    return sum(x[n]*y[n] for n in range(len(x)))

#---Method C.
def dotprod(x,y):
    assert len(x) == len(y)
    return sum(x[n]*y[n] for n in range(len(x)))

#---Method D.
def dotprod(x,y):
    assert type(x) == type(y) == list
    assert len(x)  == len(y)
    return sum(x[n]*y[n] for n in range(len(x)))

我的答案在脚注里。 3

编写函数时,考虑测试不会产生编译或运行时错误的任何先决条件和边界条件。养成这种习惯将会节省你几个小时的调试时间。下面是一个奇特的断言,当一个更简单的形式就可以了,它可能不值得花时间去编程。尽管如此,你应该知道这样的形式是可能的。

    import sys
    assert x>0, 'in function ' + sys._getframe().f_code.co_name + \
                ' x =  ' + str(x)

输出:AssertionError: in function doIt x =  -1

此外,不要在 Python 断言周围放置圆括号或方括号。非空的元组或列表总是被评估为真(语法毒药)。

程序员构建的错误陷阱可以打印比断言更多的信息,可以关闭文件,并且可以将信息保存在错误文件中。那么为什么有人更喜欢使用内置的 assert 语句,而不是编写一个错误陷阱呢?回答:

  1. 断言立即被视为错误陷阱,而不是函数任务的一部分。
  2. 断言比用户构建的错误陷阱更快更容易编写。
  3. IDE 会将光标放在程序中的断言行上;一个错误陷阱通常会打印一条错误信息并退出程序。

测验 6。编写一个函数,期望接收两个大小相同的字符串,并返回不同但相对位置相同的字母数,例如,接收“abcdef”和“axcxfe”的函数将返回 4。我的答案在本章末尾。至少在 Python 中,有一种巧妙的方法可以做到这一点。

考虑在程序运行时或结束后打印一些统计数据(时髦的术语:动态性能分析)。当然,总是打印每个程序的运行时间——没有例外。这可以帮助你确定你的程序的大 O。您可能还想打印

  1. 进行递归调用的次数和达到的递归深度,
  2. 树中被访问的节点数量(我这样做是为了衡量 alpha-beta 修剪的性能。我的程序通过修剪减少了 2/3 的节点。),
  3. 已达到最大树级深度,
  4. 队列或某些其他动态数据结构的最大大小,
  5. 写入或读出文件的项目数,
  6. 或者每次移动所用的时间,也许是游戏中每次移动的平均时间。

在编写代码之前编写测试和调试实用程序有几个好处。

  1. 当你写完你的代码,你可以立即测试它,而不是花几分钟去创建一个测试。
  2. 您实际上编写了测试,而不是转移到另一个函数。
  3. 首先编写测试有助于在编写函数之前勾勒出它的轮廓。

不测试你的代码的一些原因是:1)它很无聊/有压力/很累,2)它明显减慢了我们的进度,3)我们不习惯测试,以及 4)我们担心我们可能真的会发现一个 bug。所有这些借口都是自欺欺人的形式。坦率地说,不进行测试意味着草率、懒惰和无能。

  • 学生程序员:“有什么办法可以摆脱这种痛苦吗?”
  • C.S .老师:“是的。不要一开始就陷入其中:边走边测试,防御性地编写代码。”

-

测验 6 答案:

different sameLettersInSamePlaceCount(stng1, stng2):
    return sum(ch1 != ch2 for (ch1, ch2) in zip(stng1, stng2))

好吧,如果你不知道zip功能,那么你就不可能使用它。指令

print(list(zip(stng1, stng2)))

生产

[('a', 'a'), ('b', 'x'), ('c', 'c'), ('d', 'x'), ('e', 'f'), ('f', 'e')]

zip函数值得记住。其实这个例子很值得记住。注意这个理解是生成器理解,不是列表理解(没有方括号)。注意函数名有多长。浏览一下这个简单的代码就能告诉读者它是做什么的。那么为什么不用lettersCount这个名字或者更简单的名字呢?答:因为我们想让读者确切地知道函数计算和返回什么,而不必检查函数代码。


我一生都在研究高中数学/计算机科学问题,这促使我为计算机科学专业的学生提供另外三个数学问题:

  1. 根据 f (x)给定 g (x),我们能根据 g(x)写出 f (x)吗?有时候,如果你发现了窍门。给定g(x)= af\left( bx+c\right)+d,用a\ne 0b\ne 0写出 f (x)为 g (x)和参数 a、b、c 和 d 的函数

  2. Given a\le x\le b, with a<b, find f (x) such that c\le f(x)\le d, and f(x) increases uniformly as x increases uniformly—e.g., if x is \raisebox{1ex}{$3$}\!\left/ \!\raisebox{-1ex}{$4$}\right. of the way between a and b, then f (x) is \raisebox{1ex}{$3$}\!\left/ \!\raisebox{-1ex}{$4$}\right. of the way between c and d. The need for this formula is common in graphics. For example, you need this formula to draw the fancy web shape on the right. As x ranges from G to C, y must uniformly range from H to D.

    A461100_1_En_10_Figa_HTML.jpg

  3. 一个汽车散热器能容纳quartCap夸脱。它装有百分之pct1的防冻溶液。编写一个函数antifreeze,返回正确的溶液夸脱数(四舍五入到最多 2 位小数),该溶液应被排空并重新注入纯防冻剂,以使浓度达到百分之pct2。在不可能的情况下退出程序——例如,我们假设pct1pct2是介于01之间的数字。


答案 1。f(x)=\frac{g\left(\frac{x-c}{b}\right)-d}{a},其中 x 在 f 的定义域内,由于原方程两边的 x 代表相同的值,所以两边用\frac{x-c}{b}代替,然后简化。

答案 2。f(x)=\frac{\left(x-a\right)\left(d-c\right)}{b-a}+c

代数:我知道的最简单的推导如下图。

{\displaystyle \begin{array}{l}a\le x\le b\\ {}0\le x-a\le b-a\\ {}0\le \frac{x-a}{b-a}\le 1\\ {}0\le \frac{\left(x-a\right)\left(d-c\right)}{b-a}\le \left(d-c\right)\\ {}c\le \frac{\left(x-a\right)\left(d-c\right)}{b-a}+c\le d\end{array}}

解析几何:问题自然归结为求通过点(a,c)和(b,d)的直线y= mx+{b}^{\prime }。这样一条线的斜率是m=\frac{d-c}{b-a},通过将(a,c)代入y= mx+{b}^{\prime }可以得到 b ’,这样我们就得到了{b}^{\prime }=c-\frac{d-c}{b-a}a。由此,y=\frac{d-c}{b-a}x-\frac{d-c}{b-a}a+c。这个表达式简化为y=\frac{\left(x-a\right)\left(d-c\right)}{b-a}+c

答案 3。

def antifreeze (quartCap, pct1, pct2):
    assert 0<= pct1 <=1 and 0<= pct2 <=1 and pct1 <= pct2 and quartCap > 0, \
            ["ERROR (bad input):", quartCap, pct1, pct2] # Note the FOUR cases.

    return round(quartCap*(pct2-pct1)/(1-pct1), 2)

Footnotes 1

摘自爱尔兰演说家和政治家约翰·菲尔波特·柯伦 1790 年的一次演讲。参见维基百科。

  2

如果一种语言提供了方便、简洁的快捷方式,那么这些快捷方式可以被描述为语法糖,这个术语是在 1964 年创造的。Python 中内置的字典数据结构是关联矩阵/列表的语法糖。我怀疑 Python 比其他任何语言都有更多的语法优势。

  3

问题 5 答案:我选择 C 是因为如果没有len(x) == len(y),一个错误会不被发现地传递到程序的其余部分。我认为如果没有这个错误陷阱,这个函数就没有写好。然而,我们不需要检查xy的数据类型,因为编译器会为我们做这些。编译器检查以确保xy都是可下标的,并且它们是数字的集合,而不是字符串或对象。请记住,Python 中的 times 操作符(*)是重载的,例如’cat’*3 = ‘catcatcat’。偶尔会出现奇怪的错误。世界级的程序员会测试这种可能性吗?我的观点:保护我们的代码免受每一种极其罕见的可能性,是不划算的。

十一、防御性编程

  • 到 1949 年 6 月,人们开始意识到,要把一个程序做好并不像以前那样容易。我清楚地记得当我试图运行我的第一个重要程序时(用汇编代码或者机器语言)。我强烈地意识到,我余生的大部分时间都将用来寻找我自己程序中的错误。图灵显然也意识到了这一点,因为他在会议上发表了关于“检查大型例程”的讲话。—莫里斯·威尔克斯(图灵奖获得者,1967),《计算机先驱回忆录》(麻省理工学院,1985),页 145。

使用防御性编程。因为我们预先知道会有 bug,所以我们可以在几个方面变得积极主动。我们可以编写函数

  1. 在参数到达函数后打印传递的参数(跟踪),
  2. 打印中间计算值,以及
  3. 打印错误消息以捕捉坏数据(错误的类型、错误的大小、错误的顺序、被零除和越界错误)。这被称为错误处理和崩溃报告。在这里,try/except构造有时很有用。

所有这些都被称为防御性编程或脚手架,因为其中大部分最终都会被移除。

这里有一个来自工业界的技巧:有一个全局常数叫做,比如说,errorCheck或者debug。在每个错误代码块(断言、跟踪、打印输出、try/except、类型检查等)之前。)是

if errorCheck:...

然后我们不清除错误代码;我们只要用errorCheck = False关掉它。在工业中,他们有时会将errorCheck设置为0(关闭所有检查)、1 =开启部分检查、2 =开启更多检查等。

我最近写了几行代码来捕捉一个越界的变量。我按照习惯行事,甚至对自己说,“好吧,这是浪费时间。这个变量永远不会越界”,但在下一次运行时却发现这个变量越界了。诸如此类的重复经历让我成为谨慎编程的信徒。 1

防御性编程有一个危险:它可能会隐藏错误。考虑以下代码:

def drawLine(m, b, image, start = 0, stop = WIDTH):
    step = 1
    start = int(start)
    stop =  int(stop)
    if stop-start < 0:
       step = -1
       print('WARNING: drawLine parameters were reversed.')
    for x in range(start, stop, step):
        index = int(m*x + b) * WIDTH + x
        if 0 <= index < len(image):
           image[index] = 255 # Poke in a white (= 255) pixel.

该功能从start运行到stop。如果stop小于start,它就后退一步,没有错误报告。也许我们希望这种错误在运行过程中被“修复”——隐藏起来——但是我认为我们至少应该打印一个警告,说明范围正在向后移动。也许我们应该中止这个项目。

给定矩阵 A,我们按照(i = row,j = col)约定引用单个元素(a ij )。类似地,当我们阅读一页文本时,我们首先固定在一行上,然后沿着一列阅读。不幸的是,存在竞争约定:在 xy 平面中,我们通过(x =col,y = row)绘制点,并且我们还使用(col,row)在计算机屏幕上绘制点,但是从左上角开始我们的“第一象限”,而不是左下角。因此,我们倾向于有时使用矩阵(行,列)方案,有时使用点绘图(列,行)方案。这是一个交叉的例子。我从(r = row, c = col)开始绘制矩阵,然后切换到(x = col, y = row)进行绘制。

for r in range(8):
    for c in range(8):
        if M[r][c] == 1:
           x = c*70 + 85    
           y = r*70 + 105
           canvas.create_oval(x-25,y-25, x+25, y+25, fill = 'BLACK')

没有问题,因为代码很清楚。我展示这个只是为了说明竞争约定有时会出现在相同的代码块中。

在下一个例子中,我在对同一个函数的两次不同调用中交换了 x 和 y,但是 Python 代码仍然返回相同的正确答案。这怎么可能?答:我在传递索引(关键词)之前,先给它们起了名字。寓意:了解你的语言。

def sub(x, y):
    return x-y
#-----------------------------
def main():
    print(add(x = 2, y = 1)) # Output: 1
    print(add(y = 1, x = 2)) # Output: 1

一位 YouTube 评论员建议永远不要使用xy作为矩阵坐标,而是使用rowcol。为什么呢?因为为应该是(y,x),的东西写(x,y)太容易了,而(row, col)不太可能互换。

Footnotes 1

在一个函数中放置太多的错误陷阱会掩盖函数应该做的事情。在这种情况下,有时,陷阱应该移动到它们自己的功能中。

十二、重构

当你的程序完成时,不要走开。考虑重新设计它——只要最后期限不是迫在眉睫。重新设计,不是为了优化,而是为了使工作代码更容易理解、调试、修改和与其他代码集成,这种做法很常见,以至于有了一个名字:重构。 1 重构是重新考虑变量和函数名称,将多任务函数分解为单任务函数,应用逐步细化,重新考虑数据结构的选择,考虑内聚性与耦合性,并重写代码以使其更清晰、更高效。

我曾经写过一个简单的函数,它接收一个字符串和一个布尔变量。脑子里立刻响起了警报声。这个功能是在做两个不同的任务吗?不完全是。Boolean 只是告诉函数按字母顺序或频率顺序打印字符串。后来我发现我需要这个函数来打印字符串中出现的字符。我想到我可以传递布尔值TrueFalse,或者让接收参数默认为None

然而,这是一种糟糕的编程方式。你见过产生三个值的布尔变量吗?编程人员需要假设一些情况永远不会发生,即使它们是可能的。我最终的解决方案是向函数传递一个整数,该函数只需要值 0、1 和 2。其他值会导致出现警告消息,默认值为 0,但不会抛出异常——不会中止程序。

为什么要有默认值?因为函数的用户可能不关心输出是如何排序的,可能不想麻烦地传递参数,或者可能不知道可用的选项。因此,该功能变得更加强大。

赋值:写一个函数返回一个数组中目标元素的第一个索引,否则返回-1。下面的代码展示了三种不同的方法:

# Method 1 (ugh!)
def indx(array, target): # 11 lines
    if array == []:
        return -1
    n = 0
    found = -1
    length = len(array)
    while n != length:
        if array[n] == target:
            found = n
            break
        n += 1
    return found

这第一种方法是我用标准想法所能做到的最低效的方法。是一个假想的学生产生的代码,他从来不会问代码是否能以更干净的形式写出来(重构)。

# Method 2 Refactored

def indx(array, target): # 3 lines
    for n in range(len(array)):
        if array[n] == target: return n
    return -1

这第二种方法是我最好的尝试。我认为内置的index函数会使代码更短。(我错了。)

# Method 3  built-in (Easiest to understand)
def indx(array, target): # 4 lines
    try:
       return array.index(target)
    except:
       return -1

这些例子说明了为什么学生需要看其他学生的代码,或者至少是老师的代码。通过这种方式学到的课程会让学生终身难忘。

重构为你提供了设计经验,有助于下一个项目。从不重新设计让你停留在新手水平。有时候函数A会调用函数B。后来我们用函数A调用函数C并使用来自函数B的东西。嗯,那么函数C应该是调用函数B,而不是函数A。但是如果我们做了这样的重新设计,那么我们以后可能会丢弃函数C,并且不得不返回。所以有一段时间我们生活在低效的设计中。只有当程序完成时,或者失去控制时,我们才会考虑重新设计。

我不认为用一个精心制作的设计来完成一个程序是可能的,或者至少是高效的。一路上会改变太多。有些问题非常难,直到你看到你的程序崩溃和烧毁,你不太可能理解任务的困难,或考虑所有的特殊情况,或意识到你最好用数字串,而不是数字列表。 2

  • 软件设计师从需求陈述中以一种理性的、无错误的方式推导出他的设计的画面是非常不现实的。从来没有一个系统是以这种方式开发出来的,也许将来也不会有。甚至教科书和论文中展示的小程序开发都是不真实的。它们被修改和润色,直到作者向我们展示了他希望他做了什么,而不是实际发生了什么。—大卫·帕纳斯和保罗·克莱门茨,见史蒂夫·麦康奈尔,《代码完整》,第二版。(微软出版社,2004 年),第 74 页。
  • 如果说我们在过去几十年中学到了什么,那就是编程与其说是一门科学,不如说是一门手艺。要写干净的代码,首先要写脏的代码,然后再清理。大多数大一的程序员并没有特别好地遵循这个建议。他们认为首要的目标是让程序运行起来。一旦“工作”了,他们就进入下一个任务。大多数经验丰富的程序员都知道这是职业自杀。——罗伯特·c·马丁,《廉洁守则》(普伦蒂斯霍尔出版社,2009 年),第 30 页。
  • 我们不太可能在第一次尝试时就设计好一个库或接口。正如弗雷德·布鲁克斯曾经写道,“计划扔掉一个;无论如何,你会的。”Brooks 写的是大型系统,但是这个想法适用于任何大型软件。通常直到你已经构建并使用了程序的一个版本,你才能足够好地理解问题,从而得到正确的设计。—布莱恩·w·柯尼根和罗布·派克,《编程的实践》(艾迪森-韦斯利,1999),第 87 页。
  • 坦率地说,自顶向下设计是重新设计你已经知道如何编写的程序的好方法。—P.J .普劳格尔,《有目的的编程》(普伦蒂斯霍尔出版社,1993 年),第 2 页。

在硬科学中有一种学说认为,如果你不能用一个数字来衡量某样东西,那么它就不存在。 3 有些人认为这种想法是错误的。你不能用一个数字来衡量爱,爱是存在的。就我个人而言,我不太确定有一天爱情不会用一组数字来衡量。但是如果这个教义是假的也没关系,因为对教义的信仰培养了生产性思维。 4 所以我们问:可读性可以用什么单位来衡量?在检查脚注之前决定。 5

重构是一项艰苦的工作。有些程序写起来太累了,我只想在它们最终运行的时候就完成,不要重新设计它们。当然,当我第二年回到他们身边时,我很难理解我自己的代码。

  • 基于一些早期的使用进行重构,然后不得不在不久之后撤销它是相当普遍的。——Kent Beck,测试驱动开发(Addison-Wesley,2003),第 102 页。

在我们结束这个话题之前,绝对不要在这样的情况下使用if-else:

if x > 0:
    return True
else:
    return False

这种结构有时被称为反习惯用法(糟糕的设计)。相反,我们应该这样写:

return x > 0

测验 7。重构下面的代码。答案在脚注里。 6

if x > 0:
   if ch in {'A','B','C'}:
      return True
   else:
      return False
else:
   return False

下面这段代码是我偶尔会用到的一个 shell 程序。

"""+===========+=======-=======*========-========+============+
   ||                        TITLE                           ||
   ||                 by M. Stueben (DATE)                   ||
   ||                                                        ||
   ||   Description:                                         ||
   ||   Language:    Python Ver. 3.4\.                        ||
   ||   Graphics:    None                                    ||
   ||   References:  None                                    ||
   +===========+=======-=======*========-========+============+
"""
######################<START OF PROGRAM>#######################

def fn():
    pass
#==========+====<GLOBAL IMPORTS AND CONSTANTS>=================
None
#===========================<MAIN>=============================

def main():
    pass
#--------------------------------------------------------------
if __name__ == '__main__':
   from math import sqrt; from random import random, randint, uniform, shuffle
   from sys import setrecursionlimit; setrecursionlimit(100)
   from time import clock; START_TIME = clock(); main(); print('~-'*16)
   print('PROGRAM RUN TIME:%6.2f'%(clock()-START_TIME), 'seconds.')
#  import winsound; winsound.Beep(1500,500) # Frequency, milliseconds
#########################<END OF PROGRAM>######################

注意底部的五行。第一行导入了学校算法中经常需要的数学函数。第二行将可能的递归深度设置为 100,而不是默认的 1000。很少需要更大的深度。无限递归,这是我的代码中的一个常见错误,用 1000 次递归调用花费的时间太长而失败。接下来的两行计算并打印程序运行时间。最后一行发出哔哔声(如果需要的话),宣布程序结束。

假设您需要让用户输入四个选项中的一个。有一种方法可以做到:

input('Enter PUsh, pOp, View, or Quit. Choice (U,O,V,Q):')

这是另一种方法:

def userChoice():
    msg = ''
    pr = """
Enter u for push.
Enter o for pop.
Enter v for view.
Enter q for quit (or push the enter key).

Enter choice: """
    while True:
        try:
           choice = input(msg+pr).strip()[0].lower()
        except:
           return 'q'
        if choice not in 'uovq':
            msg = 'ERROR: "' + choice +'" is an invalid choice. Try again.\n'
        else:
            return choice

第二种方法占用更多的空间,也更难调试,但是给了程序一个更漂亮的界面和更健壮的代码。值得付出额外的努力吗?如果你有时间,如果其他人会使用你的程序,那么也许是这样。对于初稿,单行代码更好。

对于简单的if语句,尽可能避免否定的if-测试,因为否定比肯定语句更难解析。我希望你已经记住了德摩根定律:

not (A and B) → (notA)  or  (not B).
not (A  or B) → (not A) and (not B).

测验 8。应用德摩根定律,重构下面的循环体。我的解决方案如下。

for n in range(5):
    if not A or x >= 10:
       doSomething

测验 8 答案:

for n in range(5):
    if A and (x < 10):continue
    doSomething

第二个版本取消了一个not并减少了缩进。

一些专家更喜欢在if测试中使用<而不是>,因为这与数字线一致,数字线将较小的数字放在左边。这似乎是合理的,除非出于心理原因,“>”确实更适合一个表达方式——例如,“??”。回想一下,在英语课上,建议你避免被动写作(“球被男孩击中了。”),而且更喜欢主动写作(“男生击球。”).当然可以。但如果球在故事里比谁打了球更重要,那我们不是更喜欢所谓的被动版吗?无论如何,在if测试中,可能重要的是x,而不是0.001,?? 可能只是一个任意的小数字。

小心多个if,尤其是最后一个else。考虑用一组if语句替换嵌套的else if语句,也许是通过颠倒结构或者用returnbreakcontinue结束每个if。为什么?简单的ifelse if更容易调试。

  • 在我们多年分析工业编程问题的过程中,我们发现由多个嵌套的if语句导致的复杂性是逻辑错误最常见的原因。——Tom Rugg 和 Phil Feldman,《Turbo Pascal 技巧、诀窍和陷阱》( Que,1986 年),第 132 页。
# LOGIC error (beginner's error 1, bleeding ifs)
    x = 1
    if x == 1: x = 2
    if x == 2: x = 3
    if x == 3: x = 4
    print(x) # output: 4 (but the programmer expected 2)

# LOGIC error (beginner's error 2, back-stabbing else)
    x = 1
    if x == 1: x = 2
    if x == 3: x = 4
    else:      x = 5
    print(x) # output: 5 (but the programmer expected 2)

# Using returns, breaks, and continues can make code easier to debug.
def doIt(x):
     if x == 1:
        return 2
     if x == 2:
        return 3
     if x == 3:
        return 4

# Here is the useful subscripted list trick:
def doIt(x):
     return['-',2,3,4][x]

纠结的代码:ifelifelse语句,缩进几个层次,有时可以戏剧性地重构。熟练地应用这些技巧需要一些练习,所以也许你应该在每次测验后掩盖解决方案,直到你能想到一个重构。

测验 9。重构此函数的主体,使其更具可读性:

def doIt(a,b,c):
    if a == 1:
        if b == 1:
            if c == 1:
                print ('abc')
            else:
                print('ab')
        else:
            print('a')
    else:
        print('-')

测验 9 答案:

def doIt(a,b,c):
    if a != 1:
        print('- '); return
    if b != 1:
        print('a '); return
    if c != 1:
        print('ab'); return
    print('abc')

这里,重构使测试变成了否定的,这违反了前面给出的建议。一般规则都有例外。

测验 10。重构这段代码。以下是两个解决方案。

#---BLOCK 1 (22 lines).
    if a == 1:
       if b == 1:
          if c == 1:
             print ('abc')
          else:
             print ('ab-')
       else:
          if c == 1:
             print('a-c')
          else:
             print('a--')
    else:
       if b == 1:
          if c == 1:
              print ('-bc')
          else:
              print('-b-')
       else:
          if c == 1:
             print('--c')
          else:
             print ('---')

混乱的代码(多个if else语句)通常可以通过垂直对齐的重复and语句来改善。

测验 10 个答案:

#---BLOCK 2 (8 lines).
    if a == 1 and b == 1 and c == 1: print('abc')
    if a == 1 and b == 1 and c == 0: print('ab-')
    if a == 1 and b == 0 and c == 1: print('a-c')
    if a == 0 and b == 1 and c == 1: print('-bc')
    if a == 0 and b == 0 and c == 1: print('--c')
    if a == 0 and b == 1 and c == 0: print('-b-')
    if a == 1 and b == 0 and c == 0: print('a--')
    if a == 0 and b == 0 and c == 0: print('---')

#---Block 3 (8 simpler lines)
    if (a,b,c) == (1,1,1): print('abc')
    if (a,b,c) == (1,1,0): print('ab-')
    if (a,b,c) == (1,0,1): print('a-c')
    if (a,b,c) == (1,0,0): print('a--')
    if (a,b,c) == (0,1,1): print('-bc')
    if (a,b,c) == (0,1,0): print('-b-')
    if (a,b,c) == (0,0,1): print('--c')
    if (a,b,c) == (0,0,0): print('---')

前面两个方案有点做作。如果标识符是函数调用,代码看起来就不会那么令人印象深刻。这是同样的测验,同样的答案。

#---BLOCK 1 (again).
    if inStock(item):
       if name in customerList:
          if price-1 < payment <= price:
             print ('abc')
          else:
             print ('ab-')
       else:
          if price-1 < payment <= price:
             print('a-c')
          else:
             print('a--')
    else:
       if name in customerList:
          if price-1 < payment <= price:
              print ('-bc')
          else:
              print('-b-')
       else:
          if price-1 < payment <= price:
             print('--c')
          else:
             print ('---')

#---BLOCK 2 (again).
    if (    inStock(item) and
            name in customerList and
            price-1 < payment <= price):  print('abc')
    if (    inStock(item) and
            name in customerList and
        not(price-1 < payment <= price)): print('ab-')
    if (    inStock(item) and
        not name in customerList and
            price-1 < payment <= price):  print('a-c')
    if (    inStock(item) and
        not name in customerList and
        not(price-1 < payment <= price)): print('a--')
    if (not inStock(item) and
            name in customerList and
            price-1 < payment <= price):  print('-bc')
    if (not inStock(item) and
            name in customerList and
        not(price-1 < payment <= price)): print('-b-')
    if (not inStock(item) and
        not name in customerList and
            price-1 < payment <= price):  print('--c')
    if (not inStock(item) and
        not name in customerList and
        not(price-1 < payment <= price)): print('---')

测验 11。这里,未改进的Block 1似乎比重构的Block 2更容易调试。难道没有办法改善Block 1?是的,改进(Block 3)在本章末尾。

测验 12。重构这段代码,显著减少行数:

#---BLOCK 1 (13 lines).
    if a == 1:
       if b == 1:
          if c == 1:
             print(doIt())
          else:
             print ('error 3')
             return
       else:
          print('error 2')
          return
    else:
       print('error 1')
       return

我的解决方案(Block 2)在本章末尾。

测验 13。简化/改进以下代码:

def selectCourse(name):
    if name != '':
        courseName = name
    else:
        courseName = 'Computer Science 101'
    return courseName

测验 13 答案:

def selectCourse(name):
    assert type(name) == str
    return name or 'Computer Science 101'

assert是保证name不是None()[]、0 或False所必需的。“or诡计”是正当的,还是我掉进了“巧妙代码”的陷阱?2002 年,高露洁大学的一位暑期教师劝阻我不要使用利用语言古怪的伎俩。他可能不赞成这个准则。

测验 11 答案:

#---Block 3 (6 lines)
    (item, payment, name) = (0,0,0)
    msg = ['-', '-', '-']
    if inStock(item):              msg[0] = 'a'
    if name in customerList:       msg[1] = 'b'
    if price-1 < payment <= price: msg[2] = 'c'
    print (''.join(msg))

测验 12 答案:

#---BLOCK 2 (4 lines).
    if a != 1:                      print ('error 1'); return
    if a == 1 and b !=1:            print ('error 2'); return
    if a == 1 and b ==1 and c != 1: print ('error 3'); return
    print (doIt())

要记住的建议:如果你的代码有几个return,考虑重写它,使其有早的return而不是晚的return,即使你需要使你的if测试为负。

Footnotes 1

将复杂的代码分解成更容易理解的部分被称为“分解”,这是程序员在 20 世纪 80 年代发明的术语。第一次使用“重构”这个词是在 1990 年。参见维基百科,代码重构。

  2

我最早参考这一观察是在 1965 年:“在计算中,只有当一个例程被调试和测试,并且一些产品已经运行时,程序员才真正知道他应该如何在第一时间解决问题。”——弗雷德·格雷伯格(兰德)和乔治·贾夫雷(洛杉矶山谷学院),《计算机解决方案的问题》(约翰·威利,1965),第十六页。对于 C.S .老师来说,这仍然是一本优秀的书。这些作者使用了 DEC 公司的 12 位 PDP-8 小型计算机,这是迄今为止商业上最成功的计算机。它使用不同的纸带阅读机和穿孔卡片阅读机。最早的个人电脑(微型计算机)直到 1975 年才推出,当时还只是雏形。

  3

我经常说,当你能衡量你在说什么,并用数字表达时,你就对它有所了解;但是当你不能测量它,当你不能用数字表达它,你的知识是贫乏的和不令人满意的:它可能是知识的开始,但是在你的思想中,你几乎没有发展到科学的阶段,不管是什么问题。——开尔文勋爵(威廉·汤普森(1824–1907)。摘自 1883 年的一次演讲。发现在流行的演讲和地址第一卷(伦敦:麦克米伦公司,1894),73 页。

  4

我们认为纯粹的虚假没有理由拒绝一个判断。问题是:这个概念在多大程度上保护和促进了人类的生活?最虚假的概念——这些概念属于我们的先验综合判断——也是最不可或缺的概念。没有他的逻辑虚构,没有在一个虚构的绝对和不变的世界中衡量现实,没有用数字不断伪造宇宙,人类就无法继续生活。放弃所有错误的判断将意味着放弃,对生命的否定。—弗里德里希·尼采,《超越善恶》(1866 年最初在德国出版),第一部分,第四部分;这种翻译是发现在托拜厄斯但泽,数字科学的语言,第四版。(《双日锚》,1956),第 249 页。

  5

答案是时间。我们试图重构我们的代码,以减少其他人理解代码所需的时间。

  6

小测验 7 答案:return (x > 0) and (ch in {'A','B','C'}).

十三、首先编写测试(有时)

  • 我们面试并雇佣了很多测试人员。我们还没有遇到一个计算机科学毕业生在大学里学到任何关于测试的有用知识。——Cem Kaner,Jack Falk,Hung,Quoc Nguyen,测试计算机软件第二版。(威利,1999),第九页。

在工业界,测试的第一步被称为领域测试:变量、约束和正确类型的测试。接下来是单元测试(又名功能测试又名白盒测试):单个功能的测试。最后是黑盒测试:对整个程序的测试。工业界也用程序来测试程序。在学校,我们通常通过追踪数据和检查预期的答案来进行测试。我们一般不会写其他函数来测试我们的函数。这很好,除了一个例外。对于一个复杂的算法,应该先编写一个测试函数——在编写算法之前,然后在编写算法之后编写另一个测试函数。这是两个不同的测试函数。你必须看到一个例子来欣赏这个建议。下面的代码是第一个测试函数,一个冒烟测试, 1 ,这是我在编写二分搜索法之前编写的。

The Notorious Binary Search

如果您忘记了,二分搜索法是一种在数字排序列表中搜索目标数字t的索引的算法。如果t不在列表中,那么算法返回-1。如果t出现不止一次,搜索返回它的任何一个索引。因为该算法可以用每个探测消除一半的索引,所以长度为L的列表上的二分搜索法将最多采用 ceil(log 2 ( L))个探测。对于十亿个指数,这是最坏情况下的 30 次探测。这个算法听起来很容易写。不是的。

def binarySearchTest(): 
    array = [0,1,2,3,4,6,7,8,9] # <--5 is missing
    print('array   =', array)
    print('Test -9 =', binarySearch(array,-9) == -1)
    print('Test  0 =', binarySearch(array, 0) ==  0)
    print('Test  4 =', binarySearch(array, 4) ==  4)
    print('Test  5 =', binarySearch(array, 5) == -1)
    print('Test  9 =', binarySearch(array, 9) ==  9)
    print('Test 10 =', binarySearch(array,10) == -1)

这个测试代码足够好,可以捕捉到明显的错误,这就是冒烟测试应该做的。当我终于来写binarySearch的时候,几乎每一个逻辑错误都立刻被这个测试代码暴露出来。当然,修复一个错误会引入另一个错误,但是冒烟测试通常也会发现那个错误。

binarySearch函数花了我 70 分钟来编写(通过冒烟测试)和重构。我对自己的binarySearch有多自信?不是很远,因为烟雾测试很粗糙。最后一步是创建和测试 1000 个随机大小的随机整数排序数组。然后搜索每个数组中所有可能的数字,以及一些不在每个数组中的数字。下面的代码可以做到这一点。

def binarySearchTest():
    runs = 1000 # The number of random arrays to be tested.

#---A function to verify the binarySearch for a single element.
    def check(array, value):
        valueIndex = binarySearch(array, value)
        if ((valueIndex == -1) and (value in array)) or \
           ((valueIndex != -1) and (array[valueIndex] != value)):
           print('\nFALSE: array =', array)
           print('The position of', value, 'is returned as', valueIndex)
           exit()

#---Check all numbers in all random arrays created below.
    for i in range(runs):
#-------Create a random sized array each with different random values.
        arrayLength = randint( 0, 30)
        sm          = randint(-5, 20)   # sm = smallest possible value in array.
        lg          = randint(20, 40)   # lg = largest possible value in array.
        array       = sorted([randint(sm,lg) for j in range(0,arrayLength)])
#-------Test every value possible in the array and many not in the array.
        for value in range(sm-2, lg+2):
            check(array, value)
    print('True: The binarySearch function passed', runs, 'tests.')

我的二分搜索法唯一没有通过的测试是在空盘上。这是一个快速解决方案,现在我对我的代码很有信心。

My Binary Search

def binarySearch(array, target):
    # UNCHECKED preconditions: array is a list of sorted integers.
    left  = 0
    right = len(array)-1

    while left < right:
        mid = (left + right)//2   # rounds down.
        if array[mid] == target:
            return mid
        if array[mid] < target:
            if left == mid:
                left = left+1 
            else:
                left = mid
        else:
            right = mid

#---Check for empty array or possible solution where left = right.
    if (array != []) and (array[left] == target):
       return left # left = right = index of target.
    return -1      # Either array = [], or target not in array.

当我将这个版本与binarySearch的已发布版本进行比较时,我意识到我做出了一个糟糕的设计决策。我的代码使用了while left < right,而while left <= right会产生一个更简单的设计。当你开始设计一个复杂的功能时,很难确定每一个关键关系。

题外话。下面是我在网上找到的二分搜索法。注意elifelse。我把这个叫做纠结码。解开代码有一个简单的技巧:只需重复if测试。

def binarySearch(array, target): # A better design. 29.51 seconds
    left  = 0
    right = len(array)-1
    while left <= right:
        mid = (left+right)//2    # rounds down.
        if array[mid] < target:
           left = mid+1
        elif array[mid] > target:
           right = mid-1
        else:
           return mid
    return -1

下面是解开的代码:

def binarySearchUT(array, target): # Untangled code. 39.33 seconds
    left  = 0
    right = len(array)-1
    while left <= right:
        mid = (left+right)//2   # rounds down.
        if array[mid]  < target: left  = mid+1
        if array[mid]  > target: right = mid-1
        if array[mid] == target: return mid
    return -1

这个更简单,少了三行。在一次千万次运行的测试中,纠结的binarySearch以 29.51 秒跑完。解开的binarySearchUT以 39.33 秒完成。重构后的改进值得损失速度吗?被解开的二分搜索法在几乎任何阵列上仍然快如闪电。题外话结束。

一个自然的问题是我们如何测试这些测试?答案是双重的。首先,我们有目的地将坏数据(也称为故障注入)传递给我们的测试代码,以验证它能够检测到错误。第二,用简单的代码,程序在测试验证程序的同时验证测试。

应该报告所有的测试结果还是只报告第一个失败的案例?我倾向于只报告第一个失败的案例,因为测试代码应该尽可能的简单和快速。因此,当发现错误时,该函数打印信息,然后返回或就地退出。我们不想跳出嵌套的for循环,展开递归,或者携带告诉我们忽略默认True的错误标志。

尽管二分搜索法是一个有教育意义的例子,但是只有少数学校问题会从先写测试中受益——例如快速排序。在编写旅行推销员问题、A*搜索算法和困难的神经网络反向传播算法时,学生们从未超越用固定数据——也许是教师要求的数据——手动测试程序。因此,当一项作业出现时,如果先写测试会有好处,学生可能不会考虑写测试。

唐纳德·克努特教授声称第一个二分搜索法于 1946 年出版,但第一个无 bug 的二分搜索法直到 1962 年才出版。乔恩·本特利在贝尔实验室和 IBM 的课程中报告说,他要求一百多名职业程序员在两小时内写出一个正确的二分搜索法。只有 10%产生了正确的算法。 3 令人难以置信的是,就连宾利出版的《二分搜索法》也包含了一个微小的错误。 4 既然如此,我怎么可能在 70 分钟内写出正确的二分搜索法呢?首先,我在编写代码之前编写了冒烟测试,这暴露了每次实践运行中难以发现的错误。其次,我对我完成的代码进行了一千次随机测试。

然而,70 分钟不包括编写测试代码的时间。又花了一个小时。当然,这额外的时间是人们不想先写测试的一个原因——或者根本不想写。

顺便说一下,有几个二分搜索法的属性,我错过了。第一个是计算的中间值(探针)的数量。对于长度为 2 n 的数组,应该有 n+1 个或更少的探针。我从没检查过这个。第二种是选择一个大到mid = (left+right)//2会导致溢出的数组。这在 Python 中不能发生,但在 Java、C++和其他语言中可以。解决方法是mid = left + (right-left)//2。(这是本特利的微小错误。)第三,我从来没有显式测试过所有值相等的数组,除了一个元素的数组。第四,目标元素是在偶数位置还是奇数位置有关系吗?在我的测试中,我从来没有想过这一点,但是在一千次运行中,偶数和奇数位置肯定出现过多次。我错过了什么吗?我永远也不会确定。

Footnotes 1

冒烟测试是针对常见情况的简单测试。术语“冒烟测试”显然来自硬件测试。打开它。如果设备开始冒烟,请将其关闭。测试结束了。

  2

唐纳德·克努特,《计算机编程的艺术》,第 3 卷,分类和搜索,第 2 版。(Addison-Wesley,1998 年),第 6.2.1 节。

  3

乔恩·本特利,《编程珍珠》(艾迪森-韦斯利,1986),页 36。最常见的错误是无限循环。Bentley 的学生可能在纸上手写他们的代码,无法在计算机上测试他们的代码。

  4

安迪·奥兰姆和格雷格·威尔逊编辑。,美丽的代码(奥赖利,2007),第 88 页。这里有整整一章专门讨论二分搜索法。

十四、专家意见

  • 当然,一个小伙子不能指望不学习一些边远地区居民使用的困难的艺术和实践就一下子成为一个彻底的边远地区居民。如果你研究这本书,你会发现书中的提示告诉你如何去做——这样你就可以自己学习,而不是让老师来教你如何做。——巴登-鲍威尔勋爵,《童子军找男孩》(1908)的前言,在网上找到的。

这一章是我多年来收集的编程技巧列表。最重要的提示是阅读别人的代码,尤其是写得好的代码。

  1. 快速失败。例如,硬编程您的输入数据,因为每次运行都必须键入相同的输入是不必要的耗时。我曾经指派我的学生写一个程序,运行一个循环 100,000 次。这花了大约 20 秒。令人难以置信的是,一些学生试图用 100,000 这个数字来调试他们的程序。出于调试目的,他们应该将这个数字减少到 10,后来,当程序看起来工作正常时,将它改为 100,000。

  2. 使用垂直对齐来强调关系,使错误在视觉上突出,并使查找更容易。这就需要一个单倍行距的字体, 1 像快递。

    VERTICAL ALIGNMENT
    
         M = [[0, 0, 0, 0, 0, 0, 0, 0,],
              [0, 0, 0, 0, 0, 0, 0, 0,],
              [0, 0, 0, 0, 0, 0, 0, 0,],
              [0, 0, 0,-1, 1, 0, 0, 0,],
              [0, 0, 0, 1,-1, 0, 0, 0,],
              [0, 0, 0, 0, 0, 0, 0, 0,],
              [0, 0, 0, 0, 0, 0, 0, 0,],
              [0, 0, 0, 0, 0, 0, 0, 0,],]
    
    
    MORE VERTICAL ALIGNMENT
    
    for c in range(1,length-1):
          ch = chr(32)                # = blank space  = background color
          if L[c]== 1: ch = chr(9607) # = solid square = foreground color
          if maxx == max1:
             canvas.create_text(c*12 + 640-12*r, (r-1)*10, text = ch, \
                       fill = 'red', font = ('Helvetica', 8, 'bold') )
          if maxx == max2:
             canvas.create_text(c*6  + 640- 6*r, (r-1)*4,  text = ch, \
                       fill = 'red', font = ('Helvetica', 4, 'bold') )
          if maxx == max3:
             canvas.create_text(c*4  + 640- 4*r, (r-1)*3,  text = ch, \
                       fill = 'red', font = ('Helvetica', 2, 'bold') )
          if maxx == max4:
             canvas.create_text(c*2  + 640- 2*r, (r-1)*2,  text = ch, \
                       fill = 'red', font = ('Helvetica', 1, 'bold') ) 
    
    
  3. 尝试编写健壮的代码。健壮是脆弱的反义词。健壮代码是可以自我修复的代码,允许用户帮助它恢复,或者如果必须的话,优雅地崩溃。

  4. 避免全局变量。为什么呢?范围较大的变量很难跟踪以检测出意外的变化。另一方面,全局常数是可以接受的。我们用大写字母写常量名的一个原因是这样程序员就知道不要改变它们的常量值。也就是说,有些情况下全局变量是有意义的,例如,全局变量不是代码的一部分。假设您决定引入一个临时变量来计算递归函数回溯的次数。您不希望将这个变量传递给函数并增加已经很长的参数列表。只有在调试时才需要这个变量,它不会影响程序的工作方式。让它全球化。另一个可接受的全局变量是保存程序开始时间的全局变量,它可以用来打印各种函数中的时间增量。这是一个从不被其他代码修改的全局变量,它共享全局常量的属性。我的原则是,没有令人信服的理由,永远不要使用全局变量。

  5. 首字母缩写词 SESE 指的是一个函数有一个入口点(不要用goto)和一个出口点。单一入口点是有意义的。单一出口点限制太多。有时,返回或就地中断比展开多层循环更方便。在 Python 中,使用yield的函数将从循环的最后一次迭代开始,所以从某种意义上说,您可以在两个不同的点进入函数。我怀疑 SESE 规则的最初动机是它使得证明程序正确性更容易。

  6. 避免编写返回两种不同数据类型的单个变量的函数。(通常第二种数据类型是错误的指示。)为什么?因为每次调用函数都要用一个if语句来调用。这使得函数更难使用。然而,有时你需要返回多种数据类型。考虑一下不起眼的二次公式。大多数学生都会编写一个 9 行的版本,如下所示:

    def quad(a, b, c):
        from math import sqrt
        disc = b*b-4*a*c
        if disc < 0:
            return 'There are no real roots.'
        x1 = (-b+sqrt(disc))/(2*a)
        x2 = (-b-sqrt(disc))/(2*a)
        if disc == 0:
            return x1
        return x1, x2
    
    

    这段代码返回一个字符串、一组两个数字或者一个数字。注意,这个版本没有考虑到三种情况,如果a = 0 : all real numbersno roots real or otherwise-c/b。它也不总是能得到极端数字的正确答案。如果a = 6b = 1073741900c = 7,那么电脑有sqrt(b*b-4*a*c) = sqrt(b*b) =|b|。两个根将(0.0, -178956983.33333334)。但是零不可能是答案。通过检查,所有根必须是负的。接下来的版本会打印出正确答案:

    (-178956983.33333334, -6.519257560871937e-09).
    
    
    def quad(a, b, c): 
    
    #---Rescale all three coefficients to prevent overflow of b*b and 4*a*c. (Python
    #   has 16-17 digits of accuracy.) Underflow is still possible. Mathematically
    #   the roots are not changed by this process.
        m = max(abs(a),abs(b),abs(c))
        if m != 0:
            a1 = a/m
            b1 = b/m
            c1 = c/m # Now the largest parameter (a, b, c) is 1.
    
    #---Special case 1: a = 0, b = 0, and c = 0.
        if a == 0 and b == 0 and c == 0:
            return 'All real numbers are roots.'
    
    #---Special case 2: a = 0, b = 0, and c != 0.
        if a == 0 and b == 0 and c != 0:
            return 'There are no roots (real or otherwise).'
    
    #---Special case 3: a = 0 and b != 0.
        if a == 0 and b != 0:
           x1 = -c/b # = the only root.
           #-Cast as int type if possible (optional).
           if x1 == int(x1): x1 = int(x1) # This turns -0.0 into 0.
           return  x1
    
    #---Bookkeeping.
        from math import sqrt
        disc = b*b-4*a*c
    
    #---Special case 4: sqrt of negative number.
        if disc < 0:
            return 'There are no real roots.'
    
    #---Special case 5: a != 0, b = 0, c = 0 (Needed for case 6.)
        if a != 0 and b == 0 and c == 0:
            return 0
    
    #---Special case 6: Rationalize the numerator in one of the roots. Why? If b*b
    #   is much much larger than 4*a*c, then sqrt(disc) = |b|. Consequently,
    #   -b + sqrt(b*b) will be zero for b > 0, and -b - sqrt(b*b) will be zero
    #   for b < 0\. We need the "+" and "-" signs reversed in these two situations.
        if b > 0:
            x1 = (-b-sqrt(disc))/(2*a)
            x2 = (-2*c)/(b+sqrt(disc)) # = (-b+sqrt(disc))/(2*a)
        else:
            x1 = (-b+sqrt(disc))/(2*a)
            x2 = (-2*c)/(b-sqrt(disc)) # = (-b-sqrt(disc))/(2*a) 
    
    #---Cast as int types if possible (optional). This turns -0.0 int 0.
        if x1 == int(x1): x1 = int(x1)
        if x2 == int(x2): x2 = int(x2)
    
    #---Special case 7\. Only one root.
        if disc == 0: return x1
    
        return x1, x2
    
    

    这是写代码的问题代码从 9 行变成了 56 行,需要 19 行注释,需要理性化分子才能理解。值得努力吗?也许越糟越好。

    1. 在所有情况下,例如a = 0
    2. 打印输出——例如,"–0.0"应该打印为"0",并且
    3. 最大限度地发挥计算的极限,例如扩展和合理化。The code went from 9 lines to 56 lines, needing 19 lines of comments, and requiring rationalizing numerators to understand. Is it worth the effort? Maybe worse is better.  
  7. 了解你的操作顺序(又名操作层次,又名操作优先)和你的布尔属性。谁写的这个:

    a and b == True,
    
    

    大概是这个意思:

    (a and b) == True
    
    

    网上有多个评论从来不写“==真”。一个原因是为了避免类似上述的问题。我反对。两个表达式if xif x == True在 Python 中并不总是等价的(例如x = 'a')。并且两个表达式if not xif x == False在 Python 中并不总是等价的(例如x = []None'a')。在 Python 中,空字符串和列表的布尔值为False。自然地,这使得程序员想写

    if stng: doSomething
    
    

    而不是

    if len(stng) > 0: doSomething()
    
    

    if stng != '': doSomething()
    
    

    更长的版本不仅可读性更好,而且保护代码不被stng变成None或数字。回想一下,移位>>2相当于将一个整数除以 4。因为移位比除法快(除非编译器被优化),你可以考虑用

    a + b >> 2.
    
    

    代替

    a + b/4
    
    

    ,但是这两个表达式是不等价的。圆括号是必需的。

    a = 6
    b = 4
    print(a + b/4)      # output: 7.0
    print(a +  b >> 2)  # output: 3
    print(a + (b >> 2)) # output: 7
    
    

    给出输出:print(2**3**2)。答案在脚注里。 2 如果你必须查阅一个运算顺序,那么就用括号把它清楚地告诉读者。

  8. 要知道一般代码更容易复用,但是具体代码更容易写。除非您怀疑您将扩展一个函数,否则可能不值得您花费时间将其通用化。下面是我输入一个整数的 Python 函数,它使用了一个try / except构造。这样我就可以捕捉任何类型的运行时错误。

    def dataInput():
        s = 'Enter an integer:'
        posLimit =  float('inf')
        negLimit = -float('inf')
        while True:
           try:
              data = input(s)
              num  = int(data) # a non-int will raise exception.
              if not (negLimit < num < posLimit): raise Error #out-of-bounds?
           except:
              s = '"' + str(data) + '" is NOT an integer! \
                  Try again. \nEnter an integer:'
           else:
              print('input = ', num)
              return num
    
    

    我决定重写上面的函数来打印两种错误消息,并接受输入边界的参数——而不是硬编程它们。结果是一个更复杂的函数。这是编程中常见的困境。我们接受更强大的,和/或更通用的 3 (可扩展性,在最初的设计中考虑了未来的增长)以增加编写时间、增加大小和增加复杂性为代价吗?答案往往是个人的。在这种情况下,我回到了上面的简单版本。(对我来说,越差有时越好。)我曾经写过一个 9×9 网格的数独求解器。然后我把它的一部分重写为一个 n×n 的网格。一般案件比具体案件短得多。不幸的是,它也很难调试。下面是 9x9 代码,后面是 n×n 代码。您更愿意调试哪个?

    #---Build list of 9x9 blocks.
        block = [[],[],[], [],[],[], [],[],[],]
    
        block[0] = [matrix[0][0].value, matrix[0][1].value, matrix[0][2].value,
                   matrix[1][0].value, matrix[1][1].value, matrix[1][2].value,
                   matrix[2][0].value, matrix[2][1].value, matrix[2][2].value,]
    
        block[1] = [matrix[0][3].value, matrix[0][4].value, matrix[0][5].value,
                   matrix[1][3].value, matrix[1][4].value, matrix[1][5].value,
                   matrix[2][3].value, matrix[2][4].value, matrix[2][5].value,]
    
        block[2] = [matrix[0][6].value, matrix[0][7].value, matrix[0][8].value,
                   matrix[1][6].value, matrix[1][7].value, matrix[1][8].value,
                   matrix[2][6].value, matrix[2][7].value, matrix[2][8].value,]
    
        block[3] = [matrix[3][0].value, matrix[3][1].value, matrix[3][2].value,
                   matrix[4][0].value, matrix[4][1].value, matrix[4][2].value,
                   matrix[5][0].value, matrix[5][1].value, matrix[5][2].value,]
    
        block[4] = [matrix[3][3].value, matrix[3][4].value, matrix[3][5].value,
                   matrix[4][3].value, matrix[4][4].value, matrix[4][5].value,
                   matrix[5][3].value, matrix[5][4].value, matrix[5][5].value,]
    
        block[5] = [matrix[3][6].value, matrix[3][7].value, matrix[3][8].value,
                   matrix[4][6].value, matrix[4][7].value, matrix[4][8].value,
                   matrix[5][6].value, matrix[5][7].value, matrix[5][8].value,]
    
        block[6] = [matrix[6][0].value, matrix[6][1].value, matrix[6][2].value,
                   matrix[7][0].value, matrix[7][1].value, matrix[7][2].value,
                   matrix[8][0].value, matrix[8][1].value, matrix[8][2].value,]
    
        block[7] = [matrix[6][3].value, matrix[6][4].value, matrix[6][5].value,
                   matrix[7][3].value, matrix[7][4].value, matrix[7][5].value,
                   matrix[8][3].value, matrix[8][4].value, matrix[8][5].value,]
    
        block[8] = [matrix[6][6].value, matrix[6][7].value, matrix[6][8].value,
                   matrix[7][6].value, matrix[7][7].value, matrix[7][8].value,
                   matrix[8][6].value, matrix[8][7].value, matrix[8][8].value,]
    
    #---Build list of nxn of blocks.
        block  = []
        for n in range(MAX):
            block.append([])
        for n in range(MAX):
            for r in range(blockHeight):
                for c in range(blockWidth):
                      row = (n//blockWidth)*blockHeight+r
                      col = (n%blockHeight*blockWidth) +c
                      block[n].append(matrix[row][col].value)
    
    
  9. 避免所谓的神奇数字。幻数是由常数表示的数。如果您在整个程序中使用 10 作为数组的长度,那么您可能会发现自己在程序中将每一个与数组长度相关的 10 改为 100。更好的方法是将所有数组的长度设置为 MAX,也就是设置为 10。有一些小的例外。我们不需要area = PI * radius ** TWO里的TWO = 2。我们不需要FEET_PER_MILE = 5280,但也许我们需要评论# 5280 = feet-per-mile。如果我们需要 10 秒钟的暂停,而10在程序中只出现一次,那么也许10pause = 10更好。

    secondsInAnHour = 3600
    time = round(clock() - START, 2) # START is global time in secs.
    hours = int(time/secondsInAnHour)
    time -= hours  * secondsInAnHour
    
    

    另一个例外是使用蒙混因素。“忽悠”这个词在这里的意思是“欺骗”如果一个程序的结果总是相差 2,那么将所有结果加 2,并在代码中记录下来。如果最后期限到了,这也许是可以接受的。(为正确的工作使用正确的工具。 4 )但这是治标不治本。话虽如此,但有一个很大的例外——至少在我看来是这样的:一般比具体更难理解。当使用反向传播编写我的第一个人工神经网络程序时,我更喜欢使用幻数。那是我写过的最难的程序。我需要使它尽可能简单(更少的变量)。

  10. 不重复代码(干:不重复自己)。这是职业程序员的一大法则。下面是一个测试井字游戏输赢的函数。我更喜欢第二个版本。为什么呢?对一个部分的改变不需要对重复的部分进行改变。如果更改是一个 bug 修复,您可能不会想到在另一个没有执行的行中进行 bug 修复。

```py
FIRST VERSION

def result(board):
    score = 'XXX'
    B = board
    if B[0] + B[1] + B[2] == score or B[3] + B[4] + B[5] == score or \
       B[6] + B[7] + B[8] == score or B[0] + B[3] + B[6] == score or \
       B[1] + B[4] + B[7] == score or B[2] + B[5] + B[8] == score or \
       B[0] + B[4] + B[8] == score or B[2] + B[4] + B[6] == score:
       return 'win'
    score = 'OOO'
    if B[0] + B[1] + B[2] == score or B[3] + B[4] + B[5] == score or \
       B[6] + B[7] + B[8] == score or B[0] + B[3] + B[6] == score or \
       B[1] + B[4] + B[7] == score or B[2] + B[5] + B[8] == score or \
       B[0] + B[4] + B[8] == score or B[2] + B[4] + B[6] == score:
       return 'win'
    return 'unk'

```

```py
SECOND (BETTER)

VERSION

def result(board):
    B = board
    for score in ('XXX', 'OOO'):
        if B[0] + B[1] + B[2] == score or B[3] + B[4] + B[5] == score or \
           B[6] + B[7] + B[8] == score or B[0] + B[3] + B[6] == score or \
           B[1] + B[4] + B[7] == score or B[2] + B[5] + B[8] == score or \
           B[0] + B[4] + B[8] == score or B[2] + B[4] + B[6] == score:
           return 'win'
    return 'unk'

```

不重复自己是在因式分解共性。我曾经编写过一个程序,对一个特定的目标节点运行四种不同的深度优先搜索:查找任意路径、查找最少节点路径、查找最小成本路径和查找所有路径。主函数是一堆函数调用和打印结果。给我想要的简单性的是将普通的打印代码分解成一个`printResults`函数。我的代码如下。

```py
def printResults(root, goal, path1, path2, path3, distance, pathsList):
    print('   == DFS SEARCHING ==')
    print('1\. Random        path from', root, 'to', goal, 'is', path1)
#--------------------------------------------------------------
    print('2\. Fewest-nodes  path from', root, 'to', goal, 'is', path2)
#--------------------------------------------------------------
    print('3\. Shortest-dist path from', root, 'to', goal, 'is', path3,
         '(', distance,'Km.)')
#--------------------------------------------------------------

    if pathsList == []:

        print('4\. There are no paths.')
        return
    print('4\. All paths from', root, 'to', goal, 'are listed below.')
    count = 0
    pathsList.sort(key = len)
    for path in pathsList:
        count += 1
        print('--%2d'%count, '. ', path, sep = '')
    print('\n---TOTAL search time =', round(clock() - startTime, 2),
           'seconds.')
#=============================<MAIN>===========================

def main():
    root = 'A'; goal = 'B'
    path1            = DFS_AnyPath          (root,  goal)
    path2            = DFS_FewestNodes      (root,  goal)
    path3, distance  = DFS_ShortestCostPath (root,  goal)
    pathList         = DFS_AllPaths         (root,  goal)
    printResults(root, goal, path1, path2, path3, distance, pathList)

```

关键是主函数现在简单易懂了,因为所有的打印都被推到了`printResults`函数中。

11. 不要为了速度或内存使用而优化。这是初学者可能犯的最大错误之一。如果要优化的话,只在程序写完之后。如果一个人在 64 个方向上爬山,也许我们可以通过预先计算 64 个正弦和余弦来优化,并将它们放在一个查找表中,而不是每一步都重新计算它们。话又说回来,如果你的程序足够快,何必呢?有一次我尝试这样做,时间只减少了 23%。我们需要文件中二进制表示的速度吗?根据我的经验,答案是否定的。文本文件更好,因为它们更容易使用和直观检查。如果优化(提高速度、减少内存需求、提高准确性、减少代码行数)会使代码块更加难以理解,那么您必须进行成本/收益分析。目标值得努力吗?你的时间难道不能用来做些别的事情吗?更好是足够好的敌人。有时候,少就是多。* * * * 玩最好的招式并不总是一个好主意,尤其是当你不得不花很多时间去寻找它们的时候。——西蒙·韦伯,老虎象棋第三版。(巴茨福德,2005),第 15 页。If optimization (increasing speed, reducing memory needs, increasing accuracy, decreasing lines of code) will make a block of code much harder to understand, then you must do a cost/benefit analysis. Is the goal worth the effort? Couldn’t your time be better spent doing something else? Better is the enemy of good enough. Sometimes, less really is more. * 我的代码优化方法是什么?99%的情况下,简单粗暴的方法就能奏效。—Ken Thompson(贝尔实验室,UNIX 的创造者,UTF-8 的设计者),见 Peter Seibel,Coders in Work(Apress,2009),第 470 页。* * * * 我认为性能在计算机科学领域被大大高估了,因为你在性能方面需要的是足够好的性能。不需要最好的表现。—Barbara Liskov (2008 年图灵奖获得者),见于 Edgar G. Daylight,《软件工程的黎明》(比利时:孤独的学者,2012),第 155 页。  12. 不要写聪明的代码。 5 聪明的代码是滋生 bug 的温床。在下面的等价例子中,A 是最好的,因为它最容易理解,也最容易调试。

```py
#---A.
    if random() < 0.8:
       theta += 0.3
    else:
       theta -= 0.1

#---B.
    theta = theta - 0.1 + (random()<0.8)*0.4

#---C.
    theta += [-0.1, 0.3][random() < 0.8]

#---D.
    theta += choice ([-0.1, 0.3, 0.3, 0.3, 0.3])

```

简单代码:

```py
   if x >  y: z = z + 3
   if x <= y: z = z - 5

```

巧妙代码(避免):

```py
   z = z + 3*(x > y) - 5*(x <= y)

```

有几种方法可以模拟 Python 中不存在的“switch”语句。问这些结构是否是聪明的代码是一个好问题。在旧代码中必要的巧妙技巧在现代语言中可能不再需要。例如:
1.  整数`num`由多少位数字组成?

    ```py
    print('length =', 1 if num == 0 else floor(log10(abs(num))+1))
    print('length =', len(str(abs(num))))   # simpler

    ```

2.  确定整数`num`右侧的第三位数字。

    ```py
    print('third digit from right =', (abs(num)//100)%10)

    print('third digit from right =', int(str(num)[-3])# simpler

    ```

3.  确定整数`num`左边的第三个数字。

    ```py
    length = 1 if num == 0 else floor(log10(abs(num))+1)
    print('third digit from left =', abs(num)//pow(10, length-3)%10)

    print('third digit from left =', int(str(abs(num))[2]))   # simpler

    ```

13. 当心剑桥教授查尔斯·巴贝奇(1791-1871)的诅咒——或者更确切地说,是降临在巴贝奇教授身上的诅咒。查尔斯·巴贝奇可能是第一个将现代计算机概念化的人。他请求英国政府拨款建造差异引擎 1 号。在建造这个东西的过程中,他意识到它可以做得更好。他放弃了最初的设计,重新开始。通过差异引擎 2,他有了更多的见解,并重新开始(分析引擎)。当助学金(1842 年 17000 英镑)用完时,他仍然没有电脑。事实上,他从来没有造过电脑。对于业余程序员来说,这个教训就是要记录下改进的日志,以构建到下一个设计中。不要将它们合并到当前的项目中(特性蔓延),否则你可能永远也完成不了。 14. 考虑结对编程,而不是通常的单独编程。这意味着需要一个合作伙伴。驾驶员键入代码,而导航员在一旁观看并提出建议。最终,他们交换了位置。许多优秀的程序员宁愿写自己的代码,也不愿意带着一个较弱的同学。有消息称工业程序员需要大约 8-12 个小时来适应这个过程。缺点是两个程序员编写一个程序要比他们单独工作多花 15%的时间。 8 结对编程的好处是:程序的 bug 明显更少,可读性更强。程序员们互相学习,学生程序员也获得了一些与他人合作的经验。结对编程在工业界很流行。和两个不同的伙伴试两次。在我职业生涯的大部分时间里,我要求我的学生根据戴尔·卡内基的《如何赢得朋友和影响他人》(1936 年首次出版,目前亚马逊上有 6000 多条顾客评论)写一篇文章。我公开的理由是计算机科学需要人们在团队中工作。但真正的原因是,太多人的人际交往能力很弱,实际上需要阅读这本书。他们需要被说服,以避免争论,很少批评,提供真诚的——只有真诚的——赞美,让其他人说更多的话。你我遇到过多少人会不必要地引起摩擦,却懒得对周围的人说简单的感谢之词?二十年后,我的学生感谢我布置了这本书。 * 不能进行团队合作的优秀程序员不应该让自己处于被传统编程职位聘用的境地——这对所有相关人员来说都是一场灾难,他们的代码对任何继承它的人来说都将是一场噩梦。我实际上认为,如果你不能进行团队合作,那就是缺乏才华。——吉多·范·罗苏姆(Python 语言的创始人),见于 Frederico Biancuzzi 和 Shane Warden,《编程大师》(O'Reilly,2009),第 28 页。For most of my career I have required my students to write an essay based on reading Dale Carnegie’s How to Win Friends and influence People (first published in 1936, and currently with over 6000 customer reviews on Amazon). My public justification was that computer science requires people to work in teams. But the real reason is that too many people have weak people skills and actually need to read this book. They need to be convinced to avoid arguments, to rarely criticize, to offer sincere—and only sincere—compliments, and to let other people do much of the talking. How many people have you and I met who needlessly cause friction and don’t bother to give simple words of appreciation to those around them? I have had students thank me twenty years later for assigning this book.   15. 当心专家的建议。刚刚给了你专家的建议,其中大部分是常识,为什么要警告你呢?因为专业软件开发人员的工作环境与 C.S .学生大不相同。软件专业人员是一个不断变化的团队的一部分,该团队致力于遗留代码的大型项目的发展。他们编写的软件通常面向需要便捷界面的最终用户。团队协作至关重要。编程风格的一致性是必要的。软件设计师提出的以下问题,学生很少会问:相比之下,学生程序员,尤其是高中生,只是试图编写将在老师面前运行一次的算法。 1. 这个程序容易安装吗? 2. 它会根据可用的计算机内存进行自我调整吗? 3. 界面直观吗? 4. 用户可以修改界面吗? 5. 学习曲线陡吗? 6. 用户能很快得到结果吗? 7. 软件是否在需要的地方向用户提供性能警告? 8. 是否检测到错误输入并通知用户? 9. 该软件是否依赖于互联网网站,这些网站可能会改变或关闭? 10. 它能处理其他软件生成的文件吗? 11. 可以自动更新吗? 12. 它在几个操作系统上运行吗? 13. 是否经过潜在用户的良好测试? 14. 它的设计是否考虑到了未来的增强? 15. 客户支持容易吗? 16. 其数据是否安全并受到保护?In contrast, the student programmer, especially in high school, is only trying to code up algorithms that will be run one time in front of the teacher.  

Footnotes 1

字体和类型或字样经常互换使用。字体与字样的属性相关联,例如,Calibri 斜体、Calibri 粗体或 Calibri 单倍行距都是 Calibri 字样的不同字体。字体是指字符的核心形状。罗伯特·哈里斯在《视觉风格的元素》(Houghton Mifflin,2007)中声称,字体分为四大类:衬线字体(有像 Times Roman 这样的扩展字体)、无衬线字体(没有像你正在阅读的 Calibri 字体那样的扩展字体)、手写字体(像 Lucinda 手写的草书字体)和新奇字体(像 Juice ITC)。

  2

232 = 2**(3**2) = 512.堆叠指数是我所知道的唯一从右向左计算的代数表达式。

  3

有时,您希望设计通用的代码,并且可以轻松扩展以处理更大的数据集。这只有在代码也是可伸缩的情况下才有意义。如果一个程序或算法在小数据集下工作良好,但在大数据集下效率明显较低,则该程序/算法是不可伸缩的。例如,当数据大小增加时,插入排序 O(n 2 )比冒泡排序 O(n 2 )更具可伸缩性,但比 O(nlog(n))排序的可伸缩性差。二分搜索法 O(log(n))具有极强的可伸缩性,哈希表 O(1)对于任何大小的数据集都具有良好的可伸缩性。(不幸的是,在哈希表中保存可搜索数据所需的内存必须比保存数据实际所需的空间多 50%到 100%。随着数据集的增加,哈希键也必须改变。)Python 对于小程序(1000 行以下)来说很棒,但是对于大程序来说就不行了——也就是说,这种语言是不可伸缩的——这主要是因为它缺乏类型检查,并且是一种解释型语言。参见维基百科,s.v .,可扩展性。

  4

至少从 1907 年开始,这就是“真正锻炼工具”的广告口号。

  5

同样的建议也适用于写作。当文字变得引人注目时,它就转移了它所表达的思想。这是散文和歌词的一个区别。

1.“无论何时,当你有一种冲动想写一篇特别好的文章时,全心全意地服从它,然后在把你的手稿送到出版社之前删除它。谋杀你亲爱的。”——阿瑟·奎勒-库奇爵士,《写作的艺术》(G.P .普特南的儿子,1916),第 281 页。

2."杀了你的宝贝,杀了你的宝贝,即使这会伤了你这个自私的小流氓的心,也要杀了你的宝贝。"——斯蒂芬·金,《论写作》(西蒙&舒斯特出版社,2000 年),第 224 页。

3."寻找所有花里胡哨的词语,并去掉它们."——雅克·巴尔赞,《简单而直接,作家的修辞》(哈珀与罗出版社,1975),第 27 页。读这本书。巴尔尊是公认的天才。

4.通读你的作文,每当你看到你认为特别好的一段,就把它删掉。[这是一位大学导师的陈述,由约翰逊博士在 1773 年回忆。资料来源:詹姆斯·包斯威尔的《塞缪尔·约翰逊传》(1791)。]

5.每隔一段时间,你就会冒出一个似乎有自己生命的短语或段落。它是你希望自己能一直做到的那种聪明和机智的结合。当你写这种东西的时候,使劲咽下去,然后扔掉。两个月后,你会认出这是一篇无关紧要的紫色散文。—P.J. Plauger,计算机语言(1991 年 10 月),“技术写作”,第 32 页。

  6

参见《现代世界中的数学》,《科学美国人读本》(W.H. Freeman,1968),第 53-56 页。2002 年,巴贝奇差速发动机 2 终于建成。它耗时 17 年完成,包含约 8000 个零件,重量近 5 吨。

  7

我父亲是一名出色的扑克玩家。他曾经提到他年轻时是一个狂热的桥牌手。当我问他为什么放弃比赛时,他回答说“因为我的搭档一直是个白痴。”(我父亲和其他人相处得不好。)这是有才华的程序员可能不想被指派为合伙人的一个原因。而且,一个人做所有事情的挑战和乐趣都被冲淡了。

  8

Andy Oram 和 Greg Wilson,编辑,制作软件(O'Reilly,2011),314 页。

  9

如果有人曾经为编写代码列出了十条戒律,我有一个关于第十一条戒律的建议:当心古鲁、牧师、解释者和教条。你应该为自己考虑。

十五、一堂设计课

索科尔斯基先生从他的桌子上站起来,走到教室前面的电脑控制台前。这是他在史摩维尔高中教高级计算机科学课的第四天。他接替了休病假的帕姆·琼斯。索科尔斯基面对的是一大堆毫无笑容的面孔。

“我假设每个人都已经完成了第一项任务。稍后我会过来检查你的每一个程序。但是我想让一个学生为全班演示他或她的程序。我可以有一个志愿者吗?”

没有人举手。索科尔斯基先生拿起他的年级记录本,浏览学生名册。

"罗杰,在 1 到 26 之间选一个数字."

“好的,七个,”学生回答道。

“七,那是,让我们看看,那是安娜。安娜,请你上来演示一下你的程序好吗?”

安娜是个聪明的大三学生。她在琼斯小姐的指导下做得很好,她对教学的热情和对编程的热爱得到了她所有学生的赞赏。索科尔斯基先生与众不同。他以前从未当过高中老师,这一点在他的第一项任务中表现得很明显。

安娜拿着她的笔记本电脑来到控制台前。她接上了视频电缆,输入了她的程序名:mult。屏幕上出现了一个问号。她输入了 2。又出现了一个问号。她输入了 3。答案 6 出现了,后面又是一个问号。安娜一言不发地走回她的座位。从她生硬的态度来看,她显然认为这项任务是浪费时间。

“对不起安娜,但我不能给你这个节目任何荣誉。就是缺。”

安娜看上去既困惑又恼火。“但这正是你要求的。将两个数相乘的程序。”

“好吧,让我想想,”索科尔斯基先生说。他拿出一份作业,读了起来。写一个程序,接受两个数字作为输入,并打印它们的乘积。假设这个任务是一个更大的商业项目的一部分,也就是说,它需要一个用户友好的界面,并且必须包含重要的功能。当然,你的程序必须重构。总是努力付出比预期更多的东西。”

“我做到了,”安娜说。“你还想要什么?”

“好吧,一个用户应该知道他正在运行什么程序。你需要一个标题和一些用户说明。你的问号是什么意思?对用户要明确。你的程序似乎会永远运行下去。用户应该如何退出程序?从笔记本电脑上取下电池?所以让我问你一个问题。在程序打印出答案后,你认为用户应该看到一条信息说“输入 X 退出”,还是“输入 C 继续添加”?这是个好主意吗?”

“嗯,是的,我想是的。但就是这么简单的程序,谁在乎呢?”安娜说。

“给你打分的人会在乎,不管那有多重要。但无论如何,我的建议实际上是一个可怜的建议。我们不希望用户输入不必要的信息。当请求第一个数字时,用户指令可能是,“输入一个数字或“X”退出。”因此,用户只需输入另一个数字即可继续,而不是先输入“C”再输入一个数字。"

安娜看上去有些恼怒,但什么也没说。

“好吧,也许我没有把任务讲得足够清楚。所以我有个主意。在这期间做这个,我们将在 30 分钟后回到教室再次讨论这个项目。而且一定要付出比预期更多。如果你提前完成了,那就开始第二项任务。”

三十分钟后,安娜再次展示了她修改过的程序。

“你给我的比我要求的多吗,安娜?”

“我给了你想要的,”安娜说。

“嗯,也许这就够了。安娜,我可以建议你输入的两个数字吗?”

“当然可以。”

"好,我希望第一个数字是 1 . "

安娜走进了 1 号。

"第二个数字是三分之一,也就是 1 后面跟一个斜杠,后面跟一个 3 . "

“这样的数字是行不通的,索科尔斯基先生。我已经试过了。你必须像这样输入它们。”

安娜输入了0.333333333。她的程序打印了0.333333333

“好吧,但是假设我不希望输出中有那么多小数位。为什么不给我一个选择,安娜?”

"你说用户不应该被问题纠缠,所以我听从了你的建议."

“实际上,我说过不应该要求用户输入任何不必要的信息。所以为什么不把缺省的小数位数设为两位呢,除非输出是整数。并允许用户输入一个数字,输入“X”退出,或者输入“P”将默认精度从 2 更改为 1。这样用户就可以忽略这些选项。”

阶级反应是各种形式的烦恼。

“好吧,那就把这个程序搞定,我周一再看看大家的作品。祝你周末愉快,经常想起我。”

索科尔斯基的讽刺并没有被接受,但他在第一天就与一名学生发生了冲突,其他学生对他的愤怒感到厌倦。

周一,安娜再次用索科尔斯基之前用过的数字演示了她的程序。

“到目前为止还不错,安娜。但是你给我的比我要求的多吗?”

“你告诉我,索科尔斯基先生。”

“好,输入 2 作为你的第一个数字,然后输入字符串‘happy’我想知道两倍的快乐是什么?是的,我想知道那是什么数字。"

安娜输入了 2,后面是“快乐”

程序回答“不是一个数字。请重新输入一个数字。

“啊,非常好的安娜。你预料到了我的无理而扭曲的要求。但是如果我只按回车键,什么都不输入呢?”

“这也是行得通的。我测试过了,”安娜说。

“好吧,那这个程序就改进得多了。但是一个改进是不仅仅打印一个数字的输出,而是打印第一个输入,后面跟着一个乘号,第二个输入,后面跟着一个等号,最后是乘积。这更好,因为它允许用户检查输入错误。所以现在需要这样做。”

他转向全班。

“你们上周写的这个程序的第一个版本可能是一个很好的初稿。我是认真的。当程序不能正确地乘以 2 乘以 3 时,你可以花费大量的时间为特殊情况编程,没有浪费。所以首先要有一个工作的基本版本,并保持它的工作状态。然后为特殊情况编写代码,就像我刚才给安娜的两个例子。有时您需要首先编写测试,有时自动化测试。但那是另一个教训。”

“有四种电脑错误。大多数学生只知道一个。大多数学生认为,如果他们的程序在所有合理的情况下每次都做了它应该做的事情,那么程序就完成了。但这是不真实的。像这样的程序只消除了第一类错误:编译错误和逻辑错误。第二种错误是代码可读性错误或样式错误。关于某人的代码,你能说的最糟糕的事情是,为了调试它,或者修改它,你必须从头开始重写它。第三种错误是功能性错误。程序是否拥有用户想要的所有特性?第四种错误是接口错误。这个程序使用起来直观吗?程序是否在正确的时间给用户他或她需要的信息?由于我们大部分的学校作业都是实现算法,所以很少遇到功能和接口错误。”

“那么,我来问你。你能考虑给我们的“mult”程序更多的功能吗?为什么一个学生会启动我们的程序而不是去拿计算器?我们的程序在将两个数字相乘时可能会做些什么,而这是计算器不容易做到的?”

全班鸦雀无声。

"好吧,我们就坐在这里,直到有人想到办法为止。"

过了一会儿,一个学生举起了手。“我们可以让用户输入一个带逗号的大数字,因为如果它是一个巨大的数字,这使得数字更可读。”

“好吧,我们可以这样做,这样会使程序更好。这是一个很好的建议,但是为了我们的目的,我将忽略逗号。这将是一个小功能的大量工作。我认为我们的时间可以更好地利用。那么,我们还能让我们的程序做些什么呢,把它限制在两个数相乘?”

另一个学生举起了手。“你可以放入‘1/3’。我的意思是我们可以为此编程。”

“这是一个非常好的特性,但是这看起来是不是需要大量的工作?我的意思是你必须扫描输入的斜线,然后试着读出两边的数字。不过,我还是喜欢你的想法。我们还能给这个项目增加什么?

全班又一次安静下来。

"所以,我想,我们明天再考虑这个问题."

安娜举起了手。"我想我们可以允许用户改变两个输入数字的基数."

“是的,一点不错。每个输入数字都有自己的基数。当然,在内部,我们以十为基数工作。我们只需要把 b 进制的输入转换成十进制。我们如何做到这一点?嗯,int casting 的过程非常简单。下面是 6 进制 23 的代码,10 进制 15:int('23',6)。这有点尴尬,但只有在不使用 base 10 时才需要它。

但是‘1/3’的输入不会直接翻译成数字。那么有没有人知道如何在不扫描的情况下做到这一点,这将是太多的工作?"

全班又一次安静下来。

“我将建议一个你们大多数人不知道的技巧:精彩的 Python eval指令,它是一个表达式解析器。在网上查一下,但是这里有一个例子,把 6 进制的 23 乘以‘1/3’:

input(x)       # x = "int('23',6)*1/3"
print(eval(x)) # Output: 5.0

“事实上,用eval你可以计算任何算术表达式。所以,在角落里拿起我的《如何计算算术表达式》讲义,然后到你们的电脑上。”

当安娜坐下后,她去网上找了一些关于eval命令的例子。这个问题开始引起她的兴趣。她没有想到任何人需要将不同基数的数字相乘是不切实际的。总是坐在安娜旁边的伊丽莎白靠了过来。"安娜,你能理解这项任务吗?"

“是的,我想我马上会的。”

“好吧。我想我会等你完成,然后得到你的帮助。”

伊丽莎白的帮助想法是复制一些安娜的代码。安娜注意到索科尔斯基先生似乎从来没有注意到任何抄袭。他偶尔警告班上的同学要当心从同学那里获得太多的帮助,但鼓励学生用代码想法互相帮助。安娜一度想知道为什么伊丽莎白不能自己写很多代码。只是看起来没那么难。安娜停下来,环顾四周。爱丽丝也在阅读关于eval函数的内容。数学天才尤里已经开始编程了。她听到阿维告诉大卫,“你写精确部分,我写计算部分。”其他学生在网上聊天或玩耍。他们中有太多的人在侍候他们的同学。安娜突然想到这门课只对几个学生开放。其他人只是记忆和复制代码的关键部分,并且似乎满足于这样做。这只是世界上又一件对安娜来说没有意义的怪事。

安娜继续看书。

伊丽莎白在安娜的键盘旁边放了一块新口香糖。

结局

我偶然看到一本有趣的 C.S .的书,名为《测试计算机软件》,第二版。Kaner、Falk 和 Nguyen(Wiley,1999 年)。这本书有一个附录,列举了 340 多个常见的软件错误。大多数错误都与界面和功能有关,这是工业界比计算机科学班的学生更关心的问题。在第一页,作者有一个惊人的例子,引起了我的兴趣。他们要求读者考虑编写一个不超过两个数相加的程序。然后他们用这个简单的例子来说明在设计和测试中什么是行业认为合适的。

首先,他们考虑了接口。用户知道程序应该做什么吗?用户知道他们在程序中的位置吗?有屏幕说明吗?他们清楚了吗?有没有对安全的执念?用户如何停止程序?输入是否与最终答案一起显示?它们是否排成一行或以视觉上吸引人的方式展示?

接下来是关于功能的问题。输入不正确会发生什么?它中止了整个程序还是用户有机会改正它?数字前后可以有空格吗?两个输入基数可以换吗?精度可以改变吗?

因为我作为老师和学生的学校作业很少包括关于接口和功能的问题,所以我决定写这个程序,把加法改为乘法。

我的第一步是编写代码将基数 b 改为基数 10。我写的代码允许用户输入一个“B”或“b”,而不是一个数字,如果他想改变一个基数。但是这使得界面不方便。我放弃了这段代码,编写了新的代码,允许用户输入一个数字或者一个以数字为基数的元组——比如(12,8)。但是这对用户来说太多了(括号和逗号)。我再次放弃了我的代码,编写了新的代码,允许用户输入一个数字、两个数字(一个带基数的数字)或三个数字(一个带基数的数字,后跟所需的小数精度)。这需要我用空格或逗号来分隔数字。如果输入了多余的空格,我就必须把它们去掉。

所有这些重新设计和编程工作持续了几天。花费最多时间的是考虑如何检测无效的用户输入。编程变得既令人沮丧又耗时。我敢给学生布置如此折磨人的作业吗?我学到的唯一一课是,与学术项目相比,消费者项目有多难。事实上,两天过去了,没有任何编程。

最终,我有了一个新想法,允许在同一行上输入两个输入数字,仅用星号将它们分开。然后突然想到了 Python eval函数。这个函数与一个try/except块和一个整型转换成一个基数相结合将会使所有这些讨厌的问题消失。我写了一些测试代码,发现sqrtlog等 Python 内置函数被eval完美评估。甚至多余的空格都被eval忽略了。我的实际程序突然变得容易编写了。

所有这些花了五天的时间思考和编程。为什么我没有马上想到用eval?答案是,在我去寻找新的想法之前,我必须对我的代码感到不满意。只有某种失败促使我寻找新的设计。我想这可能是我改进代码设计的唯一方法。在这五天里,我个人学到了很多东西。不幸的是,学生们每门课的时间有限,不允许像这样的作业被分配到课堂上。只有少数学生能够取得进步,而大多数学生可能都是靠自己取得这样的进步的。能给学生的,就是想象中的索科尔斯基先生给的那种作业。在几乎所有的最初几堂计算机科学课中,基本思想需要尽早给出,学生们只是通过将各部分放在一起或通过查找关键主题来建立编程技能。在一些简单的版本被证明不合适之后,索科尔斯基先生更进一步,允许这项任务不断发展。下面是我最后的节目。也许你能明白为什么这个程序花了我五天时间。

"""+==========+========-========*========-========+===========+
   ||                 The Multiplying Program                ||
   ||              by M. Stueben (October 8, 2017)           ||
   ||                                                        ||
   || Description:See printDirections().                     ||
   || Language:   Python Ver. 3.4\.                           ||
   || Graphics:   None                                       ||
   || References:  Cem Kaner, Jack Falk, Hung Quoc Nguyen, Testing||
   ||              Computer Software, 2nd Ed. (John Wiley, 1999), ||
   ||             pages 1-7\.                                 ||
   +==========+========-========*========-========+===========+
"""

#####################<START OF PROGRAM>########################

def printDirections():
   print('+-------------------------------------------------+')
   print('|        == THE MULTIPLICATION PROGRAM ==         |')
   print('|      by M. Stueben (Ver. 1.0, August 2017)      |')
   print('|DIRECTIONS:                                      |')
   print('|1\. Enter a first number, followed by an asterisk (*),|')
   print('|   followed by a second number. Examples:        |')
   print('|    5280 * 3.14, (-27 + 6) * (1/3), sqrt(100) * log(10).  |')
   print('|2\.  Push enter to see the output.                 |')
   print('|OPTIONS:                                        |')
   print('|3\. Enter X to exit the program.                 |')
   print('|4\. Enter P to change the precision (default = 2) of any                                       |')
   print('|   float output.                                |')
   print("|5\. To enter, say 21 in base 19, type int('21',19).                                |")
   print('|   Special case: 0X12 and 0x12 both are 18 in base 10\.                                     |')
   print('|6\. The user will be requested to re-enter any bad input.                                       |')
   print('+------------------------------------------------+')
   print('\n RESULTS:')
#------------------------------------The multiplying program--

def requestPrecisionFromUser():

    msg ='Choose the decimal precision of your answer (from 0 to 17):'
    while True:
        data = input (msg)
        ch = data.strip()
        if ch in {'X', 'x'}:
           print (' Goodbye.')
           return
        try:
           precision = int(data)
           if (precision < 0)or(precision> 17)or(type(precision) != int):
              raise Error
        except:
           msg = 'Bad input. Choose a non-negative integer (0 to 17).'
           continue
        return precision
#------------------------------------The multiplying program--

def requestAndMultiplyTwoNumbers():
#---Initialize.
    from math import sqrt, log, log10
    precision      = 2
    problemCounter = 0
    errorMsg       = ''

    while True: 

        msg = errorMsg \
              + 'Enter expression * expression, P (precision), or X (exit).'
        data = input(msg) # Dialog box

#-------Check for 'X or x'.
        ch = data.strip()
        if ch in {'X', 'x'}:
            print (' Goodbye.')
            return

#-------Check for 'P or p.
        if ch in {'P', 'p'}:
            precision = requestPrecisionFromUser()
            errorMsg = ''
            continue

#-------Attempt to calculate an answer.
        try:
            answer = eval(data)
            if not isinstance(answer,(int, float)): raise exception
            errorMsg = ''
        except:
            errorMsg = '============ BAD INPUT ===========\n'\
                     + 'You entered -->   ' + data +'.\n'
            continue

#-------Print the answer.
#       Sample output: "1\. 1.23 * 4.56 = 5.61 [decimal precision = 2.]"
        problemCounter += 1
        if type(answer) == float:
           print('    ', str(problemCounter) + '. ', data, ' = ', \
                 round(answer, precision), \
                 ' [decimal precision = ', precision, '.]', sep ='')
        else:
           print('   ', str(problemCounter) + '.', data, '=', answer)
#==========================<MAIN>===========================

def main():

    printDirections()
    requestAndMultiplyTwoNumbers()
#============<GLOBAL CONSTANTS and GLOBAL IMPORTS>============
if __name__ == '__main__':
     from time import clock; START_TIME = clock(); main(); print('- '*12);
     print('RUN TIME:%6.2f'%(clock()-START_TIME), 'seconds.');
#######################<END OF PROGRAM>#######################

问题:为什么允许甚至向学生介绍eval功能?网上到处都是远离这个 Python 函数的警告。为了测试eval有多危险,我在我的 Windows E目录下创建了一个名为filex.py的虚假文件。然后,我通过在 Python 中运行这一行来销毁文件。

eval("__import__('os').remove('e:filex.py')")

我想这一行对于让一个试用程序自行删除可能是有用的。

只有当eval函数接受来自不可信来源的用户输入时,它才是危险的。由于学生通常是唯一能接触到他或她自己的代码的人,这种担心对于学校的问题是没有根据的。eval函数可以在程序中创造奇迹,就像这里一样。向学生介绍eval是一个讨论eval可以对恶意代码做什么的机会,更有趣的是,是什么促使人们变得恶意。

eval函数的讨论,以及对某些编程风格的绝对坚持,很容易变成情感的争论,而不是逻辑的争论。

下面的大纲是我信奉的一种设计方法论,但就像他们在 Zen 里说的,必须经历才能欣赏。

如何着手一个重大的计算机科学项目

  1. 留出比你认为你需要的更多的时间。你可以花很多时间在一个程序上,但除了一些关于如何不写程序的见解之外,你没有什么可以展示的。
  2. 计划专注。这意味着远离那个诱人但健谈的同学。如果你有一个伙伴,那么考虑结对编程。
  3. 理解问题(=分析+程序规范)。这可能意味着构建一些例子。你也在寻找关系和洞察力。
  4. 选择你的数据类型,然后设计/重新设计你的程序。
    1. 制作一个最小的设计。首先编写必须具备的功能,因此,这是一个早期的工作程序。后来,该有的功能都加了进去。最后,如果可能的话,编写函数。[在一个聪明的井字游戏程序中,第一个版本将是一个程序,其中计算机合法地玩,但随机地移动。]
    2. 预计最初的设计可能很差,您的数据类型可能需要更改。 
  5. 写代码。
    1. 使用逐步细化和自我文档化的代码(很少注释)。
    2. 使用断言和错误陷阱。
    3. 写完之后测试每个关键函数(白盒测试)。
    4. 在编写复杂的算法之前,考虑编写一个粗糙的测试函数。
    5. 考虑在编写一个复杂的算法之后,用数百个随机输入来测试它。 
  6. 根据新的见解、编程困难、用户反馈以及可能变化的规范,根据需要经常返回步骤 4 重新设计程序并改变数据类型。再次强调,接受最初的版本经常被证明是失败的,这确保了在第二个或更高版本中的成功。
  7. 通过测试整个程序(黑盒测试)来修复最终的错误。你可能忽略了一些特殊情况或边缘情况。
  8. 重构整个程序。这是你学习编程的地方。
  9. 反思你的错误和吸取的教训。

十六、当心 OOP

  • 我的观点是 OOP 是对社区犯下的最大的欺诈之一。事实是 OOP 最重要的一个方面是几十年前设计的方法:子程序和数据的封装。其余的都是糖霜。我曾经说过封装是对象编程所提供的 70%,但是我想我要把它改成 90%。—Thomas Kurtz,见于《编程大师》(O'Reilly,2009),第 91 和 93 页。[达特茅斯大学的托马斯·库尔茨教授和约翰·凯米尼教授在 1963-64 年间共同开发了 BASIC 语言。Kemeny 在 1986 年获得了 IEEE 计算机先锋奖,同样的工作,Kurtz 在 1991 年获得了该奖。这次采访时,80 岁的库尔茨已经退休 15 年,不再写代码了。]
  • Potok 等人的一项研究表明,OOP 和过程化方法之间的生产率没有显著差异。—维基百科,面向对象编程。

时隔多年,OOP 仍然备受争议。1c++语言(C 带类)并没有取代 C 语言。对类的一个宣称的理由是通过继承的代码重用(an 是一种关系)。当然,我们已经通过剪切粘贴和导入库文件(模块)实现了代码重用。一些类与它们的应用耦合得如此紧密,以至于它们不容易被重用。通过类重用代码的优势在工业界比在学校问题中更受重视。根据没有继承的对象和类进行编程有时被称为基于对象的编程。

也就是说,我曾经使用继承将四个处理向量的函数(方法)导入到一个Vector类中。那些函数只适用于我使用向量编程的特定问题,我不希望我的Vector类被重新设计。但这更多的是两个类的组合(a 有一个关系),而不是继承。

继承的另一个优点是,对父代码的单个更改就是对其所有子代码的更改,因为所有子代码的共性只存在于一个地方:父代码。然而,即使对于没有类的程序,通用性也可以被分解到函数中。

类最有用的优点是封装(将函数和数据捆绑成一种新的数据类型,一种抽象, 2 并创建一种迷你语言来操纵它们)。如果类模拟了现实中的某些东西,甚至是程序员对某个问题的观点,那么程序员就可以根据对象而不是它们的单个部分来思考和编写代码。从物体的角度思考就像从和弦而不是单个音符的角度思考音乐一样。这听起来很棒,但是我从未遇到过从抽象数据类型中受益匪浅的有价值的问题。我所遇到的是人为的问题,这些问题被设计成需要封装以供学生学习——例如,汽车和摩托车从车辆继承而来。

封装就是设计,一个高效的设计往往来自于把几个低效的设计扔出去。你可以花很多时间来尝试生成一个泛型类。专家给我们以下建议:

  1. 尽量写出与现实紧密对应的自然函数。类(抽象)的全部意义在于它们应该使思考和编程更加直观。与其试图设计一个接近最优的类,不如设计一个易于扩展的类。
  2. 尽管许多声明承诺从面向对象分析到设计的平稳过渡,但实际上这种过渡一点也不平稳。——Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(“四人组”),设计模式,可重用面向对象软件的要素(Addison-Wesley,1995),第 11 页和第 353 页。

随着封装的出现,数据隐藏在私有数据中,也就是说,数据要么不能被访问,要么只能通过限制修改的 getters 和 setters 来访问。用户可以重写这个类,这样就可以无限制地访问私有数据,但这将是一个不同的类。如果一个类经过了很好的测试,那么使用这个类的程序中的错误就不太可能在这个类中被发现。当然,对于任何库模块中经过良好测试的函数也是如此。

很少谈论对象的是对象之间相互通信的方式。根据一些 OOP 专家的观点,高效的消息传递是至关重要的。

当我编写第一个神经网络程序时,我认为创建节点类是有意义的,因为网络是由节点组成的。不幸的是,Node 类似乎让事情变得复杂了。于是我重写了网络,没有上任何课。然后我决定重新编写网络程序,作为只有一个对象的神经网络类。这不应该有任何意义,因为那样的话,该对象将没有其他对象与之交互。有什么意义?但是,事实上,它简化了编程。由于许多内部实例变量的半全局属性,我不必在类方法中传递或返回它们。由于网络很小,全球化的负面影响不会发生。尽管如此,我最终还是重新编写了这个程序,但没有这个类。

具有类的多态性支持运算符重载。例如,当处理向量时,我们可以重载一个Vector类中的所有操作符,并最终编写如下代码

F = 3*(B+C)/4 - A/2,

而不是:

F = vectMinus(scalarMult(3/4, VectAdd(B,C)), scalarMult(1/2, A)).

现在你明白我为什么构建了一个Vector类了。运算符重载产生了积极的影响——大约十行代码。这种努力是值得的,主要是因为我的学生学会了如何构建一个类,并将其应用于一个严重的问题:用漂亮的 Nelder-Mead 算法进行搜索。

Java 允许程序员重载函数,但不允许重载操作符。在 Python 中,可以重载操作符,但不能重载函数。我认为这是因为在 Python 中,任何函数都会接受使用星号(*)操作符的可变大小和类型的参数列表(签名)。参见下面的代码。单个doIt()函数实际上是过载的。

def doIt(*args):
    if len(args) == 1:
        print(args[0])
        return
    if type(args[1]) == list:
        print('list')
    else:
        print(args[1])
#-------------------------------
def main():
    doIt(1)        # output: 1
    doIt(1,'A')    # output: A
    doIt(1,[1,2,]) # output: list

在 C++和 Python 中,可以重载已经存在的运算符,但不能引入新的运算符。继续我的向量例子,如果我想写一行关于叉积的代码,我不能使用字母“x”作为操作符。相反,我必须编写类似于A = B.crossProd(C),A = Vector.crossProd(B,C的代码,或者重载星号(*)操作符。

工业界告诉我们,在可以根据对象编写代码的大型程序中,类是有意义的。在大多数学校问题中,这种意识似乎是缺乏的。

题外话。你能想出一个不能按比例绘制的简单几何图形吗?答案在脚注里。 3 结束题外话。

Footnotes 1

见维基百科/面向对象编程/批评。

  2

编程中的抽象被认为有两个部分:接口和实现。类接口是方法的集合,例如,getters、setters、finders、modifiers、reporters 等。—用于处理数据。实现由私有方法和该类所有方法主体中的基本语句组成。好处是细节从界面中抽象出来(隐藏起来)。这使得编程更容易。对于所有的类,你需要的方法类型的最小数量是六个:构造函数、getter、setter、mutator(改变对象的一部分)、对象的比较(=,!=,也可能>),还有一台打印机。在 Python 中,你实际上不需要 getters 和 setter——例如,Oop.x = 5Oop.setX(5)是不必要的。

  3

没有带单位的叉积图可以按比例绘制。如果向量AB具有以米为单位的标量,那么垂直叉积向量C = AxB将具有以平方米为单位的标量。还要注意,向量中的标量必须都没有单位,或者必须都有相同的单位。否则量级就不存在了。这是一位物理老师告诉我的,奇怪的是,我从未在数学书上发现这个事实。后来我在大卫·r·考斯顿的另一本优秀的书《生物学家的数学》(伦敦:爱德华·阿诺德,1977 年)第 37 页发现了这个错误。作者试图通过测量茎的长度和花的数量来找出两种植物之间的“距离”。

十七、函数的演变

  • 当我编程的时候,最让我头疼的两件事是给事物命名和把事物放在哪里。我得出的结论是,它们是同一个问题。每一个名字是否代表了我想说的关于命名的事物的一切,一起出现的名字是否唤起了似乎一起去的想法?如果我命名事物有困难,我经常发现问题是事物不应该在一起,或者它们不应该在一起。—戴尔·埃默里,理解耦合和内聚,YouTube 视频。

我将向你们展示我曾经处理过的一个小问题:替换字符串中的一个字符。由于 Python 字符串是不可变的(不能更改),所以必须编写一行代码来解决这个限制。那么为什么不使用可变类型呢,比如 list?列表不能是字典的键。所有语言都有其局限性和不完美性。

这个问题中的九个字符串代表一个井字棋盘。空板看起来像这样:

board ='---------'.

走了两步后,棋盘可能看起来像这样:

board = '----X---O'.

那么,我们如何从'---------'进行到'----x----'?回答:我们把字符串分开,替换一个连字符,然后把字符串粘在一起:

0.我的第一次尝试立即解决了问题:

board = board[:position] + char + board[position + 1:]

理由:作者不能让这一行更简单了。

1.第一个改进:把线做成函数:

def insertMove(board, position, char):
    return board[:position] + char + board[position + 1:]

理由:函数调用insertMove(board, position, char比指令本身更具描述性。

2.第二个改进:把板子塞进一个列表。

   def insertMove(board, position, char, boardCollection):
       newBoard = board[:position] + char + board[position + 1:]
       boardCollection.append(newBoard)
       return newBoard

理由:原来每个新的板都需要存储在一个名为boardCollection的列表中,因此有了append行。(后来,这些木板变成了字典键。在程序构造的这个时刻,我没有任何值可以和键匹配。所以,我只是将这些键加载到一个列表中,而不是一个字典中。)

通过将两条指令放在同一个函数中,两行代码(插入和存储)减少为一个函数调用。但是,该函数现在执行两项任务,而不是一项。任何程序员都应该警惕,这(一个函数中的两个任务)使得修改更加困难,错误更加难以发现。

3.第三个改进:改函数名。

   def insertMoveAndStoreBoardInDictionary(board, position, char, boardCollection):
       newBoard = board[:position] + char + board[position + 1:]
       boardCollection.append(newBoard)
       return newBoard

理由:函数的名字必须随着函数的发展而改变。

4.第四个改进:把功能拆分成两个功能。

   def insertXAndStoreBoardInDictionary(board, position, boardCollection):
       newBoard = board[:position] + 'X' + board[position + 1:]
       boardCollection.append(newBoard)
       return newBoard

   def insertOAndStoreBoardInDictionary(board, position, boardCollection):
       newBoard = board[:position] + 'O' + board[position + 1:]
       boardCollection.append(newBoard)
       return newBoard

理由:如果函数的名字告诉我们哪个字母('X''O')被插入到电路板中,程序将更容易调试。警报响起:这个代码违反了 DRY(不要重复自己)原则。还有,这两个函数真的能让程序更容易调试吗?注意,这个改进确实从参数列表中删除了char

5.第五个也是最后一个改进:回到每个功能一个任务的原则,这仍然违反了 DRY 原则。

   def insertX(board, position):                           
       return board[:position] + 'X' + board[position + 1:]

   def insertO(board, position):                           
       return board[:position] + 'O' + board[position + 1:]

理由:我厌倦了每次查看代码时脑子里响起的警报。我打破了两个原则,一个任务一个功能。我继续打破 DRY 原则,因为我爱上了这段代码的可读性。我告诉自己,因为这两个功能在物理上彼此接近,所以我不太可能忘记做两个而不是一个更改。

6.尝试改进:更改函数名称(被拒绝) :

   def insertXInBoard(board, position): ...

理由:InBoard使名称更长,对理解没有什么帮助,主要是因为board是第一个参数的名称。这是一个很好的例子,说明了一个精心选择的参数如何与一个函数名相结合来提高理解。

7.尝试改进:使用 OOP(被拒绝)。

我考虑将数据和它的功能组合成一个类对象。然后,代替写作

   insertX(board, position),

我会写

   board.insertX(position).

这有帮助吗?我的猜测是否定的,但是在很多情况下,程序员无法知道封装是否带来了优势,除非程序编写一次有封装,一次没有封装。一般规则是,除非对象彼此交互并进行有效的通信,否则它们不会带来好处。

那么,摆弄这些代码有什么意义呢?这是函数调用吗

insertX(board, position)

明显优于原来的单线:

board = board[:position] + char + board[position + 1:] ?

我觉得函数调用更好,因为它帮助我们更快更好地理解,而且事半功倍。这种讨论似乎是对细节的执着。但是对细节的痴迷恰恰是编程、交流复杂想法、下棋和任何创造性活动的正确态度。如果我们很少重新思考我们的设计,因为它们“足够好”,那么我们就没有获得足够的高质量设计的经验。