Reputation: 16958
I have a class with a lot of very similar properties:
class myClass(object):
def compute_foo(self):
return 3
def compute_bar(self):
return 4
@property
def foo(self):
try:
return self._foo
except AttributeError:
self._foo = self.compute_foo()
return self._foo
@property
def bar(self):
try:
return self._bar
except AttributeError:
self._bar = self.compute_bar()
return self._bar
...
So thought I would write a decorator to do the property definition work.
class myDecorator(property):
def __init__(self, func, prop_name):
self.func = func
self.prop_name = prop_name
self.internal_prop_name = '_' + prop_name
def fget(self, obj):
try:
return obj.__getattribute__(self.internal_prop_name)
except AttributeError:
obj.__setattr__(self.internal_prop_name, self.func(obj))
return obj.__getattribute__(self.internal_prop_name)
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.func is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
class myClass(object):
def compute_foo(self):
return 3
foo = myDecorator(compute_foo, 'foo')
def compute_bar(self):
return 4
bar = myDecorator(compute_bar, 'bar')
This works well, but when I want to use the @myDecorator('foo')
syntax, it gets more complicated and cannot figure what the __call__
method should return and how to attach the property to its class.
For the moment I have :
class myDecorator(object):
def __init__(self, prop_name):
self.prop_name = prop_name
self.internal_prop_name = '_' + prop_name
def __call__(self, func):
self.func = func
return #???
def fget(self, obj):
try:
return obj.__getattribute__(self.internal_prop_name)
except AttributeError:
obj.__setattr__(self.internal_prop_name, self.func(obj))
return obj.__getattribute__(self.internal_prop_name)
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.func is None:
raise AttributeError("unreadable attribute")
return self.fget(obj)
class myClass(object):
@myDecorator('foo')
def compute_foo(self):
return 3
c = myClass()
print(c.foo)
and it returns: AttributeError: 'myClass' object has no attribute 'foo'
Upvotes: 1
Views: 138
Reputation: 16958
I ended up with a metaclass, to make the subclassing easier. Thanks to Brendan Abel for hinting in this direction.
import types
class PropertyFromCompute(property):
def __init__(self, func):
self.func = None
self.func_name = func.__name__
self.internal_prop_name = self.func_name.replace('compute', '')
def fget(self, obj):
try:
return obj.__getattribute__(self.internal_prop_name)
except AttributeError:
obj.__setattr__(self.internal_prop_name, self.func())
return obj.__getattribute__(self.internal_prop_name)
def __get__(self, obj, objtype=None):
if obj is None:
return self
if self.func is None:
try:
self.func = obj.__getattribute__(self.func_name)
except AttributeError:
raise AttributeError("unreadable attribute")
return self.fget(obj)
class WithPropertyfromCompute(type):
def __new__(cls, clsname, bases, dct):
add_prop = {}
for name, obj in dct.items():
if isinstance(obj, types.FunctionType) and name.startswith('compute_'):
add_prop.update({name.replace('compute_',''): PropertyFromCompute(obj)})
dct.update(add_prop)
return super().__new__(cls, clsname, bases, dct)
class myClass(object, metaclass=WithPropertyfromCompute):
def compute_foo(self):
raise NotImplementedError('Do not instantiate the base class, ever !')
class myChildClass(myClass):
def compute_foo(self):
return 4
base = myClass()
try:
print(base.foo)
except NotImplementedError as e:
print(e)
print(myClass.foo)
child = myChildClass()
print(child.foo)
Upvotes: 0
Reputation: 2280
You could always use the wraps trick to pass arguments to your decorator as follows:
from functools import wraps
class myDecorator(property):
def __init__(self, prop_name):
self.prop_name = prop_name
def __call__(self, wrappedCall):
@wraps(wrappedCall)
def wrapCall(*args, **kwargs):
klass = args[0]
result = wrappedCall(*args, **kwargs)
setattr(klass, self.prop_name, result)
return wrapCall
class myClass(object):
@myDecorator('foo')
def compute_foo(self):
return 3
c = myClass()
c.compute_foo()
print c.foo
Upvotes: 2
Reputation: 37499
If you want to use the @decorator
syntax you're not going to be able to remap the property to a different name on the class. That means your compute_x
methods are going to have to be renamed the same as the attribute.
EDIT: It is possible to remap the names, but you'd need to use a class decorator as well.
class MyProperty(property):
def __init__(self, name, func):
super(MyProperty, self).__init__(func)
self.name = name
self.internal_prop_name = '_' + name
self.func = func
def fget(self, obj):
try:
return obj.__getattribute__(self.internal_prop_name)
except AttributeError:
obj.__setattr__(self.internal_prop_name, self.func(obj))
return obj.__getattribute__(self.internal_prop_name)
def __get__(self, obj, objtype=None)
if obj is None:
return self
if self.func is None:
raise AttributeError('unreadable')
return self.fget(obj)
def myproperty(*args)
name = None
def deco(func):
return MyProperty(name, func)
if len(args) == 1 and callable(args[0]):
name = args[0].__name__
return deco(args[0])
else:
name = args[0]
return deco
class Test(object):
@myproperty
def foo(self):
return 5
Without the class decorator, the only time the name argument would be relevant would be if your internal variable name was different from the function name, so you could so something like
@myproperty('foobar')
def foo(self):
return 5
and it would look for _foobar
instead of _foo
, but the attribute name would still be foo
.
However, there is a way you could remap the attribute names, but you'd have to use a class decorator as well.
def clsdeco(cls):
for k, v in cls.__dict__.items():
if isinstance(v, MyProperty) and v.name != k:
delattr(cls, k)
setattr(cls, v.name, v)
return cls
@clsdeco
class Test(...)
@myproperty('foo')
def compute_foo(self):
pass
This will go through all the attributes on the class and find any that are MyProperty
instances and check if the set name is the same as the mapped name, if not, it will rebind the property to the name passed into the myproperty
decorator.
Upvotes: 1