I think nesting function calls and assigning results to variables is not a bad solution, especially if you capture the whole in a single function with descriptive name. The name of the function is self-documenting, allows for reuse of the complex structure, and the local variables created in the function only exist for the lifetime of the function execution, so your namespace remains clean.
Having said that, depending on the type of the values being passed around, you could subclass whatever that type is and add the functions as methods on the class, which would allow you to chain the calls, which I'd consider very Pythonic if the superclass lends itself to it.
An example of that is the way the pandas
library works and has changed recently. In the past, many pandas.DataFrame
methods would take an optional parameter called inplace
to allow a user to change this:
import pandas as pd
df = pd.DataFrame(my_data)
df = df.some_operation(arg1, arg2)
df = df.some_other_operation(arg3, arg4)
...
To this:
import pandas as pd
df = pd.DataFrame(my_data)
df.some_operation(arg1, arg2, inplace=True)
df.some_other_operation(arg3, arg4, inplace=True)
With inplace=True
, the original DataFrame
would be modified (or at least, the end result would suggest that's what happened), and in some cases it might still be returned, in other cases not at all. The idea being that it avoided the creation of additional data; also, many users would use multiple variables (df1
, df2
, etc.) to keep all the intermediate results around, often for no good reason.
However, as you may know that pattern is a bad idea in pandas
for several reasons.
Instead, modern pandas
methods consistently return the DataFrame
itself, and inplace
is being deprecated where it still exists. But because all functions now return the actual resulting DataFrame
, this works:
import pandas as pd
df = pd.DataFrame(my_data)\
.some_operation(arg1, arg2)
.some_other_operation(arg3, arg4)
(Note that this originally worked for many cases already in pandas
, but consistency is not the point here, it's the pattern that matters)
Your situation could be similar, depending on what you're passing exactly. From the example you provided, it's unclear what the types might be. But given that you consistently pass the function result as the first parameter, it seems likely that your implementation is something like:
def fun1(my_type_var, a, b, c):
# perform some operation on my_type_var and return result of same type
return operations_on(my_type_var, a, b, c)
def fun2(my_type_var, a, b, c):
# similar
return other_operations_on(my_type_var, a, b, c)
...
In that case, it would make more sense to:
class MyType:
def method1(self, a, b, c):
# perform some operation on self, then return the result, or self
mt = operations_on(self, a, b, c)
return mt
...
Because this would allow:
x = MyType().method1(arg1, arg2, arg3).method2(...)
And depending on what your type is exactly, you could choose to modify self
and return that, or perform the operation with a new instance as a result like in the example above. What the better idea is depends on what you're doing, how your type works internally, etc.