I have implemented a through
function for myself in a project.
Since I am still quite new in python and want to learn it correctly, I would be happy about any feedback including style, variable naming...
A first question is how to name the function. Because of Mathematica's documentation I called it through
. Is there a more intuitive name?
def through(list_of_functions, value, recurse_level=np.infty):
"""Calls each function in a list
This function is passing the value as argument to each function
in a list.
For a one dimensional list consisting of
only functions it is equivalent to::
[f(value) for f in list]
If an element is not callable and not iterable, it is not called.
An iterable is either directly appended or will be recursed through
depending on recurse_level.
Args:
list_of_functions (list):
value (any type):
recurse_level (int):
Returns:
list:
"""
new_list = []
for possible_function in list_of_functions:
try:
new_list.append(possible_function(value))
except TypeError: # item not callable; could be iterable
# If it is iterable and recurse_level is not negative
# recurse through elements
if recurse_level >= 0:
try:
new_list.append(through(possible_function,
value,
recurse_level))
recurse_level = recurse_level - 1
except TypeError: # not an iterable; append without calling
new_list.append(possible_function)
else:
new_list.append(possible_function)
return new_list
An example:
In: test = [[1, 2], [3, (lambda x : x ** 2)], (lambda x : x ** 3)]
In: through(test, 4, recurse_level=1)
Out: [[1, 2], [3, 16], 64]
In: through(test, 4, recurse_level=0)
Out: [[1, 2], [3, <function __main__.<lambda>>], 64]
2 Answers 2
You have some bugs.
Mutation of
recurse_level
: Since you reassignrecurse_level = recurse_level - 1
, you get this weird behaviour:>>> through([[id], [id], [id], [id]], 'blah', recurse_level=2) [[4361400536], [4361400536], [4361400536], [<built-in function id>]]
Catching
TypeError
: You catchTypeError
here:try: new_list.append(possible_function(value)) except TypeError: # item not callable; could be iterable ...
You are expecting that type
TypeError
might be thrown ifpossible_function
is not callable. But the exception handler could also be triggered by an exception withinpossible_function
when it is being executed. To be more precise, you should check forcallable(possible_function)
instead of catchingTypeError
.Similarly, your other
except TypeError
should be a check for the existence ofpossible_function.__iter__
.
Suggested solution
In the spirit of functional programming, you should write the code without .append()
or other mutations.
Also note that using NumPy just for its infinity is overkill.
def through(functions, value, recurse_level=float('inf')):
if callable(functions):
return functions(value)
elif recurse_level < 0 or not hasattr(functions, '__iter__'):
return functions
else:
return [through(f, value, recurse_level - 1) for f in functions]
-
\$\begingroup\$ Thank you for the great answer! Is it even overkill, If I import it anyway? I like
np.infty
a bit more to read thanfloat('inf')
. \$\endgroup\$mcocdawc– mcocdawc2016年11月21日 11:58:46 +00:00Commented Nov 21, 2016 at 11:58 -
\$\begingroup\$ If you are already using NumPy then it's fine. \$\endgroup\$200_success– 200_success2016年11月21日 15:53:30 +00:00Commented Nov 21, 2016 at 15:53
-
\$\begingroup\$ Perhaps you could add
or isinstance(functions, str)
in your answer/code. I forgot about it in my function definition, which was a bug in my case and I think that most people intuitively would treat strings not as iterables in this context. \$\endgroup\$mcocdawc– mcocdawc2016年11月23日 15:10:35 +00:00Commented Nov 23, 2016 at 15:10
Naming
- In Clojure, there's a similar function named
juxt
. Not saying that's a better name by any means, just mentioning what it happens to be called in one other place. Upon further reflection, what you are doing is really just a recursive form of function application. So a suitable name might reflect that. Perhapsapply_rec
? - Use the shortest name that conveys sufficient meaning. For example,
list_of_functions
could be shortened tofunctions
(or even justfs
).possible_function
could be shortened tof
. (Single-character names are not good names in general, but for generic entities that have a well-known mathematical notation, they are a good fit.)
Use Better Checks for Callable / Iterable
You check if something's callable by trying to call it and catching a TypeError
exception. However, if you call a function, and that function happens to through a TypeError
, then your code will incorrectly assume it's not a function. You can use the callable
function instead, to determine whether or not something is callable.
A similar situation exists in the way you check for iterables. In this case, you can use the iter
function as a fail-fast way to check if something's iterable.
Consider Using a List Comprehension
Reading your code, I thought it was a bit ironic that you used a list comprehension in the docstring to describe the functions behavior, but not in the code itself. Granted, due to the recursive nature, it's not quite as straight-forward. But with just a little refactoring, it should be possible. Here's how I would do it:
def apply_rec(f, x):
"""
If f is a function, applies f to x and returns the result.
Otherwise, if f is iterable, applies every element in f to x,
returning a list of the results.
If f is neither callable nor iterable, returns f.
"""
if callable(f):
return f(x)
else:
try:
fs = iter(f)
except:
# Not iterable => return f
return f
return [apply_rec(f, x) for f in fs]
I've omitted the recursion limit, but that should be trivial to add.
Explore related questions
See similar questions with these tags.