除了使用新实例外,限制类变量的修改

时间:2018-02-09 05:51:55

标签: python python-3.x

这是以下帖子的后续问题(检查链接不需要理解问题)

Counter variable for class

我们将idCounter设置为类Student的类变量,并计算创建的实例数。

这是班级:

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)

现在,我们实例化几个实例,然后检查idCounter

的值
student1 = Student()
student2 = Student()
student3 = Student()
student4 = Student()

Student.idCounter
4

但是,如果你能做到这一点,维持一个计数器就没有用了:

Student.idCounter = 2000

现在创建新实例:

student5 = Student()

并检查idCounter

Student.idCounter

2001

idCounter可以在没有运行__init__的情况下搞砸柜台。

如何创建一个仅在__init__运行时递增的计数器(或任何类变量)?并且不能通过从类中调用类变量来独立修改,如上所示。

是否有一种通用的方法来限制使用语法修改类变量?

ClassName.ClassVariable = new_value

谢谢。

3 个答案:

答案 0 :(得分:3)

修改

使用property改进版本但原则相同:

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

现在:

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

旧版

使用描述符和元类:

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

似乎实现了这个目标:

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

这仍然可以故意破坏:

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

但不是偶然的。

答案 1 :(得分:3)

我的第一个版本看起来像这样(使用sphinx / rST标记):

class Student:
    """
    Student class description...

    .. py:attribute:: idCounter

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

如果由于某种原因严厉的警告是不够的,我会接受我在评论中提出的建议,即在元类上使用property。我会使用property代替@MichaelMüller建议的自定义描述符,原因有两个:1)使用属性的实际工作较少,这是自动只读的:不需要重新发明轮子; 2)该属性将引发AttributeError,我觉得这比NotImplementedError更合适。

解决方案看起来像这样:

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)

请注意,idCounter中实际存在名为Student.__dict__的属性。我发现这是隐藏属性的底层存储的最优雅方式,因为由于属性,您永远不会通过Student.idCounter意外地修改它。但是,由于Python将类__dict__优化为只读,因此我添加了一个间接级别,通过使实际计数器值为list而不是int来实现增量。

答案 2 :(得分:2)

tl; dr:不要传递活物;传递一个愚蠢的表示,并传递它仅供参考(不要接受它)如果你想要任何安全措施防止篡改。

您可以使用class-private属性和属性保护属性不被修改:

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

它按预期工作,不允许轻易篡改自己。 对于那些不了解私有属性如何工作的人来说,篡改企图看起来令人费解和误导。

工作原理:

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

尽管如此,这不是防弹的。如果有足够的决心,可以访问私有类属性:

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

同样适用于任何隐藏属性的答案(给出一个数字);只要__dict__可见,隐藏属性就不会完全隐藏。

可以围绕属性访问构建更复杂的防御,包括检查堆栈以确定调用者。但只要您传递一个具有任何访问主状态的Python对象,就会出现安全漏洞,并且确定的攻击者将能够更改该主状态。

对于真正的防篡改访问,当您将数据传递给不信任方时:

  • 只传递Student个对象作为属性的哑存储,并且函数从这些属性计算某些东西(不改变它们)。
  • 将您的状态保存在数据库中,并且永远不会在Student个对象中传递对该数据库的任何引用。
  • 仅在修改主状态的任何API调用中接受学生ID或其他与DB相关的标识符。
  • 更新时始终从数据库中查找该状态。
  • 验证API的调用者,以便他们只能使用他们应该使用的学生ID。

在您的特定情况下,这可能是或不是过度杀伤;你没有描述它。