CS61a Chapter 1: Building Abstractions with Functions

1.1   Getting Started

1.1.1   Programming in Python

Python is a prominent as well as widely used language which can be used in web, game, science and etc. (which can be well illustrated in the first chapter of this ✬Python3 Tutorial✬ and ✬the Zen of Python✬)

1.1.2   Installing Python

1.1.3   Interactive Sessions

Type python at a terminal prompt and an interactive session can be started.

Interactive controls: 

        To access your typing history, press <Control>-P (previous) and <Control>-N (next).

        <Control>-D exits a session, which discards this history.

        Up and down arrows also cycle through history.

1.1.4   First Example

Statements & Expressions: Python code consists of expressions and statements.

        (Computer programs consist sentenses to compute value and carry out action)

        Statements typically describe actions. (not evaluated but executed)

        Expressions typically describe computations. (evaluated)

Functions

        Functions encapsulate logic that manipulates data.

Objects

        An object seamlessly bundles together data and the logic that manipulates that data. (which will be further discussed in Chapter 2.)

Interpreters:

        Programs to implement such a procedure that evaluates compound expressions. (which will be further discussed in Chapter 3.)

✬ In the end, we will find that all of these core concepts are closely related: functions are objects, objects are functions, and interpreters are instances of both. However, developing a clear understanding of each of these concepts and their role in organizing code is critical to mastering the art of programming.

1.1.5   Errors

The nature of computers is described in ✬Stanford's intorductory course✬ as "powerful + stupid".

Some guiding principles of debugging:

  1. Test incrementally
  2. Isolate errors
  3. Check assumptions
  4. Consult others

1.2   Elements of Programming 

Every powerful language has three such mechanisms:

  • primitive expressions and statements
  • means of combination
  • means of abstraction                

 In programming, we deal with two kinds of elements: functions and data. (But they can be not so distinct)

1.2.1   Expressions

Mathematical expressions can use infix notation, where the operator (e.g., +, -, *, or /) appears in between the operands (numbers).

1.2.2   Call Expressions

A call expression has subexpressions: the operator is an expression that precedes parentheses, which enclose a comma-delimited list of operand expressions.

 (The function max is called with arguments 7.5 and 9.5, and returns a value of 9.5.)

Function's advantages compared with infix notation:

  1. Functions can take an arbitrary number of arguments.
  2. Function notation extends in a straightforward way to nested expressions.

1.2.3   Importing Library Functions

To use the elements that comprise the Python Library, one imports them.

eg.

>>> from math import sqrt
>>> sqrt(256)
16.0
>>> from operator import add, sub, mul
>>> add(14, 28)
42
>>> sub(100, mul(7, add(8, 4)))
16

More can be found in the ✬Python 3 Library Docs✬.

1.2.4   Names and the Environment

If a value(function) has been given a name, we say that the name binds to the value(function). (the assignment operator: "=", whose life purpose is to bind a name to a value)

The memory that keeps track of the names, values and bindings is called an environment.

A typical difference between python and C++:

        In order to change a and b in python, you only need to:

>>> x, y = 3, 4.5
>>> y, x = x, y

1.2.5   Evaluating Nested Expressions

To evaluate a call expression, Python will:

  1. Evaluate the operator and operand subexpressions.
  2. Apply the function that is the value of the operator subexpression to the arguments that are the values of the operand subexpressions.

eg. an expression tree

1.2.6   The Non-Pure Print Function

Pure functions

        Functions have some input (their arguments) and return some output (the result of applying them). (Reliable in compound call expressions) (Concurrent programs will be discussed in Chapter 4, where pure functions exert massive benefits.)

eg. abs()

Non-pure functions

        Applying a non-pure function can generate side effects, which make some change to the state of the interpreter or computer.

eg. print()

 An eloquent example:

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

1.3   Defining New Functions

How to define a function:

        Function definitions consist of a def statement that indicates a <name> and a comma-separated list of named <formal parameters>, then a return statement, called the function body, that specifies the <return expression> of the function, which is an expression to be evaluated whenever the function is applied:

def <name>(<formal parameters>):
    return <return expression>

        Both def statements and assignment statements bind names to values, and any existing bindings are lost.

1.3.1   Environments

An environment in which an expression is evaluated consists of a sequence of frames.

Each frame contains bindings, each of which associates a name with its corresponding value.

There is a single global frame.

eg. An environment diagram (available in ✬Online Python Tutor✬)

        The name appearing in the function(the box) is called the intrinsic name.

        The name in a frame is a bound name.

Function Signatures:

        A description of the formal parameters of a function.

1.3.2   Calling User-Defined Functions

Evaluating a user-defined function follows a computional procedure:

  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.

 Name Evaluation:

        A name evaluates to the value bound to that name in the earliest frame of the current environment in which that name is found.

1.3.3   Example: Calling a User-Defined Function

1.3.4   Local Names

The choice of the names for formal parameters does not affect one function's behaviour, since they remain local to the body of a function. 

In other words, the scope of a local name is limited to the body of the user-defined function that defines it. When a name is no longer accessible, it is out of scope.

1.3.5   Choosing Names

Well-chosen function and parameter names are essential for the human interpretability of function definitions. (Pay attention to the ✬style guide for Python code✬.)

Basic and Recommended Guidelines:

  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 (eg. print, add, square) or the name of the quantity that results (eg. 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.

1.3.6   Functions as Abstractions

We can write a function without concerning ourselves with how to achieve it. The details of how the action is designed can be suppressed, to be considered at a later time, which is a so-called functional abstraction.

Note: A function definition should be able to suppress details.

Aspects of a functional abstraction: Three core attributes should be considered.

  1. The domain of a function is the set of arguments it can take.
  2. The range of a function is the set of values it can return.
  3. The intent of a function is the relationship it computes between inputs and output (as well as any side effects it might generate).

1.3.7   Operators

Python expressions with infix operators can often be thought of as short-hand for call expressions. (eg. 2 + 3 is short for add(2, 3))

Note: The // operator rounds the result down to an integer. (eg. 5 // 4 == 1)


1.4   Designing Functions

We now concentrate on the topic of what makes a good function.

Fundamentally, the qualities of good functions all reinforce the idea that functions are abstractions.

  • Each function should have exactly one job. That job should be identifiable with a short name and characterizable in a single line of text. In other words, functions that perform multiple jobs in sequence should be divided into multiple functions.
  • Don't repeat yourself(DRY) is a central tenet of software engineering. Same logics should be implemented only once, given a name, and applied multiple times.
  • Functions should be defined generally. Squaring is not in the Python Library precisely because it is a special case of the pow function, which raises numbers to arbitrary powers.

1.4.1   Documentation

A function definition will often include documentation describing the function, called a docstring, which must be indented along with the function body. (Referring to ✬docstring guidelines✬)

eg.

>>> 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

When you call help with the name of a function as an argument, you see its docstring (type q to quit Python help).

eg.

>>> help(pressure)

Comments

        Comments in Python can be attached to the end of a line following the # symbol. (which don't ever appear in Python's help, and they are ignored by the interpreter.)

1.4.2   Default Argument Values

eg. the variable n has a default value.

>>> 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

Note: The = symbol in the def statement indicates a default value to use when the pressure function is called. (not performing assignment)


1.5   Control

Control statements will give us the ability to make comparisons and to perform different operations depending on the result of a comparison.

1.5.1   Statements

Rather than being evaluated, statements are executed, and executing statements can involve evaluating subexpressions contained within them. (Expressions can also be executed as statements, in which case they are evaluated, but their value is discarded.)

1.5.2   Compound Statements

In general, Python code is a sequence of statements. 

A compound statement is so called because it is composed of other statements (simple and compound). Compound statements typically span multiple lines and start with a one-line header ending in a colon, which identifies the type of statement.

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

eg. assignment statements are simple statements; def is a <header>

We say that the header controls its suite(a set of <statement>s), and a header and an indented suite of statements is called a clause.

Practical Guidance

        When indenting a suite, all lines must be indented the same amount and in the same way (use spaces, not tabs (WHY?)). Any variation in indentation will cause an error.

1.5.3   Defining Functions II: Local Assignment

Assignment statements within a function body cannot affect the global frame.

1.5.4   Conditional Statements

Conditional statements: a series of headers + suites

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

Boolean contexts

        The expressions inside the header statements of conditional blocks are said to be in boolean contexts: their truth values matter to control flow, but otherwise their values are not assigned or returned.

Boolean values

        True and False. The built-in comparison operations, >, <, >=, <=, ==, !=, return these values.

Boolean operators:

        and, or and not

Short-circuiting feature:

To evaluate the expression <left> and <right>:

  1. Evaluate the subexpression <left>.
  2. If the result is a false value v, then the expression evaluates to v.
  3. Otherwise, the expression evaluates to the value of the subexpression <right>.

To evaluate the expression <left> or <right>:

  1. Evaluate the subexpression <left>.
  2. If the result is a true value v, then the expression evaluates to v.
  3. Otherwise, the expression evaluates to the value of the subexpression <right>.

To evaluate the expression not <exp>:

  1. Evaluate <exp>; The value is True if the result is a false value, and False otherwise.

1.5.5   Iteration

Iterative control structures are another mechanism for executing the same statements many times.

eg. Fibonacci Number

>>> def fib(n):
        """Compute the nth Fibonacci number, for n >= 2."""
        pred, curr = 0, 1   # Fibonacci numbers 1 and 2
        k = 2               
        while k < n:
            pred, curr = curr, pred + curr
            k = k + 1
        return curr
>>> result = fib(8)

while clause contains a header expression followed by a suite:

while <expression>:
    <suite>

To execute a while clause:

  1. Evaluate the header's expression.
  2. If it is a true value, execute the suite, then return to step 1.

 Note: Press <Control>-C to force Python to stop looping.

1.5.6   Testing

Testing a function is the act of verifying that the function's behavior matches expectations.

test is a mechanism for systematically performing this verification.

Assertions:

        Programmers use assert statements to verify expectations, such as the output of a function being tested. An assert statement has an expression in a boolean context, followed by a quoted line of text (single or double quotes are both fine, but be consistent) that will be displayed if the expression evaluates to a false value.

eg. Asserting Fibonacci number

>>> assert fib(8) == 13, 'The 8th Fibonacci number should be 13'
>>> 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'

Note: 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: A convenient approach to place simple tests.

        The first line of a docstring should contain a one-line description of the function, followed by a blank line. A detailed description of arguments and behavior may follow. In addition, the docstring may include a sample interactive session that calls the function.

eg. Summing naturals

>>> 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

Then, the interaction can be verified via the ✬doctest module✬.

eg.

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

To verify the doctest interactions for only a single function, we use a doctest function called run_docstring_examples, whose first argument is the function to test, second should always be the result of the expression globals(), a built-in function that returns the global environment, which the interpreter needs in order to evaluate expressions, and third argument is True to indicate that we would like "verbose" output: a catalog of all tests run.

eg.

>>> 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 writing Python in files, all doctests in a file can be run by starting Python with the doctest command line option:

python -m doctest <python_source_file>

Note: The key to effective testing is to write (and run) tests immediately after implementing new functions. A test that applies a single function is called a unit test. And Exhaustive unit testing is a hallmark of good program design.


1.6   Higher-Order Functions

One of the things we should demand from a powerful programming language is the ability to build abstractions by assigning names to common patterns and then to work in terms of the names directly. (Functions work!)

1.6.1   Functions as Arguments

Consider the following three functions:

eg1. Compute natural numbers' summations

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

eg2. Compute natural cubes' summations 

>>> def sum_cubes(n):
        total, k = 0, 1
        while k <= n:
            total, k = total + k*k*k, k + 1
        return total
>>> sum_cubes(100)
25502500

eg3. Compute 8/(2n-1)*(2n+1) summations 

>>> def pi_sum(n):
        total, k = 0, 1
        while k <= n:
            total, k = total + 8 / ((4*k-3) * (4*k-1)), k + 1
        return total
>>> pi_sum(100)
3.1365926848388144

They clearly share a common underlying pattern. So they can be abstracted into one function.

eg. Abstract version of summation

>>> 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)
55

1.6.2   Functions as General Methods

Two big ideas in computer science:

  1. Names and functions allow us to abstract away a vast amount of complexity.
  2. It is only by virtue of the fact that we have an extremely general evaluation procedure for the Python language that small components can be composed into complex processes.

1.6.3   Defining Functions III: Nested Definitions

Without nested definitions, the global frame would become cluttered with names of small functions, which must all be unique and we would be constrained by particular function signatures. Nested function definitions address both of these problems.

eg. Compute the square root of a number

>>> 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)

Like local assignment, local def statements only affect the current local frame. 

Lexical scope

        Locally defined functions also have access to the name bindings in the scope in which they are defined. In the upper example, sqrt_update refers to the name a, which is a formal parameter of its enclosing function sqrt (locally defined functions are often called closures). This discipline of sharing names among nested definitions is called lexical scoping.

Two extensions to the model in order to enable lexical scoping:

  1. Each user-defined function has a parent environment: the environment in which it was defined.
  2. When a user-defined function is called, its local frame extends its parent environment.

 Extended Environments:

        An environment can consist of an arbitrarily long chain of frames, which always concludes with the global frame.

Two key advantages of lexical scoping:

  • The names of a local function do not interfere with names external to the function in which it is defined, because the local function name will be bound in the current local environment in which it was defined, rather than the global environment.
  • A local function can access the environment of the enclosing function, because the body of the local function is evaluated in an environment that extends the evaluation environment in which it was defined.

1.6.4   Functions as Returned Values

An important feature of lexically scoped programming languages is that locally defined functions maintain their parent environment when they are returned.

Once many simple functions are defined, function composition is a natural method of combination to include in our programming language. That is, given two functions f(x) and g(x), we might want to define h(x) = f(g(x)).

eg. h(x) = f(g(x))

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

Note: The 1 in compose1 is meant to signify that the composed functions all take a single argument. (which is just part of the function name.)

1.6.5   Example: Newton's Method

1.6.6   Currying

We can use higher-order functions to convert a function that takes multiple arguments into a chain of functions that each take a single argument, which is called currying. In other words, given a function f(x, y), we can define a function g such that g(x)(y) is equivalent to f(x, y), where g is a higher-order function that takes in a single argument x and returns another function that takes in a single argument y.

Currying is useful when we require a function that takes in only a single argument.

eg. Automate currying and uncurrying

>>> 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

1.6.7   Lambda Expressions

We can create function values on the fly using lambda(λ) expressions, which evaluate to unnamed functions. (where assignment and control statements are not allowed.)

eg. lambda

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

lambda's syntax

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

1.6.8   Abstractions and First-Class Functions

As programmers, we should be alert to opportunities to identify the underlying abstractions in our programs, build upon them, and generalize them to create more powerful abstractions.The significance of higher-order functions is that they enable us to represent these abstractions explicitly as elements in our programming language, so that they can be handled just like other computational elements.

1.6.9   Function Decorators

Python provides special syntax to apply higher-order functions as part of executing a def statement, called a decorator.

eg. 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

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)

More information about decorators can be found in the ✬short tutorial on decorators✬.


1.7   Recursive Functions

A function is called recursive if the body of the function calls the function itself, either directly or indirectly.

An example problem: write a function that sums the digits of a natural number.

>>> 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

1.7.1   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.

A recursive leap of faith:

        Treat a recursive call as a functional abstraction. (That is to ignore how the function is implemented and trust that it works.)

1.7.2   Mutual Recursion

When a recursive procedure is divided among two functions that call each other, the functions are said to be mutually recursive

eg. Even or odd

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

eg. Single recursive version

>>> 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

The computational process evolved by a recursive function can often be visualized using calls to print.

eg. Cascading

>>> 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

1.7.4   Tree Recursion

A function calling itself more than once is called tree recursion.

eg. Fibonacci number

>>> def fib(n):
    if n == 1:
        return 0
    if n == 2:
        return 1
    else:
        return fib(n-2) + fib(n-1)
>>> result = fib(6)

1.7.5   Example: Partitions

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值