The following are a set of functions for creating simple properties - like Ruby's attr_reader, attr_writer, and attr_accessor.
If, inside a class definition, you write:
        attribute(foo=1, bar=2)
simple properties named 'foo' and 'bar' are created for this class. Also, private instance variables '__foo' and '__bar' will be added to instances of this class.
By "simple properties", I mean something like the following:
        ''' assume we're inside a class definition and         self.__foo and self.__bar have been instantiated.         '''         def get_foo(self):                 return self.__foo         def set_foo(self, value):                 self.__foo = value         def del_foo(self):                 del self.__foo         def get_bar(self):                 return self.__bar         def set_bar(self, value):                 self.__bar = value         def del_bar(self):                 del self.__bar         foo = property(fget=get_foo, fset=set_foo, fdel=del_foo, doc="foo")         bar = property(fget=get_bar, fset=set_bar, fdel=del_bar, doc="bar")
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | """ attribute.py
Provides functions for creating simple properties.
If, inside a class definition, you write:
attribute(foo=1, bar=2)
simple properties named 'foo' and 'bar' are created for this class.
Also, private instance variables '__foo' and '__bar' will be added
to instances of this class.
USEAGE:
# assumes attribute.py is on path
from attribute import *
class MyClass(object):
readable(foo=1, bar=2) # or, attribute('r', foo=1, bar=2)
writable(fro=3, boz=4) # or, attribute('w', fro=3, boz=4)
attribute(baz=5)
This is equivalent to the following:
class MyClass(object):
def __init__(self):
self.__foo = 1
self.__bar = 2
self.__fro = 3
self.__boz = 4
self.__baz = 5
def get_foo(self):
return self.__foo
def get_bar(self):
return self.__bar
def set_fro(self, value):
self.__fro = value
def set_boz(self, value):
self.__boz = value
def get_baz(self):
return self.__baz
def set_baz(self, value):
self.__baz = value
def del_baz(self):
del self.__baz
foo = property(fget=get_foo, doc="foo")
bar = property(fget=get_bar, doc="bar")
fro = property(fset=set_fro, doc="fro")
boz = property(fset=set_boz, doc="boz")
baz = property(fget=get_baz, fset=set_baz, fdel=del_baz, doc="baz")
"""
__all__ = ['attribute', 'readable', 'writable']
__version__ = '3.0'
__author__ = 'Sean Ross'
__credits__ = ['Guido van Rossum', 'Garth Kidd']
__created__ = '10/21/02'
import sys
def mangle(classname, attrname):
"""mangles name according to python name-mangling
conventions for private variables"""
return "_%s__%s" % (classname, attrname)
def class_space(classlevel=3):
"returns the calling class' name and dictionary"
frame = sys._getframe(classlevel)
classname = frame.f_code.co_name
classdict = frame.f_locals
return classname, classdict
# convenience function
def readable(**kwds):
"returns one read-only property for each (key,value) pair in kwds"
return _attribute(permission='r', **kwds)
# convenience function
def writable(**kwds):
"returns one write-only property for each (key,value) pair in kwds"
return _attribute(permission='w', **kwds)
# needed because of the way class_space is resolved in _attribute
def attribute(permission='rwd', **kwds):
"""returns one property for each (key,value) pair in kwds;
each property provides the specified level of access(permission):
'r': readable, 'w':writable, 'd':deletable
"""
return _attribute(permission, **kwds)
# based on code by Guido van Rossum, comp.lang.python 2001-07-31
def _attribute(permission='rwd', **kwds):
"""returns one property for each (key,value) pair in kwds;
each property provides the specified level of access(permission):
'r': readable, 'w':writable, 'd':deletable
"""
classname, classdict = class_space()
def _property(attrname, default):
propname, attrname = attrname, mangle(classname, attrname)
fget, fset, fdel, doc = None, None, None, propname
if 'r' in permission:
def fget(self):
value = default
try: value = getattr(self, attrname)
except AttributeError: setattr(self, attrname, default)
return value
if 'w' in permission:
def fset(self, value):
setattr(self, attrname, value)
if 'd' in permission:
def fdel(self):
try: delattr(self, attrname)
except AttributeError: pass
# calling fget can restore this attribute, so remove property
delattr(self.__class__, propname)
return property(fget=fget, fset=fset, fdel=fdel, doc=doc)
for attrname, default in kwds.items():
classdict[attrname] = _property(attrname, default)
|
For simple properties, defining the get, set, and/or del methods you'll pass to property() can be repetitive.
attribute.py provides functions which can be used in any new-style class to simplify (and reduce the code required for) the creation of these properties.
You may also be interested in the following metaclass implementation: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/197965
correction. should have been:
def set_baz(self, value): self.__baz = value
baz = property(fset=set_baz)
not "bar"
sorry for any confusion,
Sean
correction. should have been:
def set_baz(self, value): self.__baz = value
baz = property(fset=set_baz)
not "bar"
sorry for any confusion,
Sean
re: correction. The corrections noted above have been made.
Breaks on inheritance. mangle's reliance on obj.__class__ means that it'll be unable to find the private variable if someone attempts to fetch a property via a subclass. To fix it, we need to get the class name somehow. Fortunately, that's in the stack, too. :)
Posted updated module as source distribution. I've posted a source distribution for this [1], with an explanation on my blog [2]. To avoid treading on Sean's code, I've used distutils to give appropriate credit: him as the author, with me in as a maintainer. Sean, if you want to take back over and maintain at your site, let me know. 1: http://www.deadlybloodyserious.com/Python/files/prop-1.5.zip
2: http://www.deadlybloodyserious.com/Python/2003/01/09.html#a1047
Small Problem. Hi.
First, thank you for pointing out this algorithms limitation with
regard to inheritance. I've taken your suggestions and incorporated
them into the code(as you'll notice). I've made some changes; mostly
for personal taste. For instance, I changed __classAndLeftOperands__()
to __outside__() (yours is more descriptive, but it is quite long)
Anyway, there was one change I made that was not purely cosmetic:
className = stack[1][2]
became
cls = stack[-3:][0][-2]
This is important. On my system, for instance, to gain access to the appropriate stack frame from the front(?) of the stack, I need to call:
className = stack[6][2]
to get the intended results. My system has more overhead, seemingly.
Anyway, accessing the data from the back of the stack seems more reliable, as is evidenced by the original __leftoperands__() having
worked on your system.
Other than that, this is a fine improvement. Thanks.
Now, if you can figure out how to remove the requirement for having to provide the private instance variables for each property, that'd be great.
Aha! I'd noticed that my code was extremely brittle -- readonly() would fail if I compiled my application with py2exe, and intermittently if I tried to reload a module. I'll try it with your modifications!
No go. Py2exe strips source lines... ... so __outside__ can't get the source line. I'm exploring aloud on my blog, and will report back if I make any progress. Or, not. :)
http://www.deadlybloodyserious.com/Python/2003/01/10.html#a1050
Got it! Try something along these lines:
... and then...
... to create the read-only attributes. Not depending on the source, it works properly under py2exe. That said, the syntax leaves a lot to be desired.
Better yet! This works under python, python -OO, and py2exe:
When you update the code, set __version__ to "1.7"; using a float also breaks py2exe. :)
Sorry. Unfortunately, the new __outside__() doesn't work on my system.
Using the test code I provide in the comments, but with all code
in __main__ below test = Test() commented out and the new
__outside__() with print statements, i.e.,
def __outside__():
 ...
 cls, names = code.co_name, list(code.co_names)
 print "class: ", cls
 print "names: ", names
 ...
 args = names[:names.index(caller)]
 print "args: ", args
 ...
I get the following output:
class: Test
names:['__name__', '__module__', 'readonly', 'foo', 'bar', 'writeonly', 'fro', 'boz', '__init__']
caller: readonly
args: ['__init__', 'boz', 'fro', 'writeonly', 'bar', 'foo']
class: Test
names: ['__name__', '__module__', 'readonly', 'foo', 'bar', 'writeonly', 'fro', 'boz', '__init__']
caller: writeonly
args: ['__init__', 'boz', 'fro']
You'll notice that the args for readonly is _not_ ['foo', 'bar'].
So, unfortunately, __outside__() remains brittle, so long as
your intent is to use py2exe.
Version 1.6 seems ...ok..., otherwise.
deprecate __outside__(). For the moment __outside__() remains brittle. I propose that it be deprecated and that readonly2() replace readonly().
readonly2() appears to be stable. The syntax is ok, for now.
I do have one minor change:
def __readonly(name):
 name = mangle(cls, name)
 def get(obj):
 return property(fget=get)
I realized that mangle() was being called every time a property was used, not just when the property is created, which is, of course, unnecessary.
Until __outside__() can be made stable, I recommend that it not be used.
Let me know what you think.
If you agree, I will update the current version to reflect these decisions.
Just one more idea:
What I'd like to see is
def readonly(**kwds): ...
so that the usage would be
readonly(foo=1, bar=2)
More importantly, I'd like to remove the requirement for providing the private instance variables '__foo', '__bar',
i.e., I'd prefer not to have to do this:
readonly('foo', 'bar')
...
def __init__(self):
 self.__foo = 1
 self.__bar = 2
 ...
In the meantime, readonly2() will suffice.
Let me know your thoughts.
Sean
This works. Try this:
def readonly(**kwds):
 stack = traceback.extract_stack()
 cls = stack[-2:][0][-2]
 frame = sys._getframe(1)
 def __readonly(name, default):
 for name,value in kwds.items():
usage: readonly(foo=1, bar=2)
Note: does not require you to provide private instance variables __foo and __bar (!)
It's a bit of a bodge, because you have to set the private instance variable every time the property is called. It's nicer in writeonly, where this bodge is not required.
I like this. Let me know what you think before I update the recipe.
fixed bodge. def __readonly(name, default):
 name = mangle(cls, name)
 def get(obj):
 return property(fget=get)
This avoids calling setattr() every time property is called. It is only called if getattr() fails, i.e., on the first call. All subsequent calls to getattr() will succeed, and setattr() will not be called.
Version 1.8. I've decided to go ahead and post version 1.8. I've changed __outside__() so that now it simply takes care of the repetitive code that was in both readonly() and writeonly().
Let me know if this latest version gives you any problems. For now, I'm very happy with this version. This is what I was hoping to make all along. Thanks for your help.
Sean
Aah. I think I get it, now. The problem with my __outside__ is that it assumes that readonly() or writeonly() are the LAST items called. In your test code, __init__ is defined lastish, hence the problem. I guess we could search through the frame above's locals to check to see whether the names are defined.
The new keyword argument style certainly saves us having to grope around for the names, but can we stick with the use of sys._getframe()? Its results seem a little more deterministic than wading through the trace.
co_stacksize? code.co_stacksize might have the number of lvars we need.
... but it's still breaking if I use readonly() twice, so I'm not quite there yet.
Note also that I missed that my earlier code was accidentally reversing the arguments. Whups.
I'm not so allergic to having to define the instance variables myself in __init__, especially as I'd be using self.__varname inside my class for performance reasons. I'm also worried about readability; with the first syntax, it's immediately obvious to the reader that variables are being defined, even if they're not sure exactly how.
I've removed the traceback code, and have replaced it with sys._getframe() code.
You can still initialize the corresponding private instance variables(CPIVs) yourself, and you can still use them as self.__foo inside your class. But, the thing I like about how readonly() and writeonly() work now, is that you don't have to initialize those variables. What does this accomplish? Well, if you look at the test code I include in the discussion, you'll see a reduction from 18 lines to 5 lines of code(from defining read/writeonly props. the old way to defining them our way). In fact, you save 3 lines of code for each read/writeonly prop. If you init your CPIVs, you still save 2 lines/prop. It's up to you whether you want to declare them yourself or not.
Regarding readability, I agree that
foo, bar = readonly()
more obviously appears to be assigning readonly properties to the class. Still,
readonly(foo=1, bar=2)
has the benefit of creating self.__foo and self.__bar, "automagically", with default values. The previous syntax would continue to require that CPIVs be hand-coded. That is not what I want.
Explicit may be better than implicit, but in this case, I would say:
Is it more obvious from
foo, bar = readonly()
that you must make CPIVs by hand, or
is it more obvious from
readonly(foo=1, bar=2)
that CPIVs have been created for you?
When I think about it, neither is overly obvious, but I think the former is less so than the latter.
You could combine them, I suppose.
foo, bar = readonly(foo=1, bar=2)
This does appear to be more explicit, if not entirely more obvious. And it allows you to supply default or initial values for your readonly properties.
Note: foo, bar, fro, boz = readonly(1,2,3,4), is just wrong
foo, bar, fro, boz = readonly(foo=1, bar=2, fro=3, boz=4),
I think, is better. While
readonly(foo=1, bar=2, fro=3, boz=4)
alone, without assignment, is acceptable to me. Or, perhaps
__readonly__(foo=1, bar=2, fro=3, boz=4)
to let people know there's "automagic" happening...
I wish ASPN had stuck with linear, non-heirachial discussion threads. I think we're vectoring in on a choice between
foo, bar = readonly(foo=2)
(foo
defaulted,bar
requiring manual attribute creation) and something like__readonly__('bar', foo=2)
. I like the double-underscorage making it obvious that we're doing something a little different.Aaaah. Of course. If we make initialisation mandatory, then we know how many items to kick out, and we don't need to grope through frames trying to figure out the names of our lvars...
It seems to me the ideas in this recipe could be accomplished more easily using a metaclass. First, suppose that __readonly__ creates one or more temporary instances of some class and stores them in its caller's locals dict. Assuming this dict is the locals of the class-defining function, it is the same dict which is passed to the metaclass's __new__ function, along with the new class's name. So in the __new__ function, you simply hunt through the dict to look for instances of your temporary class, and then replace them with properly formed property instances. E.g.:
Then real classes can use ExtProp as a mixin base class to get this special property support.
I like this solution. It's clever, and reading it was an education. The only advantage I see to Garth's and my solution over yours is the fact that our solution does not require users to make their classes subclass ExtProp to get the desired behaviour. If they put rowo.py in their path, they can just import the behaviour, e.g.
 from rowo import *
But, that's not much of a difference. It's just a matter of taste, I suppose. I happen to prefer:
 from rowo import *
 class MyClass(object):
to
 from ExtProp import *
 class MyClass(ExtProp):
Probably because the latter gives me the impression that my class is dependent upon another class. The former does not leave me with this impression. Other than that, I really did enjoy reading your code. Thank you very much for your contribution.
Sean