如何制作一个类属性?

def*_*ode 113 python properties class-method

在python中,我可以使用@classmethod装饰器向类添加方法.是否有类似的装饰器向类中添加属性?我可以更好地展示我在说什么.

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
Run Code Online (Sandbox Code Playgroud)

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

我想要类属性的原因是我可以延迟加载类属性,这似乎足够合理.

Mah*_*der 82

这是我将如何做到这一点:

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
Run Code Online (Sandbox Code Playgroud)

在我们打电话的时候,设定者没有工作Bar.bar,因为我们正在打电话 TypeOfBar.bar.__set__,但事实并非 如此Bar.bar.__set__.

添加元类定义解决了这个问题:

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)
Run Code Online (Sandbox Code Playgroud)

现在一切都会好的.

  • 对于使用 python 3 的用户,在类定义中,使用 `class Bar(metaclass= ClassPropertyMetaClass):` 而不是内部的 `__metaclass__ = ClassPropertyMetaClass`。 (4认同)
  • `__set__`永远不会在我的例子中被调用...所以我可以一直设置我的`classproperty`的值..(我没有为我的classproperty定义一个cetter) (2认同)
  • `__set__` 函数有一个小问题: `type_ = type(obj)` 行需要跟在 `if type_ == ClassPropertyMetaClass: type_ = obj` 行(写在这行后面)。否则它无法在实例级别正常工作。您还可以使用包 [classutilities](https://github.com/david-salac/classutilities/) 来代替(请在 PyPi.org 上查看)来实现此逻辑。 (2认同)

jch*_*chl 37

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

class classproperty(object):
    def __init__(self, f):
        self.f = f
    def __get__(self, obj, owner):
        return self.f(owner)
Run Code Online (Sandbox Code Playgroud)

需要注意的是,您不能将其用于可写属性.虽然e.I = 20会引发一个AttributeError,但Example.I = 20会覆盖属性对象本身.

  • 关于在这种情况下如何进行 mypy 打字有什么想法吗? (3认同)

dap*_*wit 25

我想你可以用元类做到这一点.因为元类可以像类的类(如果有意义的话).我知道你可以__call__()为metaclass 分配一个方法来覆盖调用类,MyClass().我想知道property在元类上使用装饰器是否操作类似.(我之前没试过,但现在我好奇......)

[更新:]

哇,确实有效:

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
Run Code Online (Sandbox Code Playgroud)

注意:这是在Python 2.7中.Python 3+使用不同的技术来声明元类.使用:class MyClass(metaclass=MetaClass):,删除__metaclass__,其余是相同的.

  • 像`property`这样的描述符需要在类型的字典中才能发挥其魔力.因此,类定义中的那些主要影响类实例的行为,对类本身的行为影响最小(因为类是实例的类型).将描述符移动到元类允许它们在类本身上运行它们的魔法(因为元类是类的类型). (2认同)

And*_*rew 21

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

你可以用元类来做这件事......大多数情况下.Dappawit几乎可以工作,但我认为它有一个缺陷:

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

class Foo(object, metaclass=MetaFoo):
    _thingy = 23
Run Code Online (Sandbox Code Playgroud)

这会让你获得Foo的一个类属性,但是有一个问题......

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....?
Run Code Online (Sandbox Code Playgroud)

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

在找到我认为的答案之前,我在这方面打了很长时间.Python @properties是描述符的子集,并且从描述符文档(强调我的):

属性访问的默认行为是从对象的字典中获取,设置或删除属性.例如,a.x有一个查找链,从a.__dict__['x'],然后type(a).__dict__['x'],继续通过type(a) 排除元类的基类开始.

因此,方法解析顺序不包括我们的类属性(或元类中定义的任何其他内容).这可能使该行为不同,但(引文需要)我已经得到的印象google搜索,开发商有一个很好的理由(我不明白)内置物业装饰的子类做这种方式.

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

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

    @property
    def thingy(self):
        return type(self).thingy
Run Code Online (Sandbox Code Playgroud)

现在Foo().thingy适用于类和实例!如果派生类替换了它的底层_thingy(这是最初让我进行此搜索的用例),它也将继续做正确的事情.

这对我来说并不是100%满足 - 不得不在元类和对象类中进行设置,感觉它违反了DRY原则.但后者只是一个单线调度员; 我现在对它很好,如果你真的想要的话,你可以将它压缩成lambda或者其他东西.

  • 这应该是最佳答案,因为它普遍适用,也适用于子类。 (2认同)
  • 谢谢你千遍!现在清楚了!哇哦! (2认同)

X''*_*X'' 6

如果您使用Django,则它具有内置的@classproperty装饰器。

from django.utils.decorators import classproperty
Run Code Online (Sandbox Code Playgroud)

  • 刚刚看到您添加了导入行。嗯,您是对的,只是没有记录在这里!我在这里找不到任何结果:https://docs.djangoproject.com/en/2.2/search/?q=classproperty现在看起来不错。 (2认同)
  • 它没有被删除,而是移至另一个模块。https://docs.djangoproject.com/en/3.1/ref/utils/#django.utils.function.classproperty (2认同)

Art*_*are 5

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

我发现以下方法有效。使用您想要的所有类属性和设置器定义一个元类。IE,我想要一个title带有 setter 属性的类。这是我写的:

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...
Run Code Online (Sandbox Code Playgroud)

现在像往常一样制作你想要的实际类,除了让它使用你在上面创建的元类。

# 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...
Run Code Online (Sandbox Code Playgroud)

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