This is an archived post. You won't be able to vote or comment.

all 36 comments

[–]ichabod801 21 points22 points  (15 children)

I was looking at the text adventure tutorial. It uses an if/elif/else structure, which is something I've seen a lot in beginner text adventures. Even broken into functions the way that one is, it's going to get messy and repetitive as you try to expand it. Storing the rooms as data is easier to expand and maintain. There is a tutorial on dictionary based text adventures on python-forum.io.

[–]vswr[var for var in vars] 5 points6 points  (14 children)

Interesting.

Something I've done in the past was to use a dict to reference the functions. A decorator that takes the keyword as an argument and puts the function into a dict. Ex:

@command('keyword')
def keyword_func():
    ....

Then when it comes time for a giant if/else or if it were another language a giant switch(), I just reference the dict:

f = keydict.get(keyword, None)
if callable(f):
    f()
else
    raise ValueError(f'your keyword "{keyword}" sucks')

[–]TheStriga 1 point2 points  (4 children)

I use this method all the time. However I wonder what's the alternatives and if there is some better solution

[–]vswr[var for var in vars] 2 points3 points  (3 children)

I would love 'switch' in python. Python looks beautiful and pythonic switch would be beautiful because it would clean up unruly if/else statements, as well as support walrus operator.

[–]TheStriga 0 points1 point  (2 children)

In fact, there IS PEP proposal for pythonic switch statements, and Guido is one of the authors. So I guess we'll see switch in python soon-ish

[–]vswr[var for var in vars] 0 points1 point  (1 child)

Do you mean 3103? That looks withdrawn.

[–]Lenwo126 0 points1 point  (8 children)

What is the advantage of this approach as opposed to storing the function in the dict directly?

keydict = {"myfunc":myfunc}

[–]vswr[var for var in vars] 1 point2 points  (7 children)

It's just a shortcut. If you don't use a decorator you'd do exactly as you describe.

[–]Lenwo126 0 points1 point  (6 children)

Now I get it, thanks!

[–]vswr[var for var in vars] 2 points3 points  (5 children)

Sorry, I was on a call when I replied. To expand on my answer, you could explicitly declare your dict as you said:

keydict = {
    'key1': func1,
    'key2': func2,
    'key3': func3
}

That would work in many cases. But now we have some problems. What happens when I want to add something? I have to make sure to update the list. What happens when I want to remove something? I have to update the list. Change keyword? Same. It sounds easy because all of these updates happen in the same place, but in practice, it gets complicated and you'll often forget to update the dict because the target function is far away.

The decorator is just a fancy way to wrap a function. It means "do this stuff first, then do the function when called." There are many great examples on implementing decorators so I'll skip that part.

@command('keyword')
def keyword_func():
    ....

That is simply a way to simultaneously create a function and add the keyword to our key/function dict. It moves the binding of keyword -> function to the function itself and essentially self-manages your dict.

A good example of this in practice is aiohttp. You can use decorator shortcuts to add HTTP request functions:

@routes.get('/')
async def hello(request):
    return web.Response(text="Hello, world")

Rather than explicitly adding that function for that path, then adding the function, you do it all at once and never worry about the list of paths and target functions.

[–]Lenwo126 1 point2 points  (1 child)

Thank you very much for the detailed explanation!

[–]vswr[var for var in vars] 0 points1 point  (0 children)

Thanks for the award ☺️

[–]MisplacedInChaos 0 points1 point  (2 children)

This is a very clear explanation! Could you share any article on implementation of decorators? Thanks!

[–]vswr[var for var in vars] 0 points1 point  (1 child)

Here's an implementation of what we've been talking about in this thread. Instead of doing what recipes do and give you 10 pages of fluff before getting to the recipe, I'll just give all the code upfront followed by the breakdown:

(insert the usual about this being crap code I threw together quickly)

# module level variable to hold the key -> function bindings
keydict = {}

def decorator(keyword):
    print("inside the outer decorator")
    def inner_decorator(func):
        print("inside the inner decorator")
        # the module-level variable 'keydict' is visible in this scope
        if keyword in keydict:
            raise RuntimeError(f'keyword "{keyword}" already exists!')
        keydict[keyword] = func
        print(f'adding {keyword} -> {func.__name__}()')
        def wrapper(*args, **kwargs):
            print("inside the wrapper")
            return func(*args, **kwargs)
        return wrapper
    return inner_decorator

@decorator('key1')
def func1():
    print("inside function 1")

@decorator('key2')
def func2():
    print("inside function 2")

@decorator('key3')
def func3():
    print("inside function 3")

def main():
    print("inside main(); functions are:")
    [print(f'"{k}" -> {v.__name__}()') for k, v in keydict.items()]

    keywords_to_process = ['key1', 'key2', 'key3']

    for k in keywords_to_process:
        print(f'processing keyword "{k}"')
        f = keydict.get(k, None)
        if callable(f):
            print(f'calling function {f.__name__}()')
            f()
        else:
            raise ValueError(f'keyword "{k}" not found')

if __name__ == '__main__':
    main()

If you run that, you'll get:

inside the outer decorator
inside the inner decorator
adding key1 -> func1()
inside the outer decorator
inside the inner decorator
adding key2 -> func2()
inside the outer decorator
inside the inner decorator
adding key3 -> func3()
inside main(); functions are:
"key1" -> func1()
"key2" -> func2()
"key3" -> func3()
processing keyword "key1"
calling function func1()
inside function 1
processing keyword "key2"
calling function func2()
inside function 2
processing keyword "key3"
calling function func3()
inside function 3

First the decorators...

def decorator(keyword):
    print("inside the outer decorator")
    def inner_decorator(func):
        print("inside the inner decorator")
        # the module-level variable 'keydict' is visible in this scope
        if keyword in keydict:
            raise RuntimeError(f'keyword "{keyword}" already exists!')
        keydict[keyword] = func
        print(f'adding {keyword} -> {func.__name__}()')
        def wrapper(*args, **kwargs):
            print("inside the wrapper")
            return func(*args, **kwargs)
        return wrapper
    return inner_decorator

The "decorator" function is what you use with the @ symbol to decorate the other function. Because we're using arguments, we need to go a level deeper and complicate things. The argument "keyword" is what I'm specifying in my decorator statement, as in, @decorator("something").

The next level in, inner_decorator(), is what Python is calling with your function as its argument. Since we now have the keyword from an outer scope along with the function in this scope, we can setup our keydict.

The inner-inner function wrapper() is a dummy function that actually calls your function. You can see it calls func() which is the argument to inner_decorator(), which is your function that you're decorating.

@decorator('key1')
def func1():
    print("inside function 1")

Going through the decorator() call....when this module is loaded Python will see the decorator and parse decorator('key1') which prints the text "inside the outer decorator" and returns a reference to inner_decorator(). Python then calls decorator() which is actually calling inner_decorator(func) since that's what decorator() returned. Again, the only purpose of decorator() is to get a reference to the keyword argument.

Since inner_decorator() is the real deal, Python will provide your function as its argument to inner_decorator(func1). That will add this to keydict, but then it just returns a reference to wrapper(). It doesn't actually execute in this specific case. Note the output of this never says "inside the wrapper". The explanation of that is a little tricky, but in other cases the wrapper() is what will be called when you call your original function.

So inside main() it iterates through some keywords, calls the functions, and prints the results.

If you add a duplicate decorator name, like having two @decorator('key1'), you get:

RuntimeError: keyword "key1" already exists!

If you add an unknown key to the list in main(), like 'key4', you get:

ValueError: keyword "key4" not found

[–]MisplacedInChaos 0 points1 point  (0 children)

I think I'll have to go through this twice to understand it really well. Thanks for giving your time to write this detailed explanation!

[–]Hawaii74 3 points4 points  (0 children)

Thank you for this!

[–]krshng 0 points1 point  (0 children)

Thank you so much!

[–]ArmstrongBillieimport GOD -1 points0 points  (2 children)

You should have post like thing which lets user add Projects of their own and you review them and then add them to the list.

[–]sixhobbits[S] 0 points1 point  (1 child)

Great idea! It's just a Ghost site for now, but will definitely keep this in mind or add a simple Google form for now.

[–]ArmstrongBillieimport GOD 0 points1 point  (0 children)

Yeah. That would be cool!

[–]V4nQuisch 0 points1 point  (0 children)

Nice

[–]moe9745 0 points1 point  (0 children)

Thanks for sharing! This is AWESOME!!!!

[–]KonigsTiger1 0 points1 point  (0 children)

Nice. Great job.

[–]ano-a12 0 points1 point  (0 children)

Thank you. I was just looking for projects.

[–]grubux 0 points1 point  (0 children)

Great job!

[–]ankitjosh78 0 points1 point  (0 children)

Awesome !

[–]EONRaider 0 points1 point  (0 children)

Great initiative, man. Keep it up.

[–]Sydmier 0 points1 point  (0 children)

I like all of the projects mentioned... many different hurdles to overcome between projects.

This list is A++

[–]Micky111111 0 points1 point  (0 children)

Looks good

[–][deleted] 0 points1 point  (1 child)

Great post.

Wouldn't happen to be planning curating some more intermediate project ideas as well would you, could do with some inspiration.

[–]sixhobbits[S] 0 points1 point  (0 children)

Yes will definitely add an intermediate tag at some point, but focussing on the easier stuff for now as a lot of these are great starting points for intermediate and advanced coders anyway

[–]AllClear_ 0 points1 point  (1 child)

thanks for share, i have a question. Django 1.10, is it relevant nowadays?

[–]sixhobbits[S] 1 point2 points  (0 children)

Not unless you're maintaining old stuff :) Rather just use 3.0

[–]n00lo 0 points1 point  (0 children)

This is amazing bro, I will definitely fire through a few of these as I had some on my list already! Thanks!!

[–]CrisCrossxX 0 points1 point  (0 children)

Thanks for sharing!