0

I have posted a similar question before but I interpreted my problem wrong, so I flagged it for deletion and come forth with the new and correct problem.

My general goal is the following: I want to have a property on a class, so I implement it on a metaclass using a property as suggested on my question Implementing a class property that preserves the docstring. Additionally, I want users to be able to subclass the base class and override this property with static values. The thing here is that if the user does not provide an attribute, I want to calculate a sensible default and since all configuration is done at the class level, I need a property at the class level.

A basic example will show this:

class Meta(type):
    @property
    def test(self):
        return "Meta"


class Test(object):
    __metaclass__ = Meta


class TestSub(Test):
    test = "TestSub"


class TestSubWithout(Test):
    pass

print(TestSub.test, TestSubWithout.test)

Here is what it prints:

('Meta', 'Meta')

And what I want it to print:

('TestSub', 'Meta')

Essentially, on TestSub the user overrides the test attribute himself. Now, TestSub is the correct output, since the user provided it. In the case of TestSubWithout, the user instead did not provide the attribute and so the default should be calculated (of course, the real calculation has more than just a static return, this is just to show it).

I know what happens here: First the attribute test is assigned to TestSub and then the metaclass overrides it. But I don't know how to prevent it in a clean and pythonic way.

Community
  • 1
  • 1
javex
  • 7,198
  • 7
  • 41
  • 60
  • "First the attribute test is assigned to TestSub and then the metaclass overrides it." - that's not quite what happens. The `'TestSub'` value is still there in the class dict, but since `property` instances are [data descriptors](http://docs.python.org/2/reference/datamodel.html#invoking-descriptors), a property found in the metaclass will be used in preference to a value found in the dict of the class or its ancestors. – user2357112 Mar 17 '14 at 23:46
  • possible duplicate of [How to create a class property with a metaclass that only overrides when the class itself doesn't have it defined?](http://stackoverflow.com/questions/22462796/how-to-create-a-class-property-with-a-metaclass-that-only-overrides-when-the-cla) – Matthew Trevor Mar 18 '14 at 00:17
  • @MatthewTrevor Yes that is the post I was referring to in the beginning. I didn't link it because I expect it to be deleted by a mod in favor of this question. – javex Mar 18 '14 at 00:28

2 Answers2

1

I cleanest way I could come up with is creating a subclass of property that handles this case:

class meta_property(property):
    def __init__(self, fget, fset=None, fdel=None, doc=None):
        self.key = fget.__name__
        super(meta_property, self).__init__(fget, fset, fdel, doc)

    def __get__(self, obj, type_):
        if self.key in obj.__dict__:
            return obj.__dict__[self.key]
        else:
            return super(meta_property, self).__get__(obj, type_)

This handles the case by storing the name of the function and returning the overridden value if it is present. This seems like an okayish solution but I am open to more advanced suggestions.

javex
  • 7,198
  • 7
  • 41
  • 60
  • If you want inherited values to take priority over the property, this won't work. The class's value only takes effect for that specific class, not its subclasses. – user2357112 Mar 17 '14 at 23:54
  • @user2357112 I don't find that to be true, I tested it with multiple inheritances, it still works. – javex Mar 18 '14 at 00:32
  • Define `class Foo(object): __metaclass__ = Meta; test = 'Foo'` and `class Bar(Foo): pass`. `Bar` doesn't inherit `Foo`'s `test` value. – user2357112 Mar 18 '14 at 00:41
1

A property is a "data descriptor", which means that it takes precedence in the attribute search path over values stored in a instance dictionary (or for a class, the instance dictionaries of the other classes in its MRO).

Instead, write your own non-data descriptor that works like property:

class NonDataProperty(object):
    def __init__(self, fget):
        self.fget = fget

    def __get__(self, obj, type):
        if obj:
            return self.fget(obj)
        else:
            return self

    # don't define a __set__ method!

Here's a test of it:

class MyMeta(type):
    @NonDataProperty
    def x(self):
        return "foo"

class Foo(metaclass=MyMeta):
    pass # does not override x

class Bar(metaclass=MyMeta):
    x = "bar" # does override x

class Baz:
    x = "baz"

class Baz_with_meta(Baz, metaclass=MyMeta):
    pass # inherited x value will take precedence over non-data descriptor

print(Foo.x) # prints "foo"
print(Bar.x) # prints "bar"
print(Baz_with_meta.x) # prints "baz"
Blckknght
  • 100,903
  • 11
  • 120
  • 169
  • What's the advantage of this over my solution? Do I miss something here? To me it seems very similar. – javex Mar 18 '14 at 00:33
  • Your type of property won't work in the `Baz_with_meta` case. The inherited instance value will be overridden by the metaclass's property. – Blckknght Mar 18 '14 at 00:41
  • You are right, I read up on data- vs. non-data-descriptors. Yours works better. – javex Mar 18 '14 at 11:48