web-dev-qa-db-ja.com

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

属性をいつプライベートにするか、プロパティを使用するかどうかはわかりません。

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

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

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

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

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

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

70

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

52
Silas Ray

ちょうど私の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
52
siebz0r

これは、次の仮定を回避する方法です。

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

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

@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
43
Oz123

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

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 __== '__main__':
    try:
        p = Point(2, 3)
        print(p.x, p.y)
        p.x = 9
    except Exception as e:
        print(e)
3
Ronald Aaronson

Oz123のクラスデコレータが好きですが、明示的なクラスラッパーと、クロージャ内のクラスを返すクラスFactoryメソッドで__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()
0
Apollo Marquis

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

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

0
memeplex

最初の解決策では読み取り専用属性を削除してから__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 __== '__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)クラスの読み取り専用メソッドを変更して、読み取り専用の制限を無効にすることができます。

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

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

0
Michael

それが私の回避策です。

@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
rusiano