简体   繁体   English

是否可以重载 Python 分配?

[英]Is it possible to overload Python assignment?

Is there a magic method that can overload the assignment operator, like __assign__(self, new_value) ?是否有一种神奇的方法可以重载赋值运算符,例如__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?我应该吃药吗?

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.给名字赋值是 Python 的一个基本特性,没有提供任何钩子来改变它的行为。

However, assignment to a member in a class instance can be controlled as you want, by overriding .__setattr__() .但是,可以通过覆盖.__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.请注意,有一个成员变量_locked ,用于控制是否允许赋值。 You can unlock it to update the value.您可以解锁它以更新值。

不,因为赋值是一种语言内在的,它没有修改钩子。

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.但是,您可以通过使用引发异常的__setitem__()__setattr__()方法创建包装对象来模拟所需的行为,并将“不可更改”的内容保留在里面。

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 .上面的示例假设代码驻留在名为tst的模块中。 You can do this in the repl by changing tst to __main__ .您可以通过将tst更改为__main__repl执行此操作。

If you want to protect access through the local module, make all writes to it through tst.var = newval .如果要保护通过本地模块的访问,请通过tst.var = newval对其进行所有写入。

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.它将键var和值1在全局字典中。 It is roughly equivalent to calling globals().__setitem__('var', 1) .它大致相当于调用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 .这将启动一个几乎正常运行的 REPL,但拒绝任何设置变量val尝试。 You could also use execfile(filename, myg) .您也可以使用execfile(filename, myg) Note this doesn't protect against malicious code.请注意,这并不能防止恶意代码。

I will burn in Python hell, but what's life without a little fun.我会在 Python 地狱中燃烧,但是没有一点乐趣的生活是什么。


Important disclaimers :重要免责声明

  • I only provide this example for fun我只是为了好玩提供这个例子
  • I'm 100% sure I don't understand this well我 100% 确定我不太了解这一点
  • 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)*这不适用于 jupyter(也可能是 ipython)*

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.也许你不能重载赋值,但你可以(至少使用 Python ~3.9)实现你想要的,即使在顶级命名空间。 It will be hard doing it "properly" for all cases, but here's a small example by hacking audithook s:在所有情况下都很难“正确”地做到这一点,但这里有一个通过破解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: *对于 Jupyter,我找到了另一种看起来更简洁的方法,因为我解析了 AST 而不是代码 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)

No there isn't不,没有

Think about it, in your example you are rebinding the name var to a new value.想想看,在您的示例中,您将名称 var 重新绑定到一个新值。 You aren't actually touching the instance of Protect.您实际上并未触及 Protect 实例。

If the name you wish to rebind is in fact a property of some other entity ie myobj.var then you can prevent assigning a value to the property/attribute of the entity.如果您希望重新绑定的名称实际上是某个其他实体的属性,即 myobj.var,那么您可以阻止为该实体的属性/属性赋值。 But I assume thats not what you want from your example.但我认为那不是你想要的例子。

Yes, It's possible, you can handle __assign__ via modify ast .是的,有可能,您可以通过修改ast来处理__assign__

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该项目位于https://github.com/RyanKung/assign和更简单的要点: https://gist.github.com/RyanKung/4830d6c8474e6bcefa4edd13f122b4df : https://gist.github.com/RyanKung/4830d6c8474e6bcefa4edd13f122b4df

Generally, the best approach I found is overriding __ilshift__ as a setter and __rlshift__ as a getter, being duplicated by the property decorator.通常,我发现的最佳方法是将__ilshift__重写为 setter,将__rlshift__为 getter,并由属性装饰器进行复制。 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).它很少使用( __lrshift__较少,但可以考虑)。

Within using of PyPi assign package only forward assignment can be controlled, so actual 'strength' of the operator is lower.在使用 PyPi 分配包时,只能控制前向分配,因此算子的实际“强度”较低。 PyPi assign package example: 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

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

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.在全局命名空间中这是不可能的,但您可以利用更高级的 Python 元编程来防止创建Protect对象的多个实例。 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.尽管有这种模式,您永远无法阻止重新分配全局变量名称本身。

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

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).正如 Ryan Kung 所提到的,可以对包的 AST 进行检测,因此如果分配的类实现了特定的方法,则所有分配都会产生副作用。 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 https://github.com/patgolez10/assignhooks

The package can be installed as: pip3 install assignhooks该包可以安装为: pip3 install assignhooks

Example <testmod.py>:示例 <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, eg <test.py>对其进行检测,例如 <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 $ 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

Beginning Python 3.8, it is possible to hint that a value is read-only using typing.Final .从 Python 3.8 开始,可以使用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.这意味着在运行时没有任何变化,允许任何人更改值,但如果您使用任何可以读取类型提示的 linter,那么如果用户尝试分配它,它将警告用户。

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 .另一种常见模式是公开函数而不是模块常量,例如sys.getrecursionlimitsys.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.尽管用户可以执行module.get_x = my_get_x ,但用户显然试图破坏它,但无法修复。 In this way we can prevent people from "accidentally" changing values in our module with minimal complexity.通过这种方式,我们可以防止人们以最小的复杂性“意外”更改模块中的值。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM