How to restrict setting an attribute outside of constructor?

主宰稳场 提交于 2021-01-27 04:01:46

问题


I want to forbid further assignments on some attributes of a class after it was initialized. For instance; no one can explicitly assign any value to 'ssn' (social security number) property after the Person instance 'p' has been initialized. _setattr_ is also being called while assigning the value inside _init_ method, thus it is not what I want. I'd like to restrict only further assignments. How can I achieve that?

class Person(object):
    def __init__(self, name, ssn):
        self.name = name
        self._ssn = ssn

    def __setattr__(self, name, value):
        if name == '_ssn':
            raise AttributeError('Denied.')
        else:
            object.__setattr__(self, name, value)

>> p = Person('Ozgur', '1234')
>> AttributeError: Denied.

回答1:


The usual way is to use a "private" attribute starting with an underscore, and a read-only property for public access:

import operator

class Person(object):
    def __init__(self, name, ssn):
        self.name = name
        self._ssn = ssn
    ssn = property(operator.attrgetter("_ssn"))

Note that this does not really hinder anybody to change the attribute _ssn, but the leading _ documents that the attribute is private.




回答2:


Python does not support private or protected attributes. You need to implement the descriptor protocol instead. The standard library provides decorators to do that succinctly.

Just declare the attribute with two underscores in front of it in the init method. It is called name mangling and prevents the attribute from being accessible via __ssn, although it can still be accessed and modified by _Person__ssn in this case. However, if you do not explicitly define a setter for it will raise an AttributeError.

Of course if someone has an intention to misuse the API that person can if he is very intent. But it will not happen by accident.

import re

class Person:
    """Encapsulates the private data of a person."""

    _PATTERN = re.COMPILE(r'abcd-efgh-ssn')

    def __init__(self, name, ssn):
       """Initializes Person class with input name of person and
       his social security number (ssn).
       """
       # you can add some type and value checking here for defensive programming
       # or validation for the ssn using regex for example that raises an error
       if not self._PATTERN.match(ssn):
           raise ValueError('ssn is not valid')
       self.__name = name
       self.__ssn = snn

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        self.__name = value

    @property
    def ssn(self):
        return self.__ssn

>>> p = Person('aname', 'abcd-efgh-ssn')
>>> p.ssn
'abcd-efgh-ssn'
>>> p.ssn = 'mistake'
AttributeError: 'can't set attribute'



回答3:


Just pointing out that we could still modify _ssn.

Objects have the special attribute, __dict__ that is a dictionary that maps all instance attributes of the object with their corresponding values. We can add/update/delete instance attributes directly by modifying the __dict__ attribute of an object.

We can still modify _snn like this:

p = Person('Ozgur', '1234')

p.__dict__.get('_ssn') # returns '1234'

p.__dict__['_ssn'] = '4321'

p.__dict__.get('_ssn') # returns '4321'

As we can see, we were still able to change the value of _ssn. By design, there isn't a straight forward way, if any, to circumvent attribute access in Python in all cases.

I'll show a more common way to restrict attribute access using property() as a decorator:

class Person(object):
    def __init__(self, name, ssn):
        self.name = name
        self._ssn = ssn

    @property
    def ssn(self):
        return self._ssn

    @ssn.setter
    def ssn(self, value):
        raise AttributeError('Denied')


>> p = Person('Ozgur', '1234')
>> p.ssn
>> '1234'
>> p.ssn = '4321'
>> AttributeError: Denied

Hope this helps!




回答4:


You can bypass setattr by directly assigning to the instance dictionary in the constructor. Of course, this trick can always be used to spoof any other hoops you employ to make _ssn read-only (after an initial write)

 class Person(object): 
    def __init__(self, name, ssn):    
        self.name = name
        self.__dict__['_ssn'] = ssn 

    def __setattr__(self, name, value):
        if name == '_ssn':
            raise AttributeError('Denied.')
        else:
            object.__setattr__(self, name, value)


来源:https://stackoverflow.com/questions/10929004/how-to-restrict-setting-an-attribute-outside-of-constructor

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!