关于python:如何创建类属性?

How to make a class property?

在python中,我可以使用@classmethod修饰器向类中添加一个方法。是否有类似的修饰符向类中添加属性?我最好把我说的话说出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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

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

我之所以需要类属性,是因为我可以懒惰地加载类属性,这似乎很合理。


我会这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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

我们称之为Bar.bar时,setter不起作用,因为我们正在呼叫TypeOfBar.bar.__set__,不是Bar.bar.__set__

添加元类定义可以解决此问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)

现在一切都会好起来的。


如果您定义classproperty如下,那么您的示例将完全按照您的要求工作。

1
2
3
4
5
class classproperty(object):
    def __init__(self, f):
        self.f = f
    def __get__(self, obj, owner):
        return self.f(owner)

警告是,不能将此用于可写属性。虽然e.I = 20将引发AttributeError,但Example.I = 20将覆盖属性对象本身。


我想你可以用元类来做这个。因为元类可以类似于类的类(如果有意义的话)。我知道您可以为元类分配一个__call__()方法来重写调用类MyClass()的过程。我想知道在元类上使用property修饰器是否也会有类似的操作。(我以前没试过,但现在我很好奇…)

[更新:]

哇,真管用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

注意:这是在Python2.7中。python 3+使用不同的技术来声明元类。使用方法:class MyClass(metaclass=MetaClass):,取下__metaclass__,其余相同。


[答:基于python 3.4编写;元类语法在2中有所不同,但我认为该技术仍然有效]

你可以用一个元类来实现这一点…主要是。Dappawit几乎成功了,但我认为它有一个缺陷:

1
2
3
4
5
6
7
class MetaFoo(type):
    @property
    def thingy(cls):
        return cls._thingy

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

这在foo上为您提供了一个ClassProperty,但有一个问题…

1
2
3
4
5
6
7
8
9
10
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....?

这到底是怎么回事?为什么我不能从实例中访问类属性?

在我找到我相信的答案之前,我在这上面打了很长一段时间。python@properties是描述符的子集,从描述符文档(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.

因此,方法解析顺序不包括类属性(或元类中定义的任何其他属性)。有可能使内置属性装饰器的子类行为有所不同,但(需要引用)我有这样一个印象,即开发人员有一个很好的理由(我不理解)这样做。

这并不意味着我们运气不好;我们可以很好地访问类本身的属性……我们可以从实例中的type(self)获取类,我们可以使用它来生成@property调度程序:

1
2
3
4
5
6
class Foo(object, metaclass=MetaFoo):
    _thingy = 23

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

现在,Foo().thingy既适用于类,也适用于实例!如果一个派生类替换它的基础_thingy,它也将继续做正确的事情(这是我最初在这个搜索中使用的用例)。

这对我来说并不是100%令人满意——必须在元类和对象类中进行设置,感觉这违反了dry原则。但后者只是一个单线调度器;我对它的存在基本上是满意的,如果你真的想要的话,你可以把它压缩成一个lambda或者其他东西。


据我所知,如果不创建新的元类,就无法为类属性编写setter。

我发现以下方法有效。定义一个包含所有所需类属性和setter的元类。我想要一个拥有title属性和setter的类。我写的是:

1
2
3
4
5
6
7
8
9
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...

现在,将您想要的实际类设置为普通类,除非让它使用您在上面创建的元类。

1
2
3
4
5
6
7
8
# 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...

如果我们只在单个类上使用这个元类,那么像上面那样定义这个元类有点奇怪。在这种情况下,如果您使用的是python 2样式,那么实际上可以在类体中定义元类。这样,模块范围中就没有定义它了。


如果您只需要延迟加载,那么您可以使用类初始化方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

但元类方法似乎更清晰,并且具有更可预测的行为。

也许你要找的是单体设计模式。在Python中实现共享状态有一个很好的质量保证。


我偶然想出了一个与@andrew非常相似的解决方案,只不过是干的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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

这是最好的答案:

"metaproperty"被添加到类中,以便它仍然是实例的属性

  • 不需要在任何类中重新定义thingy
  • 在实例和类中,该属性作为"类属性"工作。
  • 您可以灵活定制继承方式
  • 在我的例子中,我实际上为每个孩子定制了不同的_thingy,而没有在每个类中定义它(也没有默认值),方法是:

    1
    2
    3
       def __new__(mc1, name, bases, nmspc):
           nmspc.update({'thingy': MetaFoo.services, '_thingy': None})
           return super(MetaFoo, mc1).__new__(mc1, name, bases, nmspc)