Restrict Modification Of Class Variables Except With A New Instance
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"