Friday, September 25, 2009

Private member variables in Python

Today, I stumbled upon the "Module Pattern" in the enlightening JavaScript: The Good Parts by Douglas Crockford. Using nested scopes and closures, one can have real private member variables for JavaScript. For the fun of it, I tried it with Python and it works, too. At least, kind of.

Have a look at the following piece of code. In it, we create an object of Quote. Since the class has been declared in a factory method, it has access to its local variables, "txt" in this case.

def create(arg):
txt = str(arg)

class Quote:
def get_txt(self):
return txt

return Quote()

quote = create("My tables! Meet it is, I set it down, ...")
print(dir(quote))
print(quote.get_txt())

Since it is not a variable of Quote itself, "txt" is not accessible from the outside. Yet, Python keeps those variables around, even after create has returned. The script delivers the following output:

['__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'get_txt']
My tables! Meet it is, I set it down, ...

Now, what about changing the value? Let us add a simple setter method.

def create(arg):
txt = str(arg)

class Quote:
def get_txt(self):
return txt

def set_text(self, quote):
txt = quote

return Quote()

hamlet = create("There is something rotten in the state of Denmark.")
print(hamlet.get_txt())
hamlet.set_text("I'll make a ghost of him who lets me.")
print(hamlet.get_txt())

Seems straightforward enough, but results in the following undesired output.

There is something rotten in the state of Denmark.
There is something rotten in the state of Denmark.

Apparently, Python does not use lexical scoping when doing a name lookup for write access. Instead, it creates a local variable - which is discarded the same moment the setter-method returns.

Read access works though, and we can use this for a work-around by modifying objects instead of assigning values to names.

class Privates:
pass

def create(arg):
privates = Privates()
setattr(privates, "txt", str(arg))

class Quote:
def get_txt(self):
return privates.txt

def set_text(self, quote):
privates.txt = quote

return Quote()

hamlet = create("There is something rotten in the state of Denmark.")
print(hamlet.get_txt())
hamlet.set_text("I'll make a ghost of him who lets me.")
print(hamlet.get_txt())

This produces:

There is something rotten in the state of Denmark.
I'll make a ghost of him who lets me.

All things considered, this is pretty ugly. Python is all about openness, and privates are not. Also, the class definition is hidden because of a technical necessity. This is bad.

Summing it up: Nice that you can do it, but ... please don't.

(Full code here.)