0

I am fairly new to Python and have been learning about decorators. After messing around with Flask, I am trying to write some code that simulates their route handler/decorators, just to understand how decorators (with arguments) work.

In the code below, the route decorator seems to call itself once the script runs. My question is, how is it possible that app.route() gets called when i run this script, and what is really happening here? Notice i don't call my index() function anywhere directly.

# test.py

class Flask(object):

    def __init__(self, name):
        self.scriptname = name

    def route(self, *rargs, **kargs):
        args = list(rargs)
        if kargs:
            print(kargs['methods'])
        def decorator(f):
            f(args[0])
        return decorator

app = Flask(__name__)

@app.route("/", methods = ["GET","PUT"])
def index(rt):
    print('route: ' + rt)

the above prints this in my terminal:

$ python test.py
['GET', 'PUT']
route: /

Any insight would be appreciated.

Matt Fiocca
  • 1,533
  • 13
  • 21

2 Answers2

2

@app.route("/", methods = ["GET","PUT"]) is an executable statement: it calls the route() method of the app object. Since it's at module level, it will be executed when the script is imported.

Now, the result of calling app.route(...) is a function, and because you've used the @ to mark it as a decorator, that function will wrap index. Note that the syntax is just a shortcut for this:

index = app.route(...)(index)

in other words, Python will call the function returned by app.route() with index as a parameter, and store the result as the new index function.

However, you're missing a level here. A normal decorator, without params, is written like this:

@foo
def bar()
   pass

and when the module is imported, foo() is run and returns a function that wraps bar. But you're calling your route() function within the decorator call! So actually your function needs to return a decorator function that itself returns a function that wraps the original function... headscratching, to be sure.

Your route method should look more like this:

def route(self, *rargs, **kargs):
    args = list(rargs)
    if kargs:
        print(kargs['methods'])
    def decorator(f):
        def wrapped(index_args):
            f(args[0])
        return wrapped
    return decorator
Daniel Roseman
  • 588,541
  • 66
  • 880
  • 895
  • Notice that `index` is not being called. The decorating function is executed at definition time. – That1Guy Sep 30 '13 at 20:34
  • @That1Guy yes, just updating to explain that. – Daniel Roseman Sep 30 '13 at 20:36
  • ah, ok, this is starting to make sense. When i replace my ```def route(self, *rargs, **kargs):``` with the one you've attached, i get ```TypeError: decorator() takes 0 positional arguments but 1 was given``` – Matt Fiocca Sep 30 '13 at 20:42
  • Sorry, `f` should have been the argument to `decorator`, not `wrapped`. The latter of course takes the arguments you're passing to `index`. – Daniel Roseman Sep 30 '13 at 20:44
  • @daniel, but otherwise, i think you've answered what i was after. The difference between using a class method as a decorator vs stand alone function. thank you – Matt Fiocca Sep 30 '13 at 20:46
  • Hmm, well it isn't to do with using a method vs a standalone function: this is just as much true as if you'd written `route()` as a standalone. The point is about using `@route` vs `@route(foo, bar)`. – Daniel Roseman Sep 30 '13 at 20:47
  • oh, right, the inclusion of the parenthesis, as any function gets fired? – Matt Fiocca Sep 30 '13 at 20:52
-1

Basically... app.route(index, "/", ["GET", "PUT"]) is a function. And this is the function which is going to be called instead of index.

In your code, when you call index(), it calls app.route(index, "/", ["GET", "PUT"]). This starts by printing kargs['methods'], then creates the decorator function:

def decorator(f):
    f(args[0])

This decorator will call the decorated function (f) with one argument, args[0], which here is "/". This prints route: /.

The best explanation of decorators I've found is here: How to make a chain of function decorators?

If you dont want the self-firing, you can define your decorator this way:

def route(*rargs, **kargs):
    args = list(rargs)
    if kargs:
        print(kargs['methods'])
    def decorator(f):
        f(args[0])
    return decorator

@app.route("/", methods = ["GET","PUT"])
def index(rt):
    print('route: ' + rt)

However, the rt argument of index will never be used, because route always calls index with args[0] which is always \...

Community
  • 1
  • 1
GL770
  • 2,910
  • 1
  • 14
  • 9
  • in that example, they call ```print hello()``` at the end of the script. To my understanding, if they left that last line out, nothing should actually print though, right? So, are you saying that simply including the ```@app.route()``` above ```index()``` would auto fire? and if it were just say a regular decorating function like ```@route```, it would wait to be fired when ```index()``` is called? – Matt Fiocca Sep 30 '13 at 20:25
  • Indeed, if the `print hello()` was not here then nothing would print. – GL770 Sep 30 '13 at 20:32
  • As @Daniel_Roseman says, @app.route is an executable statement, because `app` is an instance of the class `Flask`. If you want it not to be executed, you need to define route as a standalone function (like `makebold` in the example), and not inside a class. – GL770 Sep 30 '13 at 20:34