views:

220

answers:

3

Hello,

I am translating some code from lisp to Python.

In lisp, you can have a let construct with the variables introduced declared as special and thus having dynamic scope. (See http://en.wikipedia.org/wiki/Dynamic_scope#Dynamic_scoping)

How can I do likewise in Python? It seems the language does not support this directly, if true, what would be a good way to emulate it?

Thanks.

-William

+2  A: 

Dynamic Scoping Considered Harmful.

Don't use it; don't emulate it.

If you need to emulate it, define a dynamic_scope module to emulate this behavior and import the module in all source files. This module should have methods begin which is called in the first line of your functions that use dynamic scopes, end, get, and set. The get and set methods should implement looking up the call chain for variable names where the call chain is implemented by begin and end. Then refactor your code to eliminate dynamic scopes.

Justice
Dynamic scoping *can* be an incredibly useful feature in languages that support it well. I've made tiny (3-4 line) changes to large Common Lisp programs that would have taken huge (but mechanically simple) modifications without it. Sometimes it's the natural solution to a problem. That said, it's not at all natural in Python, and I wouldn't suggest porting it directly -- that does seem like a recipe for maintenance pain.
Ken
+5  A: 

Hi --

I feel Justice is plain right in his reasoning here.

On the other hand -- I can't resist implementing proof of concept for still another programing paradigm "unnatural" to Python -- I simply love doing this. :-)

So, I created a class whose objects'attributes are scopped just like you require (and can be created dynamically). As I said, it is just in a proof of concept state - but I think most usual errors, (like trying to access a variable ina scope it is not defined at all) should have errors raised, even if not the proper ones (IndexError due to a stack underflow instead of AttributeError, for example)

import inspect


class DynamicVars(object):
    def __init__(self):
        object.__setattr__(self, "variables", {})

    def normalize(self, stackframe):
        return [hash(tpl[0]) for tpl in stackframe[1:]]

    def __setattr__(self, attr, value):
        stack = self.normalize(inspect.stack())
        d = {"value": value, "stack": stack}
        if not attr in self.variables:
            self.variables[attr] = []
            self.variables[attr].append(d)
        else:
            our_value = self.variables[attr]
            if our_value[-1]["stack"] == stack:
                our_value[-1]["value"] = value
            elif len(stack) <= len(our_value):
                while our_value and stack !=  our_value["stack"]:
                    our_value.pop()
                our_value.append(d)
            else: #len(stack) > len(our_value):
                our_value.append(d)
    def __getattr__(self, attr):
        if not attr in self.variables:
            raise AttributeError
        stack = self.normalize(inspect.stack())
        while self.variables[attr]:
            our_stack = self.variables[attr][-1]["stack"]
            if our_stack == stack[-len(our_stack):]:
                break
            self.variables[attr].pop()
        else:
            raise AttributeError
        return self.variables[attr][-1]["value"]


# for testing:
def c():
    D = DynamicVars()
    D.c = "old"
    print D.c
    def a():
        print D.c
    a()
    def b():
        D.c = "new"
        a()
    b()
    a()
    def c():
        D.c = "newest"
        a()
        b()
        a()
    c()
    a()

c()
jsbueno
Congratulations! Thanks to your hard work, the world of programming has yet one more solution that will make its way deep into the hearts of numerous critical applications!
Justice
And after all, Lisp's "special variables" are not so terrible, are they? They're like environment variables in bash. What's terrible is languages where dynamic scoping is the default. Fortunately not many of those are left.
Jason Orendorff
+2  A: 

Here's something that works a bit like Lisp's special variables, but fits a little better into Python.

_stack = []

class _EnvBlock(object):
    def __init__(self, kwargs):
        self.kwargs = kwargs
    def __enter__(self):
        _stack.append(self.kwargs)
    def __exit__(self, t, v, tb):
        _stack.pop()

class _Env(object):
    def __getattr__(self, name):
        for scope in reversed(_stack):
            if name in scope:
                return scope[name]
        raise AttributeError("no such variable in environment")
    def let(self, **kwargs):
        return _EnvBlock(kwargs)
    def __setattr__(self, name, value):
        raise AttributeError("env variables can only be set using `with env.let()`")

env = _Env()

You can use it like this:

with env.let(bufsize=8192, encoding="ascii"):
    print env.bufsize  # prints 8192
    a()  # call a function that uses env.bufsize or env.encoding

The effects of env.let last for the duration of the with block.

Note that if you use threads, you'll definitely want a different _stack for each thread. You could use threading.local to implement that.

Jason Orendorff
This is intended as a compromise between "don't do that" and stack inspection (which seems like it would be slow and hard to verify).
Jason Orendorff