Is it possible to overload Python assignment?
Solution 1
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
No, as assignment is a language intrinsic which doesn't have a modification hook.
Solution 3
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
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
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.
Caruccio
Updated on October 12, 2021Comments
-
Caruccio over 2 years
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?
-
Sven Marnach almost 12 yearsBe assured, this won't happen in Python 4.x.
-
Caruccio almost 12 yearsAlmost there! I tried to overload the module's
__dict__.__setattr__
butmodule.__dict__
itself is read-only. Also, type(mymodule) == <type 'module'>, and it's not instanceable. -
Caruccio almost 12 yearsA singleton is not enough, since
var = 1
does not calls the singleton mechanism. -
zigg almost 12 yearsNow I'm tempted to go write a PEP for subclassing and replacing the current scope.
-
jathanism almost 12 yearsUnderstood. I apologize if I wasn't clear. A singleton would prevent further instances of an object (e.g.
Protect()
) from being created. There is no way to protect the originally assigned name (e.g.var
). -
jtpereyda over 8 yearsUsing
@property
with a getter but no setter is a similar way to pseudo-overload assignment. -
Joseph Garvin almost 7 yearsThis is dark magic! I fully expected to just find a bunch of answers where people suggest using an object explicitly with an overridden setattr, didn't think about overriding globals and locals with a custom object, wow. This must make PyPy cry though.
-
zezollo over 6 yearsThere's something I don't get... Shouldn't it be
print('called with %s' % self)
? -
Vedran Šego about 6 years
getattr(self, "_locked", None)
instead ofself.__dict__.get("_locked")
. -
steveha about 6 years@VedranŠego I followed your suggestion but used
False
instead ofNone
. Now if someone deletes the_locked
member variable, the.get()
call won't raise an exception. -
Vedran Šego about 6 years@steveha Did it actually raise an exception for you?
get
defaults toNone
, unlikegetattr
which would indeed raise an exception. -
steveha almost 6 yearsAh, no, I didn't see it raise an exception. Somehow I overlooked that you were suggesting to use
getattr()
rather than.__dict__.get()
. I guess it's better to usegetattr()
, that's what it's for. -
Mad Physicist over 5 years@Caruccio. Unrelated, but 99% of the time, at least in CPython, 1 behaves as a singleton.
-
HelloGoodbye over 4 yearsThere are a few things I don't understand: 1) How (and why?) does the string
'c'
end up in thev
argument for the__assign__
method? What does your example actually show? It confuses me. 2) When would this be useful? 3) How does this relate to the question? For it to correspond the the code written in the question, wouldn't you need to writeb = c
, notc = b
? -
mutableVoid almost 3 yearsI'm not sure if things are different for my version / implementation of python, but for me this works only when trying to access variables form outside of the protected module; i.e. if I protect the module
tst
and assign Protect() to a variable namedvar
twice within the moduletst
, no exception is raised. This is in line with the documentation stating that direct assignment utilizes the non-overridable globals dict directly. -
Perkins almost 3 yearsI don't remember which version of python I tested that with. At the time, I was surprised it protected the variable from local changes, but now I cannot replicate that. It is worth noting that
tst.var = 5
will throw an exception, butvar = 5
will not. -
Gary over 2 years@mad-physicist How do I set this to default when I run a python shell? I did try overriding globals for the same. Not sure if I am able to run a python executable to run the above override all the way when I run a python command not in a shell but a code. Any idea how I can do it?
-
Gary over 2 yearsHow do I set this to default when I run a python shell? I did try overriding globals for the same. Not sure if I am able to run a python executable to run the above addautdithook all the way when I run a python command not in a shell but a code. Any idea how I can do it making the audit hook the default?
-
Gary over 2 yearsLooking at this docs.python.org/3/c-api/sys.html#c.PySys_AddAuditHook docs.python.org/3/library/audit_events.html This Audit Hooks were definitely a fantastic change! It solves my purpose with a little tweak but any way I can completely support python executable runs through command line or third party call all the time with such hooks by default (Python environment default config)? May be I am missing something? Probably another PEP which someone can take and file this. Or is it really needed?
-
Kostas Mouratidis over 2 yearsI'm pretty sure this only works because the Python REPL runs
exec
on every line, but runningpython file.py
does not. Maybe the "correct" way forward would be to do something like what you're trying by going into C territory, but I'm not familiar with that. Another way could be relying on hooking the import system instead of audit hooks: you could for example read the file your magic code gets imported into and parsing it somehow. That could be fun. -
Mad Physicist over 2 years@Gary. #1) sounds like code smell to me. #2) just run the statements shown here at the beginning of your driver script.
-
Mad Physicist over 2 yearsOP is interested in the case where you unbind a name, not where you bind it.
-
Gary over 2 years@mad-physicist Code smell. No. It is not. There are use cases. But Driver script? I did not understand. I would want to explore that? What is a driver supposed to mean? How do I do that?
-
Gary over 2 years@mad-physicist This solves my purpose a little more. But I have to call this at all files/modules to be consistent. docs.python.org/3/library/audit_events.html
-
Mad Physicist over 2 years@Gary. A driver script is just the script containing your "main". I agree that this approach is somewhat limiting. "There are use cases" doesn't mean this isn't code smell. I'm curious to know which use-case you have in mind for which this is the optimal solution.
-
Gary over 2 years@mad-physicist run an event everytime across modules (multiple modules/file modules) on value assignation or some activity on a variable value like change event. Was checking if I could work on a thing like github.com/python-lang-codes/strongtypes long before. Stopped after github.com/ganeshkbhat/peps/blob/master/pep-9999.rst pep was rejected. Had also tried manipulating AST for this but didnt quite work since target change event was never captured before. But later saw docs.python.org/3/library/audit_events.html was released in 3.8v which solves the purpose somewhat.
-
Gary over 2 yearsyes. The could be one way. But that would not affect the shell or the command in any way. Probably I could do with managing the same hook in every file. But it kind of seems redundant
-
Mad Physicist over 2 years@Gary Sounds like you can trivially write a small class to do this, or just use a property somewhere. Sure you have to write an extra
.x
in your access, but the code is legible and easy to maintain that way. Modifying AST makes your code unportable. -
Mad Physicist over 2 years@Gary. You can subclass your module. See here for example: stackoverflow.com/q/4432376/2988730