3

I've been tasked with creating a function "make_counter" which takes a number as its only argument. It should return a dictionary containing two functions that can be invoked to increment and decrement the number and then return it.

The behaviour should be as follows:

counter = make_counter(10)
up = counter['up']
down = counter['down']
print(up()) # 11
print(down()) # 10
print(down()) # 9
print(up()) # 10

with these being the answers it gives out.

However I've tried the following:

def make_counter(num):

    def up(num = num):
        num += 1
        return num
        
    def down(num = num):
        num -= 1
        return num
        
    return {"up": up, "down": down}
counter = make_counter(10)
up = counter["up"]
down = counter["down"]

print(up()) # 11
print(down()) # 9
print(down()) # 9
print(up()) #11

And these are the answers I get instead, ie. it doesn't remember the count.

I don't understand why this exact solution works in JavaScript but the equivalent doesn't work here in Python. And furthermore, why the inner functions in Python require default arguments or they can't "see" what num is from the parent function outside. Any help would be appreciated.

  • I'm pretty sure this wouldn't work in JavaScript, either, if `num` is a parameter to the inner functions. – Barmar Jan 19 '23 at 16:45
  • Python can't "see" the `num` variable because any variable that's assigned inside a function is a local variable by default. You need to use `global` or `nonlocal` to override this default. – Barmar Jan 19 '23 at 16:48

3 Answers3

3

You're incrementing the local variable num. You need to use a closure variable. Use the nonlocal keyword to make num refer to the variable in the enclosing function.

def make_counter(num):

    def up():
        nonlocal num
        num += 1
        return num
        
    def down():
        nonlocal num
        num -= 1
        return num
        
    return {"up": up, "down": down}
Barmar
  • 741,623
  • 53
  • 500
  • 612
2

I would make a class with a pair of methods

class Counter:
    def __init__(self, num):
        self.num = num

    def inc(self):
        self.num += 1
        return self.num

    def dec(self):
        self.num -= 1
        return self.num

Then your function can create an instance of this class and return bound methods which operate on the instance's state

def make_counter(num):
    c = Counter(num)
    return {'up': c.inc, 'down': c.dec}

Usage

>>> counter = make_counter(10)
>>> up = counter['up']
>>> down = counter['down']
>>> up()
11
>>> down()
10
>>> down()
9
>>> up()
10
Cory Kramer
  • 114,268
  • 16
  • 167
  • 218
0

The reason this doesn't work:

def make_counter(num):

    def up(num = num):
        num += 1
        return num
        
    def down(num = num):
        num -= 1
        return num

is that up and down are both defining their own num variables within their own scopes, which shadow the num from the outer scope of make_counter. Re-assigning num within those functions does not re-assign it at the make_counter scope.

Using nonlocal is one way to solve this (allowing assignment in an inner scope to rebind a variable from a nonlocal scope rather than shadowing it with a new local variable). Another option is to define a mutable object in the outer scope, which can then be mutated (as opposed to rebound) from an inner scope. For example:

def make_counter(num):
    num_box = [num]
    def inc(i):
        num_box[0] += i
        return num_box[0]
    return {
        "up": lambda: inc(+1),
        "down": lambda: inc(-1),
    }

A list is just one example of a mutable container that you could put an int value into. But in practice, you would usually do this by defining an object class, and mutating the object directly from within its methods.

Samwise
  • 68,105
  • 3
  • 30
  • 44