web-dev-qa-db-fra.com

Est-il possible de surcharger Python affectation?

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?

64
Caruccio

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.

55
steveha

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

26
msw

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.

8
Lev Levitsky

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.

3
Perkins

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é.

2
jathanism

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
2
Timofey Kazantsev

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.

2
Tim Hoffman

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__.

1
Perkins

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
1
ecn

Oui, c'est possible, vous pouvez gérer __assign__ via modifier ast.

pip install assign

Testez avec:

class T():
    def __assign__(self, v):
        print('called with %s' % v)
b = T()
c = b

Tu auras

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

1
Ryan Kung