4

Say I have a decorator like this:

def repeat(repeat_count):
    def decorator(func):
        def wrapped(self):
            for X in range(repeat_count):
                 func() # Do Function
        return wrapped
    return decorator

and a class like this

class SomeClass(object):
    def __init__(self, do_count):
        self.some_method = repeat(do_count)(self.some_method)

    def some_method(self): 
        print("I'm Doing Something")

Because a decorator just returns a method, it's clear that this works. However it unbinds the some_method function from the class instance, so I can no longer do something like:

>>> sc = SomeClass(10)
>>> sc.some_method()
# TypeError: wrapped() missing 1 required positional argument: 'self'

I get an exception because self is no longer automatically passed. To make this work I can simply do this:

sc.some_method(sc)

but I'd much rather not. Is there any way to rebind the method to the instance, preferably without any extra imports (as TypeMethod) would be able to accomplish.

wim
  • 338,267
  • 99
  • 616
  • 750
Mohsin Kale
  • 197
  • 2
  • 11
  • All I did was end up calling the wrapped method when returning it. This was just an example I wrote in less then 3 minutes, cut me some slack. As for the repost claim, I've specifically stated I'd prefer to avoid any extra imports as the main solution in the post you've linked to gives. – Mohsin Kale Jul 27 '18 at 16:59
  • There are answers in that question that don't require any imports. – Aran-Fey Jul 27 '18 at 17:00
  • With "dynamically decorating" you mean explicitly decorating in the init method (or at least not using the @ syntax)? – syntonym Jul 27 '18 at 17:03
  • @Aran-Fey That answer is *10 years old* and has a lot of Python 2 baggage that is irrelevant here. – wim Jul 27 '18 at 17:03
  • None of the alternatives are very visually pleasing or pythonic IMHO but point taken. – Mohsin Kale Jul 27 '18 at 17:04
  • @syntonym, the latter. I was hoping python would have some sort of built in wrapper for decorating class methods outside of the @ notation. – Mohsin Kale Jul 27 '18 at 17:05
  • @MohsinKale Your question is fine! But we generally discourage edits to fix errors in the code after receiving answers about how to fix those errors in the code. This makes the question and answer(s) incongruent, causing the content to be less helpful for future readers. – wim Jul 27 '18 at 17:12
  • 2
    As @wim says - please don't make edits that invalidate the answers you've received. Thank you. – Jon Clements Jul 27 '18 at 17:14
  • 1
    @JonClements It's unfortunate that the answer now mentions something no longer in the question, but the answer does not answer the question Moshin wanted to ask. He did not change his question by editing but edited to make it clearer. Having to open a new question because of typo seems unhelpful to me. – syntonym Jul 27 '18 at 20:50
  • @syntonym OK, you've convinced me. The part "I get an exception because self is no longer automatically passed" would not be possible without a typo (because they would have seen an error at decoration time, when doing `SomeClass(10)`, not at call time of the method). – wim Jul 27 '18 at 21:34
  • @syntonym Updated question (and answer). Thanks for drawing it to my attention - sorry, I messed up on this one. – wim Jul 27 '18 at 22:17

2 Answers2

2

I get an exception because self is no longer automatically passed.

Actually, it is still automatically passed. You got this exception because the way you defined the decorator required it to be passed twice.

From within the runtime of wrapped, func is already bound (i.e. it has already self passed). By defining wrapped to accept one positional argument, you've required to pass in self again, which is why sc.some_method(sc) worked correctly. The self is passed twice, as required - once implicitly, and once explicitly.

The smallest fix to your code is to remove self from the signature of wrapped, because that is already passed implicitly as per the descriptor protocol in the binding of self.some_method.

def repeat(repeat_count):
    def decorator(func):
        def wrapped():
            for X in range(repeat_count):
                 func() # Do Function
        return wrapped
    return decorator

However, this is not really the best solution. You'll want to accept *args and **kwargs so your decorator can be applied regardless of the signature of the decorated function:

def repeat(repeat_count):  # <-- the "decorator maker"
    def decorator(func):  # <-- the decorator
        def wrapped(*args, **kwargs):  # <-- this will replace "func"
            for X in range(repeat_count):
                 func(*args, **kwargs)  # <-- note: pass along the arguments!
        return wrapped  # <-- note: no call here!
    return decorator
wim
  • 338,267
  • 99
  • 616
  • 750
  • 1
    Sorry, that was a typo. This was just an example. In what I'm actually doing, I'm using args and kwargs so no worries there. – Mohsin Kale Jul 27 '18 at 16:56
  • If you were actually using *args and **kwargs in the correct places, then you wouldn't have had the bug in the first place, so.... – wim Jul 27 '18 at 21:38
  • @wim What if I wanted to access, from withing the `wrapped` scope, some property of the instance? I tried to set in the `__init__` of `SomeClass` the following : `self.some_method = repeat(do_count)(SomeClass.some_method)` And also adding `self` as a positional argument to the `wrapped` function. Still, invoking `sc = SomeClass(5) sc.some_method()` resulted in the same error as @MohsinKale 's – Eliran Abdoo Jul 28 '18 at 10:22
  • 1
    @EliranAbdoo Inside `wrapped`, the instance is available as `func.__self__`. – wim Jul 28 '18 at 14:49
1

For a rather simple case, where you don't need to access self from the decorator itself, you can simply use the following (which is pretty much what you already did, minus the wrapper function invocation, and the passing of self). The method passed to the decorator is already bounded to self when you assign

self.some_method = repeat(do_count)(self.some_method)

The full code:

def repeat(repeat_count):
    def decorator(func):
        def wrapped():
            for X in range(repeat_count):
                 func()
        return wrapped
    return decorator


class SomeClass(object):
    def __init__(self, do_count):
        self.a = 3
        self.some_method = repeat(do_count)(self.some_method)

    def some_method(self): print("Accesing my a from inside: %d" % self.a)


sc = SomeClass(5)
sc.some_method()

output:

Accesing my a from inside: 3
Accesing my a from inside: 3
Accesing my a from inside: 3
Accesing my a from inside: 3
Accesing my a from inside: 3
Eliran Abdoo
  • 611
  • 6
  • 17