Don't Make It Callable
Wed 13 February 2019 by Moshe ZadkaThere is a lot of code that overloads the
__call__
method.
This is the method that
"calling"
an object activates:
something(x, y, z)
will call
something.__call__(x, y, z)
if
something
is a member of a Python-defined class.
At first, like every operator overload, this seems like a nifty idea. And then, like most operator overload cases, we need to ask: why? Why is this better than a named method?
The first use-case is easily done better with a named method,
and more readably:
accepting callbacks.
Let's say that the function
interesting_files
will call the passed-in callback with names of interesting files.
We can,
of course,
use
__call__
:
class PrefixMatcher: def __init__(self, prefix): self.prefix = prefix self.matches = [] def __call__(self, name): if name.startswith(self.prefix): self.matches.append(name) def random_match(self): return random.choice(self.matches) matcher = PrefixMatcher("prefix") interesting_files(matcher) print(matcher.random_match())
But it is more readable, and obvious, if we...don't:
class PrefixMatcher: def __init__(self, prefix): self.prefix = prefix self.matches = [] def get_name(self, name): if name.startswith(self.prefix): self.matches.append(name) def random_match(self): return random.choice(self.matches) matcher = PrefixMatcher("prefix") interesting_files(matcher.get_name) print(matcher.random_match())
We can pass the
matcher.get_name
method,
which is already callable
directly to
interesting_files
:
there is no need to make PrefixMatcher
callable
by overloading
__call__
.
If something really is nothing more than a function call with some extra arguments, then either a closure or a partial would be appropriate.
In the example above,
the
random_match
method was added to make sure that the class
PrefixMatcher
is justified.
If this was not there,
either of these implementations would be more appropriate:
def prefix_matcher(prefix): matches = [] def callback(name): if name.startswith(prefix): matches.append(name) return callback, matches matcher, matches = prefix_matcher("prefix") interesting_files(matcher) print(random.choice(matches))
This uses the function closure to capture some variables and return a function.
def prefix_matcher(prefix, matches, name): if name.startswith(prefix): matches.append(name) matches = [] matcher = functools.partial(prefix_matcher, "prefix", matches) interesting_files(matcher) print(random.choice(matches))
This uses the funcotools.partial
functions
to construct a function that has some of the arguments
"prepared".
There is one important use case for __call__
,
but it is specialized:
it is a powerful tool when constructing a
Python-based DSL.
Indeed,
this is exactly the time when we want to trade away
"doing exactly when the operator always does"
in favor of
"succint syntax dedicated to the task at hand."
A good example of such a DSL is stan,
where the __call__
function is used to
construct XML tags with attributes:
div(style="color: blue")
.
In almost every other case, avoid the temptation to make your objects callable. They are not functions, and should not be pretending.