125 votes

Quand un attribut doit-il être privé et devenir une propriété en lecture seule ?

Je ne sais pas quand un attribut doit être privé et si je dois utiliser l'option property .

J'ai lu récemment que les setters et les getters ne sont pas pythoniques mais que l'utilisation de l'option property Le décorateur est OK.

Mais que se passe-t-il si j'ai un attribut qui ne doit pas être défini depuis l'extérieur de la classe mais qui peut être lu (attribut read-only). Cet attribut doit-il être privé, et par privé je veux dire avec un trait de soulignement, comme ceci self._x ? Si oui, alors comment puis-je le lire sans utiliser le getter ? La seule méthode que je connais pour l'instant est d'écrire

@property
def x(self):
    return self._x

De cette façon, je peux lire les attributs par obj.x mais je ne peux pas le régler obj.x = 1 donc c'est bon.

Mais dois-je vraiment me préoccuper de définir un objet qui ne doit pas être défini ? Peut-être que je devrais simplement le laisser. Mais encore une fois, je ne peux pas utiliser l'underscore parce que lire obj._x est bizarre pour l'utilisateur, je devrais donc utiliser obj.x et là encore, l'utilisateur ne sait pas qu'il ne doit pas définir cet attribut.

Quelle est votre opinion et vos pratiques ?

97voto

siebz0r Points 3960

Juste mes deux cents, Silas Ray est sur la bonne voie, mais j'ai eu envie d'ajouter un exemple ;-)

Python est un langage non sécurisé par les types et vous devrez donc toujours faire confiance aux utilisateurs de votre code pour qu'ils l'utilisent comme une personne raisonnable (sensée).

Par PEP 8 :

N'utilisez qu'un seul trait de soulignement pour les méthodes non publiques et les variables d'instance.

Pour disposer d'une propriété en lecture seule dans une classe, vous pouvez utiliser l'attribut @property vous devrez hériter de la décoration object lorsque vous le faites pour utiliser les classes du nouveau style.

Exemple :

>>> class A(object):
...     def __init__(self, a):
...         self._a = a
...
...     @property
...     def a(self):
...         return self._a
... 
>>> a = A('test')
>>> a.a
'test'
>>> a.a = 'pleh'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute

88voto

Silas Ray Points 11950

En général, les programmes Python devraient être écrits en partant du principe que tous les utilisateurs sont des adultes consentants, et donc responsables de l'utilisation correcte des choses par eux-mêmes. Cependant, dans les rares cas où il n'est pas logique qu'un attribut soit paramétrable (comme une valeur dérivée ou une valeur lue à partir d'une source de données statique), la propriété getter-only est généralement le modèle préféré.

70voto

Oz123 Points 4677

Voici un moyen d'éviter l'hypothèse selon laquelle

tous les utilisateurs sont des adultes consentants, et sont donc responsables de l'utilisation correctement les choses.

veuillez voir ma mise à jour ci-dessous

Utilisation de @property est très verbeux, par exemple :

   class AClassWithManyAttributes:
        '''refactored to properties'''
        def __init__(a, b, c, d, e ...)
             self._a = a
             self._b = b
             self._c = c
             self.d = d
             self.e = e

        @property
        def a(self):
            return self._a
        @property
        def b(self):
            return self._b
        @property
        def c(self):
            return self._c
        # you get this ... it's long

Utilisation de

Pas de trait de soulignement : c'est une variable publique.
Un trait de soulignement : c'est une variable protégée.
Deux traits de soulignement : c'est une variable privée.

Sauf le dernier, c'est une convention. Vous pouvez toujours, si vous essayez vraiment, accéder aux variables avec un double soulignement.

Alors, que faisons-nous ? Devons-nous renoncer à avoir des propriétés en lecture seule en Python ?

Regardez ! read_only_properties Le décorateur à la rescousse !

@read_only_properties('readonly', 'forbidden')
class MyClass(object):
    def __init__(self, a, b, c):
        self.readonly = a
        self.forbidden = b
        self.ok = c

m = MyClass(1, 2, 3)
m.ok = 4
# we can re-assign a value to m.ok
# read only access to m.readonly is OK 
print(m.ok, m.readonly) 
print("This worked...")
# this will explode, and raise AttributeError
m.forbidden = 4

Vous demandez :

Où se trouve read_only_properties venant de ?

Content que vous demandiez, voici la source pour Propriétés en lecture seule :

def read_only_properties(*attrs):

    def class_rebuilder(cls):
        "The class decorator"

        class NewClass(cls):
            "This is the overwritten class"
            def __setattr__(self, name, value):
                if name not in attrs:
                    pass
                elif name not in self.__dict__:
                    pass
                else:
                    raise AttributeError("Can't modify {}".format(name))

                super().__setattr__(name, value)
        return NewClass
    return class_rebuilder

mise à jour

Je ne m'attendais pas à ce que cette réponse suscite autant d'attention. C'est surprenant. Cela m'a encouragé à créer un paquet que vous pouvez utiliser.

$ pip install read-only-properties

dans votre shell python :

In [1]: from rop import read_only_properties

In [2]: @read_only_properties('a')
   ...: class Foo:
   ...:     def __init__(self, a, b):
   ...:         self.a = a
   ...:         self.b = b
   ...:         

In [3]: f=Foo('explodes', 'ok-to-overwrite')

In [4]: f.b = 5

In [5]: f.a = 'boom'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-5-a5226072b3b4> in <module>()
----> 1 f.a = 'boom'

/home/oznt/.virtualenvs/tracker/lib/python3.5/site-packages/rop.py in __setattr__(self, name, value)
    116                     pass
    117                 else:
--> 118                     raise AttributeError("Can't touch {}".format(name))
    119 
    120                 super().__setattr__(name, value)

AttributeError: Can't touch a

6voto

Ronald Aaronson Points 200

Voici une approche légèrement différente des propriétés en lecture seule, qui devraient peut-être être appelées propriétés en écriture unique, puisqu'elles doivent être initialisées, n'est-ce pas ? Pour les paranoïaques d'entre nous qui s'inquiètent de pouvoir modifier les propriétés en accédant directement au dictionnaire de l'objet, j'ai introduit une manipulation "extrême" des noms :

from uuid import uuid4

class ReadOnlyProperty:
    def __init__(self, name):
        self.name = name
        self.dict_name = uuid4().hex
        self.initialized = False

    def __get__(self, instance, cls):
        if instance is None:
            return self
        else:
            return instance.__dict__[self.dict_name]

    def __set__(self, instance, value):
        if self.initialized:
            raise AttributeError("Attempt to modify read-only property '%s'." % self.name)
        instance.__dict__[self.dict_name] = value
        self.initialized = True

class Point:
    x = ReadOnlyProperty('x')
    y = ReadOnlyProperty('y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

if __name__ == '__main__':
    try:
        p = Point(2, 3)
        print(p.x, p.y)
        p.x = 9
    except Exception as e:
        print(e)

4voto

Elijah Points 186

Je ne suis pas satisfait des deux réponses précédentes concernant la création de propriétés en lecture seule, car la première solution permet de supprimer puis de définir l'attribut en lecture seule et ne bloque pas l'accès à la propriété. __dict__ . La deuxième solution pourrait être contournée par des tests - trouver la valeur qui est égale à celle que vous avez définie et la modifier éventuellement.

Maintenant, pour le code.

def final(cls):
    clss = cls
    @classmethod
    def __init_subclass__(cls, **kwargs):
        raise TypeError("type '{}' is not an acceptable base type".format(clss.__name__))
    cls.__init_subclass__ = __init_subclass__
    return cls

def methoddefiner(cls, method_name):
    for clss in cls.mro():
        try:
            getattr(clss, method_name)
            return clss
        except(AttributeError):
            pass
    return None

def readonlyattributes(*attrs):
    """Method to create readonly attributes in a class

    Use as a decorator for a class. This function takes in unlimited 
    string arguments for names of readonly attributes and returns a
    function to make the readonly attributes readonly. 

    The original class's __getattribute__, __setattr__, and __delattr__ methods
    are redefined so avoid defining those methods in the decorated class

    You may create setters and deleters for readonly attributes, however
    if they are overwritten by the subclass, they lose access to the readonly
    attributes. 

    Any method which sets or deletes a readonly attribute within
    the class loses access if overwritten by the subclass besides the __new__
    or __init__ constructors.

    This decorator doesn't support subclassing of these classes
    """
    def classrebuilder(cls):
        def __getattribute__(self, name):
            if name == '__dict__':
                    from types import MappingProxyType
                    return MappingProxyType(super(cls, self).__getattribute__('__dict__'))
            return super(cls, self).__getattribute__(name)
        def __setattr__(self, name, value): 
                if name == '__dict__' or name in attrs:
                    import inspect
                    stack = inspect.stack()
                    try:
                        the_class = stack[1][0].f_locals['self'].__class__
                    except(KeyError):
                        the_class = None
                    the_method = stack[1][0].f_code.co_name
                    if the_class != cls: 
                         if methoddefiner(type(self), the_method) != cls:
                            raise AttributeError("Cannot set readonly attribute '{}'".format(name))                        
                return super(cls, self).__setattr__(name, value)
        def __delattr__(self, name):                
                if name == '__dict__' or name in attrs:
                    import inspect
                    stack = inspect.stack()
                    try:
                        the_class = stack[1][0].f_locals['self'].__class__
                    except(KeyError):
                        the_class = None
                    the_method = stack[1][0].f_code.co_name
                    if the_class != cls:
                        if methoddefiner(type(self), the_method) != cls:
                            raise AttributeError("Cannot delete readonly attribute '{}'".format(name))                        
                return super(cls, self).__delattr__(name)
        clss = cls
        cls.__getattribute__ = __getattribute__
        cls.__setattr__ = __setattr__
        cls.__delattr__ = __delattr__
        #This line will be moved when this algorithm will be compatible with inheritance
        cls = final(cls)
        return cls
    return classrebuilder

def setreadonlyattributes(cls, *readonlyattrs):
    return readonlyattributes(*readonlyattrs)(cls)

if __name__ == '__main__':
    #test readonlyattributes only as an indpendent module
    @readonlyattributes('readonlyfield')
    class ReadonlyFieldClass(object):
        def __init__(self, a, b):
            #Prevent initalization of the internal, unmodified PrivateFieldClass
            #External PrivateFieldClass can be initalized
            self.readonlyfield = a
            self.publicfield = b

    attr = None
    def main():
        global attr
        pfi = ReadonlyFieldClass('forbidden', 'changable')
        ###---test publicfield, ensure its mutable---###
        try:
            #get publicfield
            print(pfi.publicfield)
            print('__getattribute__ works')
            #set publicfield
            pfi.publicfield = 'mutable'
            print('__setattr__ seems to work')
            #get previously set publicfield
            print(pfi.publicfield)
            print('__setattr__ definitely works')
            #delete publicfield
            del pfi.publicfield 
            print('__delattr__ seems to work')
            #get publicfield which was supposed to be deleted therefore should raise AttributeError
            print(pfi.publlicfield)
            #publicfield wasn't deleted, raise RuntimeError
            raise RuntimeError('__delattr__ doesn\'t work')
        except(AttributeError):
            print('__delattr__ works')

        try:
            ###---test readonly, make sure its readonly---###
            #get readonlyfield
            print(pfi.readonlyfield)
            print('__getattribute__ works')
            #set readonlyfield, should raise AttributeError
            pfi.readonlyfield = 'readonly'
            #apparently readonlyfield was set, notify user
            raise RuntimeError('__setattr__ doesn\'t work')
        except(AttributeError):
            print('__setattr__ seems to work')
            try:
                #ensure readonlyfield wasn't set
                print(pfi.readonlyfield)
                print('__setattr__ works')
                #delete readonlyfield
                del pfi.readonlyfield
                #readonlyfield was deleted, raise RuntimeError
                raise RuntimeError('__delattr__ doesn\'t work')
            except(AttributeError):
                print('__delattr__ works')
        try:
            print("Dict testing")
            print(pfi.__dict__, type(pfi.__dict__))
            attr = pfi.readonlyfield
            print(attr)
            print("__getattribute__ works")
            if pfi.readonlyfield != 'forbidden':
                print(pfi.readonlyfield)
                raise RuntimeError("__getattr__ doesn't work")
            try:
                pfi.__dict__ = {}
                raise RuntimeError("__setattr__ doesn't work")
            except(AttributeError):
                print("__setattr__ works")
            del pfi.__dict__
            raise RuntimeError("__delattr__ doesn't work")
        except(AttributeError):
            print(pfi.__dict__)
            print("__delattr__ works")
            print("Basic things work")

main()

Il n'y a pas d'intérêt à faire des attributs en lecture seule, sauf lorsque vous écrivez code de bibliothèque Il s'agit d'un code distribué à d'autres personnes pour qu'elles l'utilisent afin d'améliorer leurs programmes, et non d'un code destiné à un autre usage, comme le développement d'applications. Le site __dict__ Le problème est résolu, car le __dict__ est maintenant de l'immuable types.MappingProxyType Les attributs ne peuvent donc pas être modifiés par l'intermédiaire du __dict__ . Réglage ou suppression __dict__ est également bloqué. La seule façon de modifier les propriétés en lecture seule est de modifier les méthodes de la classe elle-même.

Bien que je pense que ma solution soit meilleure que les deux précédentes, elle peut être améliorée. Ce sont les faiblesses de ce code :

  1. Ne permet pas d'ajouter à une méthode dans une sous-classe qui définit ou supprime un attribut en lecture seule. Une méthode définie dans une sous-classe est automatiquement empêchée d'accéder à un attribut en lecture seule, même en appelant la version de la méthode de la super-classe.

  2. Les méthodes en lecture seule de la classe peuvent être modifiées pour contourner les restrictions en matière de lecture seule.

Cependant, il n'y a aucun moyen, sans éditer la classe, de définir ou de supprimer un attribut en lecture seule. Cela ne dépend pas des conventions de nommage, ce qui est une bonne chose car Python n'est pas très cohérent avec les conventions de nommage. Cela fournit un moyen de créer des attributs en lecture seule qui ne peuvent pas être modifiés avec des failles cachées sans éditer la classe elle-même. Il suffit de lister les attributs en lecture seule lors de l'appel du décorateur comme arguments et ils deviendront en lecture seule.

Crédit à La réponse de Brice pour obtenir les classes et les méthodes de l'appelant.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X