Is it possible to overload Python assignment?

52,564

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.

Share:
52,564
Caruccio
Author by

Caruccio

Updated on October 12, 2021

Comments

  • Caruccio
    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
    Sven Marnach almost 12 years
    Be assured, this won't happen in Python 4.x.
  • Caruccio
    Caruccio almost 12 years
    Almost there! I tried to overload the module's __dict__.__setattr__ but module.__dict__ itself is read-only. Also, type(mymodule) == <type 'module'>, and it's not instanceable.
  • Caruccio
    Caruccio almost 12 years
    A singleton is not enough, since var = 1 does not calls the singleton mechanism.
  • zigg
    zigg almost 12 years
    Now I'm tempted to go write a PEP for subclassing and replacing the current scope.
  • jathanism
    jathanism almost 12 years
    Understood. 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
    jtpereyda over 8 years
    Using @property with a getter but no setter is a similar way to pseudo-overload assignment.
  • Joseph Garvin
    Joseph Garvin almost 7 years
    This 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
    zezollo over 6 years
    There's something I don't get... Shouldn't it be print('called with %s' % self)?
  • Vedran Šego
    Vedran Šego about 6 years
    getattr(self, "_locked", None) instead of self.__dict__.get("_locked").
  • steveha
    steveha about 6 years
    @VedranŠego I followed your suggestion but used False instead of None. Now if someone deletes the _locked member variable, the .get() call won't raise an exception.
  • Vedran Šego
    Vedran Šego about 6 years
    @steveha Did it actually raise an exception for you? get defaults to None, unlike getattr which would indeed raise an exception.
  • steveha
    steveha almost 6 years
    Ah, 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 use getattr(), that's what it's for.
  • Mad Physicist
    Mad Physicist over 5 years
    @Caruccio. Unrelated, but 99% of the time, at least in CPython, 1 behaves as a singleton.
  • HelloGoodbye
    HelloGoodbye over 4 years
    There are a few things I don't understand: 1) How (and why?) does the string 'c' end up in the v 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 write b = c, not c = b?
  • mutableVoid
    mutableVoid almost 3 years
    I'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 named var twice within the module tst, no exception is raised. This is in line with the documentation stating that direct assignment utilizes the non-overridable globals dict directly.
  • Perkins
    Perkins almost 3 years
    I 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, but var = 5 will not.
  • Gary
    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
    Gary over 2 years
    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 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
    Gary over 2 years
    Looking 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
    Kostas Mouratidis over 2 years
    I'm pretty sure this only works because the Python REPL runs exec on every line, but running python 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
    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
    Mad Physicist over 2 years
    OP is interested in the case where you unbind a name, not where you bind it.
  • Gary
    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
    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
    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
    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
    Gary over 2 years
    yes. 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
    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
    Mad Physicist over 2 years
    @Gary. You can subclass your module. See here for example: stackoverflow.com/q/4432376/2988730