Skip to content Skip to sidebar Skip to footer

Restrict Modification Of Class Variables Except With A New Instance

This is a follow up question to the following post (checking the link is not required to understand the question) Counter variable for class We set idCounter as a class variable fo

Solution 1:

EDIT

Improved version with a property but same principle:

classMeta(type):

    def__init__(cls, *args, **kwargs):
        cls.__value = 0super().__init__(*args, **kwargs)

    @propertydefidCounter(cls):
        return cls.__value

classStudent(metaclass=Meta):

    def__init__(self):
        self.__class__._Meta__value += 1

Now:

>>>s1 = Student()>>>Student.idCounter
1
>>>s2 = Student()>>>Student.idCounter
2
>>>Student.idCounter = 100
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-64-a525899df18d> in <module>()
----> 1 Student.idCounter = 100

AttributeError: can't set attribute  

Old version

Using a descriptor and a metaclass:

classCounter:
    def__init__(self):
        self.value = 0def__get__(self, instance, cls):
        returngetattr(instance, '_{}__hidden_counter'.format(instance.__name__ ))
    def__set__(self, instance, value):
        raise NotImplementedError

classMeta(type):
    idCounter = Counter()

classStudent(metaclass=Meta):
    __hidden_counter = 0def__init__(self):
        Student.__hidden_counter += 1

seems to achieve this:

>>> s1 = Student()
>>> Student.idCounter
1
>>> s2 = Student()
>>> Student.idCounter
2
>>> Student.idCounter = 200---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
<ipython-input-51-dc2483b583f6> in <module>()
----> 1 Student.idCounter = 200

<ipython-input-46-b21e03bf3cb3> in __set__(self, instance, value)
      5return getattr(instance, '_{}__hidden_counter'.format(instance.__name__ ))
      6     def __set__(self, instance, value):
----> 7         raise NotImplementedError89 class Meta(type):

NotImplementedError:
>>> Student.idCounter
2

This can still intentionally be broken:

>>>Student._Student__hidden_counter = 100>>>Student.idCounter
100

but not by accident.

Solution 2:

tl;dr: Don't pass a live object; pass a dumbed-down representation, and pass it for reference only (do not accept it back) if you want any security against tampering.

You can protect an attribute from modification with a class-private attribute and a property:

classStudent(object):
    __counter = 0def__init__(self):
        self.__class__.__counter += 1# Only works within the class.self.__ordinal = self.__counter

    @propertydefordinal(self):
        returnself.__ordinal

It works as expected, and does not allow to easily tamper with itself. Tampering attempts look puzzling and misleading to those who don't know how private attributes work.

How it works:

>>>s1 = Student()>>>s1.ordinal
1
>>>s2 = Student()>>>s2.ordinal
2
>>>s2.ordinal = 88
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>>s2.__ordinal = 88# A cunning student.>>>s2.__ordinal  # Gasp!
88
>>>s2.ordinal  # Nope. The private attribute is not touched.
2
>>>Student.__counter  # Trying to override the true source.
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Student' has no attribute '__counter'
>>>Student.__counter = 123# This appears to succeed!>>>s3 = Student()>>>s3.ordinal  # Again, the private attribute is not touched.
3
>>>_

Despite the above, this is not bulletproof. With enough determination, the private class attribute can be accessed:

>>>Student._Student__counter = 999>>>s1000 = Student()>>>s1000.ordinal
1000

Same applies to any hidden-attribute answers (a number is given); as long as __dict__ is visible, the hidden attribute is not exactly hidden.

Much more sophisticated defenses can be built around attribute access, including inspecting the stack to determine the caller. But as long as you pass a Python object that has any access to the master state, you have a security hole, and a determined attacker will be able to alter that master state.

For real tamper-proof access, when you pass data to a non-trusted party:

  • Only pass Student objects as dumb stores of attributes, and functions computing something from these attributes (not mutating them).
  • Keep your state in a database, and never pass any references to that database in your Student objects.
  • Only accept the student ID, or some other DB-related identifier, in any API calls that modify the master state.
  • Always look up that state from the database when updating it.
  • Authenticate the callers of your API, so that they only can work with student IDs they supposed to.

This may be or be not an overkill in your particular situation; you did not describe it.

Solution 3:

My first version would look something like this (using sphinx/rST markup):

classStudent:
    """
    Student class description...

    .. py:attribute:: idCounter

       A student ID counter. Do not modify this class attribute
       manually!
    """
    idCounter = 0
    ...

If a stern warning is not adequate for some reason, I would go with the suggestion I made in the comments, of using a property on the metaclass. I would use a property instead of the custom descriptor that @MikeMüller suggests for two reasons: 1) It's less actual work to use a property, which is automatically read-only: no need to reinvent the wheel; 2) The property will raise an AttributeError, which I feel is much more appropriate than NotImplementedError.

The solution would look something like this:

classStudentMeta(type):
    def__new__(meta, name, bases, attrs):
        attrs['idCounter'] = [0]
        returntype.__new__(meta, name, bases, attrs)
    @propertydefidCounter(cls):
        return cls.__dict__['idCounter'][0]

classStudent(metaclass=StudentMeta):
    def__init__(self):
        self.gpa = 0
        self.record = {}
        # Each time I create a new student, the idCounter increment
        __class__.__dict__['idCounter'][0] += 1
        self.name = 'Student {0}'.format(__class__.idCounter)

Notice that there is actually an attribute named idCounter in Student.__dict__. I find this to be the most elegant way to hide the underlying storage for a property, since you can never accidentally modify it via Student.idCounter because of the property. However, since Python optimizes class __dict__s to be read-only, I have added a level of indirection to make the increment possible by making the actual counter value a list instead of an int.

Post a Comment for "Restrict Modification Of Class Variables Except With A New Instance"