Self function

Gabriel Genellina gagsl-py2 at yahoo.com.ar
Fri May 8 13:28:50 EDT 2009


En Wed, 06 May 2009 23:32:23 -0300, Luis Alberto Zarrabeitia Gomez  
<kyrie at uh.cu> escribió:

>
> Quoting Steven D'Aprano <steven at REMOVE.THIS.cybersource.com.au>:
>
>> But regardless, everyone is missing the most important point: why are  
>> you
>> copying and pasting code in the first place? That is surely very close  
>> to
>> the top of the list of Worst Ever Anti-Patterns, and it should be  
>> avoided
>> whenever possible.
>
> [btw, I took this long before jumping into this thread for this precise
> reason... Copy/paste is an evil anti-pattern.
>
>> In Python, we can avoid much copy-and-paste coding with decorators.  
>> Given
>> one function, we can create as many variants as we want, differing in  
>> pre-
>> processing of arguments and post-processing of results, by using
>> decorators. This is a powerful ability to have, but it's crippled for
>> many recursive functions, because recursive functions in Python don't
>> actually call themselves, they call whatever happens to be bound to  
>> their
>> name at runtime.
>
> A bit offtopic: a while ago I think I saw a recipe for a decorator that,  
> via
> bytecode hacks, would bind otherwise global names to the local namespace  
> of the
> function. Can anyone remember it/point me to it? An @bind decorator that  
> would
> 'localize' all the global names, including the still unexistent but  
> already know
> function name, would guarantee that at least recursion calls the same  
> function
> instead of "whatever happens to be bound to their name at runtime". If  
> it wasn't
> a hack, anyway.

I could not find such thing, but here it is something similar (mostly a  
proof-of-concept).
Basically, modifies the bytecode so global references become local ones,  
and then injects a new argument whose default value is the desired value.  
It can be used to make all references to the function name to refer to the  
function itself; also, the __function__ variant can be implemented.

<code>
 from opcode import HAVE_ARGUMENT, opmap

def iter_code(co):
   """Iterate over a code object."""
   code = co.co_code
   n = len(code)
   i = 0
   while i < n:
     op = ord(code[i])
     if op >= HAVE_ARGUMENT:
       oparg = ord(code[i+1]) + ord(code[i+2])*256
       yield code[i:i+3], op, oparg
       i += 3
     else:
       yield code[i], op, None
       i += 1

def replace_bytecode(code, iname_global, iname_local):
   LOAD_GLOBAL = opmap['LOAD_GLOBAL']
   LOAD_FAST = opmap['LOAD_FAST']
   STORE_GLOBAL = opmap['STORE_GLOBAL']
   STORE_FAST = opmap['STORE_FAST']
   for codestr, op, oparg in iter_code(code):
     if op==LOAD_GLOBAL and oparg==iname_global:
       codestr = chr(LOAD_FAST) + chr(iname_local % 256) + chr(iname_local  
// 256)
     elif op==STORE_GLOBAL and oparg==iname_global:
       codestr = chr(STORE_FAST) + chr(iname_local % 256) + chr(iname_local  
// 256)
     yield(codestr)

class GlobalToLocalWarning(RuntimeWarning): pass

def global_to_local(function, refname, value):
   """
   Make all references to `refname` local instead of global.
   Actually, transform
     def function(a,b,c):
   into
     def function(a,b,c,refname=value)
   """
   code = function.func_code
   if refname not in code.co_names:
     import warnings
     warnings.warn(GlobalToLocalWarning('no reference to "%s" found in %s'  
% (refname, function.func_name)), stacklevel=2)
     return function
   # insert refname just after the last argument
   varnames = list(code.co_varnames)
   varnames.insert(code.co_argcount, refname)
   # new bytecode using LOAD_FAST instead of LOAD_GLOBAL
   iname_global = code.co_names.index(refname) # LOAD_GLOBAL/STORE_GLOBAL  
operand
   iname_local = code.co_argcount # LOAD_FAST/STORE_FAST operand
   rawcode = ''.join(replace_bytecode(code, iname_global, iname_local))
   function.func_code = type(code)(
      code.co_argcount + 1,
      code.co_nlocals + 1,
      code.co_stacksize,
      code.co_flags,
      rawcode,
      code.co_consts,
      code.co_names,
      tuple(varnames),
      code.co_filename,
      code.co_name,
      code.co_firstlineno,
      code.co_lnotab
      )
   # the desired value is the default value of the new, fake argument
   if function.func_defaults is None:
     function.func_defaults = (value,)
   else:
     function.func_defaults += (value,)
   return function

# decorator: function name refers to itself
def recursive3(function):
   return global_to_local(function, function.func_name, function)

@recursive3
def fibo(n):
   if n<=1: return 1
   return fibo(n-1) + fibo(n-2)
   # those "fibo" won't be global anymore

# decorator: name '__function__' refers to function
def recursive4(function):
   return global_to_local(function, '__function__', function)

def factorial(n):
   @recursive4
   def _factorial(n, acc):
     if n<=1: return acc
     return __function__(n-1, n*acc)
   return _factorial(n, 1)

print fibo, fibo.__name__, fibo.func_name, fibo.func_code.co_name
assert fibo(5)==8
F10=factorial(10)
assert F10==2*3*4*5*6*7*8*9*10

foo = fibo
bar = factorial
del fibo, factorial
assert foo(5)==8
assert bar(10)==F10

import inspect
assert inspect.getargspec(foo).defaults[0] is foo
</code>

-- 
Gabriel Genellina




More information about the Python-list mailing list