Python 3.7のデータクラス(構造体でデータをグループ化する必要があるときに通常使用するもの)を読んでいます。dataclassがdataclassのデータ要素のゲッター関数とセッター関数を定義するプロパティデコレーター。もしそうなら、これはどこかで説明されていますか?または、使用可能な例はありますか?
それは確かに機能します:
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クラス機能を自由に使用できます。
現在、私が見つけた最良の方法は、別の子クラスのプロパティでデータクラスフィールドを上書きすることでした。
_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})
を設定することで、これを自動的に上書きできるはずです。
見つけることができるデータクラスとプロパティに関する非常に詳細な投稿に従います 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
いくつかのラッピングは良いかもしれません:
# 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
@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
を明示的に初期化しません。上記のアイデアから、@ 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
ここでは、データクラスAData
とmixinAOpMixin
を定義して、データに対する操作を実装します。
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