98

有没有一种神奇的方法可以重载赋值运算符,比如__assign__(self, new_value)

我想禁止重新绑定一个实例:

class Protect():
  def __assign__(self, value):
    raise Exception("This is an ex-parrot")

var = Protect()  # once assigned...
var = 1          # this should raise Exception()

可能吗?疯了吗?我应该吃药吗?

4

12 回答 12

89

你描述的方式是绝对不可能的。给名字赋值是 Python 的一个基本特性,并且没有提供任何钩子来改变它的行为。

但是,可以通过覆盖来控制对类实例中成员的分配.__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

请注意,有一个成员变量_locked控制是否允许赋值。您可以解锁它以更新值。

于 2012-06-13T23:41:14.907 回答
32

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

于 2012-06-13T23:20:26.733 回答
9

我不认为这是可能的。在我看来,赋值给一个变量并没有对它之前引用的对象做任何事情:只是变量“指向”了一个不同的对象。

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!

__setitem__()但是,您可以通过使用或引发异常的方法创建包装对象来模仿所需的行为__setattr__(),并在其中保留“不可更改”的内容。

于 2012-06-13T23:20:10.400 回答
8

在模块内部,这绝对是可能的,通过一点黑魔法。

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

上面的示例假设代码驻留在名为tst. 您可以repl通过更改tst为来执行此操作__main__

如果要保护通过本地模块的访问,请通过tst.var = newval.

于 2018-09-21T07:25:16.800 回答
6

使用顶级命名空间,这是不可能的。当你跑

var = 1

它将键var和值存储1在全局字典中。它大致相当于调用globals().__setitem__('var', 1). 问题是您无法在正在运行的脚本中替换全局字典(您可能可以通过弄乱堆栈,但这不是一个好主意)。但是,您可以在辅助命名空间中执行代码,并为其全局变量提供自定义字典。

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()

这将启动一个几乎正常运行的 REPL,但拒绝任何设置变量的尝试val。你也可以使用execfile(filename, myg). 请注意,这不能防止恶意代码。

于 2016-11-17T02:14:58.717 回答
3

不,没有

想想看,在您的示例中,您将名称 var 重新绑定到一个新值。您实际上并没有接触 Protect 的实例。

如果您希望重新绑定的名称实际上是某个其他实体的属性,即 myobj.var,那么您可以阻止为实体的属性/属性分配值。但是我认为这不是您想要的示例。

于 2012-06-13T23:32:31.830 回答
3

一般来说,我发现最好的方法是__ilshift__作为 setter 和__rlshift__getter 覆盖,由属性装饰器复制。它几乎是最后一个被解析的运算符 (| & ^) 并且逻辑较低。很少用(__lrshift__少用,但可以考虑)。

在使用 PyPi 分配包时,只能控制前向分配,因此操作符的实际“强度”较低。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

输出:

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'

与移位相同:

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

输出:

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

因此<<=,在属性中获取价值的操作员是视觉上更干净的解决方案,它不会试图让用户犯一些反思性错误,例如:

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
于 2018-07-22T21:28:27.913 回答
2

在全局命名空间中这是不可能的,但您可以利用更高级的 Python 元编程来防止Protect创建对象的多个实例。单例模式就是一个很好的例子。

在 Singleton 的情况下,您将确保一旦实例化,即使引用实例的原始变量被重新分配,该对象也将持续存在。任何后续实例只会返回对同一对象的引用。

尽管有这种模式,但您永远无法阻止重新分配全局变量名本身。

于 2012-06-13T23:48:06.690 回答
2

是的,有可能,您可以__assign__通过 modify处理ast

pip install assign

测试:

class T():
    def __assign__(self, v):
        print('called with %s' % v)
b = T()
c = b

你会得到

>>> import magic
>>> import test
called with c

该项目位于https://github.com/RyanKung/assign 并且更简单的要点:https://gist.github.com/RyanKung/4830d6c8474e6bcefa4edd13f122b4df

于 2017-10-26T16:54:42.280 回答
2

我将在 Python 地狱中燃烧,但没有一点乐趣的生活是什么。


重要免责声明

  • 我提供这个例子只是为了好玩
  • 我100%确定我不太了解这个
  • 从任何意义上说,这样做甚至可能都不安全
  • 我认为这不实用
  • 我不认为这是一个好主意
  • 我什至不想认真尝试实现这一点
  • 这不适用于 jupyter(也可能是 ipython)*

也许您不能重载赋值,但即使在顶级命名空间中,您也可以(至少使用 Python ~3.9)实现您想要的。在所有情况下都很难“正确”地做到这一点,但这里有一个黑客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)

这是一个例子:

>>> a = "123"
>>> a = 123
Traceback (most recent call last):
  File "<stdin>", line 21, in hook
Exception: Cannot re-assign variable

>>> a
'123'

*对于 Jupyter,我发现了另一种看起来更简洁的方法,因为我解析的是 AST 而不是代码对象:

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)
于 2021-10-11T10:42:11.667 回答
1

正如其他人所说,没有办法直接做到这一点。但是,它可以被类成员覆盖,这对许多情况都有好处。

正如 Ryan Kung 所提到的,可以对包的 AST 进行检测,以便如果分配的类实现特定方法,所有分配都会产生副作用。基于他处理对象创建和属性分配案例的工作,修改后的代码和完整描述可在此处获得:

https://github.com/patgolez10/assignhooks

该软件包可以安装为:pip3 install assignhooks

示例 <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)

对其进行检测,例如 <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()

将产生:

$ 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
于 2021-02-18T22:17:59.853 回答
1

一个丑陋的解决方案是在析构函数上重新分配。但这不是真正的重载分配。

import copy
global a

class MyClass():
    def __init__(self):
            a = 1000
            # ...

    def __del__(self):
            a = copy.copy(self)


a = MyClass()
a = 1
于 2016-08-26T12:55:23.807 回答