Python, Properties and Future Class Changes
December 10, 2007
For a while now I’ve been thinking about class attributes in dynamically typed languages. I usually view these languages as “less than 50,000 line program languages”. For more lines than that I personally want the type safety and compile checks of a statically typed compiled language (like C++). Granted, a 50,000 line Python program would probably have as much functionality as a half million line C++ program, and with a full suite of unit tests the Python program may be just as reliable and resilient to change (if not more so) than a static compiler checking your work ala C++.
Anyway, because of this “small project” view, and increased experience, most of the time I don’t want to create boilerplate code in my dynamic language class… and accessor methods seem like just that. Ruby has a way around that with its attr_reader
/attr_writter
, but I do most of my scripting in Python.
But let me add that in languages that lack the ability to retain the same client interface while completely changing the behavior: direct data access to a function call in this case, I’d still write boilerplate getters/setters. Like when I’m writing Applescript modules… So you could say that Ruby/Python does encapsulation one better: you can change from accessing data to calling a function without knowing it.
The other day I ran across blog post at codefork.com that talks about Python’s take on encapsulation. Python’s approach is very pragmatic: modify the members directly then when you need to - and only when you need to - use Python’s property keyword to make Python call a instance method instead of looking for a data member of that name.
Version 1 of your class could be: class Person: def __init__(self): self.age = None jeff = Person() jeff.age = 5 # maturity
Version 2 could be: class Person(object): def __init__(self): self._age = None def get_age(self): return self._age def set_age(self, value): self._age = value def del_age(self): del self._age age = property(get_age, set_age, del_age) jeff = Person() jeff.age = 5
And the codefork article stops there. But there’s more…
Say we plan to deprecate something (the age
property), but we want to give our callers time to adjust before we pull the rug out from under them. We take another Python feature to do this: decorators.
To keep this simple we’ll use the most simple decorator: there is a better version of this in the Decorator Library (jump to it)
Version 3: import warnings def deprecated(fObj): warnings.warn("%s is depricated as of version 3. It will be removed completly for version 4" % (fObj.__name__), DeprecationWarning, stacklevel=3 ) return fObj class Person(object): def __init__(self): self._age = None @deprecated def get_age(self): return self._age @deprecated def set_age(self, value): self._age = value @deprecated def del_age(self): del self._age age = property(get_age, set_age, del_age) jeff = Person() jeff.age = 5
warning.warn generates a warning which either could be ignored by the program, sent to standard out, or raise an exception - depending on a value you pass to Python.
The decorator is called automatically by Python before the actual function is called. Think of it like manually calling deprecated(); jeff.get_age() - although its more technically deprecated(jeff.get_age)
.
Normally decorators make one wonder “so why not just call the one function then the other function and avoid the extra language feature?” In cases like these, where changing client code isn’t an option, or if the function gets called automatically - an age = 5 will call set_age() - decorators make it easy to change stuff around without anybody else knowing.
So we’ve gone through the whole life of an attribute: creation/ “let’s get it working for now”; refinement/ “ok: this needs to happen differently, but lets not tear up everything to do it”; to death/ “ok, we have to remove this, there are better ways now… we need to announce that its going away so people avoid it… then we can pull life support”. We have a way to avoid the broilerplate of tradition encapsulation and also retaining complete control over the class’ destiny - which is what encapsulation is about: preventing internal changes from affecting callers.