CS61A Spring 2018 Lecture 2: Names - Notes
Powerful Elements in Python
We have identified in Python some of the elements that must appear in any powerful programming language:
- Numbers and arithmetic operations are primitive built-in data values and functions.
- Nested function application provides a means of combining operations.
- Binding names to values provides a limited means of abstraction.
Types of Expressions
Defining New Functions
Assignment is a simple means of abstraction: binds names to values
Function definition is a more powerful means of abstraction: binds names to expressions
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>
⚠️ The second line must be indented — most programmers use four spaces to indent. The return expression is not evaluated right away; it is stored as part of the newly defined function and evaluated only when the function is eventually applied.
Function Signatures. Functions differ in the number of arguments that they are allowed to take. To track these requirements, we draw each function in a way that shows the function name and its formal parameters. A description of the formal parameters of a function is called the function's signature.
max can take an arbitrary number of arguments. It is rendered as
max(...). Regardless of the number of arguments taken, all built-in functions will be rendered as
<name>(...), because these primitive functions were never explicitly defined.
⭐ Execution procedure for def statements:
- Create a function with signature
- Set the body of that function to be everything indented after the first line
<name>to that function in the current frame
>>> def square(x): return mul(x, x)
💡 We can also use
square as a building block in defining other functions. For example, we can easily define a function
sum_squares that, given any two numbers as arguments, returns the sum of their squares:
>>> def sum_squares(x, y): return add(square(x), square(y)) >>> sum_squares(3, 4) 25
User-defined functions are used in exactly the same way as built-in functions. Indeed, one cannot tell from the definition of
square is built into the interpreter, imported from a module, or defined by the user.
def statements and assignment statements bind names to values, and any existing bindings are lost. For example,
g below first refers to a function of no arguments, then a number, and then a different function of two arguments.
>>> def g(): return 1 >>> g() 1 >>> g = 2 >>> g 2 >>> def g(h, i): return h + i >>> g(1, 2) 3
An environment in which an expression is evaluated consists of a sequence of frames, depicted as boxes. Each frame contains bindings, each of which associates a name with its corresponding value.There is a single global frame. Assignment and import statements add entries to the first frame of the current environment. So far, our environment consists only of the global frame.
This environment diagram shows the bindings of the current environment, along with the values to which names are bound.
Functions appear in environment diagrams as well. An
import statement binds a name to a built-in function. A
def statement binds a name to a user-defined function created by the definition. The resulting environment after importing
mul and defining
square appears below:
Edit code in Online Python Tutor%3A%0A++++return+mul(x,+x))
Each function is a line that starts with
func, followed by the function name and formal parameters. Built-in functions such as
mul do not have formal parameter names, and so
... is always used instead.
The name of a function is repeated twice, once in the frame and again as part of the function itself. The name appearing in the function is called the intrinsic name. The name in a frame is a bound name. There is a difference between the two: different names may refer to the same function, but that function itself has only one intrinsic name.
The name bound to a function in a frame is the one used during evaluation. The intrinsic name of a function does not play a role in evaluation.
Edit code in Online Python Tutor%0Amax(1,+2)++%23+Causes+an+error)
Calling User-Defined Functions
To evaluate a call expression whose operator names a user-defined function, the Python interpreter follows a computational process. 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:
- Bind the arguments to the names of the function's formal parameters in a new local frame.
- 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. Therefore, a new local frame is introduced every time a function is called, even if the same function is called twice.
⚠️ Notice that the entire
def statement is processed in a single step. The body of a function is not executed until the function is called (not when it is defined).
⚠️ The "Return value" in the
square() frame is not a name binding; instead it indicates the value returned by the function call that created the frame.
Even in this simple example, two different environments are used. The top-level expression
square(-2)is evaluated in the global environment, while the return expression
mul(x, x) is evaluated in the environment created for by calling
mul are bound in this environment, but in different frames.
Looking Up Names In Environments
Every expression is evaluated in the context of an environment.
So far, the current environment is either:
- The global frame alone, or
- A local frame, followed by the global frame.
The order of frames in an environment affects the value returned by looking up a name in an expression. We stated previously that a name is evaluated to the value associated with that name in the current environment. We can now be more precise:
⭐ 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.
E.g., to look up some name in the body of the
- Look for that name in the local frame.
- If not found, look for it in the global frame.
(Built-in names like “max” are in the global frame too, but we don’t draw them in environment diagrams.)
Our conceptual framework of environments, names, and functions constitutes a model of evaluation; while some mechanical details are still unspecified (e.g., how a binding is implemented), our model does precisely and correctly describe how the interpreter evaluates call expressions.
One detail of a function's implementation that should not affect the function's behavior is the implementer's choice of names for the function's formal parameters. Thus, the following functions should provide the same behavior:
>>> def square(x): return mul(x, x) >>> def square(y): return mul(y, y)
⭐ This principle -- that the meaning of a function should be independent of the parameter names chosen by its author -- has important consequences for programming languages. The simplest consequence is that the parameter names of a function must remain local to the body of the function. The model of computation is carefully designed to ensure this independence.
We say that 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. This scoping behavior isn't a new fact about our model; it is a consequence of the way environments work.
Well-chosen function and parameter names are essential for the human interpretability of function definitions. The following guidelines are adapted from the style guide for Python code, which serves as a guide for all (non-rebellious) Python programmers.
- Function names are lowercase, with words separated by underscores. Descriptive names are encouraged.
- Function names typically evoke operations applied to arguments by the interpreter (e.g.,
square) or the name of the quantity that results (e.g.,
- Parameter names are lowercase, with words separated by underscores. Single-word names are preferred.
- Parameter names should evoke the role of the parameter in the function, not just the kind of argument that is allowed.
- 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.
💡 There are many exceptions to these guidelines, even in the Python standard library. Like the vocabulary of the English language, Python has inherited words from a variety of contributors, and the result is not always consistent.
Functions as Abstractions
sum_squares is defined in terms of the function
square, but relies only on the relationship that
square defines between its input arguments and its output values.
We can write
sum_squares without concerning ourselves with how to square a number. 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.
Aspects of a functional abstraction. To master the use of a functional abstraction, it is often useful to consider its three core attributes:
- The domain of a function is the set of arguments it can take.
- The range of a function is the set of values it can return.
- The intent of a function is the relationship it computes between inputs and output (as well as any side effects it might generate).
Understanding functional abstractions via their domain, range, and intent is critical to using them correctly in a complex program. For example, any
square function that we use to implement
sum_squares should have these attributes:
- The domain is any single real number.
- The range is any non-negative real number.
- The intent is that the output is the square of the input.
These attributes do not specify how the intent is carried out; that detail is abstracted away.
Python provides two infix operators:
//. The former is normal division, so that it results in a floating point, or decimal value, even if the divisor evenly divides the dividend:
>>> 5 / 4 1.25 >>> 8 / 4 2.0
// operator, on the other hand, rounds the result down to an integer:
>>> 5 // 4 1 >>> -5 // 4 -2
These two operators are shorthand for the
>>> from operator import truediv, floordiv >>> truediv(5, 4) 1.25 >>> floordiv(5, 4) 1
💡 You should feel free to use infix operators and parentheses in your programs. Idiomatic Python prefers operators over call expressions for simple mathematical operations.
We now turn to 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. Functions that perform multiple jobs in sequence should be divided into multiple functions.
- Don't repeat yourself is a central tenet of software engineering. The so-called DRY principle states that multiple fragments of code should not describe redundant logic. Instead, that logic should be implemented once, given a name, and applied multiple times. If you find yourself copying and pasting a block of code, you have probably found an opportunity for functional abstraction.
- Functions should be defined generally. Squaring is not in the Python Library precisely because it is a special case of the
powfunction, which raises numbers to arbitrary powers.
These guidelines improve the readability of code, reduce the number of errors, and often minimize the total amount of code written. Decomposing a complex task into concise functions is a skill that takes experience to master. Fortunately, Python provides several features to support your efforts.
A function definition will often include documentation describing the function, called a docstring, which must be indented along with the function body. Docstrings are conventionally triple quoted. The first line describes the job of the function in one line. The following lines can describe arguments and clarify the behavior of the function:
>>> 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).
When writing Python programs, include docstrings for all but the simplest functions. Remember, code is written only once, but often read many times. The Python docs include docstring guidelines that maintain consistency across different Python projects.
Comments. Comments in Python can be attached to the end of a line following the
# symbol. For example, the comment
Boltzmann's constant above describes
k. These comments don't ever appear in Python's
help, and they are ignored by the interpreter. They exist for humans alone.
Default Argument Values
In Python, we can provide default values for the arguments of a function. When calling that function, arguments with default values are optional. If they are not provided, then the default value is bound to the formal parameter name instead. For instance, if an application commonly computes pressure for one mole of particles, this value can be provided as a default:
>>> 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
= symbol means two different things in this example, depending on the context in which it is used. In the
def statement header,
= does not perform assignment, but instead indicates a default value to use when the
pressure function is called. By contrast, the assignment statement to
k in the body of the function binds the name
k to an approximation of Boltzmann's constant.
>>> pressure(1, 273.15) 2269.974834 >>> pressure(1, 273.15, 3 * 6.022e23) 6809.924502
pressure function is defined to take three arguments, but only two are provided in the first call expression above. In this case, the value for
n is taken from the
def statement default. If a third argument is provided, the default is ignored.
💡 As a guideline, most data values used in a function's body should be expressed as default values to named arguments, so that they are easy to inspect and can be changed by the function caller. Some values that never change, such as the fundamental constant
k, can be bound in the function body or in the global frame.