The iterator range
is an efficient way to create a list of indices, which is very common in the context of 'for'-loops:
range(5) ## This is an iterator -- not quite a list.
list(range(5)) ## This is [0, 1, 2, 3, 4].
## This will print out 2, 3, and 4, each on its own line.
for i in range(2, 5):
print(i)
## This will print out 2, 4, 6, and 8, each on its own line:
for i in range(2, 10, 2):
print(i)
## A common pattern for iterating over the indices of a list `vals`:
for i in range(len(vals)):
## do something with `i`
Since range
doesn't build its list, it is much faster for very large values. Try these out in the ipython terminal:
%time x = range(50000000)
%time x = list(range(50000000))
Python has some very nice conventions for assignment statements:
x += 1
is the same as x = x + 1
.
All the math operators support this. For instance, x *= 2
, x -= 2
x /= 2
.
For strings: s = "abc"; s += "d"
For lists: x = [1,2,3]; x += [4]
Another nice convention is simultaneous assignment: x, y = 1, 2
assigns x
the value 1
and y
the value 2
.
This also works for lists: x, y = [1, 2]
And it even works with nesting: (x, y), z = [['a', 'b'], 'c']
sets x
to 'a'
, y
to 'b'
and z
to 'c'
.
With this same convention, one can swap the values of two variables all at once:
x = 1
y = 2
x, y = y, x
x ## Equals 2 now.
y ## Equals 1 now.
Some noteworthy things about truth and falsity in Python:
1 == True ## True!
2 == False ## False!
0 == False ## True!
1 is True ## False!
0 is False ## False!
Truth and falsity on conditionals:
for x in [None, False, "", 0, [], (), {}]:
if x:
print("This code will not execute")
else:
print("This code will execute")
for x in [" ", True, [1], (1,), {1: 2}]:
if x:
print("This code will execute")
else:
print("This code will not execute")
You can join boolean-valued statements with and
and or
, and you can negate them with not
. Bracketing will let you control how different pieces of the code associate.
The in
operator tests whether the thing on the left is a member of the thing on the right. It works with str
, list
, tuple
, and dict
, among others. For dict
, it tests the keys.
Some examples inside a function definitions:
def valid_twitter_username(s):
return s.startswith("@") and " " not in s and "@" not in s[1: ]
def contains_neither_a_nor_b(s):
return not ('a' in s or 'b' in s)
In connection with the above section Truth, falsity, and identity, I should note that the behavior of not
with respect to various things that are informally true and false:
not None ## True!
not 0 ## True!
not "" ## True!
not [] ## True!
not 1 ## False!
not " " ## False!
not [""] ## False!
I've taken print()
for granted a bit, but it's worth calling out that it is a built-in method, akin to len()
and str()
. You can feed it lists of objects, and it will do its best to print them out. Here are some illustrative examples that go beyond what we've seen so far:
print("a", "b", "c", sep="_")
print("a", "b", "c", sep="_", end="***")
print("a", "b", "c", sep="_", end="\n\n")
Last week, we reviewed the basics of defining functions: def myfunc(args):
where args
is the comma-separated list of arguments to the function.
As you've seen already in a few places (see print
and sorted
above). functions can also have named, optional arguments:
def exponent(x, pow=2):
return x**pow
This function exponent
can be called as exponent(3)
, in which case pow
is set to 2
according to the default. It can also be set to other values with exponent(x, pow=3)
or exponent(x, pow=3)
.
Whether optional or not, function arguments can be called with their names in Python. For example, exponent(pow=3, x=2)
will compute x**pow
. This helps if one has a lot of arguments and the order is hard to remember.
All named arguments have to follow all unnamed ones, and unnamed arguments are assumed to be in the same order as they are in the function's definition.
Here's a variation on the first three problems from assignment 1 in which I use an optional argument to give the user control of the denominator value in sd
, and, in turn, in zscore
:
import math
def mean(vals):
return sum(vals) / len(vals)
def sd(vals, sd_corr=1):
mu = mean(vals)
if len(vals) < 2:
return 0.0
num = 0.0
for x in vals:
num += (x - mu)**2
denom = len(vals) - sd_corr
return math.sqrt(num / denom)
def zscore(vals, sd_corr=1):
mu = mean(vals)
sigma = sd(vals, sd_corr=sd_corr)
normed = []
for x in vals:
normed.append((x - mu) / sigma)
return normed
Initially, sd_corr=sd_corr
seems unintuitive, but it's just a consequence of my having used the same name for this parameter in both sd
and zscore
.