How to make a class property?
在python中,我可以使用
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 |
我们称之为
添加元类定义可以解决此问题:
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) |
现在一切都会好起来的。
如果您定义
1 2 3 4 5 | class classproperty(object): def __init__(self, f): self.f = f def __get__(self, obj, owner): return self.f(owner) |
警告是,不能将此用于可写属性。虽然
我想你可以用元类来做这个。因为元类可以类似于类的类(如果有意义的话)。我知道您可以为元类分配一个
[更新:]
哇,真管用:
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+使用不同的技术来声明元类。使用方法:
[答:基于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 witha.__dict__['x'] , thentype(a).__dict__['x'] , and continuing
through the base classes oftype(a) excluding metaclasses.
因此,方法解析顺序不包括类属性(或元类中定义的任何其他属性)。有可能使内置属性装饰器的子类行为有所不同,但(需要引用)我有这样一个印象,即开发人员有一个很好的理由(我不理解)这样做。
这并不意味着我们运气不好;我们可以很好地访问类本身的属性……我们可以从实例中的
1 2 3 4 5 6 | class Foo(object, metaclass=MetaFoo): _thingy = 23 @property def thingy(self): return type(self).thingy |
现在,
这对我来说并不是100%令人满意——必须在元类和对象类中进行设置,感觉这违反了dry原则。但后者只是一个单线调度器;我对它的存在基本上是满意的,如果你真的想要的话,你可以把它压缩成一个lambda或者其他东西。
据我所知,如果不创建新的元类,就无法为类属性编写setter。
我发现以下方法有效。定义一个包含所有所需类属性和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"被添加到类中,以便它仍然是实例的属性
在我的例子中,我实际上为每个孩子定制了不同的
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) |