Sorry for the delay in responding to this thread!

On Wed, Jul 26, 2017 at 09:22:59PM -0500, boB Stepp wrote:

> I am having a recurring problem with Python:  I can work out the
> mechanics of how various Python constructs work, such as decorators,
> but then I find myself scratching my head as to why would I want to
> use them.  The idea of replacing a function with its decorated version
> sounds cool, but what types of problems would I want to use this
> approach on?

Any time you have a series of functions (or classes) which include the 
same subsection of code, repeated (often word-for-word!), a decorator is 
a possible alternative.

Here's a simple example. You have a bunch of functions which all follow 
the same pattern when called:

- print the function name
- print their arguments
- do some work
- print their return result
- return it

Only the "work" differs. So we could write:


def func1(a, b, c):
    print("func1")
    print(a, b, c)
    result = a + b + c
    print("result", result)
    return result

def func2(a, b, c):
    print("func1")  # Oops, copy-and-paste error!
    print(a, b, c)
    result = a - b - c
    print("result", result)
    return result


and so on, but if you have a dozen such functions, that's pretty 
tedious, and the chances of copy-and-paste errors approaches 
certainty. A decorator comes to the rescue!


def decorate(func):
    @functools.wraps(func)
    def inner(a, b, c):
        print(func.__name__)
        print(a, b, c)
        result = func(a, b, c)
        print(result)
        return result
    return inner

@decorate
def func1(a, b, c):
    return a+b+c
       
@decorate
def func2(a, b, c):
    return a-b-c
        

This ensures that all such functions are guaranteed to have the correct 
wrapper code. If you decide to add printing of the date, or change 
printing to logging to a file, you only need to change it in one place, 
not a dozen.


Another use for decorators is to transform the functions in some way. 
That's especially useful for methods, where we have three different 
kinds of methods:

- regular instance methods, which need no transformation because
  they are the default;

- class methods;

- static methods.

Plus you can create your own, if needed. (A fairly advanced topic, but 
less mind-blowing than metaclasses.)

They are all written the same way, with the difference being that class 
and static methods are decorated with the built-in

- classmethod
- staticmethod

decorators, which handle transforming the function objects into 
classmethod or staticmethod descriptors. Don't worry if you don't know 
the technical details of what descriptors are or how they work. The 
point is you don't need to! You just need to call the decorator, and it 
does all the work.


class X(object):
    @classmethod
    def thingy(cls, arg):
        ...


Another example is to register a function with something else. If you 
have code where you create functions, then register them, you can 
simplify the process or at least move the registration to the top of the 
function where it is more obvious with a decorator:


def register(func):
    MyRegistry.register(func)
    return func

@register
def spam():
    ...


If you need to change the registration details, you change it in one 
place. This is an example that shows that decorators don't need to 
transform their input. In this case, we return the function unchanged.


A more complex example of this is singledispatch:

https://docs.python.org/3/library/functools.html#functools.singledispatch


Memoization (adding a cache) to a function is another good example. 
Instead of re-creating the cache logic in every single function, you 
write it once, as a decorator, and then just call the decorator.

https://docs.python.org/3/library/functools.html#functools.lru_cache



> One thing that bothers me, is that once I decorate the function, I no
> longer have access to the original, un-decorated function.

In the most general case, that is true: the decorator can do anything, 
including deleting the original and replacing it with a function that 
always returns "¿Qué?" no matter the arguments.

Most of the time, this is not a problem. The function is *incomplete* 
until it has been decorated, so you often don't care about the original. 
Do you find yourself worrying that you don't have access to the middle 
one third of your existing functions, without the start and the end? 
Probably not. All that's happened here is that you've systematically 
moved the common bits of your functions into a decorator.

But for those cases where you might care, and since Python 3.2, if you 
use the functools.wraps() helper function (itself a decorator!) to 
create your decorators, it will automatically create a __wrapped__ 
attribute that holds the original, unwrapped function.


> But on the
> other hand, if I had a decorator which I wanted to apply to multiple
> functions, then I would be DRY-er by taking this approach -- I would
> need only one decorator function and could then use it decorate as
> many other functions as it made sense to do so.

Precisely.



-- 
Steve
_______________________________________________
Tutor maillist  -  Tutor@python.org
To unsubscribe or change subscription options:
https://mail.python.org/mailman/listinfo/tutor

Reply via email to