Propriété en lecture seule Python


95

Je ne sais pas quand l'attribut doit être privé et si je dois utiliser la propriété.

J'ai lu récemment que les setters et les getters ne sont pas pythoniques et que je devrais utiliser Property Decorator. C'est bon.

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

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

De cette façon, je peux lire l'attribut obj.xmais je ne peux pas le définir, obj.x = 1donc ça va.

Mais devrais-je vraiment me soucier de définir un objet qui ne doit pas être défini? Peut-être que je devrais le laisser. Mais là encore, je ne peux pas utiliser de soulignement parce que la lectureobj._x est étrange pour l'utilisateur, donc je devrais utiliser obj.xet encore une fois l'utilisateur ne sait pas qu'il ne doit pas définir cet attribut.

Quelle est votre opinion et vos pratiques?


1
L'idée d'une propriété est qu'elle se comporte comme un attribut mais peut avoir du code supplémentaire. Si tout ce que vous voulez, c'est obtenir une valeur, je ne me dérangerais même pas: utilisez simplement self.xet ayez confiance que personne ne changera x. S'il xest important de s'assurer que cela ne peut pas être changé, utilisez une propriété.
li.davidm

De plus, ce _xn'est pas du tout étrange: par convention , cela signifie quelque chose de "privé".
li.davidm

1
Je voulais dire que la lecture de _x est étrange. Pas le nom _x lui-même. Si l'utilisateur lit directement à partir de _x, il n'est pas responsable.
Rafał Łużyński

3
Important! Votre classe doit être une classe de style nouveau, c'est-à- dire hérite de object, pour que cela vous empêche réellement de définir obj.x. Sur une classe à l'ancienne, vous pouvez toujours définir obj.x, avec des résultats assez inattendus.
Ian H

Il existe plusieurs raisons valables d'avoir des propriétés en lecture seule. La première est lorsque vous avez une valeur qui consiste à fusionner deux autres valeurs (lecture / écriture). Vous pouvez le faire dans une méthode, mais vous pouvez également le faire dans une propriété en lecture seule.
philologon le

Réponses:


68

En règle générale, les programmes Python doivent être écrits en supposant que tous les utilisateurs sont des adultes consentants et sont donc responsables de l'utilisation correcte des choses eux-mêmes. Cependant, dans les rares cas où il n'a tout simplement pas de sens pour un attribut d'être définissable (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é.


26
Il semble que votre réponse se contredit. Vous dites que les utilisateurs devraient être responsables et utiliser les choses correctement, puis vous dites que parfois il n'a pas de sens pour un attribut d'être définissable et la propriété getter est une manière préférée. À mon avis, vous pouvez ou ne pouvez pas définir l'attribut. La seule question est de savoir si je dois protéger cet attr ou le laisser. Il ne devrait y avoir aucune réponse entre les deux.
Rafał Łużyński

19
Non, j'ai dit que si vous ne pouvez littéralement pas définir une valeur, cela n'a aucun sens d'avoir un setter. Par exemple, si vous avez un objet cercle avec un membre de rayon et un attribut de circonférence dérivé du rayon, ou si vous avez un objet qui encapsule des API en temps réel en lecture seule avec un certain nombre de propriétés de lecture seule. Rien ne contredit quoi que ce soit.
Silas Ray

9
Mais l'utilisateur responsable n'essaiera pas de définir attr qui ne peut littéralement pas être défini. Et encore une fois, l'utilisateur non responsable définirait attr qui peut littéralement être défini et lèvera une erreur ailleurs dans le code en raison de son ensemble. Donc, à la fin, les deux attr ne peuvent pas être définis. Dois-je utiliser la propriété sur les deux ou ne pas l'utiliser sur aucun?
Rafał Łużyński

8
Mais l'utilisateur responsable ne doit pas essayer de définir attr qui ne peut littéralement pas être défini. En programmation, si quelque chose est strictement une valeur non réglable, la chose responsable ou sensible est de s'assurer que cela ne peut pas l'être. Ces petites choses contribuent toutes à des programmes fiables.
Robin Smith

6
C'est une position que beaucoup de gens et de langues adoptent. Si c'est une position que vous trouvez non négociable, vous ne devriez probablement pas utiliser Python.
Silas Ray

72

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

Python est un langage non sécurisé et vous devrez donc toujours faire confiance aux utilisateurs de votre code pour utiliser le code comme une personne raisonnable (sensée).

Pour PEP 8 :

N'utilisez qu'un seul trait de soulignement 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 la @propertydécoration, vous devrez hériter du objectmoment où vous le faites pour 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

9
Python n'est pas de type dangereux, il est typé dynamiquement. Et la manipulation des noms n'est pas pour rendre la triche plus difficile, mais pour éviter les conflits de noms dans les scénarios où l'héritage pourrait être problématique (si vous ne programmez pas dans le grand, vous ne devriez même pas vous en soucier).
memeplex

3
Mais vous devez quand même vous rappeler que les objets mutables peuvent quand même être modifiés en utilisant cette méthode. Par exemple, si self.__a = []vous pouvez toujours le faire a.a.append('anything')et cela fonctionnera.
Igor le

3
Je ne vois pas clairement ce que porte "une personne raisonnable (raisonnable)" dans cette réponse. Pouvez-vous être plus explicite sur le genre de choses que vous pensez qu'une personne raisonnable ferait et ne ferait pas?
winni2k

3
Pour moi, utilisez la décoration @property, vous devrez hériter de l'objet lorsque vous le faites, c'était le point essentiel de cette réponse. Merci.
akki

2
@kkm la seule façon de ne jamais permettre à un bogue de se faufiler dans le code est de ne jamais écrire de code.
Alechan le

55

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

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

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

L'utilisation @property, est très verbeuse 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 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 trait de soulignement.

Alors que faisons-nous? Abandonnons-nous les propriétés en lecture seule en Python?

Voir! read_only_propertiesdé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:

D'où read_only_propertiesvient-il?

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

mettre à jour

Je ne m'attendais pas à ce que cette réponse retienne 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 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

1
Ceci est vraiment utile et fait exactement ce que je voulais faire. Je vous remercie. Cependant, c'est pour ceux qui ont installé Python 3. J'utilise Python 2.7.8, je dois donc appliquer deux modifications mineures à votre solution: "class NewClass (cls, <b> object <\ b>):" ... "<b> super (NewClass, self) <\ b> .__ setattr __ (nom, valeur) ".
Ying Zhang

1
De plus, il faut faire attention au fait que les variables des membres de classe sont des listes et des dictionnaires. Vous ne pouvez pas vraiment les «empêcher» d'être mis à jour de cette manière.
Ying Zhang

1
Une amélioration et trois problèmes ici. Amélioration: le if..elif..elsebloc pourrait juste être if name in attrs and name in self.__dict__: raise Attr...sans passnécessaire. Problème 1: les classes ainsi décorées se retrouvent toutes avec un identique __name__, et la représentation sous forme de chaîne de leur type est également homogénéisée. Problème 2: cette décoration écrase toute coutume__setattr__ . Problème 3: les utilisateurs peuvent vaincre cela avec del MyClass.__setattr__.
TigerhawkT3

Juste un truc de langue. "Hélas ..." signifie "Triste à dire, ..." ce qui n'est pas ce que vous voulez, je pense.
Thomas Andrews

Rien ne m'empêchera de faire object.__setattr__(f, 'forbidden', 42). Je ne vois pas les read_only_propertiesajouts qui ne sont pas gérés par le double trait de soulignement.
L3viathan le

4

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 à écriture unique car 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 la manipulation "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 __name__ == '__main__':
    try:
        p = Point(2, 3)
        print(p.x, p.y)
        p.x = 9
    except Exception as e:
        print(e)

Agréable. Si vous modifiez à la dict_nameplace, par exemple, dict_name = "_spam_" + namecela supprime la dépendance uuid4et facilite le débogage.
cz

Mais alors je peux dire p.__dict__['_spam_x'] = 5de changer la valeur de p.x, donc cela ne suffit pas à modifier le nom.
Booboo

1

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 à l'attribut readonly d'être supprimé puis défini et ne bloque pas le __dict__. La deuxième solution pourrait être contournée avec des tests - trouver la valeur qui équivaut à ce que vous avez défini deux 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 ne sert à rien de créer des attributs en lecture seule, sauf lorsque vous écrivez du code de bibliothèque, du code qui est distribué à d'autres en tant que code à utiliser afin d'améliorer leurs programmes, et non du code à d'autres fins, comme le développement d'applications. Le problème __dict__ est résolu, car le __dict__ est maintenant des types immuables.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 les deux précédentes, elle pourrait être améliorée. Voici les faiblesses de ce code:

a) Ne permet pas l'ajout à 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 interdite d'accéder à un attribut en lecture seule, même en appelant la version de la superclasse de la méthode.

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

Cependant, il n'y a pas moyen sans modifier 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 si cohérent avec les conventions de dénomination. 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 modifier 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 deviendront en lecture seule.

Crédit à 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.


object.__setattr__(pfi, 'readonly', 'foobar')casse cette solution, sans modifier la classe elle-même.
L3viathan le

0

Notez que les méthodes d'instance sont également 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 badass. Ou que vous pouvez 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 ne l'est habituellement. Heureusement, il y a des attentes conventionnelles au travail qui sont si fortes qu'elles nous aveuglent sur ces autres cas (après tout, presque tout est un attribut d'une sorte en python).

En s'appuyant sur ces attentes, je pense que l'approche la plus générale et la plus légère est d'adopter la convention selon laquelle les attributs "public" (pas de trait de soulignement principal) sont en lecture seule, sauf lorsqu'ils sont explicitement documentés comme inscriptibles. Cela englobe l'attente habituelle selon laquelle les méthodes ne seront pas corrigées et les variables de classe indiquant les valeurs par défaut des instances sont mieux et encore moins. 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

Bien que j'aime le décorateur de classe d'Oz123, vous pouvez également faire ce qui suit, qui utilise 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()

vous devriez ajouter un test montrant comment cela fonctionne avec plusieurs propriétés
Oz123

0

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

quelqu'un a mentionné l'utilisation d'un objet proxy, je n'en ai pas vu d'exemple, alors j'ai fini par l'essayer, [mal].

/! \ Veuillez préférer les définitions de classe et les constructeurs de classe si possible

ce code est effectivement réécrit class.__new__(constructeur de classe) sauf pire à tous points de vue. Épargnez-vous la douleur et n'utilisez pas ce modèle si vous le pouvez.

def attr_proxy(obj):
    """ Use dynamic class definition to bind obj and proxy_attrs.
        If you can extend the target class constructor that is 
        cleaner, but its not always trivial to do so.
    """
    proxy_attrs = dict()

    class MyObjAttrProxy():
        def __getattr__(self, name):
            if name in proxy_attrs:
                return proxy_attrs[name]  # overloaded

            return getattr(obj, name)  # proxy

        def __setattr__(self, name, value):
            """ note, self is not bound when overloading methods
            """
            proxy_attrs[name] = value

    return MyObjAttrProxy()


myobj = attr_proxy(Object())
setattr(myobj, 'foo_str', 'foo')

def func_bind_obj_as_self(func, self):
    def _method(*args, **kwargs):
        return func(self, *args, **kwargs)
    return _method

def mymethod(self, foo_ct):
    """ self is not bound because we aren't using object __new__
        you can write the __setattr__ method to bind a self 
        argument, or declare your functions dynamically to bind in 
        a static object reference.
    """
    return self.foo_str + foo_ct

setattr(myobj, 'foo', func_bind_obj_as_self(mymethod, myobj))

-2

Je sais que je ramène d'entre les morts ce fil, mais je cherchais comment rendre une propriété en lecture seule et après avoir trouvé ce sujet, je n'étais pas satisfait des solutions déjà partagées.

Donc, pour revenir à la question initiale, si vous commencez avec ce code:

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

Et vous voulez rendre X en lecture seule, vous pouvez simplement ajouter:

@x.setter
def x(self, value):
    raise Exception("Member readonly")

Ensuite, si vous exécutez ce qui suit:

print (x) # Will print whatever X value is
x = 3 # Will raise exception "Member readonly"

3
Mais si vous ne faites tout simplement pas de passeur, essayer d'attribuer entraînera également une erreur (An AttributeError('can't set attribute'))
Artyer
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.