Python読み取り専用プロパティ


95

属性をプライベートにする必要があるのか​​、プロパティを使用する必要があるのか​​わかりません。

私は最近、セッターとゲッターがpythonicではなく、プロパティデコレーターを使用する必要があることを読みました。大丈夫です。

しかし、もし私が属性を持っている場合、それはクラスの外部から設定されてはいけませんが、読み取ることができます(読み取り専用属性)。この属性はプライベートである必要がありself._xますか?はいの場合、ゲッターを使用せずにそれをどのように読むことができますか?私が今知っている唯一の方法は書くことです

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

そうすれば属性を読み取ることができますobj.xが、設定できないobj.x = 1ので問題ありません。

しかし、設定してはならないオブジェクトを設定することについて本当に気にする必要がありますか?多分私はそれをそのままにしておくべきです。しかし、obj._xユーザーにとって読みが奇妙なため、アンダースコアを使用することはできません。使用する必要がありますがobj.x、ユーザーは、この属性を設定してはならないことを知りません。

あなたの意見と実践は何ですか?


1
プロパティの考え方は、属性のように動作しますが、追加のコードを含めることができるということです。必要なのが値を取得することだけである場合、私は気にself.xしないでくださいx。誰も変更しないことを使用して信頼します。x変更できないことを確認することが重要な場合は、プロパティを使用します。
li.davidm 2013年

また、_xまったく奇妙なことではありません。慣例により、「プライベート」なものを意味します。
li.davidm 2013年

1
_xからの読み取りが奇妙であることを意味しました。_x自体ではありません。ユーザーが_xから直接読み取っている場合、彼は無責任です。
ラファウŁużyński

3
重要!実際の設定を停止するには、クラスが新しいスタイルのクラス、つまりから継承するクラスである必要がありobjectますobj.x。古いスタイルのクラスでも実際にはを設定できますがobj.x、予期しない結果が生じます。
Ian H

プロパティを読み取り専用にする理由はいくつかあります。1つは、他の2つの(読み取り/書き込み)値をマージして構成される値がある場合です。これはメソッドで実行できますが、読み取り専用プロパティでも実行できます。
philologon

回答:


68

一般に、Pythonプログラムは、すべてのユーザーが大人に同意していることを前提として記述されている必要があるため、自分で正しく使用する責任があります。ただし、属性を設定可能にするだけでは意味のないまれな例(派生値や、一部の静的データソースから読み取られた値など)では、一般にgetter-onlyプロパティが推奨されるパターンです。


26
あなたの答えはそれ自体と矛盾しているようです。ユーザーは責任を持って正しく使用する必要があると述べ、属性を設定可能にすることは意味をなさない場合があり、getterプロパティが優先される方法であると述べました。私の意見では、属性を設定することはできません。唯一の問題は、私がこの属性を保護するか、そのままにするかです。その間に答えがあってはなりません。
ラファウŁużyński

19
いいえ、文字通り値を設定できない場合は、セッターを使用しても意味がありません。たとえば、radiusメンバーと、radiusから派生した外周属性を持つ円オブジェクトがある場合、またはいくつかの読み取り専用リアルタイムAPIをいくつかのゲッター専用プロパティでラップするオブジェクトがある場合などです。何も矛盾しません。
Silas Ray

9
しかし、責任のあるユーザーは、文字通り設定できないattrを設定しようとはしません。また、責任のないユーザーは文字通り設定できるattrを設定し、その設定が原因でコードの他の場所でエラーが発生します。したがって、最終的には両方のattrを設定できません。両方でプロパティを使用する必要がありますか、それともどちらでも使用しないでください。
ラファウŁużyński

8
しかし、責任のあるユーザー、文字通り設定できないattrを設定しようとすべきではありません。プログラミングにおいて、何かが厳密に設定不可能な値である場合、責任があるまたは賢明なことは、それが不可能であることを保証することです。これらの小さなことはすべて、信頼できるプログラムに貢献しています。
Robin Smith

6
それは多くの人々や言語がとる立場です。それがあなたが交渉できないと思う立場であるならば、あなたはおそらくPythonを使うべきではありません。
Silas Ray

72

ちょうど私の2セント、Silas Rayは正しい軌道に乗っていますが、例を追加したいと思いました。;-)

Pythonはタイプセーフでない言語であるため、合理的な(賢明な)人のようにコードを使用するには、コードのユーザーを常に信頼する必要があります。

PEP 8ごと:

非公開メソッドおよびインスタンス変数に対してのみ、1つの先行アンダースコアを使用します。

クラスで「読み取り専用」プロパティを使用して@property装飾を利用できるようにするにobjectは、新しいスタイルのクラスを利用するために、継承を行う必要があります。

例:

>>> class A(object):
...     def __init__(self, a):
...         self._a = a
...
...     @property
...     def a(self):
...         return self._a
... 
>>> a = A('test')
>>> a.a
'test'
>>> a.a = 'pleh'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

9
Pythonは型安全ではなく、動的に型付けされます。名前のマングリングは、カンニングを難しくするためのものではなく、継承が問題になる可能性のあるシナリオでの名前の衝突を防ぐためのものです(大規模なプログラミングをしていない場合は、気にする必要もありません)。
memeplex 2016年

3
ただし、変更可能なオブジェクトはとにかくこのメソッドを使用して変更できることを覚えておく必要があります。たとえばの場合self.__a = []、これa.a.append('anything')を行うことができ、機能します。
Igor

3
「合理的な(賢明な)人」がこの回答にどのような影響を与えているかは、私にはわかりません。あなたは、合理的な人がするだろうと思うこととそうでないことをどのように考えているかについてより明確にすることができますか?
winni2k 2017年

3
が@property装飾を利用するためには、オブジェクトから継承する必要があります。そうすることがこの答えの要点でした。ありがとう。
akki

2
@kkmバグがコードに忍び込むことを決して許さない唯一の方法は、コードを書かないことです。
Alechan

55

以下の仮定を回避する方法があります

すべてのユーザーが大人に同意しているため、自分で正しく使用する責任があります。

以下の私の更新をご覧ください

の使用@propertyは非常に冗長です。例:

   class AClassWithManyAttributes:
        '''refactored to properties'''
        def __init__(a, b, c, d, e ...)
             self._a = a
             self._b = b
             self._c = c
             self.d = d
             self.e = e

        @property
        def a(self):
            return self._a
        @property
        def b(self):
            return self._b
        @property
        def c(self):
            return self._c
        # you get this ... it's long

使用する

下線なし:パブリック変数です。
1つの下線:これは保護された変数です。
2つのアンダースコア:これはプライベート変数です。

最後のものを除いて、それは慣習です。それでも、本当に一生懸命努力すれば、二重下線で変数にアクセスできます。

どうしようか?Pythonでプロパティを読み取り専用にすることをあきらめますか?

見よ!read_only_properties救助へのデコレータ!

@read_only_properties('readonly', 'forbidden')
class MyClass(object):
    def __init__(self, a, b, c):
        self.readonly = a
        self.forbidden = b
        self.ok = c

m = MyClass(1, 2, 3)
m.ok = 4
# we can re-assign a value to m.ok
# read only access to m.readonly is OK 
print(m.ok, m.readonly) 
print("This worked...")
# this will explode, and raise AttributeError
m.forbidden = 4

あなたが尋ねる:

どこread_only_propertiesから来たの?

よろしくお願いします。read_only_propertiesのソースは次のとおりです。

def read_only_properties(*attrs):

    def class_rebuilder(cls):
        "The class decorator"

        class NewClass(cls):
            "This is the overwritten class"
            def __setattr__(self, name, value):
                if name not in attrs:
                    pass
                elif name not in self.__dict__:
                    pass
                else:
                    raise AttributeError("Can't modify {}".format(name))

                super().__setattr__(name, value)
        return NewClass
    return class_rebuilder

更新

この答えがそれほど注目されるとは思いもしませんでした。意外にもそうです。これにより、使用できるパッケージを作成するように促されました。

$ pip install read-only-properties

あなたのpythonシェルで:

In [1]: from rop import read_only_properties

In [2]: @read_only_properties('a')
   ...: class Foo:
   ...:     def __init__(self, a, b):
   ...:         self.a = a
   ...:         self.b = b
   ...:         

In [3]: f=Foo('explodes', 'ok-to-overwrite')

In [4]: f.b = 5

In [5]: f.a = 'boom'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-a5226072b3b4> in <module>()
----> 1 f.a = 'boom'

/home/oznt/.virtualenvs/tracker/lib/python3.5/site-packages/rop.py in __setattr__(self, name, value)
    116                     pass
    117                 else:
--> 118                     raise AttributeError("Can't touch {}".format(name))
    119 
    120                 super().__setattr__(name, value)

AttributeError: Can't touch a

1
これは本当に役に立ち、私がやりたかったことを正確に行います。ありがとうございました。ただし、Python 3がインストールされている人向けです。私はPython 2.7.8を使用しているため、ソリューションに2つのマイナーな微調整を適用する必要があります: "class NewClass(cls、<b> object <\ b>):" ... "<b> super(NewClass、self) <\ b> .__ setattr __(name、value) "。
Ying Zhang

1
さらに、クラスメンバー変数がリストおよび辞書であることにも注意する必要があります。この方法で更新されないようにすることはできません。
Ying Zhang

1
ここに1つの改善と3つの問題があります。改善:if..elif..elseブロックは必要if name in attrs and name in self.__dict__: raise Attr...なしである可能性がpassあります。問題1:このように装飾されたクラスはすべて同じ__name__で終わり、型の文字列表現も均質化されます。問題2:この装飾はカスタムを上書きします__setattr__。問題3:ユーザーはでこれを無効にすることができdel MyClass.__setattr__ます。
TigerhawkT3 2018年

言語のことだけです。「悲しいかな…」というのは「悲しいことですが…」という意味ではありません。
トーマスアンドリュース

私を妨げるものは何もないobject.__setattr__(f, 'forbidden', 42)read_only_properties二重下線名のマングリングによって処理されない追加を確認しません。
L3viathan

4

これは、読み取り専用プロパティへのわずかに異なるアプローチです。初期化する必要があるので、おそらく一度書き込みプロパティと呼ぶ必要がありますね。オブジェクトのディクショナリに直接アクセスしてプロパティを変更することを心配している私たちの間の偏執狂のために、私は「極端な」名前マングリングを導入しました:

from uuid import uuid4

class Read_Only_Property:
    def __init__(self, name):
        self.name = name
        self.dict_name = uuid4().hex
        self.initialized = False

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.dict_name]

    def __set__(self, instance, value):
        if self.initialized:
            raise AttributeError("Attempt to modify read-only property '%s'." % self.name)
        instance.__dict__[self.dict_name] = value
        self.initialized = True

class Point:
    x = Read_Only_Property('x')
    y = Read_Only_Property('y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

if __name__ == '__main__':
    try:
        p = Point(2, 3)
        print(p.x, p.y)
        p.x = 9
    except Exception as e:
        print(e)

いいね。dict_name代わりにマングルdict_name = "_spam_" + nameすると、依存関係が削除されuuid4、デバッグが非常に簡単になります。
2018年

しかし、私はp.__dict__['_spam_x'] = 5の値を変更すると言うことができるp.xので、これは十分な名前のマ​​ングリングを提供しません。
Booboo 2018年

1

最初の解決策ではreadonly属性を削除してから設定し、__ dict__をブロックしないため、読み取り専用プロパティを作成する前の2つの回答に不満があります。2番目のソリューションは、テストで回避できます。つまり、2つ設定したものと等しい値を見つけ、最終的にそれを変更します。

さて、コードについて。

def final(cls):
    clss = cls
    @classmethod
    def __init_subclass__(cls, **kwargs):
        raise TypeError("type '{}' is not an acceptable base type".format(clss.__name__))
    cls.__init_subclass__ = __init_subclass__
    return cls


def methoddefiner(cls, method_name):
    for clss in cls.mro():
        try:
            getattr(clss, method_name)
            return clss
        except(AttributeError):
            pass
    return None


def readonlyattributes(*attrs):
    """Method to create readonly attributes in a class

    Use as a decorator for a class. This function takes in unlimited 
    string arguments for names of readonly attributes and returns a
    function to make the readonly attributes readonly. 

    The original class's __getattribute__, __setattr__, and __delattr__ methods
    are redefined so avoid defining those methods in the decorated class

    You may create setters and deleters for readonly attributes, however
    if they are overwritten by the subclass, they lose access to the readonly
    attributes. 

    Any method which sets or deletes a readonly attribute within
    the class loses access if overwritten by the subclass besides the __new__
    or __init__ constructors.

    This decorator doesn't support subclassing of these classes
    """
    def classrebuilder(cls):
        def __getattribute__(self, name):
            if name == '__dict__':
                    from types import MappingProxyType
                    return MappingProxyType(super(cls, self).__getattribute__('__dict__'))
            return super(cls, self).__getattribute__(name)
        def __setattr__(self, name, value): 
                if name == '__dict__' or name in attrs:
                    import inspect
                    stack = inspect.stack()
                    try:
                        the_class = stack[1][0].f_locals['self'].__class__
                    except(KeyError):
                        the_class = None
                    the_method = stack[1][0].f_code.co_name
                    if the_class != cls: 
                         if methoddefiner(type(self), the_method) != cls:
                            raise AttributeError("Cannot set readonly attribute '{}'".format(name))                        
                return super(cls, self).__setattr__(name, value)
        def __delattr__(self, name):                
                if name == '__dict__' or name in attrs:
                    import inspect
                    stack = inspect.stack()
                    try:
                        the_class = stack[1][0].f_locals['self'].__class__
                    except(KeyError):
                        the_class = None
                    the_method = stack[1][0].f_code.co_name
                    if the_class != cls:
                        if methoddefiner(type(self), the_method) != cls:
                            raise AttributeError("Cannot delete readonly attribute '{}'".format(name))                        
                return super(cls, self).__delattr__(name)
        clss = cls
        cls.__getattribute__ = __getattribute__
        cls.__setattr__ = __setattr__
        cls.__delattr__ = __delattr__
        #This line will be moved when this algorithm will be compatible with inheritance
        cls = final(cls)
        return cls
    return classrebuilder

def setreadonlyattributes(cls, *readonlyattrs):
    return readonlyattributes(*readonlyattrs)(cls)


if __name__ == '__main__':
    #test readonlyattributes only as an indpendent module
    @readonlyattributes('readonlyfield')
    class ReadonlyFieldClass(object):
        def __init__(self, a, b):
            #Prevent initalization of the internal, unmodified PrivateFieldClass
            #External PrivateFieldClass can be initalized
            self.readonlyfield = a
            self.publicfield = b


    attr = None
    def main():
        global attr
        pfi = ReadonlyFieldClass('forbidden', 'changable')
        ###---test publicfield, ensure its mutable---###
        try:
            #get publicfield
            print(pfi.publicfield)
            print('__getattribute__ works')
            #set publicfield
            pfi.publicfield = 'mutable'
            print('__setattr__ seems to work')
            #get previously set publicfield
            print(pfi.publicfield)
            print('__setattr__ definitely works')
            #delete publicfield
            del pfi.publicfield 
            print('__delattr__ seems to work')
            #get publicfield which was supposed to be deleted therefore should raise AttributeError
            print(pfi.publlicfield)
            #publicfield wasn't deleted, raise RuntimeError
            raise RuntimeError('__delattr__ doesn\'t work')
        except(AttributeError):
            print('__delattr__ works')


        try:
            ###---test readonly, make sure its readonly---###
            #get readonlyfield
            print(pfi.readonlyfield)
            print('__getattribute__ works')
            #set readonlyfield, should raise AttributeError
            pfi.readonlyfield = 'readonly'
            #apparently readonlyfield was set, notify user
            raise RuntimeError('__setattr__ doesn\'t work')
        except(AttributeError):
            print('__setattr__ seems to work')
            try:
                #ensure readonlyfield wasn't set
                print(pfi.readonlyfield)
                print('__setattr__ works')
                #delete readonlyfield
                del pfi.readonlyfield
                #readonlyfield was deleted, raise RuntimeError
                raise RuntimeError('__delattr__ doesn\'t work')
            except(AttributeError):
                print('__delattr__ works')
        try:
            print("Dict testing")
            print(pfi.__dict__, type(pfi.__dict__))
            attr = pfi.readonlyfield
            print(attr)
            print("__getattribute__ works")
            if pfi.readonlyfield != 'forbidden':
                print(pfi.readonlyfield)
                raise RuntimeError("__getattr__ doesn't work")
            try:
                pfi.__dict__ = {}
                raise RuntimeError("__setattr__ doesn't work")
            except(AttributeError):
                print("__setattr__ works")
            del pfi.__dict__
            raise RuntimeError("__delattr__ doesn't work")
        except(AttributeError):
            print(pfi.__dict__)
            print("__delattr__ works")
            print("Basic things work")


main()

アプリ開発などの他の目的のコードではなく、プログラムを拡張するために使用するコードとして他のユーザーに配布されているライブラリコードを書く場合を除いて、読み取り専用の属性を作成しても意味がありません。__dict__の問題は解決されました。これは、__ dict__が不変のtypes.MappingProxyTypeであるため、__ dict__を介して属性を変更できないためです。__dict__の設定または削除もブロックされます。読み取り専用プロパティを変更する唯一の方法は、クラス自体のメソッドを変更することです。

私のソリューションは前の2つのソリューションよりも優れていると思いますが、改善することができます。これらはこのコードの弱点です:

a)読み取り専用属性を設定または削除するサブクラスのメソッドに追加することはできません。サブクラスで定義されたメソッドは、スーパークラスのバージョンのメソッドを呼び出しても、読み取り専用属性へのアクセスが自動的に禁止されます。

b)クラスのreadonlyメソッドを変更して、読み取り専用の制限を無効にすることができます。

ただし、クラスを編集せずに読み取り専用属性を設定または削除する方法はありません。これは命名規則に依存していません。Pythonは命名規則とそれほど一貫していないため、これは良いことです。これにより、クラス自体を編集しないと隠れた抜け穴で変更できない読み取り専用の属性を作成できます。引数としてデコレータを呼び出すときに読み取り専用の属性をリストするだけで、それらは読み取り専用になります。

Pythonの別のクラスの関数内で呼び出し元のクラス名を取得する方法の Briceの答えの功績は呼び出し元のクラスとメソッドを取得するため。


object.__setattr__(pfi, 'readonly', 'foobar')クラス自体を編集せずに、このソリューションを破ります。
L3viathan

0

インスタンスメソッドも(クラスの)属性であり、本当にワルになりたい場合はクラスまたはインスタンスレベルで設定できることに注意してください。または、クラス変数(クラスの属性でもある)を設定することもできます。この場合、便利な読み取り専用プロパティは、そのままではうまく機能しません。私が言おうとしていることは、「読み取り専用属性」の問題は、実際には通常認識されているよりも一般的であるということです。幸いなことに、これらの他のケースに対して私たちを盲目にするほど強い職場での従来の期待があります(結局のところ、ほとんどすべてがpythonのある種の属性です)。

これらの期待に基づいて、最も一般的で軽量なアプローチは、「パブリック」(先頭のアンダースコアなし)属性は、書き込み可能として明示的に文書化されている場合を除き、読み取り専用であるという規則を採用することだと思います。これは、メソッドにパッチが適用されず、インスタンスのデフォルトを示すクラス変数のほうがましであるという通常の期待を包含しています。特別な属性について本当に偏執的だと感じる場合は、最後のリソースメジャーとして読み取り専用記述子を使用してください。


0

私はOz123のクラスデコレーターが好きですが、次のこともできます。これは、明示的なクラスラッパーと__new__をクラスファクトリーメソッドで使用して、クロージャー内のクラスを返します。

class B(object):
    def __new__(cls, val):
        return cls.factory(val)

@classmethod
def factory(cls, val):
    private = {'var': 'test'}

    class InnerB(object):
        def __init__(self):
            self.variable = val
            pass

        @property
        def var(self):
            return private['var']

    return InnerB()

複数のプロパティでどのように機能するかを示すテストを追加する必要があります
Oz123 '19

0

それが私の回避策です。

@property
def language(self):
    return self._language
@language.setter
def language(self, value):
    # WORKAROUND to get a "getter-only" behavior
    # set the value only if the attribute does not exist
    try:
        if self.language == value:
            pass
        print("WARNING: Cannot set attribute \'language\'.")
    except AttributeError:
        self._language = value

0

誰かがプロキシオブジェクトの使用について言及しましたが、私はその例を見なかったので、それを試してしまいました。

/!\可能であれば、クラス定義とクラスコンストラクタを優先してください

このコードはclass.__new__、あらゆる点でさらに悪いことを除いて、事実上(クラスコンストラクター)を書き換えています。苦痛を省き、可能であればこのパターンを使用しないでください。

def attr_proxy(obj):
    """ Use dynamic class definition to bind obj and proxy_attrs.
        If you can extend the target class constructor that is 
        cleaner, but its not always trivial to do so.
    """
    proxy_attrs = dict()

    class MyObjAttrProxy():
        def __getattr__(self, name):
            if name in proxy_attrs:
                return proxy_attrs[name]  # overloaded

            return getattr(obj, name)  # proxy

        def __setattr__(self, name, value):
            """ note, self is not bound when overloading methods
            """
            proxy_attrs[name] = value

    return MyObjAttrProxy()


myobj = attr_proxy(Object())
setattr(myobj, 'foo_str', 'foo')

def func_bind_obj_as_self(func, self):
    def _method(*args, **kwargs):
        return func(self, *args, **kwargs)
    return _method

def mymethod(self, foo_ct):
    """ self is not bound because we aren't using object __new__
        you can write the __setattr__ method to bind a self 
        argument, or declare your functions dynamically to bind in 
        a static object reference.
    """
    return self.foo_str + foo_ct

setattr(myobj, 'foo', func_bind_obj_as_self(mymethod, myobj))

-2

私はこのスレッドを完全に復活させていることを知っていますが、プロパティを読み取り専用にする方法を検討していて、このトピックを見つけた後、すでに共有されているソリューションに満足していませんでした。

したがって、最初の質問に戻ります。このコードから始めると、

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

Xを読み取り専用にしたい場合は、次のように追加できます。

@x.setter
def x(self, value):
    raise Exception("Member readonly")

次に、以下を実行した場合:

print (x) # Will print whatever X value is
x = 3 # Will raise exception "Member readonly"

3
ただし、セッターを作成しない場合、割り当てようとするとエラーも発生します(An AttributeError('can't set attribute')
Artyer
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.