如何创建类属性?

在Python中,我可以使用__abc0 decorator向类添加方法。有没有类似的修饰器来向类添加属性?我可以更好地展示我所说的。

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

我在上面使用的语法是可能的,还是需要更多的东西?

我想要类属性的原因是我可以延迟加载类属性,这看起来很合理。

251301 次浏览

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.

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.

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.

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.

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.

[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.

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)
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


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