Gere
Gere

Reputation: 12687

Decorator for many properties in Python

Is it possible to write a decorator that creates many properties at once?

Like instead of writing

class Test:
    @property
    def a(self):
        return self.ref.a
    @property
    def b(self):
        return self.ref.b

I'd like to write

class Test:
    @properties("a", "b")
    def prop(self, name):
        return getattr(self.ref, name)

Is it possible? Do you recommend it?

Upvotes: 5

Views: 233

Answers (4)

glglgl
glglgl

Reputation: 91017

This is not possible from within the class. However, you can modify the class afterwards. See here:

def makeprop(meth, name):
    # make a property calling the given method.
    # It is not really a method, but it gets called with the "self" first...
    return property(lambda self: meth(self, name))

def propfor(cls, *names):
    def wrap(meth):
        for name in names:
            # Create a property for a given object, in this case self,
            prop = makeprop(meth, name)
            setattr(cls, name, prop)
        return meth # unchanged
    return wrap

class O(object):
    # just a dummy for your ref
    a = 9
    b = 12
    c = 199

class C(object):
    ref = O()

# Put the wanted properties into the class afterwards:
@propfor(C, "a", "b", "c")
def prop(self, name):
    return getattr(self.ref, name)

# alternative approach with a class decorator:
def propdeco(*names):
    meth = names[-1]
    names = names[:-1]
    def classdeco(cls):
        propfor(cls, *names)(meth) # not nice, but reuses code above
        return cls
    return classdeco

@propdeco("a", "b", "c", lambda self, name: getattr(self.ref, name))
class D(object):
    ref = O()

print C().a
print C().b
print C().c

print D().a
print D().b
print D().c

If you prefer the 2nd approach, you should write propdeco as

def propdeco(*names):
    meth = names[-1]
    names = names[:-1]
    def classdeco(cls):
        for name in names:
            # Create a property for a given object, in this case self,
            prop = makeprop(meth, name)
            setattr(cls, name, prop)
        return cls
    return classdeco

Upvotes: 0

jsbueno
jsbueno

Reputation: 110156

One provision in the lagnuage for what you probably really intend is writting the __setattr__ method for a class.

This method is caleed whenever an attribute that normally does not exist is is acessed on the instance:

>>> class Test(object):
...    a = 0
...    def __getattr__(self, attr):
...       return attr
... 
>>> t = Test()
>>> t.a
0
>>> t.b
'b'
>>> t.c
'c'

What you are directly asking is also possible, bute requires some hacks - that although not advisable by common sense, are widely used in production in the wild. Namely, for a property to exist in Python, it is a class attribute ound to a special type of object - one that has at least the __get__ method. (to learn more check about the "Descriptor Protocol" on Python docs).

Now, trying to create several properties at once, like the code you pasted by example, would require that the property names would be injected in the class name space from the called function. It _ is possible_, and even used in production in Python, and not evenhard to achieve. But not pretty, nonetheless.

So, a possible way of avoiding this is to have a call that returns a sequence of "property" objects - that is clean, readable and maintanable:

class MultiProperty(object):
    def __init__(self, getter, setter, name):
        self.getter = getter
        self.setter = setter
        self.name = name
    def __get__(self, instance, owner):
        return self.getter(instance, self.name)
    def __set__(self, instance, value):
        return self.setter(instance, self.name, value)

def multi_property(mgetter, msetter, *args):
    props = []
    for name in args:
        props.append(MultiProperty(mgetter, msetter, name))
    return props


class Test(object):
    def multi_getter(self, attr_name):
        # isf desired, isnert some logic here
        return getattr(self, "_" + attr_name)

    def multi_setter(self, attr_name, value):
        # insert some logic here
        return setattr(self, "_" + attr_name, value)        
    a,b,c = multi_property(multi_getter, multi_setter, *"a b c".split())

Upvotes: 2

ecatmur
ecatmur

Reputation: 157314

Recall that a decorator

@decorator(dec_args)
def foo(args):
    pass

is just syntactic sugar for writing

def foo(args):
    pass
foo = decorator(dec_args)(foo)

So it is not possible for a method decorator to result in more than one method (or property, etc.) to be added to a class.

An alternative might be a class decorator that injects the properties:

def multi_property(prop, *names):
    def inner(cls):
        for name in names:
            setattr(cls, name, property(lambda self, name=name: prop(self, name)))
    return inner

@multi_property(prop, 'a', 'b')
class Test:
    pass

However it'd usually be clearer to have each property present within the body of the class:

a = forward_property('ref', 'a')
b = forward_property('ref', 'b')

where forward_property returns a property object as appropriate implementing the descriptor protocol. This is friendlier to documentation and other static analysis tools, as well as (usually) the reader.

Upvotes: 3

Sven Marnach
Sven Marnach

Reputation: 601391

The easiest way of writing a proxy for another object is to implement __getattr__():

class Proxy(object):
    def __init__(self, ref1, ref2):
        self._ref1 = ref1
        self._ref2 = ref2
    def __getatrr__(self, name):
        if name in ["a", "b", "c"]:
            return getattr(self._ref1, name)
        if name in ["d", "e", "f"]:
            return getattr(self._ref2, name)

Note that __getattr__() is only called for attributes that are not found in the current instance, so you can also add further methods and attributes to Proxy.

Upvotes: 2

Related Questions