web-dev-qa-db-ja.com

クラスプロパティの作成方法

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

上記で使用した構文は可能ですか、それとももっと必要ですか?

クラスプロパティが必要な理由は、クラス属性を遅延ロードできるからです。

109
deft_code

これは私がこれを行う方法です:

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ではない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)

これですべてが正常になります。

76

classpropertyを次のように定義すると、例は要求どおりに機能します。

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

注意点は、書き込み可能なプロパティにはこれを使用できないことです。 e.I = 20AttributeErrorを発生させますが、Example.I = 20はプロパティオブジェクト自体を上書きします。

35
jchl

メタクラスでこれを行うことができると思います。メタクラスはクラスのクラスのようなものになる可能性があるため(それが理にかなっている場合)。 __call__()メソッドをメタクラスに割り当てて、クラス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

注:これはPython 2.7にあります。 Python 3+は、異なる手法を使用してメタクラスを宣言します。使用:class MyClass(metaclass=MetaClass):、削除__metaclass__、その他は同じです。

24
dappawit

[python 3.4に基づいて書かれた回答;メタクラスの構文は2で異なりますが、この手法は引き続き機能すると思います]

メタクラスを使用してこれを行うことができます...ほとんど。 Dappawitはほとんど動作しますが、欠陥があると思います。

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

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

これにより、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....?

ここで何が起こっているのですか?インスタンスからクラスプロパティにアクセスできないのはなぜですか?

私が答えだと思うものを見つける前に、私はこれについてかなり頭を痛めていました。 Python @propertiesは記述子のサブセットであり、 descriptor documentation (emphasis mine)から:

属性アクセスのデフォルトの動作は、オブジェクトの辞書から属性を取得、設定、または削除することですたとえば、a.xには、a.__dict__['x']で始まりtype(a).__dict__['x']で始まり、type(a)の基本クラスを含むメタクラスを除くルックアップチェーンがあります。

そのため、メソッドの解決順序には、クラスプロパティ(またはメタクラスで定義された他のもの)は含まれません。 動作が異なるビルトインプロパティデコレータのサブクラスを作成することは可能ですが、(引用が必要です)開発者には正当な理由があるという印象があります(私は理解していません)その方法でそれを行うため。

だからといって、私たちが運が悪いというわけではありません。クラス自体のプロパティに問題なくアクセスできます...そして、クラス内のtype(self)から@propertyディスパッチャを作成するために使用できます:

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

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

これで、Foo().thingyはクラスとインスタンスの両方で意図したとおりに動作します!また、派生クラスがその基礎となる_thingy(元々このハントで私を捕らえたユースケース)を置き換える場合にも正しいことをし続けます。

これは100%満足のいくものではありません。メタクラスとオブジェクトクラスの両方でセットアップを行う必要があるため、DRYの原則に違反しているように感じます。ただし、後者は1行のディスパッチャにすぎません。私はほとんどそれが存在することで大丈夫です、そしてあなたはおそらくあなたが本当に望むならラムダまたは何かにそれを圧縮することができました。

19
Andrew

私が知る限り、新しいメタクラスを作成せずにクラスプロパティのセッターを記述する方法はありません。

次の方法が機能することがわかりました。必要なすべてのクラスプロパティとセッターでメタクラスを定義します。 IE、セッターを持つtitleプロパティを持つクラスが欲しかった。ここに私が書いたものがあります:

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

上記で作成したメタクラスを使用することを除いて、実際のクラスを通常どおり作成します。

# 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スタイルを使用している場合、クラス本体内でメタクラスを実際に定義できます。そのように、それはモジュールスコープで定義されていません。

4
ArtOfWarfare

Djangoを使用する場合は、@classpropertyデコレータが組み込まれています。

from Django.utils.decorators import classproperty
3

遅延読み込みのみが必要な場合は、クラスの初期化メソッドを使用するだけで済みます。

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での共有状態の実装について a Nice SO QA があります。

1
Apalala
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

0
thinker3

私はたまたま@Andrewに非常によく似たソリューションを思いついた。

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

これはすべての答えの中で最高です:

「メタプロパティ」がクラスに追加されるため、インスタンスのプロパティのままです。

  1. どのクラスでも物事を再定義する必要はありません
  2. プロパティは、インスタンスとクラスの両方で「クラスプロパティ」として機能します。
  3. _thingyの継承方法をカスタマイズする柔軟性があります

私の場合、実際に_thingyをカスタマイズして、各クラスで定義せずに(そしてデフォルト値なしで)子ごとに異なるようにしました:

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