[Python-ideas] Before and after the colon in funciton defs.

Steven D'Aprano steve at pearwood.info
Fri Sep 23 05:17:01 CEST 2011

Nick Coghlan wrote:
> On Fri, Sep 23, 2011 at 9:51 AM, Steven D'Aprano <steve at pearwood.info> wrote:
>> With decorator syntax, the scoping rules are obvious and straightforward:
>> a = 10
>> @inject(b=a)
>> def foo():
>>    a = 20
>>    return b+a
> Please read the previous thread from June (linked earlier in this
> thread). Decorator syntax cannot work without deep magic, because the
> compiler *doesn't know* that injected names need to be given special
> treatment.

I have read the previous thread, and I'm fully aware that this would be 
special. I'm pretty sure I even said it would be special in one of my 
posts :)

Excluding the default "do nothing" position, and the minimalist "just 
add conventions to introspection tools" proposal, I believe that @inject 
is the least magical proposal made so far. The other proposals require a 
new keyword, new syntax, or both. They require the compiler to do extra 
work. @inject requires nothing from the compiler. It all happens when 
the decorator is called. Comparing it to the voodoo needed for super() 
is completely unfair.

It also offers the most benefits:

* all the benefits of the proposed "static" declaration
   (early-binding, micro-optimisation, monkey-patching)
* the ability to patch pre-existing functions
* no call-time cost (it's not a wrapper, its a new function)
* fewer side-effects than conventional monkey-patching
* familiar syntax
* obvious semantics
* no new keywords
* no extra "line-noise" symbols

What's not to like?

Yes, inject() will be special. But I don't believe it will be magic, or 
at least not deep magic. We can get most of the way using supported 
Python functionality already: we can pull a function object apart, make 
a copy of the pieces as needed, and reassemble them into a new function. 
All that is supported by Python, none of it requires messing with 
private attributes: it's not much more magic than what functools.wraps() 
already does.

We can even take the bytecode from the code object, and because it's 
just a string, we can perform transformations on it. The only "magic" is 
the knowledge of what transformations to perform.

Take this simple example:

 >>> import dis
 >>> def func():
...     c = 1
...     return c+d
 >>> dis.dis(func)
   2           0 LOAD_CONST               1 (1)
               3 STORE_FAST               0 (c)

   3           6 LOAD_FAST                0 (c)
               9 LOAD_GLOBAL              0 (d)
              12 BINARY_ADD
              13 RETURN_VALUE

@inject(d=2)(func) would probably look produce something like this:

   2           6 LOAD_CONST               2 (2)
               9 STORE_FAST               1 (d)

   3           0 LOAD_CONST               1 (1)
               3 STORE_FAST               0 (c)

   4          12 LOAD_FAST                0 (c)
              15 LOAD_FAST                1 (d)
              18 BINARY_ADD
              19 RETURN_VALUE

If we had a Python assembler to match the disassembler, it would 
probably be easy.

If we are really allergic to the idea of bytecode manipulation, perhaps 
there are other ways to solve this. Just tossing ideas around, maybe 
function objects should keep a copy of their AST around, so that instead 
of bytecode hacking, you manipulate the AST and recompile the function. 
That might be less "hacky" than manipulating the bytecode, but I don't 
know that carrying around the AST for every function is a cost that is 
worth bearing. But it's an idea.


More information about the Python-ideas mailing list