views:

78

answers:

5

I have a problem that can be simplified as follows: I have a particular set of objects that I want to modify in a particular way. So, it's possible for me to write a function that modifies a single object and then create a decorator that applies that function to all of the objects in the set.

So, let's suppose I have something like this:

def modify_all(f):
    def fun(objs):
        for o in objs:
            f(o)

@modify_all
def modify(obj):
    # Modify obj in some way.

modify(all_my_objs)

However, there may also be times when I just want to operate on one object by itself.

Is there a way to "undecorate" the modify function programmatically to get the original (single object) function back again? (Without just removing the decorator, I mean.) Or is there another approach that would be better to use in such a case?

Just for clarity and completeness, there's quite a bit more going on in the actual modify_all decorator than is illustrated here. Both modify and modify_all have a certain amount of work to carry out, which is why I thought the decorator might be nice. Additionally, I have other variants of modify that can directly benefit from modify_all, which makes it even more useful. But I do sometimes need to do things using the original modify function. I know that I can always pass in a one-element set to "trick" it into working as-is, but I'm wondering if there's a better way or a better design for the situation.

+2  A: 

Apply the decorator manually to the function once and save the result in a new name.

def modify_one(obj):
    ...

modify_some = modify_all(modify_one)
modify_some([a, b, c])
modify_one(d)
Aaron Gallagher
+2  A: 

Decorators are meant for when you apply a higher-order function (HOF) in the specific form

def f ...

f = HOF(f)

In this case, the syntax (with identical semantics)

@HOF
def f ...

is more concise, and immediately warns the reader that f will never be used "bare".

For your use case, where you need both "bare f" and "decorated f", they'll have to have two distinct names, so the decorator syntax is not immediately applicable -- but neither is it at all necessary! An excellent alternative might be to use as the decorated name an attribute of the decorated function, for example:

def modify(obj): ...

modify.all = modify_all(modify)

Now, you can call modify(justone) and modify.all(allofthem) and code happily ever after.

Alex Martelli
Function attributes like this are seldom used and most people probably are not familiar with them. I would personally avoid making them part of my API when I can.
Mike Graham
@Mike, they're widely used in the popular `numpy` package, for example, so (unless you think scientists and engineers that perform lots of scientific computation aren't "people";-), your assertion is not generally well-founded. Common Javascript idioms also use attributes of functions, so I'd further qualify the assertion by excluding many people who develop web apps. Overall, not enough is left to justify avoidance of a useful idiom that aligns **so** well with Python's general preference for well-delineated namespaces.
Alex Martelli
@Alex, As an engineer myself and frequent user of numpy, I can't place where in numpy you're talking about all these function function attributes are. Taking a look at the 241 functions in the top-level numpy module, not one of them has any attributes that are functions.
Mike Graham
@Mike, consider add, subtract, multiply, ... -- all the 2-arg `ufunc`s at http://docs.scipy.org/doc/numpy-1.4.x/reference/ufuncs.html. Each has callable attributes `.reduce`, `.accumulate`, `.reduceat`, `.outer` (and non-callable ones `.nin`, `.nout`, ...); they **are** "in the top-level `numpy` module". Their `type` isn't `function` (they're implemented in `C`, and more directly by avoiding inheritance), but they're "functions with attributes" to all intents and purposes. Anybody familiar with `multiply.accumulate` can't sensibly be suprised by `modify.all`!
Alex Martelli
+1  A: 

Alex had already posted something similar but it was my first thought too so I'll go ahead and post it because it does sidestep Mike Grahams objection to it. (even if I don't really agree with his objection: the fact that people don't know about something is not a reason to not use it. This is how people continue to not know about it)

def modify_all(f):
    def fun(objs):
        for o in objs:
            f(o)
    fun.unmodified = f
    return fun

def undecorate(f):
    return f.unmodified

@modify_all
def modify(obj):
    # Modify obj in some way.

modify(all_my_objs)
undecorate(modify)(one_obj)

You could just call undecorate to access the decorated function.

another alternative would be to handle it like this:

def modify_one(modify, one):
    return modify.unmodified(one)
aaronasterling
While most of the given answers are along similar lines as this one, I think this most closely resembles the situation I have and the solution I need. Also, I'm new to stackoverflow, so I'm not clear on how the credit assignment system works. (I'm sure there is a helpful page on the site that describes it in detail, but I haven't taken the time to find/read it yet. So please don't reply with "RTFM". :-) I am aware.) In any event, my thanks also to Alex for his answer. You guys are very helpful.
agarrett
A: 

In your particular case I'd consider using argument unpacking. This can be done with only a slight modification of your existing code:

def broadcast(f):
    def fun(*objs):
        for o in objs:
            f(o)
    return fun

@broadcast
def modify(obj):
    # Modify obj in some way.

modify(*all_my_objs)
modify(my_obj)

However, the operation of argument unpacking does take a bit of time, so this will probably be slightly slower than the original version that just passes a plain old list. If you're concerned about this slowing down your application too much, try it out and see what kind of a difference it makes.

Depending on what kind of objects you work with, another option is to make the decorator "intelligently" decide whether or not it should use a loop:

def opt_broadcast(f):
    def fun(obj):
        if isinstance(obj, list):
            for o in objs:
                f(o)
        else:
            f(o)
    return fun

@opt_broadcast
def modify(obj):
    # Modify obj in some way.

modify(all_my_objs)
modify(my_obj)

There's some potential for confusion when you do this, though, and obviously it doesn't work if you would ever be calling modify with a list when you want it to operate on the list as a single object. (In other words, if my_obj could ever be a list, don't do this)

David Zaslavsky
Typechecking here is *not* letting the decorator intelligently decide. The user *knows* whether what they have is a single thing or multiple things; don't let them discard that information and then *guess* at what it might've been when you could just have them pass that information along somehow. This might be done by separating the API into two functions, or using `*args`, or some other method.
Aaron Gallagher
Well, I meant "intelligently" - I know there isn't really any actual intelligence involved. I wasn't suggesting that this is the best option, but it is an option. (I'm surprised if you downvoted me based on that alone)
David Zaslavsky
I'm quite tired of "intelligent" APIs like this that break horribly when you give them iterators.
Aaron Gallagher
A: 

You can use a decorator to apply Alex's idea

>>> def forall(f):
...     def fun(objs):
...         for o in objs:
...             f(o)
...     f.forall=fun
...     return f
... 
>>> @forall
... def modify(obj):
...     print "modified ", obj
... 
>>> modify("foobar")
modified  foobar
>>> modify.forall("foobar")
modified  f
modified  o
modified  o
modified  b
modified  a
modified  r

Perhaps foreach is a better name

gnibbler