0

I have a class in python with the following structure:

class MyClass:
    def __init__(self, iterable):
        self.iterable = iterable

    def example_0(self):
        for i in self.iterable:
            print(i)

    def example_1(self):
        for i in self.iterable:
            print(i + 1)

    def example_2(self):
        for i in self.iterable:
            print(i + 2)

That is, I have several methods that run different operations on an iterable that is an attribute of the class. I need to run for i in self.iterable for each method in the class, and I would otherwise like to use a decorator to all these methods, something like:

class MyClass:
    def __init__(self, iterable):
        self.iterable = iterable

    @iterate
    def example_0(self, i):
        print(i)

    @iterate
    def example_1(self, i):
        print(i + 1)

    @iterate
    def example_2(self, i):
        print(i + 2)

Can you help me write this decorator such that the behavior of my class is the same as the new class?


I tried this:

class MyClass:
    def __init__(self, iterable):
        self.iterable = iterable

    def iterate(self, f):
        def func(*args, **kwargs):
            for i in self.iterable:
                f(self, i, *args, **kwargs)
        return func

    @iterate
    def example_0(self, i):
        print(i)

    @iterate
    def example_1(self, i):
        print(i + 1)

    @iterate
    def example_2(self, i):
        print(i + 2)

and it returns: TypeError: iterate() missing 1 required positional argument: 'f'.

My main issue is I'm not sure how to put the decorator within my class, as it's iterating over an attribute of the class.

David Masip
  • 2,146
  • 1
  • 26
  • 46
  • 4
    Do you know how to write decorators in general? If so, did you try to write this decorator? What went wrong with your attempt? If you're stuck, specifically why are you stuck (as you see it)? It is not possible to ["help you"](https://meta.stackoverflow.com/questions/284236) in general; we need a clear question. – Karl Knechtel Jan 21 '22 at 13:38
  • Such a decorator would look extremely similar to the methods you have right now. What exactly are you struggling with about it? Do you know how to write a such a decorator that *doesn't* iterate over something? – MisterMiyagi Jan 21 '22 at 13:39
  • Does the big answer on https://stackoverflow.com/questions/739654/how-to-make-function-decorators-and-chain-them-together help? That's the general decorator FAQ/tutorial around here, although your question isn't really a duplicate. – Karl Knechtel Jan 21 '22 at 13:39
  • @MisterMiyagi The decorator helps reducing duplicate code of the for loop. – Oliver Mohr Bonometti Jan 21 '22 at 13:40
  • 1
    Could I suggest a different approach entirely? Write *one* method that accepts a function as an argument, iterates over the iterable and applies the function to each; then you can pass in e.g. `print`, `lambda i: print(i + 1)` etc. – Karl Knechtel Jan 21 '22 at 13:41
  • I've added more context of what I've tried, I think the explanation is pretty clear. The other question doesn't really help me, as the decorators don't use an attribute of the class. – David Masip Jan 21 '22 at 13:46
  • 1
    I think the decorator should not take `self` in its init, but in func instead. The decorator itself is not a class method. – joanis Jan 21 '22 at 13:47
  • 1
    "My main issue is I'm not sure how to put the decorator within my class, as it's iterating over an attribute of the class." It doesn't need to be, because `self` will be passed in to the decorated version the same way it would be passed into the original. Putting it inside causes a problem because when you try to apply the decorator, Python wants to call `iterate` *as a method*, but there is no instance to use *ahead of time*. You can fix this by decorating the decorator (with `@staticmethod`) and removing the `self` parameter; but really it's easier to just leave it outside. – Karl Knechtel Jan 21 '22 at 13:50
  • Remember, `@iterate` and `def x(...):` is sugar for `def x(...):` and `x = iterate(x)`. `x` isn't the value you want for `self`; it's the value you want for `f`. The value you want for `self` comes later, *when the decorated method is called*. – Karl Knechtel Jan 21 '22 at 13:55

1 Answers1

2

I don't think this has much advantage, but here's an option. This also allows for additional parameters.

from functools import wraps

def iterated(f):
    @wraps(f)
    def _f(self, *args, **kwargs):
        for i in self.iterable:
            f(self, i, *args, **kwargs)
    return _f

Example:

In [11]: class MyClass2:
    ...:     def __init__(self, iterable):
    ...:         self.iterable = iterable
    ...: 
    ...:     @iterated
    ...:     def example_0(self, i, k):
    ...:         print(i + k)

In [12]: MyClass2([1,2,3]).example_0(2)
3
4
5

This is to be defined outside the class. In your failing implementation, you forgot the self parameter in the returned closure.

phipsgabler
  • 20,535
  • 4
  • 40
  • 60