Is it possible to overload Python assignment?
PythonClassMethodsAssignment OperatorMagic MethodsPython Problem Overview
Is there a magic method that can overload the assignment operator, like __assign__(self, new_value)
?
I'd like to forbid a re-bind for an instance:
class Protect():
def __assign__(self, value):
raise Exception("This is an ex-parrot")
var = Protect() # once assigned...
var = 1 # this should raise Exception()
Is it possible? Is it insane? Should I be on medicine?
Python Solutions
Solution 1 - Python
The way you describe it is absolutely not possible. Assignment to a name is a fundamental feature of Python and no hooks have been provided to change its behavior.
However, assignment to a member in a class instance can be controlled as you want, by overriding .__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
Note that there is a member variable, _locked
, that controls whether the assignment is permitted. You can unlock it to update the value.
Solution 2 - Python
No, as assignment is a language intrinsic which doesn't have a modification hook.
Solution 3 - Python
I don't think it's possible. The way I see it, assignment to a variable doesn't do anything to the object it previously referred to: it's just that the variable "points" to a different object now.
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!
However, you can mimic the desired behavior by creating a wrapper object with __setitem__()
or __setattr__()
methods that raise an exception, and keep the "unchangeable" stuff inside.
Solution 4 - Python
Inside a module, this is absolutely possible, via a bit of dark magic.
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
The above example assumes the code resides in a module named tst
. You can do this in the repl
by changing tst
to __main__
.
If you want to protect access through the local module, make all writes to it through tst.var = newval
.
Solution 5 - Python
Using the top-level namespace, this is impossible. When you run
var = 1
It stores the key var
and the value 1
in the global dictionary. It is roughly equivalent to calling globals().__setitem__('var', 1)
. The problem is that you cannot replace the global dictionary in a running script (you probably can by messing with the stack, but that is not a good idea). However you can execute code in a secondary namespace, and provide a custom dictionary for its globals.
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()
That will fire up a REPL which almost operates normally, but refuses any attempts to set the variable val
. You could also use execfile(filename, myg)
. Note this doesn't protect against malicious code.
Solution 6 - Python
No there isn't
Think about it, in your example you are rebinding the name var to a new value. You aren't actually touching the instance of Protect.
If the name you wish to rebind is in fact a property of some other entity i.e myobj.var then you can prevent assigning a value to the property/attribute of the entity. But I assume thats not what you want from your example.
Solution 7 - Python
Generally, the best approach I found is overriding __ilshift__
as a setter and __rlshift__
as a getter, being duplicated by the property decorator.
It is almost the last operator being resolved just (| & ^) and logical are lower.
It is rarely used (__lrshift__
is less, but it can be taken to account).
Within using of PyPi assign package only forward assignment can be controlled, so actual 'strength' of the operator is lower. PyPi assign package example:
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
output:
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'
The same with 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
output:
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
So <<=
operator within getting value at a property is the much more visually clean solution and it is not attempting user to make some reflective mistakes like:
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
Solution 8 - Python
In the global namespace this is not possible, but you could take advantage of more advanced Python metaprogramming to prevent multiple instances of a the Protect
object from being created. The Singleton pattern is good example of this.
In the case of a Singleton you would ensure that once instantiated, even if the original variable referencing the instance is reassigned, that the object would persist. Any subsequent instances would just return a reference to the same object.
Despite this pattern, you would never be able to prevent a global variable name itself from being reassigned.
Solution 9 - Python
Yes, It's possible, you can handle __assign__
via modify ast
.
pip install assign
Test with:
class T():
def __assign__(self, v):
print('called with %s' % v)
b = T()
c = b
You will get
>>> import magic
>>> import test
called with c
The project is at https://github.com/RyanKung/assign
And the simpler gist: https://gist.github.com/RyanKung/4830d6c8474e6bcefa4edd13f122b4df
Solution 10 - Python
I will burn in Python hell, but what's life without a little fun.
Important disclaimers:
- I only provide this example for fun
- I'm 100% sure I don't understand this well
- It might not even be safe to do this, in any sense
- I don't think this is practical
- I don't think this is a good idea
- I don't even want to seriously try to implement this
- This doesn't work for jupyter (probably ipython too)*
Maybe you can't overload assignment, but you can (at least with Python ~3.9) achieve what you want even at the top-level namespace. It will be hard doing it "properly" for all cases, but here's a small example by hacking audithook
s:
import sys
import ast
import inspect
import dis
import types
def hook(name, tup):
if name == "exec" and tup:
if tup and isinstance(tup[0], types.CodeType):
# Probably only works for my example
code = tup[0]
# We want to parse that code and find if it "stores" a variable.
# The ops for the example code would look something like this:
# ['LOAD_CONST', '<0>', 'STORE_NAME', '<0>',
# 'LOAD_CONST', 'POP_TOP', 'RETURN_VALUE', '<0>']
store_instruction_arg = None
instructions = [dis.opname[op] for op in code.co_code]
# Track the index so we can find the '<NUM>' index into the names
for i, instruction in enumerate(instructions):
# You might need to implement more logic here
# or catch more cases
if instruction == "STORE_NAME":
# store_instruction_arg in our case is 0.
# This might be the wrong way to parse get this value,
# but oh well.
store_instruction_arg = code.co_code[i + 1]
break
if store_instruction_arg is not None:
# code.co_names here is: ('a',)
var_name = code.co_names[store_instruction_arg]
# Check if the variable name has been previously defined.
# Will this work inside a function? a class? another
# module? Well... :D
if var_name in globals():
raise Exception("Cannot re-assign variable")
# Magic
sys.addaudithook(hook)
And here's the example:
>>> a = "123"
>>> a = 123
Traceback (most recent call last):
File "<stdin>", line 21, in hook
Exception: Cannot re-assign variable
>>> a
'123'
*For Jupyter I found another way that looked a tiny bit cleaner because I parsed the AST instead of the code object:
import sys
import ast
def hook(name, tup):
if name == "compile" and tup:
ast_mod = tup[0]
if isinstance(ast_mod, ast.Module):
assign_token = None
for token in ast_mod.body:
if isinstance(token, ast.Assign):
target, value = token.targets[0], token.value
var_name = target.id
if var_name in globals():
raise Exception("Can't re-assign variable")
sys.addaudithook(hook)
Solution 11 - Python
A ugly solution is to reassign on destructor. But it's no real overload assignment.
import copy
global a
class MyClass():
def __init__(self):
a = 1000
# ...
def __del__(self):
a = copy.copy(self)
a = MyClass()
a = 1
Solution 12 - Python
As mentioned by other people, there is no way to do it directly. It can be overridden for class members though, which is good for many cases.
As Ryan Kung mentioned, the AST of a package can be instrumented so that all assignments can have a side effect if the class assigned implements specific method(s). Building on his work to handle object creation and attribute assignment cases, the modified code and a full description is available here:
https://github.com/patgolez10/assignhooks
The package can be installed as: pip3 install assignhooks
Example
class SampleClass():
name = None
def __assignpre__(self, lhs_name, rhs_name, rhs):
print('PRE: assigning %s = %s' % (lhs_name, rhs_name))
# modify rhs if needed before assignment
if rhs.name is None:
rhs.name = lhs_name
return rhs
def __assignpost__(self, lhs_name, rhs_name):
print('POST: lhs', self)
print('POST: assigning %s = %s' % (lhs_name, rhs_name))
def myfunc():
b = SampleClass()
c = b
print('b.name', b.name)
to instrument it, e.g.
import assignhooks
assignhooks.instrument.start() # instrument from now on
import testmod
assignhooks.instrument.stop() # stop instrumenting
# ... other imports and code bellow ...
testmod.myfunc()
Will produce:
> $ python3 ./test.py
POST: lhs <testmod.SampleClass object at 0x1041dcc70>
POST: assigning b = SampleClass
PRE: assigning c = b
POST: lhs <testmod.SampleClass object at 0x1041dcc70>
POST: assigning c = b
b.name b