User Controls

Why does Python have Decorators instead of multi-statement Lambas?

  1. #1
    Sophie Pedophile Tech Support
    So today i learned a tiny little bit about Lambdas and Python and decorators. So where in python you would have:


    @foo
    def bar(baz, quux):
    ...


    Which would be equivalent to:


    def bar(baz, quux):
    ...

    bar = foo(bar)


    While the Lambda way would be:


    bar = foo(|baz, quux|:
    ...
    )


    Why?

    Please explain it to me as if i am five.
  2. #2
    AngryOnion Big Wig [the nightly self-effacing broadsheet]
    Anything more complicated than this makes my head hurt.
    10 CLS
    20 PRINT "Hello, world!"
    30 PRINT "I'm learning about commands in BASIC."
    40 PRINT "This text is being printed via the PRINT command."
    50 PRINT "On the next line, I'll use CLS."
    60 CLS "Now the next word would be PRINT."
    70 PRINT "Finally, on line 80, I'll use END."
    80 END "And return to PRINT"
    90 PRINT "Now my program is over."
    The following users say it would be alright if the author of this post didn't die in a fire!
  3. #3
    SBTlauien African Astronaut
    I never learned lambdas but I started. It didn't seem like something needed. Correct my if I'm wrong.
  4. #4
    Sophie Pedophile Tech Support
    Originally posted by SBTlauien I never learned lambdas but I started. It didn't seem like something needed. Correct my if I'm wrong.

    It's probably useful.
  5. #5
    Lanny Bird of Courage
    It's not so much an "instead of" relationship. Decorators don't really do what you'd want multi-line lambdas to do nor vice versa. Really both features (and all lambdas, per the python idea of a lambda) don't express anything that couldn't be expressed in the language otherwise, they're just nice syntactic sugar.

    To answer the questions separately, decorators are in the language because Guido liked annotations in Java, decorators are more powerful than annotations but use the same syntax and provide a superset of the annotation functionality. Multi-line lambdas are not in the language because Guido has a wierd dislike of functional programming (same reason map/enhancement/reduce have been relegated to functools), also he's said it will complicate the parser which is true but no one's really satisfied by that because nested list comprehensions are a lot more complexity in the parser (and for the reader) and no one has said that needs to be cut from the language.

    To go into a little more depth: The decorator syntax is just a way of giving a name to the output of a higher order functions, functions that take a function and return a function, we see the equivalence in your second example. Higher order functions don't exist in languages like C and Java, which is where python inherits a lot of its syntax. In these languages naming functions is kind of special, `int foo(int a) { return a*2; }` creates the name `foo` forever as an immutable declaration entirely different from a variable named foo. There is no way in C to say `bar = foo` down the line (yes function pointers exist but that's different). That is important for compiler optimizations like function inlining and metadata discarding (under optimized compilation the string "foo" doesn't exist anywhere in a program defining a function foo, all references are replaced by an address rather than the function's name). This means defining a function called bar in python by doing `bar = decorate(foo)` looks pretty strange to a lot of programmers. Arguably (I'd agree) it hurts readability because when you see a top-level `def` you're like "ok, cool, this package exports this function" but when you see `bar = decorate(foo)` you think "what is this? constant? global? decorated function?" plus you're still exporting the undecorated `foo` as part of the package. The decorator syntax fixes this by keeping the `def ...` syntax for defining decorated functions and prevents the export of the undecorated function, in fact it's impossible to call the undecorated function that way.

    Actually I see now why lambdas get involved, because actually this is kind of like a lambda. Lambdas are un-named functions and the pre-decorated function using the decorator syntax is also unnamed, so yeah, in terms of removing the undesirable creation of an undecorated function under some accessible name both multi-line lambdas and the decorator syntax could accomplish the same goal. The added bonus of decorators is that they read better. They're not as flexible as muti-line lambdas because they're not expressions (you can't assign them to a name with `=` or pass them as an argument) but as someone who would like to see multi-line lambdas added to the language, I don't think they would displace the need for (or at least the utility of) the decorator syntax.

    Originally posted by SBTlauien I never learned lambdas but I started. It didn't seem like something needed. Correct my if I'm wrong.

    "lambda" in the computer science sense is a very important idea, arguably the most powerful single idea in computation. "lambdas" in python are lambdas in the CS sense, but so are functions. The way we use the lambda keyword in python is really kind of a shorthand. Still a useful tool, you see them pretty often in code and they're just a handy too so I'd say it's something worth learning (honestly they're not very complex once you understand that functions are objects) but there's never been a program where you strictly needed to use the lambda keyword.
    The following users say it would be alright if the author of this post didn't die in a fire!
  6. #6
    Sophie Pedophile Tech Support
    Cool so, in what case would i realistically use higher order functions? Because all the programs i have written so far work fine without higher order functions. Save for perhaps higher order functions present in libraries and such.
  7. #7
    Lanny Bird of Courage
    A common use case is when you have some kind of behavior you want to apply to several functions but where directly invoking some new function doesn't make sense. An example might be type sniffing, you could do this:


    def add(a, b):
    assert type(a) == int
    assert tybe(b) == int

    return a + b

    def subtract(a, b):
    assert type(a) == int
    assert tybe(b) == int

    return a - b


    This works but we'd like to lift the type check out of each, so we could refactor as:


    def check_are_ints(*args):
    for arg in args:
    assert type(arg) == int

    def add(a, b):
    check_are_ints(a, b)
    return a + b

    def subtract(a, b):
    check_are_ints(a, b)
    return a - b


    And this is better but on some level we know "check my argument are integers" isn't really the business logic in addition or subtraction, and further we think about this check as being a property of the function instead of part of it's logic. Using decorators we can:


    def args_are_type(fn, arg_type=int):
    def new_func(*args):
    for arg in args:
    assert type(arg) == arg_type

    return fn(*args)
    return new_func

    @args_are_type(int)
    def add(a, b):
    check_are_ints(a, b)
    return a + b

    @args_are_type(int)
    def subtract(a, b):
    return a - b


    This lets us parameterize the type we're checking against and it lifts the check out of the body of the function.

    Another common use case is if you want to "register" a function in some way, for example:


    registered_funcs = []

    def register(fn):
    registered_funcs.append(fn)
    return fn

    @register
    def foo_bar():
    ...

    # registered_funcs == [foo_bar]


    And another example that's actually pretty common to see in the wild:


    def memoize(fn):
    memo = {}

    def new_fn(*args):
    arg_hash = sum([hash(arg) for arg in args])

    if arg_hash not in memo:
    memo[arg_hash] = fn(*args)

    return memo[arg_hash]

    @memoize
    def add(a, b):
    print a, b
    return a + b

    add(1,1)
    add(1,1)
    add(1,2)

    # outputs:
    # 1 1
    # 1 2


    Again, all things you could do before but being able to "wrap" functions like this allows you to program in a more generic way and can help separate facts "about" functions (this function is memoized, this function checks types) from their core logic.
    The following users say it would be alright if the author of this post didn't die in a fire!
  8. #8
    Sophie Pedophile Tech Support
    Originally posted by Lanny A common use case is when you have some kind of behavior you want to apply to several functions but where directly invoking some new function doesn't make sense. An example might be type sniffing, you could do this:


    def add(a, b):
    assert type(a) == int
    assert tybe(b) == int

    return a + b

    def subtract(a, b):
    assert type(a) == int
    assert tybe(b) == int

    return a - b


    This works but we'd like to lift the type check out of each, so we could refactor as:


    def check_are_ints(*args):
    for arg in args:
    assert type(arg) == int

    def add(a, b):
    check_are_ints(a, b)
    return a + b

    def subtract(a, b):
    check_are_ints(a, b)
    return a - b


    And this is better but on some level we know "check my argument are integers" isn't really the business logic in addition or subtraction, and further we think about this check as being a property of the function instead of part of it's logic. Using decorators we can:


    def args_are_type(fn, arg_type=int):
    def new_func(*args):
    for arg in args:
    assert type(arg) == arg_type

    return fn(*args)
    return new_func

    @args_are_type(int)
    def add(a, b):
    check_are_ints(a, b)
    return a + b

    @args_are_type(int)
    def subtract(a, b):
    return a - b


    This lets us parameterize the type we're checking against and it lifts the check out of the body of the function.

    Another common use case is if you want to "register" a function in some way, for example:


    registered_funcs = []

    def register(fn):
    registered_funcs.append(fn)
    return fn

    @register
    def foo_bar():
    ...

    # registered_funcs == [foo_bar]


    And another example that's actually pretty common to see in the wild:


    def memoize(fn):
    memo = {}

    def new_fn(*args):
    arg_hash = sum([hash(arg) for arg in args])

    if arg_hash not in memo:
    memo[arg_hash] = fn(*args)

    return memo[arg_hash]

    @memoize
    def add(a, b):
    print a, b
    return a + b

    add(1,1)
    add(1,1)
    add(1,2)

    # outputs:
    # 1 1
    # 1 2


    Again, all things you could do before but being able to "wrap" functions like this allows you to program in a more generic way and can help separate facts "about" functions (this function is memoized, this function checks types) from their core logic.

    Ah i see, thanks for providing the examples that always helps with my understanding of programming and programming concepts. You could say that when it comes to code i am somewhat of a visual learner. In general i learn more by seeing the code with a little explanation than just explanation alone so thank you for that.
Jump to Top