是否有可能重载 Python 赋值?
- 2025-02-24 09:29:00
- admin 原创
- 83
问题描述:
是否有一个魔术方法可以重载赋值运算符,例如__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()
有可能吗?这太疯狂了吗?我应该吃药吗?
解决方案 1:
您描述的方式绝对不可能。 分配给名称是 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
,用于控制是否允许赋值。您可以解锁它以更新值。
解决方案 2:
不,因为赋值是一种语言固有特性,它没有修改钩子。
解决方案 3:
在模块内部,这绝对是可能的,通过一点黑魔法。
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
。
解决方案 4:
我认为这是不可能的。在我看来,赋值给变量不会对它之前引用的对象产生任何影响:只是变量现在“指向”了另一个对象。
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__()
方法来模拟所需的行为,并将“不可改变”的内容保留在里面。
解决方案 5:
使用顶级命名空间,这是不可能的。当你运行
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:
我将在 Python 地狱中燃烧,但是没有一点乐趣的生活还有什么意义。
重要免责声明:
我提供这个例子只是为了好玩
我百分之百确定我不太明白这一点
从任何意义上来说,这样做可能都不安全
我认为这不切实际
我认为这不是一个好主意
我甚至不想认真尝试实现这一点
这对 jupyter 不起作用(可能对 ipython 也不起作用)*
也许你不能重载赋值,但你可以(至少在 Python ~3.9 中)在顶级命名空间中实现你想要的。在所有情况下“正确”地做到这一点会很困难,但这里有一个通过 hacking 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)
以下是示例:
>>> 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)
解决方案 7:
通常,我发现最好的方法是覆盖__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:packagespyksppykspcompiler2simple_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:packagespyksppykspcompiler2simple_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
解决方案 8:
没有
想想看,在您的示例中,您正在将名称变量重新绑定到新值。您实际上并没有触及 Protect 的实例。
如果您希望重新绑定的名称实际上是其他实体的属性,即 myobj.var,那么您可以阻止为实体的属性/特性分配值。但我认为这不是您在示例中想要的。
解决方案 9:
是的,可以,你可以__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
解决方案 10:
在全局命名空间中这是不可能的,但您可以利用更高级的 Python 元编程来防止Protect
创建对象的多个实例。单例模式就是一个很好的例子。
对于单例,您可以确保实例化后,即使重新分配引用该实例的原始变量,该对象仍会持续存在。任何后续实例都只会返回对同一对象的引用。
尽管有这种模式,您仍然无法阻止全局变量名称本身被重新分配。
解决方案 11:
正如其他人提到的,没有办法直接做到这一点。不过,它可以被类成员覆盖,这在很多情况下都是很好的。
正如 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
解决方案 12:
从 Python 3.8 开始,可以使用 来提示某个值是只读的typing.Final
。这意味着运行时不会发生任何变化,任何人都可以更改该值,但如果您使用任何可以读取类型提示的 linter,那么当用户尝试分配该值时,它会向用户发出警告。
from typing import Final
x: Final[int] = 3
x = 5 # Cannot assign to final name "x" (mypy)
这使得代码更加清晰,但它完全信任用户在运行时尊重它,不会试图阻止用户更改值。
另一种常见模式是公开函数而不是模块常量,如sys.getrecursionlimit
和sys.setrecursionlimit
。
def get_x() -> int:
return 3
尽管用户可以这样做module.get_x = my_get_x
,但用户显然试图破坏它,这是无法修复的。通过这种方式,我们可以以最小的复杂性防止人们“意外”更改模块中的值。
解决方案 13:
一个丑陋的解决方案是在析构函数上重新分配。但这不是真正的重载分配。
import copy
global a
class MyClass():
def __init__(self):
a = 1000
# ...
def __del__(self):
a = copy.copy(self)
a = MyClass()
a = 1
扫码咨询,免费领取项目管理大礼包!