4

It's my third day using Python, so forgive newbie mistakes. So here's my working code. person.test() registers a callback with the boss, the boss calls the callback, everything works fine.

class Boss:
  def registerCallback(self,cb):
    self.cb = cb
  def doCallback(self):
    self.cb()

class Person:
  def woot(self,data):
    print("Woot! ",data)

  def test(self,boss,data):
    def callback ():
      self.woot(data)
    boss.registerCallback(callback)    

boss = Boss()
person = Person()
person.test(boss,1)
boss.doCallback()

However, if I change move the callback into an exec(), the closure is lost. The callback runs, but self and data are unknown so the call to self.woot(data) fails.

class Boss:
  def registerCallback(self,cb):
    self.cb = cb
  def doCallback(self):
    self.cb()

class Person:
  def woot(self,data):
    print("Woot! ",data)

  def test(self,boss,data):
    x = "def callback():\n  self.woot(data)\nboss.registerCallback(callback)"
    exec(x,globals(),locals())

boss = Boss()
person = Person()
person.test(boss,1)
boss.doCallback()

I tried to compile() too, no luck. Any thoughts? I really don't want to manually carry a copy of self/data through the boss and back, because my real-life code is much more convoluted. I really need a way to maintain the closure.

JeramieH
  • 129
  • 9
  • 1
    My gut reaction is "Don't use `exec()`!" because it presents all kinds of issues that occur when you don't separate code and data. Why do you think you need exec() and for what? BTW, concerning callbacks, you know that instead of `foo.bar()` you can also write `tmp = foo.bar; tmp()`. This is often the easier way to create callbacks to an object. In your case, you pass along some data though, which is why it doesn't fully apply. There are libraries for function composition (binding parameters etc) included in Python that could be used instead. – Ulrich Eckhardt Mar 09 '15 at 20:23
  • I'm using exec() because the business logic is defined dynamically during runtime by the user while the application is running, the essential feature of this project. – JeramieH Mar 09 '15 at 20:39

2 Answers2

4

If you only pass locals (as the global data for the function), then things more or less work:

class Person:
  def woot(self,data):
    print("Woot! ",data)

  def test(self,boss,data):
    x = "def callback():\n  self.woot(data)\nboss.registerCallback(callback)"
    exec(x, locals())

of course, if you need the globals as well, you can pack them together:

def test(self, boss, data):
  namespace = globals().copy()
  local_copy = locals().copy()
  namespace.update(local_copy)
  x = 'def foo(): pass'
  exec(x, namespace)
mgilson
  • 300,191
  • 65
  • 633
  • 696
  • Ah, looks like everything I need is stored in locals(), and thanks to Ashwini's answer below about why it's not being taken correctly... thanks to you both, exactly what I needed. – JeramieH Mar 09 '15 at 20:57
3

Why your current code fails?

self is a free variable to callback and if you read locals()'s documentation you will find:

Free variables are returned by locals() when it is called in function blocks, but not in class blocks.

And now from exec()'s documentation:

If exec gets two separate objects as globals and locals, the code will be executed as if it were embedded in a class definition.

So, as we are passing two different objects to exec() the locals() dictionary is actually empty for callback() as it cannot access free variables anymore, hence the solution suggested by @mgilson that passes merged version of locals() and globals() should do it for you.

Community
  • 1
  • 1
Ashwini Chaudhary
  • 244,495
  • 58
  • 464
  • 504