サブクラス化:従来の属性でプロパティをオーバーライドすることは可能ですか?


14

包括的な概念の異なる実装または特殊化であるクラスのファミリーを作成したいとします。いくつかの派生プロパティにもっともらしいデフォルト実装があると仮定しましょう。これを基本クラスに入れたい

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

したがって、サブクラスはこのかなりばかげた例でその要素を自動的に数えることができます

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

Concrete_Math_Set(1,2,3).size
# 3

しかし、サブクラスがこのデフォルトを使用したくない場合はどうなりますか?これは動作しません:

import math

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

Square_Integers_Below(7)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 3, in __init__
# AttributeError: can't set attribute

プロパティをプロパティでオーバーライドする方法はあると思いますが、それは避けたいです。基本クラスの目的は、ユーザーの生活をできるだけ簡単にすることであり、(サブクラスの狭い観点から)複雑で余分なアクセスメソッドを課すことによって膨張を追加することではありません。

できますか?そうでない場合、次善の策は何ですか?

回答:


6

プロパティは、同じ名前のインスタンス属性よりも優先されるデータ記述子です。一意の__get__()メソッドを使用して非データ記述子を定義できます。インスタンス属性は、同じ名前の非データ記述子よりも優先されます。ドキュメントを参照してください。ここでの問題は、non_data_property以下で定義されているのは計算のみを目的としていることです(セッターまたはデリーターを定義することはできません)が、この例ではそうであるようです。

import math

class non_data_property:
    def __init__(self, fget):
        self.__doc__ = fget.__doc__
        self.fget = fget

    def __get__(self, obj, cls):
        if obj is None:
            return self
        return self.fget(obj)

class Math_Set_Base:
    @non_data_property
    def size(self, *elements):
        return len(self.elements)

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1, 2, 3).size) # 3
print(Square_Integers_Below(1).size) # 1
print(Square_Integers_Below(4).size) # 2
print(Square_Integers_Below(9).size) # 3

ただし、これは、この変更を行うために基本クラスにアクセスできることを前提としています。


これは完璧にかなり近いです。つまり、ゲッターのみを定義しても、割り当てのブロック以外に何もしないセッターを暗黙的に追加しても、これはポピュリティを意味しますか?面白い。私はおそらくこの答えを受け入れます。
ポールパンツァー

はい、ドキュメントによれば、プロパティは常に3つの記述子メソッド(__get__()__set__()および__delete__())をすべて定義しAttributeError、それらに関数を提供しない場合はそれらを生成します。プロパティ実装に相当するPythonを参照してください。
Arkelis

setterまたは__set__プロパティを気にしない限り、これは機能します。ただし、これは、クラスレベル(self.size = ...)だけでなくインスタンスレベルでもプロパティを簡単に上書きできることを意味します(たとえばConcrete_Math_Set(1, 2, 3).size = 10、同等に有効です)。思考の糧:)
r.ook

また Square_Integers_Below(9).size = 1、単純な属性であるため有効です。この特定の「サイズ」の使用例では、これは扱いにくいように見えるかもしれません(代わりにプロパティを使用してください)。で属性アクセスを制御することも__setattr__()できますが、それは圧倒される可能性があります。
アルケリス

1
これらの有効なユースケースが存在する可能性があることについては異論はありません。将来のデバッグ作業が複雑になる可能性があるため、警告に言及したいと思います。あなたとOPの両方が影響を認識している限り、すべて問題ありません。
r.ook

10

これは長続きする答えであり、補足になるだけかもしれません...しかし、あなたの質問は私にウサギの穴を下るのにかかりましたので、私の発見(と痛み)も共有したいと思います。

最終的には、この答えが実際の問題に役立たない場合があります。実際、私の結論は次のとおりです。これはまったく行いません。そうは言っても、詳細を探しているので、この結論の背景は少し楽しませるかもしれません。


誤解に対処する

最初の答えは、ほとんどの場合正しいですが、常にそうであるとは限りません。たとえば、次のクラスを考えます。

class Foo:
    def __init__(self):
        self.name = 'Foo!'
        @property
        def inst_prop():
            return f'Retrieving {self.name}'
        self.inst_prop = inst_prop

inst_propは、である一方で、property取り消し不能なインスタンス属性です。

>>> Foo.inst_prop
Traceback (most recent call last):
  File "<pyshell#60>", line 1, in <module>
    Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'

それはすべて、そもそもあなたがどこpropertyで定義されているかに依存ます。@propertyクラス「スコープ」(または実際にはnamespace)内で定義されている場合、それはクラス属性になります。私の例では、クラス自体はinst_propインスタンス化されるまで何も認識しません。もちろん、ここではプロパティとしてはあまり役に立ちません。


しかし、最初に、継承の解決に関するコメントに取り組みましょう...

では、継承はこの問題にどの程度正確に影響するのでしょうか?次の記事では、このトピックについて少し詳しく説明しますが、メソッド解決の順序には多少の関連がありますが、主に、深さではなく継承の幅について説明しています。

以下の設定を前提として、私たちの発見と組み合わせます:

@property
def some_prop(self):
    return "Family property"

class Grandparent:
    culture = some_prop
    world_view = some_prop

class Parent(Grandparent):
    world_view = "Parent's new world_view"

class Child(Parent):
    def __init__(self):
        try:
            self.world_view = "Child's new world_view"
            self.culture = "Child's new culture"
        except AttributeError as exc:
            print(exc)
            self.__dict__['culture'] = "Child's desired new culture"

これらの行が実行されるとどうなるか想像してみてください:

print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

結果はこうです:

Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>

方法に注意してください:

  1. self.world_viewself.culture失敗しましたが、適用できました
  2. culture存在しないChild.__dict__mappingproxyクラスの、インスタンスと混同しないでください__dict__
  3. にはculture存在しますが、c.__dict__参照されていません。

あなたは理由を推測できるかもしれません- 非プロパティとしてクラスworld_viewによって上書きさParentれたので、それChildを上書きすることもできました。一方、cultureは継承されるため、mappingproxyの内にのみ存在しますGrandparent

Grandparent.__dict__ is: {
    '__module__': '__main__', 
    'culture': <property object at 0x00694C00>, 
    'world_view': <property object at 0x00694C00>, 
    ...
}

実際に削除しようとするとParent.culture

>>> del Parent.culture
Traceback (most recent call last):
  File "<pyshell#67>", line 1, in <module>
    del Parent.culture
AttributeError: culture

には存在しないことにも気づくでしょうParent。オブジェクトがを直接参照しているためGrandparent.cultureです。


それでは、解決命令についてはどうですか?

そこで、実際の解決順序を観察することに関心があるので、Parent.world_view代わりに削除してみましょう。

del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

結果はどうだろう?

c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>

以前のworld_view property割り当てに成功したにもかかわらず、それはGrandparentのに戻りましたself.world_view!しかしworld_view、他の答えのように、クラスレベルで強制的に変更するとどうなるでしょうか。それを削除するとどうなりますか?現在のクラス属性をプロパティに割り当てるとどうなりますか?

Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

結果は次のとおりです。

# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view

# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view

# Changing Child's world view to the property
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>

c.world_viewインスタンス属性に復元されるため、これは興味深いですChild.world_viewが、は割り当てたものです。インスタンス属性を削除すると、クラス属性に戻ります。そしてChild.world_view、プロパティにを再度割り当てた後、インスタンス属性へのアクセスを即座に失います。

したがって、次の解決順序を推測できます

  1. クラス属性が存在しそれがであるproperty場合、getterまたはを介してその値を取得しますfget(これについては後で詳しく説明します)。現在のクラスが最初に基本クラスが最後に。
  2. それ以外の場合で、インスタンス属性が存在する場合は、インスタンス属性値を取得します。
  3. それ以外の場合は、非propertyクラス属性を取得します。現在のクラスが最初に基本クラスが最後に。

その場合、ルートを削除しましょうproperty

del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

それは与える:

c.culture is: Child's desired new culture
Traceback (most recent call last):
  File "<pyshell#74>", line 1, in <module>
    print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'

ターダー! Childは、cultureへの強制的な挿入に基づいて独自のものになりましたc.__dict__Child.cultureもちろん、存在せず、ParentまたはChildクラス属性で定義されていないため、Grandparentは削除されました。


これが私の問題の根本的な原因ですか?

実際には、ありません。割り当て時にまだ確認されているエラーself.cultureは、まったく異なります。しかし、継承の順序は、背景を答えに設定します-それはpropertyそれ自体です。

前述のgetter方法にproperty加えて、その袖にいくつかの巧妙なトリックがあります。この場合に最も関連するのは、行によってトリガーされるsetter、またはfsetメソッドself.culture = ...です。または関数をproperty実装していないので、pythonは何をすべきかわからず、代わりに(つまり)をスローします。setterfgetAttributeErrorcan't set attribute

ただし、setterメソッドを実装した場合:

@property
def some_prop(self):
    return "Family property"

@some_prop.setter
def some_prop(self, val):
    print(f"property setter is called!")
    # do something else...

Childクラスをインスタンス化すると、次のようになります。

Instantiating Child class...
property setter is called!

を受け取る代わりに、AttributeError実際にsome_prop.setterメソッドを呼び出しています。これにより、オブジェクトをより詳細に制御できるようになります...以前の調査結果では、プロパティに到達するにクラス属性を上書きする必要があることがわかっています。これは、基本クラス内でトリガーとして実装できます。これが新しい例です:

class Grandparent:
    @property
    def culture(self):
        return "Family property"

    # add a setter method
    @culture.setter
    def culture(self, val):
        print('Fine, have your own culture')
        # overwrite the child class attribute
        type(self).culture = None
        self.culture = val

class Parent(Grandparent):
    pass

class Child(Parent):
    def __init__(self):
        self.culture = "I'm a millennial!"

c = Child()
print(c.culture)

その結果:

Fine, have your own culture
I'm a millennial!

TA-DAH!継承されたプロパティで独自のインスタンス属性を上書きできるようになりました!


それで、問題は解決しましたか?

... あんまり。このアプローチの問題は、適切なsetter方法を使用できないことです。に値を設定したい場合がありますproperty。ただし、設定self.culture = ...すると常にgetter(この例では実際には@propertyラップされた部分である)で定義した関数が常に上書きされます。より微妙なメジャーを追加できますが、何らかの方法で常にだけではありませんself.culture = ...。例えば:

class Grandparent:
    # ...
    @culture.setter
    def culture(self, val):
        if isinstance(val, tuple):
            if val[1]:
                print('Fine, have your own culture')
                type(self).culture = None
                self.culture = val[0]
        else:
            raise AttributeError("Oh no you don't")

# ...

class Child(Parent):
    def __init__(self):
        try:
            # Usual setter
            self.culture = "I'm a Gen X!"
        except AttributeError:
            # Trigger the overwrite condition
            self.culture = "I'm a Boomer!", True

それはだwaaaaayより、他の答えよりも複雑なsize = Noneクラスレベルで。

and 、または追加のメソッドを処理する代わりに、独自の記述子を作成することも検討できます。しかし、結局のところ、が参照されると、が常に最初にトリガーされ、参照されると、常に最初にトリガーされます。私が試した限り、それを回避する方法はありません。__get____set__self.culture__get__self.culture = ...__set__


問題の核心、IMO

私がここで見る問題は、あなたがケーキを持って食べられないことです。 property以下のように意味される記述子のようなメソッドからの便利なアクセスを持ちますgetattrsetattr。これらの方法でも別の目的を達成したい場合は、問題を求めているだけです。私はおそらくアプローチを再考するでしょう:

  1. propertyこれには本当に必要ですか?
  2. メソッドは私に何か異なるものを提供できますか?
  3. 私が必要な場合はproperty、私はそれを上書きする必要があります何らかの理由はありますか?
  4. これらpropertyが当てはまらない場合、サブクラスは本当に同じファミリーに属していますか?
  5. いずれかまたはすべてpropertyのs を上書きする必要がある場合、再割り当てによって誤ってpropertysが無効になる可能性があるため、単に再割り当てするよりも別の方法が役立ちますか?

ポイント5の場合、私のアプローチはoverwrite_prop()、現在のクラス属性を上書きする基本クラスのメソッドを使用して、propertyがトリガーされないようにすることです。

class Grandparent:
    # ...
    def overwrite_props(self):
        # reassign class attributes
        type(self).size = None
        type(self).len = None
        # other properties, if necessary

# ...

# Usage
class Child(Parent):
    def __init__(self):
        self.overwrite_props()
        self.size = 5
        self.len = 10

ご覧のように、まだ少し工夫されていますが、不可解なものより少なくとも明白ですsize = None。とはいえ、最終的にはプロパティをまったく上書きせず、設計を根本から見直します。

ここまで来たなら、私と一緒にこの旅を歩いてくれてありがとう。それは楽しい小さな運動でした。


2
わあ、どうもありがとう!これを消化するには少し時間がかかりますが、私はそれを要求したと思います。
ポールパンツァー

6

A @propertyはクラスレベルで定義されます。ドキュメンテーションは、それがどのように機能するかについて徹底的に詳しく説明しいます、プロパティの設定または取得が特定のメソッドの呼び出しに解決されると言うだけで十分です。ただし、propertyこのプロセスを管理するオブジェクトは、クラス独自の定義で定義されています。つまり、クラス変数として定義されていますが、インスタンス変数のように動作します。

この結果、クラスレベルで自由再割り当てできるようになります

print(Math_Set_Base.size)
# <property object at 0x10776d6d0>

Math_Set_Base.size = 4
print(Math_Set_Base.size)
# 4

他のクラスレベルの名前(メソッドなど)と同じように、明示的に別の方法で定義することで、サブクラスでオーバーライドできます。

class Square_Integers_Below(Math_Set_Base):
    # explicitly define size at the class level to be literally anything other than a @property
    size = None

    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Square_Integers_Below(4).size)  # 2
print(Square_Integers_Below.size)     # None

実際のインスタンスを作成するとき、インスタンス変数は単に同じ名前のクラス変数をシャドウします。propertyオブジェクトは、通常、このプロセス(すなわち適用getterとsetter)を操作するためにいくつかのペテンを使用していますが、クラスレベルの名前をプロパティとして定義されていない場合、何も特別なが起こり、そしてあなたが他の変数の期待ようにそれが動作します。


ありがとう、それはすっきりと簡単です。それは、私のクラスとその祖先にどこにもプロパティがなかったとしても、継承ツリー全体を最初に検索してから、クラスレベルの名前を探してから、__dict__?また、これを自動化する方法はありますか?はい、それだけで1行だが、それはあなたがproprtiesなどの血みどろの詳細に精通していない場合は読み取ることが非常に不可解であるもののようなものだ
ポール・パンツァー

@PaulPanzer私はこれの背後にある手順を調べていなかったので、私はあなたに満足のいく答えを自分で与えることができませんでした。必要に応じて、cpythonソースコードからパズルを解くことができます。プロセスを自動化することに関しては、最初からプロパティにしないか、コメント/ドキュメント文字列を追加して、コードを読んでいる人にあなたが何をしているのかを知らせる以外に、これを行う良い方法はないと思います。 。何か# declare size to not be a @property
Green Cloak Guy

4

(へのsize)割り当てはまったく必要ありません。sizeは基本クラスのプロパティなので、子クラスのプロパティをオーバーライドできます。

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

    # size = property(lambda self: self.elements)


class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._cap = cap

    @property
    def size(self):
        return int(math.sqrt(self._cap))

    # size = property(lambda self: int(math.sqrt(self._cap)))

平方根を事前計算することで、これを(マイクロ)最適化できます。

class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._size = int(math.sqrt(self._cap))

    @property
    def size(self):
        return self._size

これは素晴らしい点です。親プロパティを子プロパティでオーバーライドするだけです!+1
r.ook

これは簡単なことですが、単純な割り当てが行うようなメモ化プロパティを作成する必要があるという不便さを課さないように、プロパティ以外のオーバーライドを許可する方法に興味があり、具体的に尋ねましたジョブ。
ポールパンツァー

なぜそのようにコードを複雑にしたいのかわかりません。その認識の小さな価格のためにsize財産やないインスタンス属性である、あなたは何をする必要はありません。何も空想を。
chepner

私の使用例は、多くの属性を持つ多くの子クラスです。これらすべてに対して1行ではなく5行を記述する必要がないため、ある程度の空想、IMOが正当化されます。
ポールパンツァー

2

sizeクラスで定義したいようです:

class Square_Integers_Below(Math_Set_Base):
    size = None

    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

別のオプションはcap、クラスに保存し、それをsizeプロパティとして定義して計算することです(これは、基本クラスのプロパティをオーバーライドしますsize)。


2

次のようにセッターを追加することをお勧めします:

class Math_Set_Base:
    @property
    def size(self):
        try:
            return self._size
        except:
            return len(self.elements)

    @size.setter
    def size(self, value):
        self._size = value

このようにして、デフォルトの.sizeプロパティを次のようにオーバーライドできます。

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1,2,3).size) # 3
print(Square_Integers_Below(7).size) # 2

1

また、次のことができます

class Math_Set_Base:
    _size = None

    def _size_call(self):
       return len(self.elements)

    @property
    def size(self):
        return  self._size if self._size is not None else self._size_call()

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self._size = int(math.sqrt(cap))

それはきちんとしたのですが、あなたは本当に必要はありません_sizeか、_size_callそこに。あなたはsize条件として関数呼び出しをベイクし、使用されない余分なクラス参照をとる代わりtry... except... にテストするため_sizeに使用することができます。とはいえ、size = Noneそもそも単純な上書きよりも不可解だと思います。
r.ook

0

基本クラスのプロパティを派生クラス内の派生クラスの別のプロパティに設定し、基本クラスのプロパティを新しい値で使用することは可能だと思います。

最初のコードではsize、基本クラスの名前とself.size派生クラスの属性の間に一種の矛盾があります。これはself.size、派生クラスの名前をに置き換えると表示されself.lengthます。これは出力します:

3
<__main__.Square_Integers_Below object at 0x000001BCD56B6080>

私たちはメソッドの名前を置き換える場合はその後、sizelengthプログラム全体のすべての出現で、これは同じ例外が発生します。

Traceback (most recent call last):
  File "C:/Users/Maria/Downloads/so_1.2.py", line 24, in <module>
    Square_Integers_Below(7)
  File "C:/Users/Maria/Downloads/so_1.2.py", line 21, in __init__
    self.length = int(math.sqrt(cap))
AttributeError: can't set attribute

修正されたコード、またはとにかく機能するバージョンは、基本クラスからSquare_Integers_Belowメソッドsizeを別の値に設定するclassを除いて、コードをまったく同じに保つことです。

class Square_Integers_Below(Math_Set_Base):

    def __init__(self,cap):
        #Math_Set_Base.__init__(self)
        self.length = int(math.sqrt(cap))
        Math_Set_Base.size = self.length

    def __repr__(self):
        return str(self.size)

そして、すべてのプログラムを実行すると、出力は次のようになります。

3
2

これが何らかの形で役立つことを願っています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.