web-dev-qa-db-fra.com

Python propriété en lecture seule

Je ne sais pas quand l'attribut devrait être privé et si je devrais utiliser une propriété.

J'ai lu récemment que les setters et les getters ne sont pas Pythonic et que je devrais utiliser un décorateur de propriété. C'est bon.

Mais que se passe-t-il si j'ai un attribut, celui-ci ne doit pas être défini en dehors de la classe mais peut être lu (attribut en lecture seule). Cet attribut doit-il être privé, et par privé, j'entends par soulignement, comme ça self._x? Si oui, comment puis-je le lire sans utiliser le getter? La seule méthode que je connaisse actuellement est d'écrire

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

De cette façon, je peux lire attribut par obj.x mais je ne peux pas le régler obj.x = 1 donc ça va.

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

Quel est ton avis et tes pratiques?

70
Rafał Łużyński

Généralement, les programmes Python doivent être écrits avec l’hypothèse que tous les utilisateurs sont des adultes consentants et qu’ils sont donc responsables de l’utilisation correcte des choses elles-mêmes. Cependant, dans les rares cas où cela n’a aucun sens un attribut à définir (comme une valeur dérivée ou une valeur lue à partir d’une source de données statique), la propriété en lecture seule est généralement le modèle préféré.

52
Silas Ray

Rien que mes deux sous, Silas Ray est sur la bonne voie, mais j’ai eu le goût d’ajouter un exemple. ;-)

Python est un langage non typé et vous devrez donc toujours faire confiance aux utilisateurs de votre code pour qu’ils l’utilisent comme une personne raisonnable.

Per PEP 8 :

Utilisez un trait de soulignement principal uniquement pour les méthodes non publiques et les variables d'instance.

Pour avoir une propriété 'en lecture seule' dans une classe, vous pouvez utiliser le @property décoration, vous devrez hériter de object pour pouvoir utiliser les classes de 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
52
siebz0r

Voici un moyen d'éviter l'hypothèse que

tous les utilisateurs sont des adultes consentants et sont donc responsables d'utiliser les choses correctement eux-mêmes.

s'il vous plaît voir ma mise à jour ci-dessous

En utilisant @property, est très prolixe, 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

En utilisant

Pas de trait de soulignement: c'est une variable publique.
Un trait de soulignement: c'est une variable protégée.
Deux caractères 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? Est-ce que nous renonçons à avoir des propriétés en lecture seule dans Python?

Voir! read_only_properties 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

Tu demandes:

Où se trouve read_only_properties provenir de?

Heureux que vous ayez demandé, voici la source de read_only_properties :

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. Étonnamment c'est le cas. Cela m'a encouragé à créer un package que vous pouvez utiliser.

$ pip install read-only-properties

dans votre python Shell:

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

Voici une approche légèrement différente des propriétés en lecture seule, qui devrait peut-être s'appeler des propriétés écriture unique, car elles doivent être initialisées, n'est-ce pas? Pour les paranoïaques parmi nous qui craignent de pouvoir modifier les propriétés en accédant directement au dictionnaire de l'objet, j'ai introduit le traitement "extrême" des noms:

from uuid import uuid4

class Read_Only_Property:
    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 = Read_Only_Property('x')
    y = Read_Only_Property('y')
    def __init__(self, x, y):
        self.x = x
        self.y = y

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

Bien que j'aime le décorateur de classe de Oz123, vous pouvez également effectuer les opérations suivantes, qui utilisent un wrapper de classe explicite et __new__ avec une méthode de classe Factory renvoyant la classe dans une fermeture:

class B(object):
    def __new__(cls, val):
        return cls.factory(val)

@classmethod
def factory(cls, val):
    private = {'var': 'test'}

    class InnerB(object):
        def __init__(self):
            self.variable = val
            pass

        @property
        def var(self):
            return private['var']

    return InnerB()
0
Apollo Marquis

Notez que les méthodes d'instance sont aussi des attributs (de la classe) et que vous pouvez les définir au niveau de la classe ou de l'instance si vous voulez vraiment être un dur à cuire. Ou que vous puissiez définir une variable de classe (qui est également un attribut de la classe), où les propriétés pratiques en lecture seule ne fonctionneront pas parfaitement. Ce que j'essaie de dire, c'est que le problème de "l'attribut en lecture seule" est en fait plus général qu'il n'est généralement perçu. Heureusement, il existe des attentes conventionnelles au travail qui sont assez fortes pour nous aveugler par rapport à ces autres cas (après tout, presque tout est un attribut quelconque en python).

S'appuyant sur ces attentes, je pense que l'approche la plus générale et la plus légère consiste à adopter la convention selon laquelle les attributs "publics" (sans trait de soulignement important) sont en lecture seule, sauf s'ils sont explicitement documentés comme pouvant être écrits. Cela présume l'attente habituelle selon laquelle les méthodes ne seront pas corrigées et que les variables de classe indiquant les instances par défaut sont préférables. Si vous vous sentez vraiment paranoïaque à propos d'un attribut spécial, utilisez un descripteur en lecture seule comme dernière mesure de ressource.

0
memeplex

Je ne suis pas satisfait des deux réponses précédentes pour créer des propriétés en lecture seule, car la première solution permet de supprimer l'attribut readonly, puis de le définir et ne bloque pas le __dict__. La deuxième solution pourrait être contournée avec des tests - trouver la valeur qui correspond à ce que vous définissez deux et la changer é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 __== '__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 est inutile de créer des attributs en lecture seule, sauf lorsque vous écrivez code de la bibliothèque, code qui est distribué à d'autres personnes en tant que code à utiliser pour améliorer leurs programmes, et non à d'autres fins, comme le développement d'applications. Le problème __dict__ est résolu car le __dict__ est maintenant du type immuable types.MappingProxyType, les attributs ne peuvent donc pas être modifiés via __dict__. La définition ou la suppression de __dict__ est également bloquée. La seule façon de modifier les propriétés en lecture seule consiste à modifier les méthodes de la classe elle-même.

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

a) N'autorise pas l'ajout à une méthode d'une sous-classe qui définit ou supprime un attribut en lecture seule. Une méthode définie dans une sous-classe ne peut pas automatiquement accéder à un attribut en lecture seule, même en appelant la version de la superclasse de la méthode.

b) Les méthodes readonly de la classe peuvent être modifiées pour contourner les restrictions en lecture seule.

Cependant, il n'y a pas moyen de ne pas éditer la classe pour définir ou supprimer un attribut en lecture seule. Cela ne dépend pas des conventions de dénomination, ce qui est bien car Python n'est pas aussi cohérent avec les conventions de dénomination. Ceci fournit un moyen de créer des attributs en lecture seule qui ne peuvent pas être modifiés avec des échappatoires masquées sans modification la classe elle-même. Répertoriez simplement les attributs à lire uniquement lors de l'appel du décorateur en tant qu'arguments et ils deviendraient en lecture seule.

Merci à la réponse de Brice dans Comment obtenir le nom de la classe de l'appelant dans une fonction d'une autre classe en python? pour obtenir les classes et méthodes de l'appelant.

0
Michael

C'est ma solution de contournement.

@property
def language(self):
    return self._language
@language.setter
def language(self, value):
    # WORKAROUND to get a "getter-only" behavior
    # set the value only if the attribute does not exist
    try:
        if self.language == value:
            pass
        print("WARNING: Cannot set attribute \'language\'.")
    except AttributeError:
        self._language = value
0
rusiano