a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1b = a[:]
print(b)- 1
-
A single
:is a shortcut that means the whole list. It is like writinga[0:len(a)]. In contrast,a[0:-1]excludes the last item because the stop is excluded.
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
After this lesson you should…
set collection and its special featureslist and dict comprehensions in place of simple for-loops, including if-clausesenumerate over a collection, as well as zip multiple collections togetherWhen Guido created Python he was partly motivated by the realization that code is read more often than it is written. With this insight in mind he created a syntax that was as close to spoken English as possible. We have seen examples of this with statements like for item in collection. When code is written in a clean, concise, and consistent manner it becomes easy to read your own code as well as someone elses. Writing clean code is called “pythonic code”.
Pythonic code means two things. First is that the formatting of the code follows the standard conventions:
find_roots. Class names conventionally use CapWords, while constants use UPPER_CASE.x = 1 instead of x=1.To facilitate nice and consistent code, there is a style guide generally referred to as pep8.
The Python community created a process where new features can be proposed for the language. These proposals are called “PEPs”, an acronym for “Python Enhancement Proposal”. A numbered list of proposals is available, along with their current status.
The second aspect of Pythonic code is how the code is written, meaning the method used for certain common tasks uses the features of Python to their fullest. Examples include:
in keyword for checking membership and scanning over collections, like if item in collection or for item in collection.try/except blocks to do checking of type and general validity.PEP8 was created very early on, and has been a core part of Python ever since. This PEP basically describes how Python code should look. A condensed version of this guide, including examples is given here.
Here are a few highlights:
Most of these are style recommendations, not Python syntax requirements. Consistent indentation is required by Python, but it could be 2 or 10; using specifically “4” spaces per level is the style convention.
Instead of listing off all the rules, the following table demonstrates several (but not all):
| Yes | No |
|---|---|
spam(ham[1], {eggs: 2}) |
spam( ham[ 1 ], { eggs: 2 } ) |
spam(1) |
spam (1) |
dct['key'] = lst[index] |
dct ['key'] = lst [index] |
i = i + 1 |
i=i+1 |
submitted += 1 |
submitted +=1 |
x = x*2 - 1 |
x = x * 2 - 1 |
hypot2 = x**2 + y*y |
hypot2 = x ** 2 + y * y |
c = (a+b) * (a-b) |
c = (a + b) * (a - b) |
c = (a+b) / (a-b) |
c = (a + b)/(a - b) |
def complex(real, imag=0.0): |
def complex(real, imag = 0.0): |
return magic(r=real, i=imag) |
return magic(r = real, i = imag) |
For a block containing several statements, put each statement on its own indented line:
if x == 4:
print(x, y)
x, y = y, xSince there are so many rules, it takes quite a while to learn them all. Luckily, there is a tool we can use called a linter. In IDE’s such as Spyder and VSCode it is possible to enable linting using a plugin or optional feature. The result looks like this:
We briefly covered slicing, but it will come up a lot more when we start to talk about numpy, so let’s take a closer look. We can use the : symbol to indicate a range of indices.
It works like this: start:stop:step. Each of these can be excluded, and defaults will be used:
start is 0.1stop is None which means it goes to the endUsing the defaults gets us the entire list:
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1b = a[:]
print(b): is a shortcut that means the whole list. It is like writing a[0:len(a)]. In contrast, a[0:-1] excludes the last item because the stop is excluded.
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
We can change the start and stop values to get a section of the list:
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1b = a[2:6]
print(b)6, not at 6.
[2, 3, 4, 5]
The end point can be specified as distance from the start, or distance from the end by using a negative number:
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1b = a[2:-3]
print(b)2 and stop at 3 from the end.
[2, 3, 4, 5, 6]
Changing the step size is also possible if we want every Nth item:
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1b = a[2:8:2]
print(b)[2, 4, 6]
And of course the step can be negative if we want to walk through the collection backwards:
a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
1b = a[6:2:-1]
print(b)[6, 5, 4, 3]
Another type of collection is called a set. This word refers to the mathematical concept of a set. A set contains no duplicate elements and is unordered. Use set() for an empty set, since {} creates an empty dictionary.
A set is defined by putting curly braces around a comma-separated sequence of values:
1s = {1, 2.0, 'text'}
print("Result:", type(s))dict. The difference is that the collection only contains values, wheres in a dict it contains key:value pairs. There is actually some logic behind this: a set cannot contain any duplicate items, and similarly a dict cannot contain any duplicate keys. So {'a': 1, 'a': 2} will become {'a': 2}.
Result: <class 'set'>
Python also includes a built-in function for creating sets:
1s = set([1, 2.0, 2.0, 2.0, 'text'])
print("Result:", s)2.0, yet the resulting set only has one instance.
Result: {1, 2.0, 'text'}
Probably the most useful application of sets is for “Venn-diagram” style logic. For instance, if you have two lists of values, you might want to know what they have in common, or what are their differences. This can be accomplished with sets.
Consider the following 2 sets:
a = {'bob', 'dave', 'jane'}
b = {'dave', 'sue', 'taylor'}We can find what the two sets have in common by finding the intersection:
a = {'bob', 'dave', 'jane'}
b = {'dave', 'sue', 'taylor'}
c = a.intersection(b)
print("Result:", c)Result: {'dave'}
Or we can find how they differ using difference:
a = {'bob', 'dave', 'jane'}
b = {'dave', 'sue', 'taylor'}
c = a.difference(b)
print("Result:", c)Result: {'jane', 'bob'}
Table A.5 gives a list of the methods available on a set object which can be used for performing a variety of other “set operations” such those shown in Figure 7.5. An abbreviated list is given below with just the ones associated with “Venn-diagram”-type logic.
set object, including the shortcut keys which have been defined
| Method | Shortcut | Description |
|---|---|---|
difference() |
- |
Returns a set containing the difference between two or more sets |
intersection() |
& |
Returns a set, that is the intersection of two other sets |
issubset() |
<= |
Returns whether another set contains this set or not |
< |
Tests for a proper subset: all items belong to the other set, but the sets are not equal | |
issuperset() |
>= |
Returns whether this set contains another set or not |
> |
Tests for a proper superset: this set contains the other set, but the sets are not equal | |
symmetric_difference() |
^ |
Returns a set with the symmetric differences of two sets |
union() |
| |
Return a set containing the union of sets |
We have seen For-Loops which iterate over the items in a collection:
c = ['a', 'b', 'c']
for item in c:
print(item)a
b
c
And we have seen For-Loops which using an index into a collection:
c = ['a', 'b', 'c']
for i in range(len(c)):
print(c[i])a
b
c
But we can do both!
c = ['a', 'b', 'c']
for i, item in enumerate(c):
print(i, item)0 a
1 b
2 c
Having access to the item and to the index where is located is quite useful:
c = [22, 33, 44]
for i, item in enumerate(c):
c[i] = item/11
print(c)[2.0, 3.0, 4.0]
We sometimes have 2 collections which we want to scan together, for example to combine the values into a dict. We can use the zip function as follows:
names = ['a', 'b', 'c']
nums = [1, 2, 3]
d = {}
for key, value in zip(names, nums):
d[key] = value
print(d){'a': 1, 'b': 2, 'c': 3}
By default, zip stops when its shortest input is exhausted.
We have been mostly looking at linear collections like lists and strings. However, we often have “multidimensional” data, like a table or spreadsheet, which can be thought of as “list of lists”. In such cases we need to use one for-loop to scan over the “outer” list, then a second for-loop to scan each “inner” list.
Another way to think about this is a 2D (or ND!) array of data, where one for-loop scans each row, then the second loop scans the columns of that row. Consider the following case of a 5-by-5 matrix:
Python offers an alternative way to build a collection in a single line. This can make simple transformations easier to read, though fewer lines do not automatically mean clearer code:
result = [item/4 for item in [-1, 0, 2, 3]]
print("Result:", result)Result: [-0.25, 0.0, 0.5, 0.75]
Technically this is called a list comprehension. They can be confusing at first, but eventually you may appreciate the conciseness they offer. It often helps readability to condense logic to a single line.
Python supports list, dictionary, and set comprehensions. There is no tuple comprehension.
Dictionary comprehensions are helpful for combining two different data sources into a single dictionary. For instance if we have a list of names, and another list of values, we can do:
name = ['a', 'b', 'c', 'd', 'e']
data = [1, 2, 3, 4, 5]
1d = {name[i]: data[i] for i in range(len(data))}
print(d)dict, like d = {'a': 1, 'b': 2, 'c': 3}.
{'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5}
Recall the “One-line If-Statement”:
limit = 5
value = 'a' if 4 < limit else 'b'
print(value)a
We can combine this if condition in comprehensions too:
vals = [1, 2, 3, 4]
limit = 3
1a = [i for i in vals if i < limit]
print(a)if filters the input: only values below limit are included, so this result is [1, 2] and may be shorter than the original list.
[1, 2]
In previous weeks we have discussed the differences between types and how Python tries to treat each variable accordingly. However, sometimes this just isn’t possible for Python and an error occurs. So far we have focused our energy on trying to intercept the variables and decide how to treat each one, but Python offers a way to handle this: try and except.
The basic idea is that we try to run some code first, and only if it fails do we do something else.
try:
c = a + b
except:
print('a and b are not compatible')This will catch every error; however, it is good practice to only catch errors which you are expecting. In the above case our print statements suggests that we are only expect TypeErrors, so we should write:
try:
c = a + b
except TypeError:
print('a and b are not compatible')Let’s look at an example:
There are more clauses we can add to a try statement. The else block runs if the try block completes normally; finally runs when leaving the statement, including when an exception propagates. This schematic uses pass as a valid placeholder for code:
try:
pass # Attempt an operation
except ValueError:
pass # Handle an anticipated ValueError
else:
pass # Execute if try completed normally
finally:
pass # Cleanup when leaving the statementThese extra clauses are not very common, but are mentioned so if/when you see them in the wild, you’ll know what you’re seeing.
In addition to the built-in functions we learned about (i.e. float(3)), and the massive collection of extras we can install and import (i.e. math.cos(3)), we will still want to create our own custom functions. The construction of such a function is quite simple:
def means define as in here we define a function. (2) We will call our function sqrt, which is the name we will use when we call it. (3) Inside the round brackets we list all the variables that our function requires or at accepts.
:! Whitespace like this crucial in Python. Refer to here: Important 7.1 for more info. The code is is identical to what we’ve been putting the cells of our Jupyter notebooks.
Python is quite flexible about many things, but it is not flexible about whitespace at all!. When Guido van Rossum created Python in 1991 he had several guiding principles. One of them was the realization that “code is read more often than it is written”. For this reason he designed the syntax to use whitespace to denote logical blocks rather than brackets and braces like other languages (i.e. C/C++). The mandatory indentation makes code quite easy for a human to read since the logical blocks are instantly identifiable.
We can use this function in our program as follows:
a has nothing to do with the variable name x inside the function.
y inside the function, does not mean that y is available outside the function. The numerical value gets returned, not the variable y. We catch the value of 3 and assign it to any variable name we like, in this case c.
return exits the function immediately and passes the value to the caller.return, or uses return alone, returns None.a can be passed to b, and a inside the function does not change the original a.To start with, let’s identify the difference between position and keyword arguments. Consider the following function:
def add_values(a, b):
return a + bWe can call it using positional arguments:
c = add_values(2, 2)Or keyword arguments:
c = add_values(a=2, b=3)When using keywords we can do this:
1c = add_values(b=3, a=2)a=2 onto a and b=3 onto b.
Lastly, note that positional arguments must come first, and then keyword arguments can follow:
c = add_values(2, b=3)In many functions there are some arguments which we don’t usually want to change. An example is something like mode='encrypt'.
This can be done in the function definition as follows:
def add(a, b, mode='normal'):
if mode == 'normal':
d = a + b
elif mode == 'absolute':
d = abs(a) + abs(b)
else:
raise ValueError("mode must be 'normal' or 'absolute'")
return d
print(add(-2, 3)) # Uses the default mode='normal'
print(add(-2, 3, mode='absolute'))1
5
The supported modes are 'normal' and 'absolute'. An unsupported mode raises an informative ValueError rather than reaching return with d undefined.
If you have a list or tuple of values, such coefficients (e.g. [1, 3, 5]), that you want to pass into a function, you can do the following:
def func(a, b, c):
print(a, b, c)
vals = [1, 3, 5]
1func(*vals)* (asterisk) tells python to unpack the values in vals and place them into a, b, and c of the function. Obviously, the length of vals has to match the number of arguments in func. This is equivalent to the “splat” operation mentioned early.
1 3 5
And taking this to the next level, we can use dicts as keyword arguments, where the key corresponds to the argument name.
def func(a, b, c):
print(a, b, c)
vals = {'a': 1, 'c':5, 'b': 3}
1func(**vals)** (double asterisk) unpacks vals as keyword arguments, so the value in vals['a'] (which is 1) is passed to parameter a. For this function, the dictionary must supply each of a, b, and c, with no unexpected keys.
1 3 5
This functionality is important because there are many packages where functions are designed to work together. The next example is a preview of NumPy arrays, SciPy statistics, and plotting, which we will study later.
There are times where we want to write a function that accepts an undetermined or unlimited number of arguments. Python has a two ways to accept an undetermined number and/or name of arguments. They both use a variation of the * and ** notation mentioned above.
We can supply a set of positional arguments like this:
*args with a single *. Also note that we don’t have to use the variable args, but this is convention.
args to the user
tuple containing the values which we sent into func, in order.
(1, 2, 3)
And we can supply “keyword” arguments like this:
**’s. Again note that using the variable name kwargs to catch these arguments is not mandatory but is convention.
dict with the given arguments as keys.
{'a': 1, 'b': 2, 'c': 3}
The main thing to note with both of the above is that inside the function, args and kwargs both become just a normal tuple or dict. In other words, the code inside the function does not know that a, b and c were passed. These get rolled into a dict called kwargs which can be used inside the function.
It is also possible to mix and match *args and **kwargs if you really want to get fancy:
def func(*args, **kwargs):
return args, kwargs
print(func(1, 2, 3, a=4, b=5, c=6))((1, 2, 3), {'a': 4, 'b': 5, 'c': 6})
And to really make things messy, you can accept some keyword arguments explicitly, and even supply default values to some, then collect the rest into **kwargs:
x, y, and z are keyword-only because they follow *args. Supplying x and y is mandatory; z defaults to 5. Extra positional arguments go into args, and extra keyword arguments go into kwargs.
x and y by name and uses the default for z. These three parameters are separate from kwargs, so they are not included in this function’s returned dictionary.
((1, 2, 3), {'a': 4, 'b': 5, 'c': 6})
The main use case for behavior like this is when calling another function from within a function, and using kwargs to collect the arguments for the inner function. For instance, let’s write a function which calls another function a specified number of times:
def inner(a, b, c):
return sum((a, b, c))
def outer(n, func, **kwargs):
s = 0
for i in range(n):
s = s + func(**kwargs)
return s
result = outer(5, func=inner, a=1, b=1, c=1)
print(result)15
The crucial thing to note above is that outer accepts inner as an argument and is able to call it without knowing any of its argument names. It just passes kwargs to func and assumes that the user supplied the correct arguments.
One place where we see **kwargs a lot is in Matplotlib functions, which we’ll introduce in a few weeks.
We are now quite familiar with writing our own functions. Some functions can be complicated and need to be explained. This is what “docstrings” are for. A docstring is a string placed at the top of a function body, building on the strings introduced in Working with Text.
A basic docstring can be a single sentence:
def square(x):
"""Return x squared."""
return x**2For a longer description, scientific Python projects often use the NumPy documentation convention below. The section headings and their underlines are a documentation convention, not a Python syntax requirement:
def calc_area_and_perimeter(r):
"""Compute the area and perimeter of a circle.
Parameters
----------
r : int or float
Nonnegative radius of the circle.
Returns
-------
result : tuple of floats
Area and perimeter, in that order. If r is in metres,
area is in square metres and perimeter is in metres.
"""
A = 3.14159*r**2
P = 2*3.14159*r
return (A, P)Recall that triple quotes allow the string to span several lines.
Python’s built-in help function displays this documentation:
help(calc_area_and_perimeter)Help on function calc_area_and_perimeter in module __main__:
calc_area_and_perimeter(r)
Compute the area and perimeter of a circle.
Parameters
----------
r : int or float
Nonnegative radius of the circle.
Returns
-------
result : tuple of floats
Area and perimeter, in that order. If r is in metres,
area is in square metres and perimeter is in metres.
The full docstring can optionally contain any of the following section, if applicable:
| Section | Description |
|---|---|
| Short summary | A one-line summary that does not use variable names or the function name |
| Deprecation warning | To warn users that the object is deprecated |
| Extended Summary | A few sentences giving an extended description. |
| Parameters | Description of the function arguments, keywords and their respective types. |
| Returns | Explanation of the returned values and their types. |
| Yields | Explanation of the yielded values and their types. |
| Receives | Explanation of parameters passed to a generator’s .send() method |
| Other Parameters | Used to describe infrequently used parameters. |
| Raises | Details which errors get raised and under what conditions |
| Warns | Details which warnings get raised and under what conditions, formatted similarly to Raises. |
| Warnings | Cautions to the user in free text/reST. |
| See Also | Used to refer to related code. |
| Notes | Provides additional information about the code, possibly including a discussion of the algorithm. |
| References | References cited in the Notes section may be listed here |
| Examples | For examples using the doctest format |
There are several accepted ways ways to format docstrings, as well described by thist SO Discussion. I strongly suggest you use the numpydoc option since you’ll be dealing with numpy and other packages in the numpy universe which all use it.
Here are some definitions to help
A script is a file we execute to run a program. A notebook which we’ve been using up to now is a form of a script.
A module is a unit of Python code that can be imported. Our examples are .py files containing reusable functions. The same file can act as a script when executed and as a module when imported.
A package organizes modules under a common name, allowing imports such as numpy.linalg. A library is a broader term for reusable code and may contain several packages or modules. We often use existing libraries such as NumPy, pandas, SciPy, and Matplotlib.
Now let’s create a module with two functions: one for taking the square root of every element in a list, and one for squaring each element.
def square_it(collection):
for i, val in enumerate(collection):
collection[i] = val**2
return collection
def root_it(collection):
for i, val in enumerate(collection):
collection[i] = val**0.5
return collectionThe above code should be placed in a new file called something like “myfuncs.py”, as shown below:
We can then use the functions in this module in a script as follows:
from myfuncs import root_it, square_it
vals = [1, 3, 5, 9]
squares = square_it(vals)
print(vals) # [1, 9, 25, 81]: the original list changed
print(squares is vals) # True: both names refer to the same listAnd the script would look like this:
if __name__ == "__main__" to Test Your ModuleIt is as important to test our code as it is to write it in the first place. In fact, in large important projects, more time is spent writing tests than writing code.
Like everything, there are many ways we can do tests, from simple (a script) to complicated (unit testing suites that are run by continuous integration services!).
The simplest way to include tests in our code is to add them at the end of our file; HOWEVER, when a module is imported everything in it is run. If our tests are long, or perhaps they are broken, then importing our module will be stalled or broken. We can fix this by using another of Python’s features: if __name__ == "__main__".
Let’s update our myfuncs.py file:
def square_it(collection):
for i, val in enumerate(collection):
collection[i] = val**2
return collection
def root_it(collection):
for i, val in enumerate(collection):
collection[i] = val**0.5
return collection
1if __name__ == "__main__":
# The code inside this "if block" is ONLY executed
# when you "run" this file. It is NOT run when you
# import the functions from this file in a script
2 vals = [1, 2, 3]
3 ans = [1, 4, 9]
4 test = square_it(vals)
5 print(test == ans)
vals = [1, 4, 9]
ans = [1, 2, 3]
test = root_it(vals)
print(test == ans)__name__ to '__main__' when it executes the file as the main program. When another file imports myfuncs, its __name__ is 'myfuncs', so the guarded block is skipped. Function definitions and other top-level statements outside the guard still execute during import.
True/False, so can visually look at the output and see if anything failed.
True
True
With the above if __name__ == "__main__" block added to our module file, we can run the file after changes to check that these particular examples still work.
We can add as many tests as we want/need to check all the “edge cases”.
Submit one Python file named A6.py containing the six functions A6Q1 through A6Q6.
Each function must return its result; printing the result is not sufficient. The checker imports your file and calls each function with its own inputs. Keep any code used to test your functions inside an if __name__ == "__main__": block so that it does not run when the file is imported. Notebook cell tags and assignments to answer are no longer needed.
Install or update the checker using the instructions in Appendix E. Run python -m ne111_checker A6 on Windows or python3 -m ne111_checker A6 on macOS from any folder. You no longer need to start the checker in the same directory as your file. Just launch the checker from anywhere then cClick Browse… in the checker to select your saved A6.py, then select a question tab and press its Run button. Save changes to your file before running the check again; the checker rereads the selected file automatically.
Given a list, tuple, or string, return a subset starting and stopping at the specified locations, using the specified step size. Use slice notation. If start, stop, or step is omitted, use Python’s default slicing behaviour.
Function: A6Q1(c, start=None, stop=None, step=None)
Examples:
A6Q1([1, 2, 3, 4, 5], 1, -2) # Returns [2, 3]
A6Q1(c='abcdefghi', start=-4, stop=2, step=-1) # Returns 'fed'Given 2 lists, find the elements in common and the elements present in only one of the lists. Return a tuple containing these two sets, in that order. Repeated elements appear only once in each set.
Function: A6Q2(first, second)
Examples:
A6Q2([1, 2, 3], [2, 3, 4]) # Returns ({2, 3}, {1, 4})Fix all the whitespace mistakes in the following function according to PEP 8. Preserve the parameter names, default values, calculations, and order of the returned roots.
def A6Q3(a, b=1, c = 5):
ans1=( -b + (b**2-4*a*c) ** 0.5 ) /(2*a) # One root
ans2 = (-b-(b**2-4*a*c)**0.5)/(2*a) # The other root
return(ans1, ans2)Write a function which accepts two arbitrary values and attempts to multiply them together. If multiplication is not possible, return None.
Function: A6Q4(a, b)
Examples:
A6Q4(2, 5) # Returns 10
A6Q4('abcd', [1, 2, 3]) # Returns NoneWrite a function which accepts between 1 and 4 integers and multiplies them all together. Use default arguments so that omitted factors do not change the product. Calling the function with no arguments or more than 4 arguments should raise TypeError.
Function: A6Q5(a, b=1, c=1, d=1)
Examples:
A6Q5(1, 2, 3) # Returns 6
A6Q5(3, 3) # Returns 9Update the following function to include a docstring with both Parameters and Returns sections. Describe the parameters and the returned value. Preserve the function’s signature and calculation.
def A6Q6(a, b, c=0):
hypt = (a**2 + b**2 + c**2)**0.5
return hypt