Sometimes you want the value of a variable to be related to its name in the source code. For example, the variable x should have the value "var x". This is especially useful when you perform symbolic computations with python (with the sympy module for example). In standard python, the only way to obtain this behavior is to repeat the name of the variable in the call to the function which creates the value. For example x, y, z = create_values(varnames = ('x', 'y', 'z')).

This snippet defines a decorator with_varnames which can be applied to the value creation function and which extracts the variable names from the python code at the point where the function is called. One only needs to write x, y, z = create_values() to achieve the same effect.

This snippet was tested under python 2.6 and 3.1.

Edited 2015 Jan 6th with docstrings and version number. Python 2.7 and 3.4.

Edited 1 Year Ago by Gribouillis

# python 2 or 3
from __future__ import print_function 
import sys 
py3 =sys .version_info >=(3 ,)

if py3 :
  byte_ord =lambda x :x 
else :
  byte_ord =ord
  
__version__ = '0.4.0'

class VarnamesError (Exception ):
  pass 

def assignment_varnames (code ,lasti ):
  """Extract variable names from a statement of the form
  x, y, z = function(...)
  in a code objet @code where @lasti is the index of the
  CPython bytecode instruction where the function is called.
  """
  import opcode 
  call ,unpack ,storef ,storen =(opcode .opmap [s ]for s in 
  ("CALL_FUNCTION","UNPACK_SEQUENCE","STORE_FAST","STORE_NAME"))
  errmsg ="simple assignment syntax 'x, y, z = ...' expected"
  varnames =[]
  co =code .co_code 
  i =lasti 
  if byte_ord (co [i ])!=call :
    raise VarnamesError (errmsg )
  i +=3 
  if byte_ord (co [i ])==unpack :
    nvars =byte_ord (co [i +1 ])+byte_ord (co [i +2 ])*256 
    i +=3 
  else :
    nvars =1 
  for j in range (nvars ):
    k =byte_ord (co [i ])
    oparg =byte_ord (co [i +1 ])+byte_ord (co [i +2 ])*256 
    if k ==storef :
      varnames .append (code .co_varnames [oparg ])
    elif k ==storen :
      varnames .append (code .co_names [oparg ])
    else :
      raise VarnamesError (errmsg )
    i +=3 
  return varnames 

def with_varnames (func ):
  """with_varnames(function) -> decorated function
    
    This decorator allows a function to extract variable names
    from the line of code where it is called, and create
    values for these variables which depend on their names.
    
    @function : a function which accepts a keyword argument 'varnames'
                (supposed to be a sequence) and returns a sequence of
                values of the same length.
    
    example code:

    # a values creation function, decorated with with_varnames

    @with_varnames
    def my_values(varnames = ()):
        return [name+str(i) for i, name in enumerate(varnames)]

    # first way to call my_values: pass all the arguments
    values = my_values(varnames = ("u", "v", "w"))
    print(values)
    
    # second way: through an assignment expression with the variable names on the left
    x, y, z = my_values()
    print(x, y, z)
    """

  import inspect ,functools 
  def wrapper (*args ,**kwd ):
    if "varnames"not in kwd :
      f =inspect .currentframe ()
      try :
        f =f .f_back 

        code ,lasti =f .f_code ,f .f_lasti 
      finally :
        del f 
      kwd ["varnames"]=assignment_varnames (code ,lasti )
    seq =func (*args ,**kwd )
    return next (iter (seq ))if len (kwd ["varnames"])==1 else seq 
  functools .update_wrapper (wrapper ,func )
  return wrapper 

if __name__ =="__main__":

  @with_varnames 
  def my_values (varnames =()):
    for i ,name in enumerate (varnames ):
      yield name +str (i )

  def test0 ():
    R =range (10 )
    x ,y ,z =my_values ()
    print (x ,y ,z )

  def test1 ():
    code =compile ("""
R = range(10)
u, v, w = my_values()
print(u, v, w)
""","<string>","exec")
    eval (code )

  test0 ()
  test1 ()

I discovered a small bug in the previous code if one assigns to a single variable like in t = my_values() . Here is a patched code which works also in this case. Sorry :)

#python 2 and 3

def with_varnames(func):
    """with_varnames(function) -> decorated function
    
    This decorator allows a function to extract variable names
    from the line of code where it is called, and create
    values for these variables which depend on their names.
    
    @function : a function which accepts a keyword argument 'varnames'
                (supposed to be a sequence) and returns a sequence of
                values of the same length.
    
    example code:

    # a values creation function, decorated with with_varnames

    @with_varnames
    def my_values(varnames = ()):
        return [name+str(i) for i, name in enumerate(varnames)]

    # first way to call my_values: pass all the arguments
    values = my_values(varnames = ("u", "v", "w"))
    print(values)
    
    # second way: through an assignment expression with the variable names on the left
    x, y, z = my_values()
    print(x, y, z)
    """
    import functools
    from traceback import extract_stack
    def wrapper(*args, **kwd):
        single_assign = False
        if not 'varnames' in kwd:
            try:
                raise RuntimeError
            except:
                L = extract_stack(limit=2)[0][3].split("=", 1)[0].split(',')
                t = tuple(item.strip() for item in L)
                single_assign = (len(t) == 1)
                kwd['varnames'] = t
        L = list(func(*args, **kwd))
        return L[0] if single_assign else L
    functools.update_wrapper(wrapper, func)
    return wrapper

if __name__ == "__main__":

    @with_varnames
    def my_values(varnames = ()):
        return [name+str(i) for i, name in enumerate(varnames)]

    # first way to call my_values: pass all the arguments
    values = my_values(varnames = ("u", "v", "w"))
    print(values)
    
    # second way: through an assignment expression with the variable names on the left
    x, y, z = my_values()
    print(x, y, z)

    t = my_values()
    print(t)

""" my output -->
['u0', 'v1', 'w2']
('x0', 'y1', 'z2')
t0
"""

Edited 6 Years Ago by Gribouillis: n/a

The previous decorator has a flaw: it works if the statement x, y, z = my_values() occurs in a python source file, but not if the statement is written in the python console or occurs in a string evaluated through the eval and exec function. To support these cases, here is a third version (tested with python 2.6 and 3.1) which extracts the variable names from the python bytecode instead of the python source code. This allows to use the decorator in the console or in exec statements. On the other hand, since bytecode is specific to the official C based implementation of python, it may not work for other implementations.

Here is the improved version

# python 2 or 3
from __future__ import print_function 
import sys 
py3 =sys .version_info >=(3 ,)

if py3 :
  byte_ord =lambda x :x 
else :
  byte_ord =ord 

class VarnamesError (Exception ):
  pass 

def assignment_varnames (code ,lasti ):
  """Extract variable names from a statement of the form
  x, y, z = function(...)
  in a code objet @code where @lasti is the index of the
  CPython bytecode instruction where the function is called.
  """
  import opcode 
  call ,unpack ,storef ,storen =(opcode .opmap [s ]for s in 
  ("CALL_FUNCTION","UNPACK_SEQUENCE","STORE_FAST","STORE_NAME"))
  errmsg ="simple assignment syntax 'x, y, z = ...' expected"
  varnames =[]
  co =code .co_code 
  i =lasti 
  if byte_ord (co [i ])!=call :
    raise VarnamesError (errmsg )
  i +=3 
  if byte_ord (co [i ])==unpack :
    nvars =byte_ord (co [i +1 ])+byte_ord (co [i +2 ])*256 
    i +=3 
  else :
    nvars =1 
  for j in range (nvars ):
    k =byte_ord (co [i ])
    oparg =byte_ord (co [i +1 ])+byte_ord (co [i +2 ])*256 
    if k ==storef :
      varnames .append (code .co_varnames [oparg ])
    elif k ==storen :
      varnames .append (code .co_names [oparg ])
    else :
      raise VarnamesError (errmsg )
    i +=3 
  return varnames 

def with_varnames (func ):
  import inspect ,functools 
  def wrapper (*args ,**kwd ):
    if "varnames"not in kwd :
      f =inspect .currentframe ()
      try :
        f =f .f_back 

        code ,lasti =f .f_code ,f .f_lasti 
      finally :
        del f 
      kwd ["varnames"]=assignment_varnames (code ,lasti )
    seq =func (*args ,**kwd )
    return next (iter (seq ))if len (kwd ["varnames"])==1 else seq 
  functools .update_wrapper (wrapper ,func )
  return wrapper 

if __name__ =="__main__":

  @with_varnames 
  def my_values (varnames =()):
    for i ,name in enumerate (varnames ):
      yield name +str (i )

  def test0 ():
    R =range (10 )
    x ,y ,z =my_values ()
    print (x ,y ,z )

  def test1 ():
    code =compile ("""
R = range(10)
u, v, w = my_values()
print(u, v, w)
""","<string>","exec")
    eval (code )

  test0 ()
  test1 ()

  """my output -->
  x0 y1 z2
  u0 v1 w2
  """

To learn how to disassemble python bytecode, look into the source code of the standard module dis. I may very well write a code snippet about this in daniweb soon :).

Edited 6 Years Ago by Gribouillis: n/a

As an example use of with_varnames, here is a function which imports an object's attributes in the current namespace

@with_varnames
def attrof(obj, varnames=()):
    # retrives a list of attribute values in an object
    return [getattr(obj, name) for name in varnames]

if __name__ == "__main__":
    import os
    # ready for the miracle ?
    chdir, mkdir, fork = attrof(os)
    print(chdir, mkdir, fork)

""" my output -->
<built-in function chdir> <built-in function mkdir> <built-in function fork>
"""

Edited 6 Years Ago by Gribouillis: n/a

The original snippet has been edited with the most recent version. It must be stressed again that this is a interesting hack which may work only for some versions and implementation of python. This snippet is not a way to go to produce robust code ;)

The article starter has earned a lot of community kudos, and such articles offer a bounty for quality replies.