Is it possible to overload Python assignment?
Asked Answered
K

13

114

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?

Kirtley answered 13/6, 2012 at 23:13 Comment(6)
Use case: people are going to write small scripts using my service API, and I want to prevent them from changing internal data and propagate this change to the next script.Kirtley
Python explicitly avoids promising that a malicious or ignorant coder will be prevented from access. Other languages allow you to avoid some programmer error due to ignorance, but people have an uncanny ability to code around them.Mortise
you could execute that code using exec in d where d is some dictionary. if the code is on module level, every assignment should get sent back to the dictionary. You could either restore your values after execution/check whether values changed, or intercept the dictionary assignment, i.e. replace the dictionary of variables with another object.Clomb
Oh no, so it's impossible to simulate VBA behaviour like ScreenUpdating = False on module levelBanuelos
You can use the __all__ attribute of your module to make it harder for people to export private data. This is a common approach for the Python Standard LibraryZion
Can we generalize this for global assignment operation? Like x=1 and x=2 will give error.Scanty
S
101

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.

Stickleback answered 13/6, 2012 at 23:41 Comment(5)
Using @property with a getter but no setter is a similar way to pseudo-overload assignment.Biparietal
getattr(self, "_locked", None) instead of self.__dict__.get("_locked").Ferrocene
@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.Stickleback
@Stickleback Did it actually raise an exception for you? get defaults to None, unlike getattr which would indeed raise an exception.Ferrocene
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.Stickleback
M
35

No, as assignment is a language intrinsic which doesn't have a modification hook.

Mortise answered 13/6, 2012 at 23:20 Comment(2)
Be assured, this won't happen in Python 4.x.Tenebrae
Now I'm tempted to go write a PEP for subclassing and replacing the current scope.Naman
V
11

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.

Virgenvirgie answered 13/6, 2012 at 23:20 Comment(0)
B
11

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.

Baffle answered 21/9, 2018 at 7:25 Comment(2)
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.Letaletch
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.Baffle
B
8

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.

Baffle answered 17/11, 2016 at 2:14 Comment(9)
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.Dubenko
@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?Forayer
@Gary. #1) sounds like code smell to me. #2) just run the statements shown here at the beginning of your driver script.Yukoyukon
@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?Forayer
@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.htmlForayer
@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.Yukoyukon
@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.Forayer
@Forayer 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.Yukoyukon
@Gary. You can subclass your module. See here for example: https://mcmap.net/q/47479/-a-module-39-s-__setattr__-and-__getattr__-when-accessing-globals/2988730Yukoyukon
R
6

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

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)
Rex answered 11/10, 2021 at 10:42 Comment(4)
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?Forayer
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?Forayer
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.Rex
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 redundantForayer
R
5

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
Renunciation answered 22/7, 2018 at 21:28 Comment(0)
S
4

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.

Secrest answered 13/6, 2012 at 23:32 Comment(1)
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.Kirtley
U
4

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

Ulna answered 26/10, 2017 at 16:54 Comment(3)
There's something I don't get... Shouldn't it be print('called with %s' % self)?Devlin
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?Stair
OP is interested in the case where you unbind a name, not where you bind it.Yukoyukon
G
2

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.

Grand answered 13/6, 2012 at 23:48 Comment(3)
A singleton is not enough, since var = 1 does not calls the singleton mechanism.Kirtley
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).Grand
@Caruccio. Unrelated, but 99% of the time, at least in CPython, 1 behaves as a singleton.Yukoyukon
A
1

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 <testmod.py>:

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. <test.py>

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
Agree answered 18/2, 2021 at 22:17 Comment(0)
H
1

Beginning Python 3.8, it is possible to hint that a value is read-only using typing.Final. What this means is that nothing changes at runtime, allowing anyone to change the value, but if you're using any linter that can read type-hints then it's going to warn the user if they attempt to assign it.

from typing import Final

x: Final[int] = 3

x = 5  # Cannot assign to final name "x" (mypy)

This makes for way cleaner code, but it puts full trust in the user to respect it at runtime, making no attempt to stop users from changing values.

Another common pattern is to expose functions instead of module constants, like sys.getrecursionlimit and sys.setrecursionlimit.

def get_x() -> int:
    return 3

Although users can do module.get_x = my_get_x, there's an obvious attempt on the user's part to break it, which can't be fixed. In this way we can prevent people from "accidentally" changing values in our module with minimal complexity.

Hisakohisbe answered 26/1, 2023 at 4:41 Comment(0)
A
0

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
Acosta answered 26/8, 2016 at 12:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.