5

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 for class Student and it counts the number of instances created .

This is the class:

class Student: # A student ID counter idCounter = 0 def __init__(self): self.gpa = 0 self.record = {} # Each time I create a new student, the idCounter increment Student.idCounter += 1 self.name = 'Student {0}'.format(Student.idCounter) 

Now, we instantiate a few instances then check the value of idCounter:

student1 = Student() student2 = Student() student3 = Student() student4 = Student() Student.idCounter 4 

However, maintaining a counter is rendered moot if you can do this:

Student.idCounter = 2000 

Now create new instance:

student5 = Student() 

and check idCounter:

Student.idCounter 2001 

idCounter can simply screw up the counter without ever running __init__.

How can you create a counter (or any class variable) that will only increment when __init__ runs ? and cannot be modified independently by calling the class variable from the class as shown above.

Is there a general way to restrict a class variable from being modified using the syntax?

ClassName.ClassVariable = new_value 

Thank you.

7
  • 2
    Indentation needs fixing Commented Feb 9, 2018 at 5:58
  • 2
    You can't with pure Python. Everything is public. Commented Feb 9, 2018 at 5:59
  • 2
    Usually, documenting is all you need. Tell the user not to do it and let them accept the consequences if they don't listen. Commented Feb 9, 2018 at 6:00
  • 2
    That being said, you can make the counter a read only property on the metaclass, stash the actual value in the class's __dict__ (where the property will make it effectively invisible), and override __new__ very carefully. Commented Feb 9, 2018 at 6:01
  • Python had no private instance variables or methods, except for those of numbers ((1).to_bytes(7,'little')) Commented Feb 9, 2018 at 6:06

3 Answers 3

4

EDIT

Improved version with a property but same principle:

class Meta(type): def __init__(cls, *args, **kwargs): cls.__value = 0 super().__init__(*args, **kwargs) @property def idCounter(cls): return cls.__value class Student(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:

class Counter: def __init__(self): self.value = 0 def __get__(self, instance, cls): return getattr(instance, '_{}__hidden_counter'.format(instance.__name__ )) def __set__(self, instance, value): raise NotImplementedError class Meta(type): idCounter = Counter() class Student(metaclass=Meta): __hidden_counter = 0 def __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) 5 return getattr(instance, '_{}__hidden_counter'.format(instance.__name__ )) 6 def __set__(self, instance, value): ----> 7 raise NotImplementedError 8 9 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.

Sign up to request clarification or add additional context in comments.

Comments

3

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:

class Student(object): __counter = 0 def __init__(self): self.__class__.__counter += 1 # Only works within the class. self.__ordinal = self.__counter @property def ordinal(self): return self.__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.

1 Comment

Thank you @9000 for your detailed response, greatly appreciate taking the time to type it up and demonstrate. this is veritably helpful.
3

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

class Student: """ 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:

class StudentMeta(type): def __new__(meta, name, bases, attrs): attrs['idCounter'] = [0] return type.__new__(meta, name, bases, attrs) @property def idCounter(cls): return cls.__dict__['idCounter'][0] class Student(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.

4 Comments

Any particular reason you replaced super() with explicit type?
Making an instantance of Student raises TypeError: 'mappingproxy' object does not support item assignment.
@Mike. You are right: stackoverflow.com/q/48717893/2988730. I asked a question to find a solution.
The answer to this question is very similar to my answer (EDIT). I thought about a mutable structure, such as a list or a dict but decided to use an integer instead. My solution is one line shorter, though. ;) And the first sentence: Probably the best way: just pick another name. Call the property x and the dict key '_x', so you can access it the normal way. is just my solution.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.