提问人:Caruccio 提问时间:6/14/2012 最后编辑:EricCaruccio 更新时间:1/26/2023 访问量:66307
是否可以重载 Python 赋值?
Is it possible to overload Python assignment?
问:
有没有一种神奇的方法可以重载赋值运算符,比如?__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()
可能吗?是不是疯了?我应该吃药吗?
答:
我认为这是不可能的。在我看来,对变量的赋值不会对它之前引用的对象做任何事情:只是变量现在“指向”另一个对象。
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__()
不,因为赋值是一种语言固有的,没有修改钩子。
评论
不,没有
想想看,在您的示例中,您将名称 var 重新绑定到一个新值。 您实际上并没有触及 Protect 的实例。
如果您要重新绑定的名称实际上是其他实体的属性,即 myobj.var 然后,您可以阻止为实体的属性/属性分配值。 但我认为这不是你想要从你的例子中得到的。
评论
__dict__.__setattr__
module.__dict__
你描述它的方式是绝对不可能的。为名称赋值是 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
评论
@property
getattr(self, "_locked", None)
而不是。self.__dict__.get("_locked")
False
None
_locked
.get()
get
None
getattr
getattr()
.__dict__.get()
getattr()
在全局命名空间中,这是不可能的,但您可以利用更高级的 Python 元编程来防止创建对象的多个实例。单例模式就是一个很好的例子。Protect
对于单例,您将确保一旦实例化,即使引用实例的原始变量被重新分配,该对象也会保留。任何后续实例都将只返回对同一对象的引用。
尽管存在这种模式,但您永远无法阻止重新分配全局变量名称本身。
评论
var = 1
Protect()
var
一个丑陋的解决方案是在析构函数上重新分配。但这不是真正的过载分配。
import copy
global a
class MyClass():
def __init__(self):
a = 1000
# ...
def __del__(self):
a = copy.copy(self)
a = MyClass()
a = 1
使用顶级命名空间,这是不可能的。跑步时
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)
评论
是的,这是可能的,您可以通过修改来处理。__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
评论
print('called with %s' % self)
'c'
v
__assign__
b = c
c = b
一般来说,我发现最好的方法是作为 setter 和 getter 进行覆盖,由属性装饰器复制。
它几乎是最后一个被解析的运算符 (| & ^) 和逻辑较低。
它很少使用(较少,但可以考虑在内)。__ilshift__
__rlshift__
__lrshift__
在使用 PyPi assign 包时,只能控制前向分配,因此算子的实际“强度”较低。 PyPi assign 包示例:
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
在模块内部,这绝对是可能的,通过一点黑暗魔法。
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
评论
tst
var
tst
tst.var = 5
var = 5
正如其他人所提到的,没有办法直接做到这一点。不过,对于类成员来说,它可以被覆盖,这在许多情况下都有好处。
正如 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
我会在 Python 地狱中燃烧,但如果没有一点乐趣,生活又算得了什么。
重要免责声明:
- 我提供这个例子只是为了好玩
- 我 100% 确定我不太明白这一点
- 从任何意义上讲,这样做甚至可能不安全
- 我认为这是不切实际的
- 我不认为这是一个好主意
- 我什至不想认真尝试实现这一点
- 这不适用于 jupyter(可能也是 ipython)*
也许你不能重载赋值,但你可以(至少使用 Python ~3.9)即使在顶级命名空间中也能实现你想要的。在所有情况下都很难“正确”地做到这一点,但这里有一个小例子,通过黑客攻击: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)
下面是示例:
>>> 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)
评论
exec
python file.py
从 Python 3.8 开始,可以使用 暗示值是只读的。这意味着在运行时没有任何变化,允许任何人更改值,但如果您使用任何可以读取类型提示的 linter,那么它会警告用户是否尝试分配它。typing.Final
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
评论
exec in d
ScreenUpdating = False
__all__
属性使用户更难导出私有数据。这是 Python 标准库的常用方法