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 sameSomeClass()syntax, so classes are also callable.>
class Cat:def __init__(self, name):self.name = namekeanu = Cat("Keanu")keanu.nameResult:
'Keanu'
We can also make instances themselves callable. When we define the
.__call__dunder method,some_instance(some_arg)callssome_instance.__call__(some_arg).>
class Adder:def __init__(self, first_value):self.first_value = first_valuedef __call__(self, second_value):return second_value + self.first_valueadder = Adder(3)adder(2)Result:
5
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 = 0def __call__(self, value):self.total += valuecounter = Counter()counter(3)counter(2)counter(4)counter.totalResult:
9
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 = funcself.call_count = 0def __call__(self, *args, **kwargs):self.call_count += 1return self.func(*args, **kwargs)def add(x, y):return x + yadd = CountCalls(add)results = (add(3, 4), add(2, 6), add(8, 1))- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
resultsResult:
(7, 8, 9)
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
add.call_countResult:
3
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@CountCallschange.>
class CountCalls:def __init__(self, inner_func):self.inner_func = inner_funcself.call_count = 0def __call__(self, *args, **kwargs):self.call_count += 1return self.inner_func(*args, **kwargs)CountCallsdef add(x, y):return x + yresults = [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)
Take a moment to appreciate how neatly these seemingly-unrelated Python features fit together. Instantiating a class works like calling a function:
SomeClass(). The@decoratorsyntax 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 ofCountCalls. 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 = 0def __call__(self, f):def decorated(*args, **kwargs):self.call_count += 1return f(*args, **kwargs)return decoratedcounter = CountCalls()counterdef add(x, y):return x + ycounterdef subtract(x, y):return x - yresults = (add(1, 2), add(5, 6), subtract(7, 3))- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
resultsResult:
(3, 11, 4)
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
counter.call_countResult:
3
Sometimes we want to ask Python "is this value callable or not?" We can use the built-in
callablefunction for that. It returns a boolean.>
add = lambda x, y: x + yclass Cat:passclass Doubler:def __call__(self, n):return n * 2- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
callable(add)Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
callable(add(1, 2))Result:
False
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
callable(Cat)Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
callable(Cat())Result:
False
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
callable(Doubler)Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
callable(Doubler())Result:
True
- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
callable(Doubler()(2))Result:
False
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 = lowerself.upper = upperdef __call__(self, value):if value < self.lower:return self.lowerif value > self.upper:return self.upperreturn valuelimiter1 = Limiter(0, 5)assert limiter1(10) == 5assert limiter1(-3) == 0assert limiter1(4) == 4limiter2 = Limiter(2, 4)assert [limiter2(n) for n in range(7)] == [2, 2, 2, 3, 4, 4, 4]- Goal:
None
- Yours:
None