web-dev-qa-db-ja.com

Pythonデータクラスとプロパティデコレータ

Python 3.7のデータクラス(構造体でデータをグループ化する必要があるときに通常使用するもの)を読んでいます。dataclassがdataclassのデータ要素のゲッター関数とセッター関数を定義するプロパティデコレーター。もしそうなら、これはどこかで説明されていますか?または、使用可能な例はありますか?

14
GertVdE

それは確かに機能します:

from dataclasses import dataclass

@dataclass
class Test:
    _name: str="schbell"

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, v: str) -> None:
        self._name = v

t = Test()
print(t.name) # schbell
t.name = "flirp"
print(t.name) # flirp
print(t) # Test(_name='flirp')

実際、なぜそうすべきではないのですか?結局、得られるのは、型から派生した古き良きクラスです。

print(type(t)) # <class '__main__.Test'>
print(type(Test)) # <class 'type'>

たぶんそれが、プロパティが特に言及されていない理由です。ただし、 PEP-557の要約 は、よく知られているPythonクラス機能の一般的なユーザビリティについて言及しています。

データクラスは通常のクラス定義構文を使用するため、継承、メタクラス、ドキュメント文字列、ユーザー定義メソッド、クラスファクトリ、およびその他のPythonクラス機能を自由に使用できます。

11
shmee

現在、私が見つけた最良の方法は、別の子クラスのプロパティでデータクラスフィールドを上書きすることでした。

_from dataclasses import dataclass, field

@dataclass
class _A:
    x: int = 0

class A(_A):
    @property
    def x(self) -> int:
        return self._x

    @x.setter
    def x(self, value: int):
        self._x = value
_

クラスは通常のデータクラスのように動作します。また、___repr___および___init___フィールド(A(x=4)ではなくA(_x=4)を正しく定義します。欠点は、プロパティを読み取り専用にすることができないことです。

このブログ投稿 は、wheels dataclass属性を同じ名前のpropertyで上書きしようとします。ただし、_@property_はデフォルトのfieldを上書きするため、予期しない動作が発生します。

_from dataclasses import dataclass, field

@dataclass
class A:

    x: int

    # same as: `x = property(x)  # Overwrite any field() info`
    @property
    def x(self) -> int:
        return self._x

    @x.setter
    def x(self, value: int):
        self._x = value

A()  # `A(x=<property object at 0x7f0cf64e5fb0>)`   Oups

print(A.__dataclass_fields__)  # {'x': Field(name='x',type=<class 'int'>,default=<property object at 0x>,init=True,repr=True}
_

これを解決する1つの方法は、継承を回避する一方で、データクラスメタクラスが呼び出された後、クラス定義の外のフィールドを上書きすることです。

_@dataclass
class A:
  x: int

def x_getter(self):
  return self._x

def x_setter(self, value):
  self._x = value

A.x = property(x_getter)
A.x = A.x.setter(x_setter)

print(A(x=1))
print(A())  # missing 1 required positional argument: 'x'
_

おそらく、カスタムメタクラスをいくつか作成し、field(metadata={'setter': _x_setter, 'getter': _x_getter})を設定することで、これを自動的に上書きできるはずです。

1
Conchylicultor

見つけることができるデータクラスとプロパティに関する非常に詳細な投稿に従います here TL; DRバージョンは、MyClass(_my_var=2)と奇妙な__repr__出力:

from dataclasses import field, dataclass

@dataclass
class Vehicle:

    wheels: int
    _wheels: int = field(init=False, repr=False)

    def __init__(self, wheels: int):
       self._wheels = wheels

    @property
    def wheels(self) -> int:
         return self._wheels

    @wheels.setter
    def wheels(self, wheels: int):
        self._wheels = wheels
0
bluesummers

いくつかのラッピングは良いかもしれません:

# Copyright 2019 Xu Siyuan
# 
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# 
# http://www.Apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License. 

from dataclasses import dataclass, field

MISSING = object()
__all__ = ['property_field', 'property_dataclass']


class property_field:
    def __init__(self, fget=None, fset=None, fdel=None, doc=None, **kwargs):
        self.field = field(**kwargs)
        self.property = property(fget, fset, fdel, doc)

    def getter(self, fget):
        self.property = self.property.getter(fget)
        return self

    def setter(self, fset):
        self.property = self.property.setter(fset)
        return self

    def deleter(self, fdel):
        self.property = self.property.deleter(fdel)
        return self


def property_dataclass(cls=MISSING, / , **kwargs):
    if cls is MISSING:
        return lambda cls: property_dataclass(cls, **kwargs)
    remembers = {}
    for k in dir(cls):
        if isinstance(getattr(cls, k), property_field):
            remembers[k] = getattr(cls, k).property
            setattr(cls, k, getattr(cls, k).field)
    result = dataclass(**kwargs)(cls)
    for k, p in remembers.items():
        setattr(result, k, p)
    return result

次のように使用できます。

@property_dataclass
class B:
    x: int = property_field(default_factory=int)

    @x.getter
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value
0
no1xsyzy

@propertyは通常、一見公開されている引数(nameなど)をゲッターとセッターを通じてプライベート属性(_nameなど)に格納するために使用されますが、データクラスは__init__()メソッドを生成します。問題は、この生成された__init__()メソッドは、パブリック属性_nameを内部的に設定しながら、パブリック引数nameを介してインターフェイスする必要があることです。これはデータクラスによって自動的には行われません。

値の設定とオブジェクトの作成に(nameを介して)同じインターフェイスを使用するために、次の戦略を使用できます(詳細については、 このブログ投稿 にも基づいています)。

from dataclasses import dataclass, field

@dataclass
class Test:
    name: str
    _name: str = field(init=False, repr=False)

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, name: str) -> None:
        self._name = name

これは、データメンバーnameを持つデータクラスから期待されるように使用できるようになりました。

my_test = Test(name='foo')
my_test.name = 'bar'
my_test.name('foobar')
print(my_test.name)

上記の実装は次のことを行います。

  • nameクラスメンバーはパブリックインターフェイスとして使用されますが、実際には何も格納しません
  • _nameクラスメンバーは、実際のコンテンツを格納します。 field(init=False, repr=False)を使用した割り当てにより、__init__()および__repr__()メソッドを構築するときに@dataclassデコレータがそれを無視するようになります。
  • nameのゲッター/セッターは実際に_nameのコンテンツを返す/設定します
  • @dataclassを通じて生成された初期化子は、先ほど定義したセッターを使用します。 _nameを明示的に初期化しません。
0
JorenV

上記のアイデアから、@ shmeeで提案されているゲッター関数とセッター関数を含む新しいクラスを作成するクラスデコレーター関数resolve_abc_propを作成しました。

def resolve_abc_prop(cls):
    def gen_abstract_properties():
        """ search for abstract properties in super classes """

        for class_obj in cls.__mro__:
            for key, value in class_obj.__dict__.items():
                if isinstance(value, property) and value.__isabstractmethod__:
                    yield key, value

    abstract_prop = dict(gen_abstract_properties())

    def gen_get_set_properties():
        """ for each matching data and abstract property pair, 
            create a getter and setter method """

        for class_obj in cls.__mro__:
            if '__dataclass_fields__' in class_obj.__dict__:
                for key, value in class_obj.__dict__['__dataclass_fields__'].items():
                    if key in abstract_prop:
                        def get_func(self, key=key):
                            return getattr(self, f'__{key}')

                        def set_func(self, val, key=key):
                            return setattr(self, f'__{key}', val)

                        yield key, property(get_func, set_func)

    get_set_properties = dict(gen_get_set_properties())

    new_cls = type(
        cls.__name__,
        cls.__mro__,
        {**cls.__dict__, **get_set_properties},
    )

    return new_cls

ここでは、データクラスADatamixinAOpMixinを定義して、データに対する操作を実装します。

from dataclasses import dataclass, field, replace
from abc import ABC, abstractmethod


class AOpMixin(ABC):
    @property
    @abstractmethod
    def x(self) -> int:
        ...

    def __add__(self, val):
        return replace(self, x=self.x + val)

最後に、デコレータresolve_abc_propを使用して、ADataからのデータとAOpMixinからの操作で新しいクラスを作成します。

@resolve_abc_prop
@dataclass
class A(AOpMixin):
    x: int

A(x=4) + 2   # A(x=6)

編集#1:pythonパッケージを作成して、抽象プロパティをデータクラスで上書きできるようにします: dataclass-abc

0