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?
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é.
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
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.
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.
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
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
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)
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()
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.
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.
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