108 votes

Est-il possible de surcharger une affectation Python ?

Existe-t-il une méthode magique permettant de surcharger l'opérateur d'affectation, par exemple __assign__(self, new_value) ?

J'aimerais interdire un re-bind 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()

Est-ce possible ? Est-ce que c'est de la folie ? Devrais-je prendre des médicaments ?

97voto

steveha Points 24808

La façon dont vous le décrivez n'est absolument pas possible. L'assignation à un nom est une caractéristique fondamentale de Python et aucun crochet n'a été fourni pour modifier son comportement.

Cependant, l'affectation à un membre dans une instance de classe peut être contrôlé comme vous le souhaitez, en remplaçant les éléments suivants .__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.

33voto

msw Points 25319

Non, car l'affectation est un langue intrinsèque qui n'a pas de crochet de modification.

11voto

Lev Levitsky Points 25303

Je ne pense pas que ce soit possible. D'après moi, l'affectation à une variable ne fait rien à l'objet auquel elle se référait auparavant : 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 désiré en créant un objet de type wrapper avec __setitem__() o __setattr__() qui lèvent une exception, et gardez les éléments "non modifiables" à l'intérieur.

10voto

Perkins Points 616

À l'intérieur d'un module, c'est tout à fait possible, grâce à 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

L'exemple ci-dessus suppose que le code réside dans un module nommé tst . Vous pouvez le faire dans le repl en changeant tst a __main__ .

Si vous voulez protéger l'accès par le module local, faites en sorte que toutes les écritures dans ce module passent par tst.var = newval .

8voto

Perkins Points 616

En utilisant l'espace de nom de premier niveau, c'est impossible. Lorsque vous exécutez

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 nom 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 va lancer un REPL qui fonctionne presque normalement, mais qui refuse toute tentative de définir la variable val . Vous pouvez également utiliser execfile(filename, myg) . Notez que cela ne protège pas contre les codes malveillants.

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