Свойство Python только для чтения

97

Я не знаю, когда атрибут должен быть частным и следует ли использовать свойство.

Недавно я прочитал, что сеттеры и геттеры не являются питоническими, и я должен использовать декоратор свойств. Все нормально.

Но что, если у меня есть атрибут, который нельзя устанавливать извне класса, но можно прочитать (атрибут только для чтения). Должен ли этот атрибут быть частным, и под частным я имею в виду с подчеркиванием 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
Кроме того, _xэто совсем не странно: условно это означает что-то «личное».
li.davidm
1
Я имел ввиду, что чтение из _x странно. Не само имя _x. Если пользователь читает напрямую из _x, он не несет ответственности.
Rafał uyński
3
Важный! Ваш класс должен быть классом нового стиля, т.е. унаследованным от object, чтобы это действительно мешало вам устанавливать obj.x. В классе старого стиля вы все еще можете установить obj.xс довольно неожиданными результатами.
Ian H
Есть несколько веских причин, чтобы иметь свойства только для чтения. Первый - это когда у вас есть значение, состоящее из слияния двух других значений (чтение / запись). Вы можете сделать это в методе, но вы также можете сделать это в свойстве только для чтения.
филолог

Ответы:

73

Как правило, программы Python должны быть написаны с предположением, что все пользователи являются взрослыми и, следовательно, несут ответственность за правильное использование вещей. Однако в редких случаях, когда просто не имеет смысла устанавливать атрибут (например, производное значение или значение, считываемое из некоторого статического источника данных), свойство только для получения обычно является предпочтительным шаблоном.

Сайлас Рэй
источник
26
Похоже, ваш ответ противоречит сам себе. Вы говорите, что пользователи должны нести ответственность и правильно использовать вещи, затем вы говорите, что иногда нет смысла устанавливать атрибут, а свойство getter является предпочтительным способом. На мой взгляд, вы можете или не можете установить атрибут. Вопрос только в том, следует ли мне защищать этот атрибут или оставить его. Между ними не должно быть ответов.
Rafał uyński
20
Нет, я сказал, что если вы буквально не можете установить значение, тогда нет смысла иметь сеттер. Например, если у вас есть объект окружности с элементом радиуса и атрибутом окружности, производным от радиуса, или у вас есть объект, который обертывает некоторый доступный только для чтения API реального времени с рядом свойств только для получения. Ничего не противоречащего.
Сайлас Рэй
9
Но ответственный пользователь не будет пытаться установить attr, который буквально невозможно установить. И снова не ответственный пользователь установит attr, который можно установить буквально, и вызовет ошибку где-то еще в коде из-за своего набора. Итак, в конце концов, оба attr не могут быть установлены. Должен ли я использовать свойство на обоих или не использовать его ни на одном?
Rafał uyński
8
Но ответственный пользователь не должен пытаться установить attr, который буквально невозможно установить. В программировании, если что-то является строго неизменяемым значением, ответственным или разумным является обеспечение того, чтобы этого не могло быть. Все эти мелочи способствуют созданию надежных программ.
Робин Смит
6
Эту позицию занимают многие люди и языки. Если эта позиция не подлежит обсуждению, вам, вероятно, не следует использовать Python.
Сайлас Рэй
75

Всего лишь два цента, Сайлас Рэй на правильном пути, но я решил добавить пример. ;-)

Python - это язык с небезопасным типом, и поэтому вам всегда придется доверять пользователям вашего кода, чтобы они использовали код как разумный (разумный) человек.

Согласно PEP 8 :

Используйте один знак подчеркивания в начале только для закрытых методов и переменных экземпляра.

Чтобы иметь свойство «только для чтения» в классе, вы можете использовать @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
siebz0r
источник
9
Python не является небезопасным по типу, он типизируется динамически. И изменение имени предназначено не для того, чтобы усложнить читерство, а для предотвращения конфликтов имен в сценариях, где наследование может быть проблематичным (если вы не программируете в целом, вам это даже не нужно).
memeplex
3
Но вы все же должны помнить, что изменяемые объекты можно изменить с помощью этого метода в любом случае. Например, если self.__a = []вы все еще можете это сделать, a.a.append('anything')и это будет работать.
Игорь
3
Мне непонятно, какое отношение к этому ответу имеет "разумный (разумный) человек". Можете ли вы более четко рассказать о вещах, которые, по вашему мнению, разумный человек будет делать и не будет делать?
winni2k
3
Для меня используйте украшение @property, вам нужно будет наследовать от объекта, когда вы это сделаете, это был весь смысл этого ответа. Спасибо.
akki
2
@kkm единственный способ никогда не позволить ошибке проникнуть в код - это никогда не писать код.
Алехан
58

Вот способ избежать предположения, что

все пользователи являются взрослыми по согласию и, следовательно, несут ответственность за правильное использование вещей.

пожалуйста, посмотрите мое обновление ниже

Использование @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

С помощью

Без подчеркивания: это общедоступная переменная.
Одно подчеркивание: это защищенная переменная.
Два подчеркивания: это частная переменная.

За исключением последнего, это условность. Вы все еще можете, если очень постараетесь, обращаться к переменным с двойным подчеркиванием.

Так что же нам делать? Откажемся ли мы от наличия свойств только для чтения в 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
Oz123
источник
1
Это действительно полезно и делает именно то, что я хотел. Спасибо. Однако это для тех, у кого установлен Python 3. Я использую Python 2.7.8, поэтому мне нужно применить к вашему решению две незначительные поправки: "class NewClass (cls, <b> object <\ b>):" ... "<b> super (NewClass, self) <\ b> .__ setattr __ (имя, значение) ".
Ying Zhang
1
Кроме того, следует быть осторожным с переменными-членами класса, являющимися списками и словарями. Вы действительно не можете «заблокировать их» от обновления таким образом.
Ин Чжан
1
Одно улучшение и три проблемы. Улучшение: if..elif..elseблок можно было просто if name in attrs and name in self.__dict__: raise Attr...не passтребовать. Проблема 1: все классы, оформленные таким образом, в конечном итоге идентичны __name__, и строковое представление их типа также гомогенизировано. Проблема 2: это украшение перезаписывает любой кастом __setattr__. Проблема 3: пользователи могут победить это с помощью del MyClass.__setattr__.
TigerhawkT3
Просто язык. «Увы ...» означает «грустно говорить ...», что, я думаю, не то, что вам нужно.
Thomas Andrews
Ничего не помешает мне сделать 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)
Booboo
источник
Ницца. Если dict_nameвместо этого вы искажаете , например, dict_name = "_spam_" + nameэто удаляет зависимость от uuid4и значительно упрощает отладку.
cz
Но тогда я могу сказать, что p.__dict__['_spam_x'] = 5нужно изменить значение p.x, так что это не обеспечивает достаточного искажения имени.
Booboo
1

Я не удовлетворен двумя предыдущими ответами на создание свойств только для чтения, потому что первое решение позволяет удалить атрибут readonly, а затем установить и не блокирует __dict__. Второе решение можно обойти с помощью тестирования - найти значение, равное тому, что вы установили, и изменить его в конечном итоге.

Теперь о коде.

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__ теперь относится к неизменяемым типам. MappingProxyType , поэтому атрибуты нельзя изменить с помощью __dict__. Установка или удаление __dict__ также заблокировано. Единственный способ изменить свойства, доступные только для чтения, - это изменить методы самого класса.

Хотя я считаю, что мое решение лучше, чем из двух предыдущих, его можно улучшить. Вот слабые стороны этого кода:

a) Не позволяет добавлять в метод в подклассе, который устанавливает или удаляет атрибут только для чтения. Методу, определенному в подклассе, автоматически запрещается доступ к атрибуту только для чтения, даже при вызове версии метода суперкласса.

б) Методы класса только для чтения можно изменить, чтобы обойти ограничения только для чтения.

Однако без редактирования класса невозможно установить или удалить атрибут только для чтения. Это не зависит от соглашений об именах, и это хорошо, потому что Python не очень согласуется с соглашениями об именах. Это дает возможность сделать атрибуты только для чтения, которые нельзя изменить с помощью скрытых лазеек без редактирования самого класса. Просто укажите атрибуты, которые будут доступны только для чтения при вызове декоратора в качестве аргументов, и они станут доступны только для чтения.

Кредит на ответ Брайса в разделе Как получить имя класса вызывающего абонента внутри функции другого класса в Python? для получения классов и методов вызывающего объекта.

Майкл
источник
object.__setattr__(pfi, 'readonly', 'foobar')нарушает это решение, не редактируя сам класс.
L3виафан
0

Обратите внимание, что методы экземпляра также являются атрибутами (класса), и вы можете установить их на уровне класса или экземпляра, если действительно хотите быть крутым. Или что вы можете установить переменную класса (которая также является атрибутом класса), где удобные свойства, доступные только для чтения, не будут работать аккуратно из коробки. Я пытаюсь сказать, что проблема с атрибутом «только для чтения» носит более общий характер, чем обычно кажется. К счастью, существуют общепринятые ожидания, которые настолько сильны, что ослепляют нас по отношению к этим другим случаям (в конце концов, почти все является каким-то атрибутом в python).

Основываясь на этих ожиданиях, я думаю, что наиболее общий и легкий подход состоит в том, чтобы принять соглашение о том, что «общедоступные» (без начального подчеркивания) атрибуты доступны только для чтения, за исключением случаев, когда они явно задокументированы как доступные для записи. Это подразумевает обычное ожидание того, что методы не будут исправлены, а переменные класса, указывающие значения по умолчанию для экземпляра, лучше не говорить. Если вы чувствуете себя действительно параноиком по поводу какого-то специального атрибута, используйте дескриптор только для чтения в качестве последней меры ресурса.

мемплекс
источник
0

Хотя мне нравится декоратор классов из Oz123, вы также можете сделать следующее, в котором используется явная оболочка класса и __new__ с помощью метода Factory класса, возвращающего класс внутри замыкания:

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
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))
оборота ThorSummoner
источник
-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