Execute Program

Python in Detail: Callables

Welcome to the Callables lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • Functions are "callable": we can call them like some_function(). We've seen that class instantiation uses the same SomeClass() syntax, so classes are also callable.

  • >
    class Cat:
    def __init__(self, name):
    self.name = name

    keanu = Cat("Keanu")
    keanu.name
    Result:
    'Keanu'Pass Icon
  • We can also make instances themselves callable. When we define the .__call__ dunder method, some_instance(some_arg) calls some_instance.__call__(some_arg).

  • >
    class Adder:
    def __init__(self, first_value):
    self.first_value = first_value

    def __call__(self, second_value):
    return second_value + self.first_value

    adder = Adder(3)
    adder(2)
    Result:
    5Pass Icon
  • Objects with a .__call__ still behave like regular objects. For example, they can have attributes that we access from outside of the object.

  • >
    class Counter:
    def __init__(self):
    self.total = 0

    def __call__(self, value):
    self.total += value

    counter = Counter()
    counter(3)
    counter(2)
    counter(4)
    counter.total
    Result:
    9Pass Icon
  • Sometimes, callable instances are useful for wrapping functions. For example, we can define a class that wraps a function and counts how many times it's called. From the outside, calling the wrapper looks exactly like calling the original function: it takes the same arguments, does the same work, and returns the same value. But internally, the wrapper class counts those calls. This lets us add the wrapper to the system without changing the existing function's body.

  • >
    class CountCalls:
    def __init__(self, func):
    self.func = func
    self.call_count = 0

    def __call__(self, *args, **kwargs):
    self.call_count += 1
    return self.func(*args, **kwargs)

    def add(x, y):
    return x + y

    add = CountCalls(add)

    results = (add(3, 4), add(2, 6), add(8, 1))
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    results
    Result:
    (7, 8, 9)Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    add.call_count
    Result:
    3Pass Icon
  • That code contained the familiar add = CountCalls(add) line. We can rewrite that line with decorator syntax: @CountCalls. The code below is identical to the code above, except for the @CountCalls change.

  • >
    class CountCalls:
    def __init__(self, inner_func):
    self.inner_func = inner_func
    self.call_count = 0

    def __call__(self, *args, **kwargs):
    self.call_count += 1
    return self.inner_func(*args, **kwargs)

    @CountCalls
    def add(x, y):
    return x + y

    results = [add(3, 4), add(2, 6), add(8, 1)]
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    (results, add.call_count)
    Result:
    ([7, 8, 9], 3)Pass Icon
  • Take a moment to appreciate how neatly these seemingly-unrelated Python features fit together. Instantiating a class works like calling a function: SomeClass(). The @decorator syntax calls a callable (like a class) on another function. Python's designers didn't add a special rule saying "classes can be used as decorators". Instead, it's a natural consequence of two separate facts: classes are callable, and decorators work with callables.

  • Using the same logic, we can also use callable instances (with .__call__ methods) as decorators. Here's a modified version of CountCalls. This time, we instantiate the class once, then use the instance as a decorator on our functions. Now we get a total of how many times all of the decorated functions were called.

  • >
    class CountCalls:
    def __init__(self):
    self.call_count = 0

    def __call__(self, f):
    def decorated(*args, **kwargs):
    self.call_count += 1
    return f(*args, **kwargs)

    return decorated

    counter = CountCalls()

    @counter
    def add(x, y):
    return x + y

    @counter
    def subtract(x, y):
    return x - y

    results = (add(1, 2), add(5, 6), subtract(7, 3))
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    results
    Result:
    (3, 11, 4)Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    counter.call_count
    Result:
    3Pass Icon
  • Sometimes we want to ask Python "is this value callable or not?" We can use the built-in callable function for that. It returns a boolean.

  • >
    add = lambda x, y: x + y

    class Cat:
    pass

    class Doubler:
    def __call__(self, n):
    return n * 2
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    callable(add)
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    callable(add(1, 2))
    Result:
    FalsePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    callable(Cat)
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    callable(Cat())
    Result:
    FalsePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    callable(Doubler)
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    callable(Doubler())
    Result:
    TruePass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    callable(Doubler()(2))
    Result:
    FalsePass Icon
  • Here's a code problem:

    Write a class, Limiter, that take a lower bound and upper bound as constructor arguments. Calling instances of the class (some_limiter(some_int)) should return a limited value:

    • If the value is less than the lower bound, return the lower bound.
    • If the value is more than the upper bound, return the upper bound.
    • Otherwise return the value unchanged.
    class Limiter:
    def __init__(self, lower, upper):
    self.lower = lower
    self.upper = upper

    def __call__(self, value):
    if value < self.lower:
    return self.lower
    if value > self.upper:
    return self.upper
    return value
    limiter1 = Limiter(0, 5)
    assert limiter1(10) == 5
    assert limiter1(-3) == 0
    assert limiter1(4) == 4

    limiter2 = Limiter(2, 4)
    assert [limiter2(n) for n in range(7)] == [2, 2, 2, 3, 4, 4, 4]
    Goal:
    None
    Yours:
    NonePass Icon