2

I have a class that I use for data analysis. Generally I need to create a couple instances of the class and then run a series of methods on each. The methods I need to run will vary from instance to instance.

Today my code (using the class) usually looks something like this:

object_alpha = MyClass()
object_alpha.method1()
object_alpha.method2()
object_alpha.method3(arg1)
object_alpha.method4()

object_bravo = MyClass()
object_bravo.method1()
object_bravo.method3(arg1)
object_bravo.method4()
object_bravo.method5()

I know the above example is not an example of method chaining. Method chaining is not currently possible because the methods do not return an object of the class.

This format gets a bit repetitive a tedious, especially with long descriptive variable names. My primary complaint is that I do not find it very readable.

I thought about changing my class to return a new object from each method call, so that I could do method chaining. But the side-effect of the methods do not change the class--they are making changes to a database via an API, so it feels strange to return a new object.

My thought is to create a runner class that would take a list of method names as strings. So that I could do something like this.

object_alpha = MyClass().runner([
    'method1',
    'method2',
    'method3(arg1)',
    'method4'
])

object_bravo = MyClass().runner([
    'method1',
    'method3(arg1)',
    'method4',
    'method5'
])

Is this a bad idea; is there a better approach?

Chris
  • 1,313
  • 2
  • 13
  • 26
  • 1
    1- Indicate clearly in your question that you understand that your code example does not represent method chaining. 2- `'method3(arg1)'` is not like the others otherwise you could use `for methodname in names: getattr(obj, methodname)()` 3- Yes, it is a bad idea. To avoid specifying an object name you are introducing one more level of indirection. What are you trying to buy? Provide the actual code otherwise the question is too broad (to specify a better alternative). 4- Look at [how `sqlalchemy` uses method chaining](http://docs.sqlalchemy.org/en/latest/glossary.html#term-method-chaining) – jfs Feb 19 '16 at 17:23
  • 1
    I don't see what you gain from your runner example. It's still just as tedious to write, but you increase the complexity of the program. See [Inner-platform effect](https://en.wikipedia.org/wiki/Inner-platform_effect). From this context, no, it does not look like a good idea design-wise. – Vincent Savard Feb 19 '16 at 17:25
  • Thanks for the comments, Ill try to clean up the question in line with the suggestions. As to what I'm trying to gain, it's less about time to write and more about readability. – Chris Feb 19 '16 at 17:32
  • There is a construct in some languages (`With` in Basic?) that allows to omit the object name (it introduces object's attributes into the local scope). Using it is usually discouraged. I remember?: the suggestion to include a similar functionality into Python was rejected. But you probably could introduce it as `with Scoped(obj): attr1(); attr2()` using `macropy` package or similar. – jfs Feb 19 '16 at 17:34
  • Why do different instances need different things to be run on them? Perhaps you should really have MyClass be a family of types, with a `run` method for subclasses to implement which runs the appropriate sequence of operations. – Daenyth Feb 19 '16 at 19:50
  • related: [The VB.NET 'With' Statement - embrace or avoid?](http://stackoverflow.com/q/283749/4279) – jfs Feb 19 '16 at 19:51
  • related: [Method cascading](https://en.wikipedia.org/wiki/Method_cascading) – jfs Feb 19 '16 at 19:57
  • related: from Python FAQ: [Why doesn’t Python have a “with” statement for attribute assignments?](https://docs.python.org/3/faq/design.html#why-doesn-t-python-have-a-with-statement-for-attribute-assignments). See also, [[Python-ideas\] Dart-like method cascading operator in Python (2013)](https://mail.python.org/pipermail//python-ideas/2013-November/024124.html) – jfs Feb 19 '16 at 20:43

2 Answers2

1

Your idea is good, but it can use one major improvement: instead of using a list of strings that you have to eval, use a list of tuples or something like that to contain the methods themselves and their arguments:

object_bravo = MyClass()
bravo_runner = [
    (object_bravo.method1, (arg1, arg2), {k1: v1, k2, v2}),
    (object_bravo.method2, (arg3), {}),
    (object_bravo.method3, (), {k3: v3}),
    (MyClass.method4, (object_bravo, arg4), {k4: v4})
]

The way to run that would be much easier than parsing strings:

for spec in bravo_runner:
    spec[0](*spec[1], **spec[2])

If you used namedtuple for the runner elements, it would look even better:

from collections import namedtuple
RunnerSpec = namedtuple('RunnerSpec', ['method', 'args', 'kwargs'])
object_bravo = MyClass()
bravo_runner = [
    RunnerSpec(object_bravo.method1, (arg1, arg2), {k1: v1, k2, v2}),
    RunnerSpec(object_bravo.method2, (arg3), {}),
    RunnerSpec(object_bravo.method3, (), {k3: v3}),
    RunnerSpec(MyClass.method4, (object_bravo, arg4), {k4: v4})
]

and the run method:

for spec in bravo_runner:
    spec.method(*spec.args, **spec.kwargs)

Conclusion

At this point, you may actually save some typing by just writing a method within the class for each sequence/scenario that uses self instead of the descriptive object name. That would save you the most in the end because you would have pre-constructed, named sequences of calls. No need to store them as lists. bravo_object.run_scenario3() is better than bravo_object.runner([big_ass_hard_to_read_list_that_is_basically_a_method_anyway]).run().

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
  • you could use `for method, args, kwargs in bravo_runner` instead of `for spec in bravo_runner`. Though I don't see much improvement in the first code example and the second is worse than the code in the question. – jfs Feb 19 '16 at 19:47
  • Yeah. Hence the conclusion. I answered the literal question despite my better judgment. Oh well. – Mad Physicist Feb 19 '16 at 20:12
1

Basically, you're trying to invent a domain-specific language here. Since its sole purpose would be to execute something much like Python, only without having to name the context object, I don't think it's worth the hassle. The pattern of returning an object from a method to allow method chaining, even though the object is not the logical result of the operation, is actually not unheard of, so I'd go for that.

Thomas Lotze
  • 5,153
  • 1
  • 16
  • 16