Python read-only property

2019-01-11 00:12发布

问题:

I don't know when attribute should be private and if I should use property.

I read recently that setters and getters are not pythonic and I should use property decorator. It's ok.

But what if I have attribute, that mustn't be set from outside of class but can be read (read-only attribute). Should this attribute be private, and by private I mean with underscore, like that self._x? If yes then how can I read it without using getter? Only method I know right now is to write

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

That way I can read attribute by obj.x but I can't set it obj.x = 1 so it's fine.

But should I really care about setting object that mustn't be set? Maybe I should just leave it. But then again I can't use underscore because reading obj._x is odd for user, so I should use obj.x and then again user doesn't know that he mustn't set this attribute.

What's your opinion and practics?

回答1:

Generally, Python programs should be written with the assumption that all users are consenting adults, and thus are responsible for using things correctly themselves. However, in the rare instance where it just does not make sense for an attribute to be settable (such as a derived value, or a value read from some static datasource), the getter-only property is generally the preferred pattern.



回答2:

Just my two cents, Silas Ray is on the right track, however I felt like adding an example. ;-)

Python is a type-unsafe language and thus you'll always have to trust the users of your code to use the code like a reasonable (sensible) person.

Per PEP 8:

Use one leading underscore only for non-public methods and instance variables.

To have a 'read-only' property in a class you can make use of the @property decoration, you'll need to inherit from object when you do so to make use of the new-style classes.

Example:

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


回答3:

Here is a way to avoid the assumption that

all users are consenting adults, and thus are responsible for using things correctly themselves.

please see my update below

Using @property, is very verbose e.g.:

   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

Using

No underscore: it's a public variable.
One underscore: it's a protected variable.
Two underscores: it's a private variable.

Except the last one, it's a convention. You can still, if you really try hard, access variables with double underscore.

So what do we do? Do we give up on having read only properties in Python?

Alas, read_only_properties decorator to the rescue!

@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

You ask:

Where is read_only_properties coming from?

Glad you asked, here is the source for 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

update

I never expected this answer will get so much attention. Surprisingly it does. This encouraged me to create a package you can use.

$ pip install read-only-properties

in your python shell:

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


回答4:

Here is a slightly different approach to read-only properties, which perhaps should be called write-once properties since they do have to get initialized, don't they? For the paranoid among us who worry about being able to modify properties by accessing the object's dictionary directly, I've introduced "extreme" name mangling:

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)


回答5:

Notice that instance methods are also attributes (of the class) and that you could set them at the class or instance level if you really wanted to be a badass. Or that you may set a class variable (which is also an attribute of the class), where handy readonly properties won't work neatly out of the box. What I'm trying to say is that the "readonly attribute" problem is in fact more general than it's usually perceived to be. Fortunately there are conventional expectations at work that are so strong as to blind us wrt these other cases (after all, almost everything is an attribute of some sort in python).

Building upon these expectations I think the most general and lightweight approach is to adopt the convention that "public" (no leading underscore) attributes are readonly except when explicitly documented as writeable. This subsumes the usual expectation that methods won't be patched and class variables indicating instance defaults are better let alone. If you feel really paranoid about some special attribute, use a readonly descriptor as a last resource measure.



回答6:

While I like the class decorator from Oz123, you could also do the following, which uses an explicit class wrapper and __new__ with a class Factory method returning the class within a closure:

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()


回答7:

That's my workaround.

@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


回答8:

I know i'm bringing back from the dead this thread, but I was looking at how to make a property read only and after finding this topic, I wasn't satisfied with the solutions already shared.

So, going back to the initial question, if you start with this code:

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

And you want to make X readonly, you can just add:

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

Then, if you run the following:

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