Skip to content

3. Common Pitfalls

Kamal Banga edited this page Aug 1, 2019 · 15 revisions

Iterators get empty after iteration

  • Iterating twice
    >>> from statistics import mean, median
    >>> cubes = (n**3 for n in range(10))
    >>> mean(nums)
    202.5
    >>> median(nums)

    StatisticsError: no median for empty data

  • Containment checking
    >>> cubes = (n**3 for n in range(10))
    >>> 125 in cubes
    True
    >>> 125 in cubes
    False

This might look unexpected behaviour 💭. Containment internally iterates over the iterable, hence the second case here is committing the same treason as the first one, i.e., iterating over an iterator twice. Iterating once exhausts an iterator and makes it empty and hence the weird behaviour.

But why does an iterator behave so?


Assignment does not copy data

a = b = [1,2]
a.append(3)

What is b? It's [1,2,3].

a = [1,2]
b = a.copy() # (shallow) copy
a.append(3)

Now, b is still [1,2].


Mutable and Immutables

Mutable default parameter value

def append5(data=[]):
  data.append(5)
  return data

Using this append5 method results in an astonishing behaviour

>>> append5()
[5]
>>> append5()
[5, 5]
>>> append5()
[5, 5, 5]

⁉️😳What just happened?

Lesson: Don't use mutable values as default parameter values 🚫

Closures

Let's say we want a running average and we implement it using a class

class Averager:
  
  def __init__(self):
    self.series = []
  
  def __call__(self, num):
    self.series.append(num)
    return sum(self.series)/len(self.series)

This can be used as

>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

Same thing can be achieved using a closure

def make_averager():
  series = []
  
  def average(num):
    series.append(num)
    return sum(series)/len(series)
  
  return average

It too achieves the same purpose, after all "Closures and Objects are equivalent". On SO

>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0

Now, let's say we rewrite our make_averager as

def make_averager():
  count = 0
  total = 0
  
  def average(num):
    count += 1
    total += num
    return total/count
  
  return average

Trying to do the same thing leads us to UnboundLocalError

avg(10)

UnboundLocalError: local variable 'count' referenced before assignment

⁉️😧

You would need to use nonlocal keyword here

Floating Point

📝Note: This pitfall is not just for Python but programming languages in general.

Floats are tricky! 1.1 + 2.2 == 3.3 gives False 😕
See Python FAQ

Take a moment to look at the function f below. Before you try running it, write on paper what the output would be of x1 = f(1/10). Now, (still on paper) plug that back into f and calculate x2 = f(x1). Keep going for 10 iterations.

def f(x):
    if x <= 1/2:
        return 2 * x
    if x > 1/2:
        return 2*x - 1

Only after you've written down what you think the answer should be, run the code below:

x = 1/10
for i in range(80):
    print(x)
    x = f(x)

What went wrong?

Direct links

Iterators

Bell Curve

Clone this wiki locally