Decorators are just functions that take a function as an argument, and return a new function.
A lot of their use comes from modifying arguments for the base function, or modifying the function's output. For example, let's say you have a function that returns a dict. You could write another function that takes the base function's output (a dict) as input, and returns a json-encoded version of the same thing. It might look like this:
>>> def process(spam):
... return { "wrapped": spam }
...
>>> def as_json(func):
... import json
... def wrapper(*args):
... output = func(*args)
... return json.dumps(output)
... return wrapper
...
>>> processor = as_json(process)
>>> processor("eggs")
'{"wrapped": "eggs"}'
Instead of
processor = as_json(process)
, you could alternatively define the function with as_json as a decorator:
>>> @as_json
... def process2(spam):
... return { "still wrapped": spam }
...
>>> process2("eggs")
'{"still wrapped": "eggs"}'
The decorator does the same thing, no special magic added. It exists to make it easier to compose functions and help make your code clearer. As mentioned, another big usage is in memoization, where you keep track of input->output in a dict, and only actually call the function if you haven't seen the args before... which makes expensive operations much faster, since you don't even bother running the function after the first time.