是否有可能重载Python赋值?

时间:2012-06-13 23:13:33

标签: python class methods assignment-operator magic-methods

是否有一种可以重载赋值运算符的魔术方法,例如__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()

有可能吗?这是疯了吗?我应该上药吗?

11 个答案:

答案 0 :(得分:52)

你描述它的方式是绝对不可能的。对名称的赋值是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,用于控制是否允许分配。您可以将其解锁以更新值。

答案 1 :(得分:26)

不,因为赋值是language intrinsic,它没有修改钩子。

答案 2 :(得分:8)

我不认为这是可能的。我看到它的方式,赋值给变量对它之前提到的对象没有任何作用:它只是变量“指向”现在的另一个对象。

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__()方法创建包装器对象来模仿所需的行为,并保留“不可更改”的内容。

答案 3 :(得分:2)

没有

考虑一下,在您的示例中,您将名称var重新绑定为新值。 您实际上并没有触及Protect的实例。

如果您要重新绑定的名称实际上是某个其他实体的财产,即 myobj.var然后你可以防止为实体的属性/属性赋值。 但我认为这不是你想要的例子。

答案 4 :(得分:2)

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

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

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

答案 5 :(得分:2)

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

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)。请注意,这并不能防止恶意代码。

答案 6 :(得分:1)

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

import copy
global a

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

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


a = MyClass()
a = 1

答案 7 :(得分:1)

是的,您可以通过修改__assign__来处理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

答案 8 :(得分:1)

通常,我发现的最佳方法是将__ilshift__用作设置方法,将__rlshift__用作获取方法,并由属性装饰器复制。 几乎是最后一个要解析的运算符,只是(|&^)并且逻辑较低。 很少使用({__lrshift__较少,但是可以考虑)。

在使用PyPi Assign软件包时,只能控制正向分配,因此操作员的实际“力量”较低。 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'

与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

输出:

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

答案 9 :(得分:1)

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

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__

答案 10 :(得分:1)

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

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

https://github.com/patgolez10/assignhooks

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

示例

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)

对其进行检测,例如

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