是否有可能重载Python赋值?

Car*_*cio 65 python methods class magic-methods assignment-operator

有没有一个可以重载赋值运算符的魔术方法,比如__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()
Run Code Online (Sandbox Code Playgroud)

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

ste*_*eha 58

你描述它的方式是绝对不可能的.对名称的赋值是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
Run Code Online (Sandbox Code Playgroud)

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

  • 将`@ property`与getter一起使用但没有setter与伪重载赋值类似. (3认同)
  • `getattr(self, "_locked", None)` 而不是 `self.__dict__.get("_locked")`。 (2认同)
  • 啊,不,我没有看到它引发异常。不知何故,我忽略了您建议使用“getattr()”而不是“.__dict__.get()”。我想最好使用 `getattr()`,这就是它的用途。 (2认同)

msw*_*msw 26

不,因为赋值是一种没有修饰钩子的语言本征.

  • 现在我很想写一个PEP用于子类化并替换当前范围. (5认同)
  • 请放心,这不会发生在Python 4.x中. (2认同)

Lev*_*sky 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!
Run Code Online (Sandbox Code Playgroud)

但是,您可以通过使用__setitem__()__setattr__()引发异常的方法创建包装器对象来模仿所需的行为,并保持"不可更改"的内容.


Per*_*ins 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
Run Code Online (Sandbox Code Playgroud)

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

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


Kos*_*dis 6

我会在 Python 地狱中燃烧,但生活怎能没有一点乐趣。


重要免责声明

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

也许你不能重载赋值,但你可以(至少使用Python ~3.9)实现你想要的,即使在顶级命名空间。对于所有情况都很难“正确”地做到这一点,但这里有一个 hacking 的小例子audithook

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)
Run Code Online (Sandbox Code Playgroud)

这是例子:

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

>>> a
'123'
Run Code Online (Sandbox Code Playgroud)

*对于 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)
Run Code Online (Sandbox Code Playgroud)


Per*_*ins 5

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

var = 1
Run Code Online (Sandbox Code Playgroud)

它将键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()
Run Code Online (Sandbox Code Playgroud)

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


小智 5

一般来说,我发现的最好方法是重写__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
Run Code Online (Sandbox Code Playgroud)

输出:

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'
Run Code Online (Sandbox Code Playgroud)

与移位相同:

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
Run Code Online (Sandbox Code Playgroud)

输出:

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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)