Welcome, guest | Sign In | My Account | Store | Cart
# The list of symbols that are included by default in the generated
# function's environment
SAFE_SYMBOLS = ["list", "dict", "tuple", "set", "long", "float", "object",
                "bool", "callable", "True", "False", "dir",
                "frozenset", "getattr", "hasattr", "abs", "cmp", "complex",
                "divmod", "id", "pow", "round", "slice", "vars",
                "hash", "hex", "int", "isinstance", "issubclass", "len",
                "map", "filter", "max", "min", "oct", "chr", "ord", "range",
                "reduce", "repr", "str", "type", "zip", "xrange", "None",
                "Exception", "KeyboardInterrupt"]
# Also add the standard exceptions
__bi = __builtins__
if type(__bi) is not dict:
    __bi = __bi.__dict__
for k in __bi:
    if k.endswith("Error") or k.endswith("Warning"):
        SAFE_SYMBOLS.append(k)
del __bi


def createFunction(sourceCode, args="", additional_symbols=dict()):
  """
  Create a python function from the given source code
  
  \param sourceCode A python string containing the core of the
  function. Might include the return statement (or not), definition of
  local functions, classes, etc. Indentation matters !
  
  \param args The string representing the arguments to put in the function's
  prototype, such as "a, b", or "a=12, b",
  or "a=12, b=dict(akey=42, another=5)"

  \param additional_symbols A dictionary variable name =>
  variable/funcion/object to include in the generated function's
  closure

  The sourceCode will be executed in a restricted environment,
  containing only the python builtins that are harmless (such as map,
  hasattr, etc.). To allow the function to access other modules or
  functions or objects, use the additional_symbols parameter. For
  example, to allow the source code to access the re and sys modules,
  as well as a global function F named afunction in the sourceCode and
  an object OoO named ooo in the sourceCode, specify:
      additional_symbols = dict(re=re, sys=sys, afunction=F, ooo=OoO)

  \return A python function implementing the source code. It can be
  recursive: the (internal) name of the function being defined is:
  __TheFunction__. Its docstring is the initial sourceCode string.

  Tests show that the resulting function does not have any calling
  time overhead (-3% to +3%, probably due to system preemption aleas)
  compared to normal python function calls.
  """
  # Include the sourcecode as the code of a function __TheFunction__:
  s = "def __TheFunction__(%s):\n" % args
  s += "\t" + "\n\t".join(sourceCode.split('\n')) + "\n"

  # Byte-compilation (optional)
  byteCode = compile(s, "<string>", 'exec')  

  # Setup the local and global dictionaries of the execution
  # environment for __TheFunction__
  bis   = dict() # builtins
  globs = dict()
  locs  = dict()

  # Setup a standard-compatible python environment
  bis["locals"]  = lambda: locs
  bis["globals"] = lambda: globs
  globs["__builtins__"] = bis
  globs["__name__"] = "SUBENV"
  globs["__doc__"] = sourceCode

  # Determine how the __builtins__ dictionary should be accessed
  if type(__builtins__) is dict:
    bi_dict = __builtins__
  else:
    bi_dict = __builtins__.__dict__

  # Include the safe symbols
  for k in SAFE_SYMBOLS:
    # try from current locals
    try:
      locs[k] = locals()[k]
      continue
    except KeyError:
      pass
    # Try from globals
    try:
      globs[k] = globals()[k]
      continue
    except KeyError:
      pass
    # Try from builtins
    try:
      bis[k] = bi_dict[k]
    except KeyError:
      # Symbol not available anywhere: silently ignored
      pass

  # Include the symbols added by the caller, in the globals dictionary
  globs.update(additional_symbols)

  # Finally execute the def __TheFunction__ statement:
  eval(byteCode, globs, locs)
  # As a result, the function is defined as the item __TheFunction__
  # in the locals dictionary
  fct = locs["__TheFunction__"]
  # Attach the function to the globals so that it can be recursive
  del locs["__TheFunction__"]
  globs["__TheFunction__"] = fct
  # Attach the actual source code to the docstring
  fct.__doc__ = sourceCode
  return fct


##################################################################
### Some tests
def test():
    # -----------------------------------------------------
    # Code to execute as function 'f' (as a string):
    s = """
if a == "BE RECURSIVE":
    print "In the recursion 1"
    return __TheFunction__("THE END", 54)
elif a == "THE END":
    print "In the recursion 2"
    return 54

print a
print b
x = True

def sayhello(s):
    print "I say hello that way: %s" % s

class SayHello(object):
    def __init__(self, s):
        self.__s = s
        print "ctor says %s" % self.__s

    def s(self):
        return self.__s
try:
    1/0
except ZeroDivisionError, ex:
    print "GOT EX", ex

print "ooo in here says", ooo.mouf()

result = a + b +1
afunction(a+1)
c = re.compile("^a").search("ba", 1)
d = re.compile("a").match("ba", 1)
sayhello("I am so happy today %s,%s" % (c, d))
o = SayHello("this works")
vvv = range(42)
print vvv
sys.stderr.write("writing to stderr\\n")

print __TheFunction__
print "============ BEGIN docstring ==========="
print __TheFunction__.__doc__
print "============ END docstring ==========="
return a*b + __TheFunction__("BE RECURSIVE", 33)
"""
    # End of source code string
    # -----------------------------------------------------

    # Create objects, functions, etc.
    class OOO:
        def __init__(self, id):
            self.__id = id
        def mouf(self):
            return "OOO: My ID is %s" % self.__id
    def F(n):
        print "F: my parameter is", n

    # Generate a first function, f, which needs the re and sys modules
    import sys, re
    OoO = OOO(64)
    f = createFunction(s, "a=3, b=4",
                       additional_symbols = dict(re=re, sys=sys,
                                                 afunction=F, ooo=OoO))
    # Generate another function
    OoO = OOO("FOR G")
    g = createFunction("print 'G: my parameter is ', p, 'and o says', o.mouf()\nreturn p",
                    "p='undefined'", dict(o=OoO))

    # Test them
    print "call f():", f()
    print "call g():", g()
    print "call f(42):", f(42)
    print "call g(22):", g(22)
    print "call f(b=42):", f(b=42)
    print "call f(b=7, a=8):", f(b=7, a=8)
    print "call f(9, 10):", f(9, 10)

    # Do some basic profiling
    def nothing(a):
        return a*42

    import time
    ITER=10000000

    time.sleep(1.) # force preemption
    
    st = time.time()
    for i in xrange(ITER):
        x = nothing(i)
    et = time.time()
    print "Basic: %fs" % (et-st)
    t = et-st

    time.sleep(1.) # force preemption

    f = createFunction("return a*42", "a")
    st = time.time()
    for i in xrange(ITER):
        x = f(i)
    et = time.time()
    print "FromString: %fs" % (et-st)
    print "FromString time = Basic%+f%%" % ((((et-st) - t) / t)*100.)

History

  • revision 2 (15 years ago)
  • previous revisions are not available