¿Es posible sobrecargar la asignación de Python?

¿Hay algún método mágico que pueda sobrecargar el operador de asignación, como __assign__(self, new_value) ?

Me gustaría prohibir un nuevo enlace para una instancia:

 class Protect(): def __assign__(self, value): raise Exception("This is an ex-parrot") var = Protect() # once assigned... var = 1 # this should raise Exception() 

¿Es posible? ¿Es una locura? ¿Debo estar en la medicina?

La forma en que lo describas es absolutamente imposible. La asignación a un nombre es una característica fundamental de Python y no se han proporcionado enganches para cambiar su comportamiento.

Sin embargo, la asignación a un miembro en una instancia de clase se puede controlar como desee, anulando .__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) >>> mx 3 >>> mx = 4 Traceback (most recent call last): File "", line 1, in  File "", line 7, in __setattr__ AttributeError: MyClass does not allow assignment to .x member 

Tenga en cuenta que hay una variable miembro, _locked , que controla si la asignación está permitida. Puedes desbloquearlo para actualizar el valor.

No, ya que la asignación es un lenguaje intrínseco que no tiene un gancho de modificación.

No creo que sea posible. A mi modo de ver, la asignación a una variable no hace nada al objeto al que hacía referencia anteriormente: es solo que la variable “apunta” a un objeto diferente ahora.

 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! 

Sin embargo, puede imitar el comportamiento deseado creando un objeto envoltorio con los __setitem__() o __setattr__() que __setattr__() una excepción y mantienen el material “inmutable” en su interior.

No no hay

Piénselo, en su ejemplo está volviendo a vincular el nombre var a un nuevo valor. En realidad no estás tocando la instancia de Proteger.

Si el nombre que desea volver a enlazar es de hecho una propiedad de otra entidad, es decir, myobj.var, entonces puede evitar asignar un valor a la propiedad / atributo de la entidad. Pero supongo que eso no es lo que quieres de tu ejemplo.

En el espacio de nombres global, esto no es posible, pero puede aprovechar la metaprogtwigción Python más avanzada para evitar que se creen varias instancias del objeto Protect . El patrón Singleton es un buen ejemplo de esto.

En el caso de un Singleton, se aseguraría de que, una vez instanciada, incluso si la variable original que hace referencia a la instancia se reasigna, el objeto persista. Cualquier instancia posterior simplemente devolvería una referencia al mismo objeto.

A pesar de este patrón, nunca podrá evitar que se reasigne un nombre de variable global.

Usando el espacio de nombres de nivel superior, esto es imposible. Cuando corres

 var = 1 

Almacena la clave var y el valor 1 en el diccionario global. Es aproximadamente equivalente a llamar a globals().__setitem__('var', 1) . El problema es que no puede reemplazar el diccionario global en una secuencia de comandos en ejecución (probablemente puede jugar con la stack, pero no es una buena idea). Sin embargo, puede ejecutar código en un espacio de nombres secundario y proporcionar un diccionario personalizado para sus globales.

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

Eso activará un REPL que casi funciona normalmente, pero rechaza cualquier bash de establecer la variable val . También puede utilizar execfile(filename, myg) . Tenga en cuenta que esto no protege contra el código malicioso.

Una solución fea es reasignar en destructor. Pero no es una tarea de sobrecarga real.

 import copy global a class MyClass(): def __init__(self): a = 1000 # ... def __del__(self): a = copy.copy(self) a = MyClass() a = 1 

Sí, es posible, puede manejar __assign__ mediante la modificación de ast .

pip install assign

Prueba con:

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

Conseguirás

 >>> import magic >>> import test called with c 

El proyecto se encuentra en https://github.com/RyanKung/assign Y la idea más simple: https://gist.github.com/RyanKung/4830d6c8474e6bcefa4edd13f122b4df

En general, el mejor enfoque que encontré es anular __ilshift__ como colocador y __rlshift__ como captador, siendo duplicado por el decorador de propiedades. Es casi el último operador que se resuelve solo (| & ^) y los lógicos son más bajos. Rara vez se usa ( __lrshift__ es menos, pero se puede tomar en cuenta).

Al usar el paquete de asignación PyPi, solo se puede controlar la asignación hacia adelante, por lo que la “fuerza” real del operador es menor. Ejemplo de paquete de asignación de PyPi:

 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 

salida:

 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  print('y.val =', y.val) AttributeError: 'int' object has no attribute 'val' 

Lo mismo con el turno:

 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 

salida:

 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  y.val = 4 AttributeError: can't set attribute 

Entonces, el operador <<= obtener valor en una propiedad es la solución mucho más limpia a nivel visual y no intenta que el usuario cometa algunos errores de reflexión como:

 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 

Dentro de un módulo, esto es absolutamente posible, a través de un poco de magia oscura.

 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 

Tenga en cuenta que incluso dentro del módulo, no puede escribir en la variable protegida una vez que se produce el cambio de clase. El ejemplo anterior asume que el código reside en un módulo llamado tst . Puede hacer esto en la repl cambiando tst a __main__ .