Existe-t-il une méthode magique qui peut surcharger l'opérateur d'affectation, comme __assign__(self, new_value)
?
Je voudrais interdire une nouvelle liaison pour une instance:
class Protect():
def __assign__(self, value):
raise Exception("This is an ex-parrot")
var = Protect() # once assigned...
var = 1 # this should raise Exception()
C'est possible? C'est fou? Dois-je prendre des médicaments?
La façon dont vous le décrivez n'est absolument pas possible. L'attribution à un nom est une caractéristique fondamentale de Python et aucun crochet n'a été fourni pour changer son comportement.
Cependant, l'affectation à un membre dans une instance de classe peut être contrôlée comme vous le souhaitez, en remplaçant .__setattr__()
.
class MyClass(object):
def __init__(self, x):
self.x = x
self._locked = True
def __setattr__(self, name, value):
if self.__dict__.get("_locked", False) and name == "x":
raise AttributeError("MyClass does not allow assignment to .x member")
self.__dict__[name] = value
>>> m = MyClass(3)
>>> m.x
3
>>> m.x = 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 7, in __setattr__
AttributeError: MyClass does not allow assignment to .x member
Notez qu'il existe une variable membre, _locked
, qui contrôle si l'affectation est autorisée. Vous pouvez le déverrouiller pour mettre à jour la valeur.
Non, car l'affectation est un langage intrinsèque qui n'a pas de hook de modification.
Je ne pense pas que ce soit possible. De mon point de vue, l'affectation à une variable ne fait rien à l'objet auquel elle se référait précédemment: c'est juste que la variable "pointe" vers un objet différent maintenant.
In [3]: class My():
...: def __init__(self, id):
...: self.id=id
...:
In [4]: a = My(1)
In [5]: b = a
In [6]: a = 1
In [7]: b
Out[7]: <__main__.My instance at 0xb689d14c>
In [8]: b.id
Out[8]: 1 # the object is unchanged!
Cependant, vous pouvez imiter le comportement souhaité en créant un objet wrapper avec les méthodes __setitem__()
ou __setattr__()
qui déclenchent une exception et gardent le contenu "immuable" à l'intérieur.
En utilisant l'espace de noms de niveau supérieur, cela est impossible. Quand tu cours
var = 1
Il stocke la clé var
et la valeur 1
Dans le dictionnaire global. C'est à peu près équivalent à appeler globals().__setitem__('var', 1)
. Le problème est que vous ne pouvez pas remplacer le dictionnaire global dans un script en cours d'exécution (vous pouvez probablement le faire en jouant avec la pile, mais ce n'est pas une bonne idée). Cependant, vous pouvez exécuter du code dans un espace de noms secondaire et fournir un dictionnaire personnalisé pour ses globaux.
class myglobals(dict):
def __setitem__(self, key, value):
if key=='val':
raise TypeError()
dict.__setitem__(self, key, value)
myg = myglobals()
dict.__setitem__(myg, 'val', 'protected')
import code
code.InteractiveConsole(locals=myg).interact()
Cela déclenchera un REPL qui fonctionne presque normalement, mais refuse toute tentative de définir la variable val
. Vous pouvez également utiliser execfile(filename, myg)
. Notez que cela ne fonctionne pas 'protège pas du code malveillant.
Dans l'espace de noms global, cela n'est pas possible, mais vous pouvez tirer parti d'une métaprogrammation Python Python $ plus avancée pour empêcher la création de plusieurs instances d'un objet Protect
. Le Motif singleton en est un bon exemple.
Dans le cas d'un Singleton, vous garantiriez qu'une fois instancié, même si la variable d'origine faisant référence à l'instance est réaffectée, que l'objet persisterait. Toutes les instances suivantes renverraient simplement une référence au même objet.
Malgré ce modèle, vous ne pourrez jamais empêcher un nom de variable globale lui-même d'être réaffecté.
En règle générale, la meilleure approche que j'ai trouvée est prioritaire __ilshift__
en tant que setter et __rlshift__
comme getter, dupliqué par le décorateur de la propriété. C'est presque le dernier opérateur à être résolu juste (| ^) et la logique est inférieure. Il est rarement utilisé (__lrshift__
est moins, mais il peut être pris en compte).
En utilisant le package PyPi assign, seule l'affectation avant peut être contrôlée, de sorte que la "force" réelle de l'opérateur est plus faible. Exemple de package PyPi assign:
class Test:
def __init__(self, val, name):
self._val = val
self._name = name
self.named = False
def __assign__(self, other):
if hasattr(other, 'val'):
other = other.val
self.set(other)
return self
def __rassign__(self, other):
return self.get()
def set(self, val):
self._val = val
def get(self):
if self.named:
return self._name
return self._val
@property
def val(self):
return self._val
x = Test(1, 'x')
y = Test(2, 'y')
print('x.val =', x.val)
print('y.val =', y.val)
x = y
print('x.val =', x.val)
z: int = None
z = x
print('z =', z)
x = 3
y = x
print('y.val =', y.val)
y.val = 4
sortie:
x.val = 1
y.val = 2
x.val = 2
z = <__main__.Test object at 0x0000029209DFD978>
Traceback (most recent call last):
File "E:\packages\pyksp\pyksp\compiler2\simple_test2.py", line 44, in <module>
print('y.val =', y.val)
AttributeError: 'int' object has no attribute 'val'
La même chose avec shift:
class Test:
def __init__(self, val, name):
self._val = val
self._name = name
self.named = False
def __ilshift__(self, other):
if hasattr(other, 'val'):
other = other.val
self.set(other)
return self
def __rlshift__(self, other):
return self.get()
def set(self, val):
self._val = val
def get(self):
if self.named:
return self._name
return self._val
@property
def val(self):
return self._val
x = Test(1, 'x')
y = Test(2, 'y')
print('x.val =', x.val)
print('y.val =', y.val)
x <<= y
print('x.val =', x.val)
z: int = None
z <<= x
print('z =', z)
x <<= 3
y <<= x
print('y.val =', y.val)
y.val = 4
sortie:
x.val = 1
y.val = 2
x.val = 2
z = 2
y.val = 3
Traceback (most recent call last):
File "E:\packages\pyksp\pyksp\compiler2\simple_test.py", line 45, in <module>
y.val = 4
AttributeError: can't set attribute
Alors <<=
L'opérateur qui obtient de la valeur dans une propriété est la solution beaucoup plus propre visuellement et il n'essaie pas de faire des erreurs de réflexion comme:
var1.val = 1
var2.val = 2
# if we have to check type of input
var1.val = var2
# but it could be accendently typed worse,
# skipping the type-check:
var1.val = var2.val
# or much more worse:
somevar = var1 + var2
var1 += var2
# sic!
var1 = var2
Non, il n'y en a pas
Pensez-y, dans votre exemple, vous reliez le nom var à une nouvelle valeur. Vous ne touchez pas réellement à l'instance de Protect.
Si le nom que vous souhaitez relier est en fait une propriété d'une autre entité, par exemple myobj.var, vous pouvez empêcher d'attribuer une valeur à la propriété/attribut de l'entité. Mais je suppose que ce n'est pas ce que vous voulez de votre exemple.
A l'intérieur d'un module, c'est absolument possible, via un peu de magie noire.
import sys
tst = sys.modules['tst']
class Protect():
def __assign__(self, value):
raise Exception("This is an ex-parrot")
var = Protect() # once assigned...
Module = type(tst)
class ProtectedModule(Module):
def __setattr__(self, attr, val):
exists = getattr(self, attr, None)
if exists is not None and hasattr(exists, '__assign__'):
exists.__assign__(val)
super().__setattr__(attr, val)
tst.__class__ = ProtectedModule
Notez que même à l'intérieur du module, vous ne pouvez pas écrire dans la variable protégée une fois le changement de classe effectué. L'exemple ci-dessus suppose que le code réside dans un module nommé tst
. Vous pouvez le faire dans repl
en remplaçant tst
par __main__
.
Une mauvaise solution consiste à réaffecter le destructeur. Mais ce n'est pas une véritable affectation de surcharge.
import copy
global a
class MyClass():
def __init__(self):
a = 1000
# ...
def __del__(self):
a = copy.copy(self)
a = MyClass()
a = 1
Oui, c'est possible, vous pouvez gérer __assign__
via modifier ast
.
pip install assign
class T():
def __assign__(self, v):
print('called with %s' % v)
b = T()
c = b
>>> import magic
>>> import test
called with c
Le projet est à https://github.com/RyanKung/assign
Et le Gist plus simple: https://Gist.github.com/RyanKung/4830d6c8474e6bcefa4edd13f122b4df