Властивість лише для читання Python


95

Я не знаю, коли атрибут повинен бути приватним, і якщо я повинен використовувати власність.

Нещодавно я читав, що сеттери та геттери не є пітонічними, і я повинен використовувати декоратор властивостей. Нічого страшного.

Але що, якщо у мене є атрибут, який не повинен бути встановлений поза класом, але може бути прочитаний (атрибут лише для читання). Чи повинен цей атрибут бути приватним, і під приватним я маю на увазі підкреслення, наприклад 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, тоді він не відповідає.
Рафал Лужинський,

3
Важливо! Ваш клас повинен бути класом нового стилю, тобто успадковується від object, щоб це насправді зупинило налаштування obj.x. Для класу старого стилю ви все ще можете встановити obj.x, з досить несподіваними результатами.
Ian H

Є кілька вагомих причин, щоб мати властивості лише для читання. Одне - це коли у вас є значення, яке складається із об’єднання двох інших значень (читання / запис). Ви можете зробити це в методі, але ви можете зробити це і в властивості лише для читання.
філолог

Відповіді:


68

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


26
Схоже, ваша відповідь суперечить самій собі. Ви говорите, що користувачі повинні нести відповідальність і правильно використовувати речі, тоді ви говорите, що іноді немає сенсу встановлювати атрибут, а властивість getter є кращим способом. На мій погляд, ви можете або не можете встановити атрибут. Питання лише в тому, чи слід мені захищати цей атрибут або залишити його. Відповідей між ними бути не повинно.
Рафал Лужинський

19
Ні, я сказав, що якщо ви буквально не можете встановити значення, то немає сенсу мати сеттер. Наприклад, якщо у вас є об’єкт кола з елементом радіуса та атрибутом окружності, який походить від радіуса, або у вас є об’єкт, який обертає деякі API реального часу лише для читання з низкою властивостей лише для отримання. Ніщо не суперечить нічому.
Silas Ray

9
Але відповідальний користувач не намагатиметься встановити attr, який буквально встановити неможливо. І знову невідповідальний користувач встановив би attr, який буквально можна встановити, і призведе до помилки десь ще в коді через його набір. Тож врешті обидва attr неможливо встановити. Чи слід використовувати власність на обох або не використовувати на жодному?
Рафал Лужинський,

8
Але відповідальний користувач не повинен намагатися встановити attr, який буквально неможливо встановити. У програмуванні, якщо щось є суто невстановлюваною величиною, відповідальним чи розумним є забезпечення того, щоб цього не могло бути. Усі ці дрібниці сприяють надійним програмам.
Робін Сміт,

6
Це позиція, яку займають багато людей та мови. Якщо це позиція, яку ви не можете обговорювати, ви, мабуть, не повинні використовувати Python.
Сайлас Рей

72

Лише мої два центи, Сайлас Рей іде правильним шляхом, проте мені захотілося додати приклад. ;-)

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

9
Python не є типом небезпечним, він динамічно набирається. І керування іменами - це не для того, щоб ускладнити обман, а для запобігання зіткненням імен у сценаріях, коли успадкування може бути проблематичним (якщо ви не програмуєте у великому, вам навіть не все одно).
memeplex

3
Але ви все-таки повинні пам'ятати, що змінні об'єкти в будь-якому випадку можна змінити за допомогою цього методу. Наприклад, якщо self.__a = []ви все ще можете це зробити, a.a.append('anything')і це буде працювати.
Ігор

3
Мені незрозуміло, що стосується "розумної (розумної) людини" до цієї відповіді. Чи можете ви більш чітко висловити ті речі, які, на вашу думку, розумна людина робила б і не робила?
winni2k

3
Для мене, щоб скористатися прикрасою @property, вам потрібно буде успадкувати об'єкт, коли ви це зробите, і вся суть цієї відповіді. Дякую.
akki

2
@kkm єдиний спосіб ніколи не дозволити помилці проникнути в код - це ніколи не писати код.
Алечан

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

Використовуючи

Без підкреслення: це загальнодоступна змінна.
Одне підкреслення: це захищена змінна.
Два підкреслення: це приватна змінна.

За винятком останнього, це конвенція. Ви все ще можете, якщо ви дуже стараєтесь, отримати доступ до змінних із подвійним підкресленням.

То що ми робимо? Чи відмовляємось ми від того, що властивості лише для читання в 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, тому я повинен застосувати до вашого рішення дві незначні зміни: "class NewClass (cls, <b> object <\ b>):" ... "<b> super (NewClass, self) <\ b> .__ setattr __ (ім'я, значення) ".
Ying Zhang

1
Крім того, слід бути обережним щодо змінних членів класу, які є списками та словниками. Ви насправді не можете заблокувати їх від оновлення таким чином.
Ying Zhang

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

Просто мовна річ. "На жаль ..." означає "Сумно говорити, ...", я думаю, це не те, що ти хочеш.
Томас Ендрюс

Мені ніщо не завадить зробити 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і значно полегшує налагодження.
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__ також заблоковано. Єдиний спосіб змінити властивості лише для читання - це зміна методів самого класу.

Хоча я вважаю, що моє рішення є кращим, ніж попередні два, його можна вдосконалити. Ось такі слабкі сторони коду:

а) Не дозволяє додавати до методу в підкласі, який встановлює або видаляє атрибут лише для читання. Методу, визначеному в підкласі, автоматично заборонено доступ до атрибута лише для читання, навіть викликаючи версію методу суперкласу.

б) Методи "тільки для читання" класу можуть бути змінені, щоб усунути обмеження лише для читання.

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

Заслуговує відповідь Бріс у розділі Як отримати ім'я класу абонента всередині функції іншого класу в python? для отримання класів та методів абонента.


object.__setattr__(pfi, 'readonly', 'foobar')розбиває це рішення, не редагуючи сам клас.
L3viathan

0

Зверніть увагу, що методи екземпляра також є атрибутами (класу), і ви могли б встановити їх на рівні класу чи екземпляра, якщо ви дійсно хотіли бути поганим. Або що ви можете встановити змінну класу (яка також є атрибутом класу), де зручні властивості лише для читання не працюватимуть акуратно. Я намагаюся сказати, що проблема "атрибут тільки для читання" насправді є більш загальною, ніж зазвичай сприймається. На щастя, на роботі є загальноприйняті сподівання, які настільки сильні, що засліплюють нас по відношенню до цих інших випадків (зрештою, майже все є певним атрибутом у python).

Спираючись на ці очікування, я вважаю, що найбільш загальним і легким підходом є прийняття конвенції, згідно з якою атрибути "public" (без провідних знаків підкреслення) читаються лише за винятком випадків, коли це явно задокументовано як запис. Це передбачає звичайне сподівання, що методи не будуть виправлені, а змінні класу, що вказують за замовчуванням екземпляра, краще не кажучи вже про те. Якщо ви відчуваєте справжній параноїк щодо якогось особливого атрибута, використовуйте дескриптор лише для читання як останній показник ресурсу.


0

Хоча мені подобається декоратор класів з Oz123, ви також можете зробити наступне, що використовує явну обгортку класу та __new__ із методом Factory 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))

-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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.