是否可以重载 Python 赋值?

Is it possible to overload Python assignment?

提问人:Caruccio 提问时间:6/14/2012 最后编辑:EricCaruccio 更新时间:1/26/2023 访问量:66307

问:

有没有一种神奇的方法可以重载赋值运算符,比如?__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()

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

python 方法 assignment-operator magic-methods

评论

2赞 Caruccio 6/14/2012
用例:人们将使用我的服务 API 编写小脚本,我想阻止他们更改内部数据并将此更改传播到下一个脚本。
7赞 msw 6/14/2012
Python 明确避免承诺将阻止恶意或无知的编码人员访问。其他语言可以避免由于无知而导致的程序员错误,但人们有一种不可思议的能力来围绕它们进行编码。
0赞 Ant6n 3/2/2014
您可以使用其中 d 是某个字典来执行该代码。如果代码是模块级别的,则每个赋值都应发送回字典。您可以在执行后恢复您的值/检查值是否更改,或者拦截字典赋值,即用另一个对象替换变量的字典。exec in d
0赞 Winand 9/1/2015
哦不,所以不可能像在模块级别那样模拟 VBA 行为ScreenUpdating = False
0赞 Ben 1/14/2017
您可以使用模块的 __all__ 属性使用户更难导出私有数据。这是 Python 标准库的常用方法

答:

11赞 Lev Levitsky 6/14/2012 #1

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

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

33赞 msw 6/14/2012 #2

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

评论

6赞 Sven Marnach 6/14/2012
请放心,这不会在 Python 4.x 中发生。
8赞 Mattie 6/14/2012
现在我很想去写一个 PEP 来子类化和替换当前范围。
4赞 Tim Hoffman 6/14/2012 #3

不,没有

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

如果您要重新绑定的名称实际上是其他实体的属性,即 myobj.var 然后,您可以阻止为实体的属性/属性分配值。 但我认为这不是你想要从你的例子中得到的。

评论

2赞 Caruccio 6/14/2012
快到了!我试图重载模块,但其本身是只读的。此外,type(mymodule) == <type 'module'>,它不可实例化。__dict__.__setattr__module.__dict__
100赞 steveha 6/14/2012 #4

你描述它的方式是绝对不可能的。为名称赋值是 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

评论

6赞 jtpereyda 12/17/2015
与 getter 一起使用但不使用 setter 与伪重载赋值的方式类似。@property
4赞 Vedran Šego 4/9/2018
getattr(self, "_locked", None)而不是。self.__dict__.get("_locked")
0赞 steveha 5/30/2018
@VedranŠego 我听从了你的建议,但用了 .现在,如果有人删除了成员变量,则调用不会引发异常。FalseNone_locked.get()
1赞 Vedran Šego 5/30/2018
@steveha 它真的给你带来了一个例外吗? 默认值为 ,这与它不同,这确实会引发异常。getNonegetattr
2赞 steveha 6/7/2018
啊,不,我没有看到它引发异常。不知何故,我忽略了您建议使用而不是.我想最好使用,这就是它的用途。getattr().__dict__.get()getattr()
2赞 jathanism 6/14/2012 #5

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

对于单例,您将确保一旦实例化,即使引用实例的原始变量被重新分配,该对象也会保留。任何后续实例都将只返回对同一对象的引用。

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

评论

0赞 Caruccio 6/14/2012
单例是不够的,因为不调用单例机制。var = 1
0赞 jathanism 6/15/2012
理解。如果我不清楚,我深表歉意。单例将阻止创建对象(例如)的进一步实例。没有办法保护最初分配的名称(例如)。Protect()var
0赞 Mad Physicist 9/18/2018
@Caruccio。不相关,但在 99% 的情况下,至少在 CPython 中,1 表现为单例。
0赞 ecn 8/26/2016 #6

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

import copy
global a

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

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


a = MyClass()
a = 1
8赞 Perkins 11/17/2016 #7

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

var = 1

它将键和值存储在全局字典中。大致相当于调用 .问题是你不能在正在运行的脚本中替换全局字典(你可能可以通过弄乱堆栈来改变,但这不是一个好主意)。但是,您可以在辅助命名空间中执行代码,并为其全局变量提供自定义字典。var1globals().__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,但拒绝任何设置变量的尝试。您也可以使用 .请注意,这并不能防止恶意代码。valexecfile(filename, myg)

评论

1赞 Joseph Garvin 7/20/2017
这是黑暗魔法!我完全期望找到一堆答案,人们建议显式使用带有覆盖的 setattr 的对象,没有考虑用自定义对象覆盖全局变量和局部变量,哇。不过,这一定会让 PyPy 哭泣。
0赞 Gary 11/29/2021
@mad物理学家 运行python shell时如何将其设置为默认值?我确实尝试过覆盖全局变量。不确定当我不是在 shell 而是在代码中运行 python 命令时,我是否能够运行 python 可执行文件以一直运行上述覆盖。知道我该怎么做吗?
0赞 Mad Physicist 11/29/2021
@Gary。#1) 对我来说听起来像是代码的味道。#2) 只需运行驱动程序脚本开头所示的语句即可。
0赞 Gary 11/30/2021
@mad物理学家代码气味。不。事实并非如此。有用例。但是驱动程序脚本?我不明白。我想探索一下吗?司机应该是什么意思?我该怎么做?
1赞 Mad Physicist 12/2/2021
@Gary。您可以对模块进行子类化。例如,请参阅此处:stackoverflow.com/q/4432376/2988730
4赞 Ryan Kung 10/27/2017 #8

是的,这是可能的,您可以通过修改来处理。__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/assignhttps://gist.github.com/RyanKung/4830d6c8474e6bcefa4edd13f122b4df

评论

0赞 zezollo 11/10/2017
有些东西我不明白......不应该是吗?print('called with %s' % self)
2赞 HelloGoodbye 11/12/2019
有几件事我不明白:1)字符串如何(以及为什么?)最终出现在方法的参数中?你的例子实际上显示了什么?这让我感到困惑。2) 这什么时候有用?3)这与问题有什么关系?为了让它与问题中编写的代码相对应,您不需要编写 ,不是吗?'c'v__assign__b = cc = b
0赞 Mad Physicist 11/29/2021
OP 感兴趣的是你解除绑定名称的情况,而不是你绑定它的地方。
4赞 Timofey Kazantsev 7/23/2018 #9

一般来说,我发现最好的方法是作为 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
10赞 Perkins 9/21/2018 #10

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

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

上面的示例假定代码驻留在名为 的模块中。您可以通过将 更改为 来执行此操作。tstrepltst__main__

如果要保护通过本地模块的访问,请通过 进行所有写入。tst.var = newval

评论

0赞 mutableVoid 8/21/2021
我不确定我的 python 版本/实现是否有所不同,但对我来说,这仅在尝试访问受保护模块之外的变量形式时才有效;即,如果我保护模块并将 Protect() 分配给模块中命名两次的变量,则不会引发异常。这与文档中指出的直接赋值直接使用不可覆盖的全局指令是一致的。tstvartst
1赞 Perkins 8/22/2021
我不记得我用哪个版本的 python 进行了测试。当时,我很惊讶它保护了变量免受局部更改的影响,但现在我无法复制它。值得注意的是,这将引发异常,但不会。tst.var = 5var = 5
1赞 patgolez10 2/19/2021 #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
6赞 Kostas Mouratidis 10/11/2021 #12

我会在 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)

评论

0赞 Gary 11/29/2021
如何在运行 python shell 时将其设置为默认值?我确实尝试过覆盖全局变量。不确定当我不是在 shell 而是在代码中运行 python 命令时,我是否能够运行 python 可执行文件来一直运行上述 addautdithook。知道我该怎么做才能使审计钩子成为默认值吗?
0赞 Gary 11/29/2021
看看这个 docs.python.org/3/c-api/sys.html#c.PySys_AddAuditHook docs.python.org/3/library/audit_events.html 这个审计钩子绝对是一个了不起的变化!它通过一些调整解决了我的目的,但是默认情况下,我可以完全支持python可执行文件通过命令行或第三方调用运行(Python环境默认配置)?可能是我错过了什么?可能是另一个 PEP,有人可以拿走并提交这个。还是真的需要?
1赞 Kostas Mouratidis 11/29/2021
我很确定这仅适用于 Python REPL 在每一行上运行,但运行则不然。也许“正确”的前进方式是通过进入 C 领域来做一些类似您正在尝试的事情,但我对此并不熟悉。另一种方法是依靠挂钩导入系统而不是审计挂钩:例如,您可以读取您的魔术代码导入的文件并以某种方式解析它。那可能很有趣。execpython file.py
0赞 Gary 12/2/2021
是的。这可能是一种方式。但这不会以任何方式影响 shell 或命令。也许我可以在每个文件中管理相同的钩子。但这似乎是多余的
1赞 Simply Beautiful Art 1/26/2023 #13

从 Python 3.8 开始,可以使用 暗示值是只读的。这意味着在运行时没有任何变化,允许任何人更改值,但如果您使用任何可以读取类型提示的 linter,那么它会警告用户是否尝试分配它。typing.Final

from typing import Final

x: Final[int] = 3

x = 5  # Cannot assign to final name "x" (mypy)

这使得代码更简洁,但它完全信任用户在运行时尊重它,不会试图阻止用户更改值。

另一种常见的模式是公开函数而不是模块常量,例如 和 。sys.getrecursionlimitsys.setrecursionlimit

def get_x() -> int:
    return 3

虽然用户可以这样做,但用户显然试图破坏它,这是无法修复的。通过这种方式,我们可以防止人们以最小的复杂性“意外”更改模块中的值。module.get_x = my_get_x