cs61a笔记-2020fall

01-debug

(2022.12.06)
持续更新cs61a 2020fall中的笔记;(¦3[▓▓]
部分内容使用的教材原文;

Introduction

Traceback (most recent call last):
  File "<pyshell#29>", line 3 in <module>
    result = buggy(5)
  File <pyshell#29>", line 5 in buggy
    return f + x
TypeError: unsupported operand type(s) for +: 'function' and 'int'

This is called a traceback message. It prints out the chain of function calls that led up to the error, with the most recent function call at the bottom. You can follow this chain to figure out which function(s) caused the problem.

Traceback Messages

这对代码中的第二行(它的缩进比第一行更大)显示了进行下一个函数调用的实际代码行。这可以让您快速了解传递给函数的参数是什么,函数是在什么上下文中使用的等等。

最后,请记住,traceback是用“最近的调用最后”组织的

Error Messages

traceback消息的最后一行是错误语句。错误语句具有以下格式

<error type>: <error message>

错误类型:所引起的错误类型(例如SyntaxError, TypeError)。这些通常是描述性的,可以帮助您缩小搜索错误原因的范围。

错误消息:关于错误原因的更详细的描述。不同的错误类型产生不同的错误消息。

Debugging Techniques

Running doctests

def foo(x):
    """A random function.

    >>> foo(4)
    4
    >>> foo(5)
    5
    """

文档字符串中看起来像解释器输出的行是doctests。要运行它们,请转到您的终端并键入

python3 -m doctest file.py

命令行工具有一个-v选项,表示详细

python3 -m doctest file.py -v

Writing your own tests

Some advice in writing tests:

  • Write some tests before you write code: this is called test-driven development. Writing down how you expect the function to behave first – this can guide you when writing the actual code.
  • Write more tests after you write code: once you are sure your code passes the initial doctests, write some more tests to take care of edge cases.
  • Test edge cases: make sure your code works for all special cases.

Using print statements

Don’t just print out a variable – add some sort of message to make it easier for you to read:

print(tmp)   # harder to keep track
print('DEBUG: tmp was this:', tmp)  # easier
Long-term debugging

The print statements described above are meant for quick debugging of one-time errors – after figuring out the error, you would remove all the print statements.

然而,如果需要定期测试文件,有时我们希望保留调试代码。如果每次运行文件时都弹出调试消息,那就有点烦人了One way to avoid this is to use a global debug variable:

debug = True
def foo(n):
i = 0
while i < n:
    i += func(i)
    if debug:
        print('DEBUG: i is', i)

相当于添加了一个flag;

Using assert statements

Python has a feature known as an assert statement, which lets you test that a condition is true, and print an error message otherwise in a single line. This is useful if you know that certain conditions need to be true at certain points in your program. For example, if you are writing a function that takes in an integer and doubles it, it might be useful to ensure that your input is in fact an integer. You can then write the following code

def double(x):
    assert isinstance(x, int), "The input to double(x) must be an integer"
    return 2 * x

Note that we aren’t really debugging the double function here, what we’re doing is ensuring that anyone who calls double is doing so with the right arguments

断言语句的一个主要好处是它们不仅仅是调试工具,您可以将它们永久地保留在代码中;软件开发中的一个关键原则是,代码崩溃通常比产生不正确的结果要好,如果代码中有错误,那么在代码中使用断言会使代码崩溃的可能性大大增加

Error Types

[Debugging (berkeley.edu)

1.2 Elements of Programming(12.24)

We can use assignment statements to give new names to existing functions.

f = max
f
<built-in function max>
>>> f(2, 3, 4)
4

In Python, names are often called variable names or variables because they can be bound to different values in the course of executing a program. When a name is bound to a new value through assignment, it is no longer bound to any previous value. One can even bind built-in names to new values.

>>> max = 5
>>> max
5

After assigning max to 5, the name max is no longer bound to a function, and so attempting to call max(2, 3, 4) will cause an error.

Pure functions. Functions have some input (their arguments) and return some output (the result of applying them)在这里插入图片描述

Non-pure functions. In addition to returning a value, applying a non-pure function can generate side effects, which make some change to the state of the interpreter or computer. A common side effect is to generate additional output beyond the return value, using the print function在这里插入图片描述

print返回的值总是None,这是一个特殊的Python值,不代表任何东西。交互式Python解释器不会自动打印None值。在print的情况下,函数本身作为被调用的副作用打印输出。

A nested expression of calls to print highlights the non-pure character of the function.

>>> print(print(1), print(2))
1
2
None None

1.3 Defining New Functions

python 不像 java一样可以对同一个函数名定义多个函数,仅以形参的数量进行区分;python中函数实现的一个不应该影响函数行为的细节是实现者对函数形式参数名称的选择

Applying a user-defined function introduces a second local frame, which is only accessible to that function. To apply a user-defined function to some arguments:

  1. Bind the arguments to the names of the function’s formal parameters in a new local frame.
  2. Execute the body of the function in the environment that starts with this frame.

The environment in which the body is evaluated consists of two frames: first the local frame that contains formal parameter bindings, then the global frame that contains everything else. Each instance of a function application has its own independent local frame.

python代码编写:

  1. Function names are lowercase, with words separated by underscores. Descriptive names are encouraged.
  2. Function names typically evoke operations applied to arguments by the interpreter (e.g., print, add, square) or the name of the quantity that results (e.g., max, abs, sum).
  3. Parameter names are lowercase, with words separated by underscores. Single-word names are preferred.
  4. Parameter names should evoke the role of the parameter in the function, not just the kind of argument that is allowed.
  5. Single letter parameter names are acceptable when their role is obvious, but avoid “l” (lowercase ell), “O” (capital oh), or “I” (capital i) to avoid confusion with numerals.

风格指南是关于一致性的。与本风格指南的一致性很重要。项目内部的一致性更重要。一个模块或功能的一致性是最重要的。

特别是:不要为了遵守这个PEP而破坏向后兼容!

Functions as Abstractions

The details of how the square is computed can be suppressed, to be considered at a later time. Indeed, as far as sum_squares is concerned, square is not a particular function body, but rather an abstraction of a function, a so-called functional abstraction. At this level of abstraction, any function that computes the square is equally good.

Thus, considering only the values they return, the following two functions for squaring a number should be indistinguishable. Each takes a numerical argument and produces the square of that number as the value.

>>> def square(x):
        return mul(x, x)
>>> def square(x):
        return mul(x, x-1) + x

In other words, a function definition should be able to suppress details. The users of the function may not have written the function themselves, but may have obtained it from another programmer as a “black box”. A programmer should not need to know how the function is implemented in order to use it. The Python Library has this property. Many developers use the functions defined there, but few ever inspect their implementation.

要掌握函数抽象的用法,考虑它的三个核心属性通常是有用的。函数的定义域是它可以接受的参数集。函数的范围是它可以返回的值的集合。函数的目的是计算输入和输出之间的关系(以及它可能产生的任何副作用)。

常见的运算符其实可以看做 一些对应名称函数(如add,multi)等的简写;

python代码编写规范

  • 代码的布局

    • 缩进:每个缩进级别使用4个空格,延续行应该使用Python的隐式行在圆括号、括号和大括号内垂直对齐被包装的元素,或者使用悬挂缩进;当使用悬挂缩进时,应考虑以下几点:第一行不应该有参数,应该使用进一步的缩进来清楚地区分它是一个连续行:

    • # Correct:
      
      # Aligned with opening delimiter.
      foo = long_function_name(var_one, var_two,
                               var_three, var_four)
      
      # Add 4 spaces (an extra level of indentation) to distinguish arguments from the rest.
      def long_function_name(
              var_one, var_two, var_three,
              var_four):
          print(var_one)
      
      # Hanging indents should add a level.
      foo = long_function_name(
          var_one, var_two,
          var_three, var_four)
      #The 4-space rule is optional for continuation lines.
      foo = long_function_name(
        var_one, var_two,
        var_three, var_four)
      
      
    • 当if-语句的条件部分长到需要跨多行写入时,两个字符关键字(即if)的组合,加上一个空格,加上一个开括号,为多行条件语句的后续行创建了一个自然的4个空格的缩进。即在if后加一个空格,可以使的下一行的内容对其的同事有4个空格的缩进;

    • # No extra indentation.
      if (this_is_one_thing and
          that_is_another_thing):
          do_something()
      
      # Add a comment, which will provide some distinction in editors
      # supporting syntax highlighting.
      if (this_is_one_thing and
          that_is_another_thing):
          # Since both conditions are true, we can frobnicate.
          do_something()
      
      # Add some extra indentation on the conditional continuation line.
      if (this_is_one_thing
              and that_is_another_thing):
          do_something()
      
    • #数字内容行于上一行差四个space
      my_list = [
          1, 2, 3,
          4, 5, 6,
          ]
      result = some_function_that_takes_arguments(
          'a', 'b', 'c',
          'd', 'e', 'f',
          )
      #或者它可以在开始多行结构的行的第一个字符下排列
      my_list = [
          1, 2, 3,
          4, 5, 6,
      ]
      result = some_function_that_takes_arguments(
          'a', 'b', 'c',
          'd', 'e', 'f',
      )
      
    • 空格是首选的缩进方法。制表符应该仅用于与已经用制表符缩进的代码保持一致。Python不允许将制表符和空格混合用于缩进。

    • 最大行长度:限制所有行最多79个字符。

      • 对于结构限制较少的长文本块(文档字符串或注释),行长度应该限制在72个字符。

      • 大多数工具中的默认包装破坏了代码的可视化结构,使其更难以理解。选择这些限制是为了避免在窗口宽度设置为80的编辑器中进行换行,即使工具在换行时在最后一列中放置标记符号。一些基于web的工具可能根本不提供动态换行。

      • 对长行进行换行的首选方法是在圆括号、方括号和大括号内使用Python隐含的行连续符\

      • 通过将表达式括在括号中,可以将长行拆分为多行。它们应该优先于使用反斜杠来进行换行。即如果情况可以使用括号进行换行,且保持可读性,则优先使用括号

      • with open('/path/to/some/file/you/want/to/read') as file_1, \
             open('/path/to/some/file/being/written', 'w') as file_2:
            file_2.write(file_1.read())
        #Another such case is with assert statements
        
    • 换行符应该在二进制运算符之前还是之后?

    • # Correct:
      # easy to match operators with operands
      income = (gross_wages
                + taxable_interest
                + (dividends - qualified_dividends)
                - ira_deduction
                - student_loan_interest)
      
    • 空白行:

      • 用两行空行包围顶层函数和类定义
      • 类中的方法定义由一个空行包围
      • 可以使用额外的空行(有节制地)来分隔相关函数组。在一堆相关的一行程序之间可能会省略空行(例如一组虚拟实现)。
      • 在函数中尽量使用空行来表示逻辑部分。
      • Python接受control-L(即^L)表单提要字符作为空白许多工具将这些字符视为页面分隔符,因此可以使用它们分隔文件中相关部分的页面。注意,一些编辑器和基于web的代码查看器可能不将control-L识别为表单提要,并在其位置显示另一个符号。
    • 源文件编码

      • 核心Python发行版中的代码应该始终使用UTF-8,并且不应该有编码声明。
    • import

      • 导入通常应该在单独的行上

      • # Correct:
        import os
        import sys
        
      • It’s okay to say this though:

      • # Correct:
        from subprocess import Popen, PIPE
        
      • 导入总是放在文件的顶部,就在任何模块注释和文档字符串之后,在模块全局变量和常量之前。导入应按以下顺序分组:

        • 标准库进口。
        • 相关第三方导入。
        • 本地应用程序/库特定的导入
      • 您应该在每组导入之间放一个空行。

      • 推荐使用绝对导入,因为如果导入系统配置错误(比如包中的目录最终位于sys.path),它们通常更可读,而且往往表现更好(或者至少给出更好的错误消息):

      • import mypkg.sibling
        from mypkg import sibling
        from mypkg.sibling import example
        
      • 然而,显式相对导入是绝对导入的一种可接受的替代方法,特别是在处理复杂的包布局时,使用绝对导入会不必要地冗长:

      • from . import sibling
        from .sibling import example
        
      • 标准库代码应该避免复杂的包布局,并始终使用绝对导入。当从包含类的模块导入类时,通常可以这样拼写:

      • from myclass import MyClass
        from foo.bar.yourclass import YourClass
        
      • 如果这种拼写导致本地名称冲突,则显式拼写:and use “myclass.MyClass” and “foo.bar.yourclass.YourClass”.

      • import myclass
        import foo.bar.yourclass
        
      • 应该避免通配符导入(from import *),因为它们不清楚名称空间中出现了哪些名称,这会使读者和许多自动化工具感到困惑。通配符导入有一个可防御的用例,即将内部接口作为公共API的一部分重新发布(例如,使用可选加速模块的定义覆盖接口的纯Python实现,而具体将覆盖哪些定义尚不清楚)。

    • 模块级别Dunder名称:Module level “dunders” (i.e. names with two leading and two trailing underscores) such as __all__, __author__, __version__, etc. should be placed after the module docstring but before any import statements except from __future__ imports.

    • """This is the example module.
      
      This module does stuff.
      """
      
      from __future__ import barry_as_FLUFL
      
      __all__ = ['a', 'b', 'c']
      __version__ = '0.1'
      __author__ = 'Cardinal Biggles'
      
      import os
      import sys
      
    • 字符串引号:在Python中,单引号字符串和双引号字符串是相同的。

    • 表达式和语句中的空格

1.4 Design functions

从根本上说,好的函数都强化了函数是抽象的这一观点。

  • 每个函数应该只有一个任务。该工作应该用一个简短的名称来标识,并且可以在一行文本中进行描述。按顺序执行多个任务的功能应该被划分为多个function。
  • 不要重复自己是软件工程的中心原则。即对于可能多次重复出现的部分代码,考虑将其放入function中

Documentation

函数定义通常包括描述函数的文案,称为文档字符串,文档字符串必须与函数体一起缩进。

>>> def pressure(v, t, n):
        """Compute the pressure in pascals of an ideal gas.

        Applies the ideal gas law: http://en.wikipedia.org/wiki/Ideal_gas_law

        v -- volume of gas, in cubic meters
        t -- absolute temperature in degrees kelvin
        n -- particles of gas
        """
        k = 1.38e-23  # Boltzmann's constant
        return n * k * t / v

在编写Python程序时,除了最简单的函数外,所有函数都要包含文档字符串。记住,代码只写一次,但经常读很多次。注释放在一行的最后;

什么是文档字符串?

所有模块通常都应该有文档字符串,模块导出的所有函数和类也都应该有文档字符串。在模块、类或__init__方法的顶层进行简单赋值后立即出现的字符串字面量称为“属性文档字符串”。紧跟在另一个文档字符串之后的字符串字面量称为“附加文档字符串”。The docstring is a phrase ending in a period. It prescribes the function or method’s effect as a command (“Do this”, “Return that”), not as a description; e.g. don’t write “Returns the pathname …”.

多行文档字符串

def complex(real=0.0, imag=0.0):
    """Form a complex number.

    Keyword arguments:
    real -- the real part (default 0.0)
    imag -- the imaginary part (default 0.0)
    """
    if imag == 0.0 and real == 0.0:
        return complex_zero
    ...

处理文档字符串的缩进

def trim(docstring):
    if not docstring:
        return ''
    # Convert tabs to spaces (following the normal Python rules)
    # and split into a list of lines:
    lines = docstring.expandtabs().splitlines()
    # Determine minimum indentation (first line doesn't count):
    indent = sys.maxsize
    for line in lines[1:]:
        stripped = line.lstrip()
        if stripped:
            indent = min(indent, len(line) - len(stripped))
    # Remove indentation (first line is special):
    trimmed = [lines[0].strip()]
    if indent < sys.maxsize:
        for line in lines[1:]:
            trimmed.append(line[indent:].rstrip())
    # Strip off trailing and leading blank lines:
    while trimmed and not trimmed[-1]:
        trimmed.pop()
    while trimmed and not trimmed[0]:
        trimmed.pop(0)
    # Return a single string:
    return '\n'.join(trimmed)
def foo():
    """
    This is the second line of the docstring.
    """
def foo():
    """A multi-line
    docstring.
    """

def bar():
    """
    A multi-line
    docstring.
    """

参数默认值

函数参数过多时,可以设置一些参数默认值

>>> def pressure(v, t, n=6.022e23):
        """Compute the pressure in pascals of an ideal gas.

        v -- volume of gas, in cubic meters
        t -- absolute temperature in degrees kelvin
        n -- particles of gas (default: one mole)
        """
        k = 1.38e-23  # Boltzmann's constant
        return n * k * t / v

1.5 Control

Statements

Rather than being evaluated, statements are executed我们已经看到了三种语句:赋值语句、def语句和return语句。这些Python代码行本身并不是表达式,尽管它们都包含作为组件的表达式。

Compound Statements

<header>:
    <statement>
    <statement>
    ...
<separating header>:
    <statement>
    <statement>
    ...
...

每种标头的专用求值规则规定了何时以及是否执行其套件中的语句。我们说头文件控制它的套件。

Defining Functions II: Local Assignment

每当应用用户定义函数时,其定义套件中的子句序列将在本地环境中执行——该环境从调用该函数创建的本地帧开始

Conditional Statements

if <expression>:
    <suite>
elif <expression>:
    <suite>
else:
    <suite>

一个逻辑表达式的真值有时可以在不计算其所有子表达式的情况下确定,这一特性称为短路。(即在涉及到and or not这些操作时可以有部分来确定整体)

在赋值语句中,逗号分隔多个名称和值。

pred, curr = curr, pred + curr

Testing

测试函数是验证函数的行为是否符合预期的行为。

测试通常采用另一个函数的形式,该函数包含对被测试函数的一个或多个示例调用,然后根据预期结果验证返回值。与大多数函数不同的是,测试需要选择和验证具有特定参数值的调用。测试还可以作为文档:它们演示如何调用函数以及什么参数值是合适的。

**Assertions.**Programmers use assert statements to verify expectations,

例如被测试函数的输出。断言语句在布尔上下文中有一个表达式,后面有一行带引号的文本(单引号或双引号都可以,但要一致),如果表达式计算为假值,就会显示这行文本。

assert fib(8) == 13, 'The 8th Fibonacci number should be 13'

当断言的表达式求值为真值时,执行断言语句没有效果。当它为false值时,assert会导致停止执行的错误。

>>> def fib_test():
        assert fib(2) == 1, 'The 2nd Fibonacci number should be 1'
        assert fib(3) == 1, 'The 3rd Fibonacci number should be 1'
        assert fib(50) == 7778742049, 'Error at the 50th Fibonacci number'

When writing Python in files, rather than directly into the interpreter, tests are typically written in the same file or a neighboring file with the suffix _test.py.

**Doctests ** Python提供了一种方便的方法,可以将简单的测试直接放在函数的文档字符串中。文档字符串的第一行应该包含函数的一行描述,后面是空行。接下来会详细描述论点和行为。此外,文档字符串可以包含一个调用函数的交互会话示例:

>>> def sum_naturals(n):
        """Return the sum of the first n natural numbers.

        >>> sum_naturals(10)
        55
        >>> sum_naturals(100)
        5050
        """
        total, k = 0, 1
        while k <= n:
            total, k = total + k, k + 1
        return total

然后,可以通过doctest模块验证交互。下面,globals函数返回全局环境的表示形式,解释器需要它来求表达式的值。

>>> from doctest import testmod
>>> testmod()
TestResults(failed=0, attempted=2)

为了只验证一个函数的doctest交互we use a doctest function called run_docstring_examples. 这个函数(不幸的是)调用起来有点复杂。它的第一个参数是要测试的函数。第二个函数应该始终是表达式globals()的结果,这是一个返回全局环境的内置函数。第三个参数为True,表示我们想要“详细”输出:运行的所有测试的目录。

>>> from doctest import run_docstring_examples
>>> run_docstring_examples(sum_naturals, globals(), True)
Finding tests in NoName
Trying:
    sum_naturals(10)
Expecting:
    55
ok
Trying:
    sum_naturals(100)
Expecting:
    5050
ok

When the return value of a function does not match the expected result, the run_docstring_examples function will report this problem as a test failure.

When writing Python in files, all doctests in a file can be run by starting Python with the doctest command line option:

python3 -m doctest <python_source_file>

字符串处理

a = str(int(9999))

a.find(‘9’,1)从1位置开始寻找子字符串为9的最初始位置;

a.count(‘9’)表示a中含有多少个子字符串9

​ 1.6 Higher-Order Functions

缺乏函数定义将使我们处于不利的地位,迫使我们总是在使用语言中原语的特定操作(在本例中是乘法)的级别上工作,而不是在更高级别的操作方面。我们的程序将能够计算平方,但我们的语言将缺乏表达平方概念的能力

我们应该要求强大的编程语言具备这样一种能力:通过为公共模式分配名称来构建抽象,然后直接根据名称来工作。Functions provide this ability

要将某些通用模式表示为命名概念,我们需要构造可以接受其他函数作为参数或返回函数作为值的函数。操作函数的函数称为高阶函数

Functions as Arguments

def <name>(n):
    total, k = 0, 1
    while k <= n:
        total, k = total + <term>(k), k + 1
    return total

作为程序设计者,我们希望我们的语言足够强大,这样我们就可以编写一个函数来表达求和本身的概念,而不仅仅是计算特定和的函数。在Python中,我们可以很容易地做到这一点:采用上面所示的通用模板,并将“slots”转换为形式参数:

1	def summation(n, term):
2	    total, k = 0, 1
3	    while k <= n:
4	        total, k = total + term(k), k + 1
5	    return total
6
7	def cube(x):
8	    return x*x*x
9
10	def sum_cubes(n):
11	    return summation(n, cube)
12
13	result = sum_cubes(3)
>>> def summation(n, term):
        total, k = 0, 1
        while k <= n:
            total, k = total + term(k), k + 1
        return total
>>> def identity(x):
        return x
>>> def sum_naturals(n):
        return summation(n, identity)
>>> sum_naturals(10)

由此,对于相似的计算,我们可以提取出其显示的抽象模板,用一个具体的其他函数来作为函数的函数 以实现 编写一个函数来表达求和本身的概念,而不仅仅是计算特定和的函数

Functions as General Methods

>>> def improve(update, close, guess=1):
        while not close(guess):
            guess = update(guess)
        return guess

这个改进函数是重复精化的一般表达式。它不指定要解决什么问题:这些细节留给作为参数传入的update和close函数。

>>> def golden_update(guess):
        return 1/guess + 1
>>> def square_close_to_successor(guess):
        return approx_eq(guess * guess, guess + 1)
>>> def approx_eq(x, y, tolerance=1e-15):
        return abs(x - y) < tolerance

Calling improve with the arguments golden_update and square_close_to_successor will compute a finite approximation to the golden ratio. 相当于,之前的imporve是一个一般的通用式,而具体处理的任务需要根据传给其中的参数 update和close来判断;

这个例子说明了计算机科学中两个相关的大思想:

  • 首先,命名和函数允许我们抽象出大量的复杂性;虽然每个函数的定义都很简单,但是由我们的求值过程所启动的计算过程却是相当复杂的
  • 其次,只有由于我们对Python语言有一个极其通用的评估过程,小的组件才能被组合成复杂的过程。

和往常一样,我们改进的新通用方法需要一个测试来检验它的正确性。

>>> from math import sqrt
>>> phi = 1/2 + sqrt(5)/2
>>> def improve_test():
        approx_phi = improve(golden_update, square_close_to_successor)
        assert approx_eq(phi, approx_phi), 'phi differs from its approximation'
>>> improve_test()

Defining Functions III: Nested Definitions

这种方法的一个负面后果是,全局框架中的小函数名称变得杂乱无章,它们必须都是唯一的。 Another problem is that we are constrained by particular function signatures: the update argument to improve must take exactly one argument。嵌套函数定义解决了这两个问题,但要求我们丰富我们的环境模型。

>>> def average(x, y):
        return (x + y)/2
>>> def sqrt_update(x, a):
        return average(x, a/x)

比如上述两个方程,对于improve来说,他需要两个函数参数以及一个变量参数,与上述两个格式不同,未解决这个问题,我们就可以利用到嵌套定义。如:

>>> def sqrt(a):
        def sqrt_update(x):
            return average(x, a/x)
        def sqrt_close(x):
            return approx_eq(x * x, a)
        return improve(sqrt_update, sqrt_close)

词法范围,本地定义的函数还可以访问定义它们的作用域中的名称绑定。这种在嵌套定义之间共享名称的规则称为词汇作用域,关键是,内部函数可以访问定义它们的环境(而不是调用它们的环境)中的名称。即上述中的两个子函数可以访问作用域中 的a变量

我们需要对环境模型进行两个扩展来启用词法作用域。每个用户定义的函数都有一个父环境:定义它的环境。当调用用户定义的函数时,它的本地框架扩展其父环境。

环境首先为SQRT添加一个本地帧,并计算sqrt_update和sqrt_close的def语句;

在这种嵌套的函数中,相当于有三个框架 sqrt的局部环境;sqrt_update的局部环境,以及全局环境;

  • 局部函数的名称不会干扰定义它的函数外部的名称,因为局部函数名称将绑定在定义它的当前局部环境中,而不是全局环境中。

Functions as Returned Values

通过创建返回值本身就是函数的函数,我们可以在程序中获得更强大的表达能力;词法限定作用域的编程语言的一个重要特性是,本地定义的函数在返回时维护它们的父环境。

1	def square(x):
2	    return x * x
3
4	def successor(x):
5	    return x + 1
6
7	def compose1(f, g):
8	    def h(x):
9	        return f(g(x))
10	    return h
11
12	def f(x):
13	    """Never called."""
14	    return -x
15
16	square_successor = compose1(square, successor)
17	result = square_successor(12)

return 169

compose1中的1表示复合函数都接受一个参数。解释器不执行此命名约定;1只是函数名的一部分。

此时,我们开始观察精确定义计算的环境模型所带来的好处。不需要修改我们的环境模型来解释我们以这种方式返回函数的能力。

相当于现在返回的与之前不同,是一个函数的函数;因此不需要考虑过多的局部或者全局环境;

Currying(局部套用?)

我们可以使用高阶函数将一个带有多个参数的函数转换为一个带有单个参数的函数链

More specifically, given a function f(x, y), we can define a function g such that g(x)(y) is equivalent to f(x, y). Here, g is a higher-order function that takes in a single argument x and returns another function that takes in a single argument y. This transformation is called currying.

>>> def curried_pow(x):
        def h(y):
            return pow(x, y)
        return h
>>> curried_pow(2)(3)
8

相当于接受第一个参数x 之后,其变为了另外一个需要接受一个参数y的另外一 个函数;Some programming languages, such as Haskell, only allow functions that take a single argument, so the programmer must curry all multi-argument procedures

>>> def curry2(f):
        """Return a curried version of the given two-argument function."""
        def g(x):
            def h(y):
                return f(x, y)	
            return h
        return g
>>> def uncurry2(g):
        """Return a two-argument version of the given curried function."""
        def f(x, y):
            return g(x)(y)
        return f

Lambda Expressions

在Python中,我们可以使用lambda表达式动态地创建函数值,该表达式计算为未命名的函数。lambda表达式的计算结果是一个函数的主体只有一个返回表达式。不允许赋值和控制语句。

>>> def compose1(f, g):
        return lambda x: f(g(x))

We can understand the structure of a lambda expression by constructing a corresponding English sentence:

     lambda            x            :          f(g(x))
"A function that    takes x    and returns     f(g(x))"

lambda表达式的结果称为lambda函数。它没有固有名称(因此Python打印作为名称),但在其他方面,它的行为与任何其他函数一样。

>>> s = lambda x: x * x
>>> s
<function <lambda> at 0xf3f490>
>>> s(12)
144

一些程序员发现使用lambda表达式中的未命名函数更简短、更直接。然而,尽管复合lambda表达式很简短,但它是出了名的难以辨认。下面的定义是正确的,但是许多程序员很难快速理解它。

>>> compose1 = lambda f,g: lambda x: f(g(x))

一般来说,Python风格更喜欢显式的def语句而不是lambda表达式,但在需要简单函数作为参数或返回值的情况下,也允许使用它们。

Abstractions and First-Class Functions

在本节开始时,我们观察到用户定义函数是一种关键的抽象机制,因为它们允许我们将一般的计算方法表示为编程语言中的显式元素。作为程序员,我们应该注意识别程序中底层抽象的机会,在它们的基础上进行构建,并对它们进行泛化以创建更强大的抽象。这并不是说我们应该尽可能用最抽象的方式编写程序;专业的程序员知道如何选择适合他们任务的抽象级别。但重要的是能够从这些抽象的角度来思考,这样我们就可以在新的环境中应用它们。高阶函数的意义在于,它们使我们能够显式地表示这些抽象

Function Decorators 函数修饰符

Python提供了特殊的语法来应用高阶函数作为执行def语句的一部分,称为decorator。Perhaps the most common example is a trace.

>>> def trace(fn):
        def wrapped(x):
            print('-> ', fn, '(', x, ')')
            return fn(x)
        return wrapped
>>> @trace
    def triple(x):
        return 3 * x
>>> triple(12)
->  <function triple at 0x102a39848> ( 12 )
36

如此,@trace之后,将trace中的函数特性 应用至triple中了;在本例中,定义了一个高阶函数trace,它返回一个函数,该函数在调用其实参之前使用输出实参的print语句。The def statement for triple has an annotation, @trace, which affects the execution rule for def As usual, the function triple is created. However, the name triple is not bound to this function. Instead, the name triple is bound to the returned function value of calling trace on the newly defined triple function. In code, this decorator is equivalent to:

>>> def triple(x):
        return 3 * x
>>> triple = trace(triple)

装饰符符号@后面也可以跟一个调用表达式。首先计算@后面的表达式(就像前面计算名称跟踪一样),然后计算def语句,最后将计算decorator表达式的结果应用于新定义的函数,并将结果绑定到def语句中的名称。

实例

>>> lambda x: x  # A lambda expression with one parameter x
______
>>> a = lambda x: x  # Assigning the lambda function to the name a
>>> a(5)
______5
>>> (lambda: 3)()  # Using a lambda expression as an operator in a call exp.
______3
>>> b = lambda x: lambda: x  # Lambdas can return other lambdas!
>>> c = b(88)
>>> c
______<function <lambda>.<locals>.<lambda> at 0x7f09e74393a0>
>>> c()
______88 #已经绑定了值,此处只是调用
>>> d = lambda f: f(4)  # They can have functions as arguments as well.
>>> def square(x):
...     return x * x
>>> d(square)
______16
>>> x = None # remember to review the rules of WWPD given above!
>>> x
>>> lambda x: x
______
>>> z = 3
>>> e = lambda x: lambda y: lambda: x + y + z
>>> e(0)(1)()
______4 此处第三个lambda不接受任何参数,因此其会从全局环境变量中寻找所需要的值,即它会寻找在全局中定义的z的值;
>>> f = lambda z: x + z
>>> f(3)
______ error
>>> higher_order_lambda = lambda f: lambda x: f(x)
>>> g = lambda x: x * x
>>> higher_order_lambda(2)(g)  # Which argument belongs to which function call?
______error
>>> higher_order_lambda(g)(2)
______4 此处第一个lambda需要的是一个return function,第二个才是需要一个具体的值
>>> call_thrice = lambda f: lambda x: f(f(f(x)))
>>> call_thrice(lambda y: y + 1)(0)
______3
>>> print_lambda = lambda z: print(z)  # When is the return expression of a lambda expression executed?
>>> print_lambda
______
>>> one_thousand = print_lambda(1000)
______
>>> one_thousand
>>> def cake():
...    print('beets')
...    def pie():
...        print('sweets')
...        return 'cake'
...    return pie
>>> chocolate = cake()
______'beets')

>>> chocolate
______unction cake.<locals>.pie at 0x7f578227f940>

>>> chocolate()
______sweets') 相当于调用了两次,第一次调用cake() 第二次加一个()相当于为pie()赋值调用

>>> more_chocolate, more_cake = chocolate(), cake
______

>>> more_chocolate
______

>>> def snake(x, y):
...    if cake == more_cake:
...        return chocolate
...    else:
...        return x + y
>>> snake(10, 20)
______<function cake.<locals>.pie at 0x7f578227f940>

>>> snake(10, 20)()
______sweets
'cake'

>>> cake = 'cake'
>>> snake(10, 20)

墙裂建议之后回看的时候,看看这一节做的练习题lab02

在这里插入图片描述

Hint. If you’re getting a local variable [var] reference before assignment error:

This happens because in Python, you aren’t normally allowed to modify variables defined in parent frames. Instead of reassigning [var], the interpreter thinks you’re trying to define a new variable within the current frame. We’ll learn about how to work around this in a future lecture, but it is not required for this problem.

python

Hint. If you’re getting a local variable [var] reference before assignment error:

这是因为在Python中,通常不允许修改父帧中定义的变量。解释器认为您试图在当前帧内定义一个新变量,而不是重新赋值[var]

1.7 Recursive Functions(建议看课程视频)

tutorial

>>> def sum_digits(n):
        """Return the sum of the digits of positive integer n."""
        if n < 10:
            return n
        else:
            all_but_last, last = n // 10, n % 10
            return sum_digits(all_but_last) + last

The problem of summing the digits of a number is broken down into two steps: summing all but the last digit, then adding the last digit。Both of these steps are simpler than the original problem.

The Anatomy of Recursive Functions

A common pattern can be found in the body of many recursive functions. The body begins with a base case, a conditional statement that defines the behavior of the function for the inputs that are simplest to process

The base cases are then followed by one or more recursive calls. Recursive calls always have a certain character: they simplify the original problem. Recursive functions express computation by simplifying problems incrementally

These two factorial functions differ conceptually. The iterative function constructs the result from the base case of 1 to the final total by successively multiplying in each term. The recursive function, on the other hand, constructs the result directly from the final term, n, and the result of the simpler problem, fact(n-1).

递归结束时将参数1传递给fact;每次调用的结果都依赖于下一次调用,直到到达基本情况为止。虽然我们可以使用我们的计算模型展开递归,但将递归调用视为函数抽象通常会更清楚**。也就是说,我们不应该关心fact(n-1)是如何在事实体中实现的;我们只要相信它能计算n-1的阶乘。**将递归调用视为函数抽象被称为 recursive leap of faith;We define a function in terms of itself, but simply trust that the simpler cases will work correctly when verifying the correctness of the function我们必须只检查n!如果这个假设成立,则计算正确。这样,验证递归函数的正确性就是一种归纳法的证明形式。

The functions fact_iter and fact also differ because the former must introduce two additional names, total and k, that are not required in the recursive implementation. 通常,迭代函数必须保持一些局部状态,这些状态在整个计算过程中不断变化。在迭代中的任何一点上,该状态描述了已完成工作的结果和剩余的工作量。递归函数利用计算调用表达式的规则将名称绑定到值,通常避免了在迭代期间正确分配局部名称的麻烦。因此,递归函数更容易正确定义

1.7.2 Mutual Recursion

当一个递归过程被分成两个相互调用的函数时,这两个函数被称为相互递归。例如:
在这里插入图片描述

通过打破两个函数之间的抽象边界,相互递归的函数可以转换为单个递归函数

> def is_even(n):
        if n == 0:
            return True
        else:
            if (n-1) == 0:
                return False
            else:
                return is_even((n-1)-1)

1.7.3 Printing in Recursive Functions

由递归函数演变的计算过程通常可以通过调用打印来可视化。

>>> def cascade(n):
        """Print a cascade of prefixes of n."""
        if n < 10:
            print(n)
        else:
            print(n)
            cascade(n//10)
            print(n)
>>> cascade(2013)
2013
201
20
2
20
201
2013

在这个递归函数中,基本情况是一个被打印的个位数。否则,在两次打印调用之间放置递归调用。在递归调用之前表达基本情况并不是严格的要求。事实上,通过观察print(n)在条件语句的两个子句中都重复出现,因此可以在它前面,可以更简洁地表达这个函数。

>>> def cascade(n):
        """Print a cascade of prefixes of n."""
        print(n)
        if n >= 10:
            cascade(n//10)
            print(n)

1.7.4 Tree Recursion

另一种常见的计算模式被称为树递归,在这种模式下,一个函数会多次调用自己。

这个递归定义相对于我们之前的尝试非常有吸引力:它完全反映了我们熟悉的斐波那契数的定义;具有多个递归调用的函数被称为树递归,因为每个调用分支成多个更小的调用,每个调用分支成更小的调用,就像树的分支从主干延伸出来时变得更小但数量更多一样。

这种树递归虽然很明确很容易的表示出斐波那契数,但其效率并不高。

CS61A 2020秋季学期的讨论课5主要涉及到关于递归的问题。这节讨论课中我们学习了递归函数的定义、调用和实例,并进行了一些练习。 首先,我们回顾了递归函数的定义。递归函数是指在一个函数的定义中调用了该函数本身的情况。这样的定义允许我们通过将问题分解为更小的子问题来解决复杂的问题。递归函数通常包括一个基本情况和一个递归情况。基本情况表示问题已经足够简单,可以直接计算出结果,而递归情况则表示将问题拆解为更小的子问题,并调用自身来解决这些子问题。 在练习中,我们通过编写递归函数来解决一系列问题。例如,我们实现了一个递归函数来计算一个列表的长度。首先,我们检查基本情况,即当列表为空时长度为0。然后,我们将问题拆解为子问题,即将列表分解为其第一个元素和其余部分。然后,我们递归地计算剩余部分的长度,并将其加上第一个元素,最终得到整个列表的长度。 在讨论课中,我们还学习了尾递归。尾递归是指递归函数中递归调用发生在函数的最后一步操作的情况。尾递归函数可以通过迭代的方式执行,而不会在每次递归调用时创建新的栈帧,从而减少了内存的使用。这对于处理大规模数据非常有用。 总而言之,本次讨论课中我们学习了递归函数的定义和使用。通过理解递归的原理和练习编写递归函数,我们能够更好地解决复杂的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值