How to make a class property? [duplicate]
Asked Answered
S

9

223

In python I can add a method to a class with the @classmethod decorator. Is there a similar decorator to add a property to a class? I can better show what I'm talking about.

class Example(object):
   the_I = 10
   def __init__( self ):
      self.an_i = 20

   @property
   def i( self ):
      return self.an_i

   def inc_i( self ):
      self.an_i += 1

   # is this even possible?
   @classproperty
   def I( cls ):
      return cls.the_I

   @classmethod
   def inc_I( cls ):
      cls.the_I += 1

e = Example()
assert e.i == 20
e.inc_i()
assert e.i == 21

assert Example.I == 10
Example.inc_I()
assert Example.I == 11

Is the syntax I've used above possible or would it require something more?

The reason I want class properties is so I can lazy load class attributes, which seems reasonable enough.

Silo answered 4/3, 2011 at 4:34 Comment(5)
I don't know python... is this the kind of stuff you are looking for ? python.org/download/releases/2.2/descrintro/#propertyCostly
if i'm reading that right, you can create methods to act as the setter and getter (as in other languages) so you can lazy load the property value on the first get... which i think is what you wanted?Costly
@White Dragon. The property feature you're looking at adds properties to class instances, not to the classes themselves. I'm asking about Example.I not e.i.Silo
Here's solution to create class property in another topic: https://mcmap.net/q/18830/-how-to-create-a-read-only-class-property-in-python-duplicateLevitation
Dupe: Using property() on classmethodsMongoose
I
129

Here's how I would do this:

class ClassPropertyDescriptor(object):

    def __init__(self, fget, fset=None):
        self.fget = fget
        self.fset = fset

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        return self.fget.__get__(obj, klass)()

    def __set__(self, obj, value):
        if not self.fset:
            raise AttributeError("can't set attribute")
        type_ = type(obj)
        return self.fset.__get__(obj, type_)(value)

    def setter(self, func):
        if not isinstance(func, (classmethod, staticmethod)):
            func = classmethod(func)
        self.fset = func
        return self

def classproperty(func):
    if not isinstance(func, (classmethod, staticmethod)):
        func = classmethod(func)

    return ClassPropertyDescriptor(func)


class Bar(object):

    _bar = 1

    @classproperty
    def bar(cls):
        return cls._bar

    @bar.setter
    def bar(cls, value):
        cls._bar = value


# test instance instantiation
foo = Bar()
assert foo.bar == 1

baz = Bar()
assert baz.bar == 1

# test static variable
baz.bar = 5
assert foo.bar == 5

# test setting variable on the class
Bar.bar = 50
assert baz.bar == 50
assert foo.bar == 50

The setter didn't work at the time we call Bar.bar, because we are calling TypeOfBar.bar.__set__, which is not Bar.bar.__set__.

Adding a metaclass definition solves this:

class ClassPropertyMetaClass(type):
    def __setattr__(self, key, value):
        if key in self.__dict__:
            obj = self.__dict__.get(key)
        if obj and type(obj) is ClassPropertyDescriptor:
            return obj.__set__(self, value)

        return super(ClassPropertyMetaClass, self).__setattr__(key, value)

# and update class define:
#     class Bar(object):
#        __metaclass__ = ClassPropertyMetaClass
#        _bar = 1

# and update ClassPropertyDescriptor.__set__
#    def __set__(self, obj, value):
#       if not self.fset:
#           raise AttributeError("can't set attribute")
#       if inspect.isclass(obj):
#           type_ = obj
#           obj = None
#       else:
#           type_ = type(obj)
#       return self.fset.__get__(obj, type_)(value)

Now all will be fine.

Icy answered 4/3, 2011 at 8:12 Comment(2)
For those who use python 3, in class definition, use class Bar(metaclass= ClassPropertyMetaClass): instead of __metaclass__ = ClassPropertyMetaClass inside.Breazeale
There is just a little problem in __set__ function: line type_ = type(obj) needs to be followed with if type_ == ClassPropertyMetaClass: type_ = obj (write it right after this line). It does not work correctly on the instance level otherwise. You can also use package classutilities instead (see it on PyPi.org) that implements exactly this logic.Bignoniaceous
S
66

If you define classproperty as follows, then your example works exactly as you requested.

class classproperty(object):
    def __init__(self, f):
        self.f = f
    def __get__(self, obj, owner):
        return self.f(owner)

The caveat is that you can't use this for writable properties. While e.I = 20 will raise an AttributeError, Example.I = 20 will overwrite the property object itself.

Stenophagous answered 4/3, 2011 at 10:13 Comment(1)
ANy thoughts on how to do the mypy typing in this case ?Landry
I
50

[answer written based on python 3.4; the metaclass syntax differs in 2 but I think the technique will still work]

You can do this with a metaclass...mostly. Dappawit's almost works, but I think it has a flaw:

class MetaFoo(type):
    @property
    def thingy(cls):
        return cls._thingy

class Foo(object, metaclass=MetaFoo):
    _thingy = 23

This gets you a classproperty on Foo, but there's a problem...

print("Foo.thingy is {}".format(Foo.thingy))
# Foo.thingy is 23
# Yay, the classmethod-property is working as intended!
foo = Foo()
if hasattr(foo, "thingy"):
    print("Foo().thingy is {}".format(foo.thingy))
else:
    print("Foo instance has no attribute 'thingy'")
# Foo instance has no attribute 'thingy'
# Wha....?

What the hell is going on here? Why can't I reach the class property from an instance?

I was beating my head on this for quite a while before finding what I believe is the answer. Python @properties are a subset of descriptors, and, from the descriptor documentation (emphasis mine):

The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. For instance, a.x has a lookup chain starting with a.__dict__['x'], then type(a).__dict__['x'], and continuing through the base classes of type(a) excluding metaclasses.

So the method resolution order doesn't include our class properties (or anything else defined in the metaclass). It is possible to make a subclass of the built-in property decorator that behaves differently, but (citation needed) I've gotten the impression googling that the developers had a good reason (which I do not understand) for doing it that way.

That doesn't mean we're out of luck; we can access the properties on the class itself just fine...and we can get the class from type(self) within the instance, which we can use to make @property dispatchers:

class Foo(object, metaclass=MetaFoo):
    _thingy = 23

    @property
    def thingy(self):
        return type(self).thingy

Now Foo().thingy works as intended for both the class and the instances! It will also continue to do the right thing if a derived class replaces its underlying _thingy (which is the use case that got me on this hunt originally).

This isn't 100% satisfying to me -- having to do setup in both the metaclass and object class feels like it violates the DRY principle. But the latter is just a one-line dispatcher; I'm mostly okay with it existing, and you could probably compact it down to a lambda or something if you really wanted.

Ibo answered 7/8, 2016 at 3:36 Comment(2)
This should be the top answer as it works universally and for sub-classes as well.Indubitable
Thank you thousand times! Now its clear! Wwhooffh!Featherstone
L
42

If you use Django, it has a built in @classproperty decorator.

from django.utils.decorators import classproperty

For Django 4, use:

from django.utils.functional import classproperty
Liddie answered 16/7, 2019 at 10:30 Comment(5)
Just saw you added the import line. Ah you're right, it's there just not documented! I couldn't find any results here: docs.djangoproject.com/en/2.2/search/?q=classproperty Looks good now.Moua
yes, indeed, I managed to dig this up by chance... I think I did a search on the library codebase with the idea that this should have been implemented somewhere.Liddie
this looks like following ~~~ class classproperty: def __init__(self, method=None): self.fget = method def __get__(self, instance, cls=None): return self.fget(cls) def getter(self, method): self.fget = method return self ~~~ not sure if helpfulMithraism
This was removed in Django 3.1 and can be found at the bottom of this file if you want to copy-pasta docs.djangoproject.com/en/3.0/_modules/django/utils/decoratorsElderly
It was not removed but moved to another module. docs.djangoproject.com/en/3.1/ref/utils/…Falcone
R
29

I think you may be able to do this with the metaclass. Since the metaclass can be like a class for the class (if that makes sense). I know you can assign a __call__() method to the metaclass to override calling the class, MyClass(). I wonder if using the property decorator on the metaclass operates similarly.

Wow, it works:

class MetaClass(type):    
    def getfoo(self):
        return self._foo
    foo = property(getfoo)
    
    @property
    def bar(self):
        return self._bar
    
class MyClass(object):
    __metaclass__ = MetaClass
    _foo = 'abc'
    _bar = 'def'
    
print MyClass.foo
print MyClass.bar

Note: This is in Python 2.7. Python 3+ uses a different technique to declare a metaclass. Use: class MyClass(metaclass=MetaClass):, remove __metaclass__, and the rest is the same.

Renfroe answered 4/3, 2011 at 4:44 Comment(4)
So it can be done, but can it be done with a method decorator?Silo
There is no decorator I'm aware of that you can use directly on the class you're interested in. However, the property decorator in the metaclass should work.... I've edited my answer to include a decorated metaclass method.Renfroe
Also see this similar question and its answers. Seems to be similar, so it may have some more helpful information :)Alboran
Descriptors like property need to be in the type's dictionary to work their magic. So those in a class definition primarily affect the behaviour of instances of the class, with minimal effect on the behaviour of the class itself (since the class is the type of the instances). Moving the descriptors to the metaclass allows them to work their magic on the class itself (since the metaclass is the type of the class).Woodrow
D
6

As far as I can tell, there is no way to write a setter for a class property without creating a new metaclass.

I have found that the following method works. Define a metaclass with all of the class properties and setters you want. IE, I wanted a class with a title property with a setter. Here's what I wrote:

class TitleMeta(type):
    @property
    def title(self):
        return getattr(self, '_title', 'Default Title')

    @title.setter
    def title(self, title):
        self._title = title
        # Do whatever else you want when the title is set...

Now make the actual class you want as normal, except have it use the metaclass you created above.

# Python 2 style:
class ClassWithTitle(object):
    __metaclass__ = TitleMeta
    # The rest of your class definition...

# Python 3 style:
class ClassWithTitle(object, metaclass = TitleMeta):
    # Your class definition...

It's a bit weird to define this metaclass as we did above if we'll only ever use it on the single class. In that case, if you're using the Python 2 style, you can actually define the metaclass inside the class body. That way it's not defined in the module scope.

Drayton answered 28/2, 2016 at 20:8 Comment(0)
E
4
def _create_type(meta, name, attrs):
    type_name = f'{name}Type'
    type_attrs = {}
    for k, v in attrs.items():
        if type(v) is _ClassPropertyDescriptor:
            type_attrs[k] = v
    return type(type_name, (meta,), type_attrs)


class ClassPropertyType(type):
    def __new__(meta, name, bases, attrs):
        Type = _create_type(meta, name, attrs)
        cls = super().__new__(meta, name, bases, attrs)
        cls.__class__ = Type
        return cls


class _ClassPropertyDescriptor(object):
    def __init__(self, fget, fset=None):
        self.fget = fget
        self.fset = fset

    def __get__(self, obj, owner):
        if self in obj.__dict__.values():
            return self.fget(obj)
        return self.fget(owner)

    def __set__(self, obj, value):
        if not self.fset:
            raise AttributeError("can't set attribute")
        return self.fset(obj, value)

    def setter(self, func):
        self.fset = func
        return self


def classproperty(func):
    return _ClassPropertyDescriptor(func)



class Bar(metaclass=ClassPropertyType):
    __bar = 1

    @classproperty
    def bar(cls):
        return cls.__bar

    @bar.setter
    def bar(cls, value):
        cls.__bar = value

bar = Bar()
assert Bar.bar==1
Bar.bar=2
assert bar.bar==2
nbar = Bar()
assert nbar.bar==2

Eisenhart answered 5/7, 2019 at 6:31 Comment(0)
I
2

I happened to come up with a solution very similar to @Andrew, only DRY

class MetaFoo(type):

    def __new__(mc1, name, bases, nmspc):
        nmspc.update({'thingy': MetaFoo.thingy})
        return super(MetaFoo, mc1).__new__(mc1, name, bases, nmspc)

    @property
    def thingy(cls):
        if not inspect.isclass(cls):
            cls = type(cls)
        return cls._thingy

    @thingy.setter
    def thingy(cls, value):
        if not inspect.isclass(cls):
            cls = type(cls)
        cls._thingy = value

class Foo(metaclass=MetaFoo):
    _thingy = 23

class Bar(Foo)
    _thingy = 12

This has the best of all answers:

The "metaproperty" is added to the class, so that it will still be a property of the instance

  1. Don't need to redefine thingy in any of the classes
  2. The property works as a "class property" in for both instance and class
  3. You have the flexibility to customize how _thingy is inherited

In my case, I actually customized _thingy to be different for every child, without defining it in each class (and without a default value) by:

   def __new__(mc1, name, bases, nmspc):
       nmspc.update({'thingy': MetaFoo.services, '_thingy': None})
       return super(MetaFoo, mc1).__new__(mc1, name, bases, nmspc)
Intumescence answered 4/1, 2019 at 19:21 Comment(0)
P
1

If you only need lazy loading, then you could just have a class initialisation method.

EXAMPLE_SET = False
class Example(object):
   @classmethod 
   def initclass(cls):
       global EXAMPLE_SET 
       if EXAMPLE_SET: return
       cls.the_I = 'ok'
       EXAMPLE_SET = True

   def __init__( self ):
      Example.initclass()
      self.an_i = 20

try:
    print Example.the_I
except AttributeError:
    print 'ok class not "loaded"'
foo = Example()
print foo.the_I
print Example.the_I

But the metaclass approach seems cleaner, and with more predictable behavior.

Perhaps what you're looking for is the Singleton design pattern. There's a nice SO QA about implementing shared state in Python.

Phosphorescence answered 4/3, 2011 at 5:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.