【问题标题】:Is it possible to overload Python assignment?是否可以重载 Python 赋值?
【发布时间】:2012-06-16 23:41:42
【问题描述】:

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

有可能吗?疯了吗?我应该吃药吗?

【问题讨论】:

  • 用例:人们将使用我的服务 API 编写小脚本,我想防止他们更改内部数据并将此更改传播到下一个脚本。
  • Python 明确避免承诺阻止恶意或无知的编码人员访问。其他语言可以让你避免由于无知而导致的一些程序员错误,但是人们有一种不可思议的能力来围绕它们编写代码。
  • 您可以使用exec in d 执行该代码,其中 d 是一些字典。如果代码在模块级别,则每个作业都应发送回字典。您可以在执行/检查值是否更改后恢复您的值,或者拦截字典分配,即用另一个对象替换变量字典。
  • 哦不,所以不可能在模块级别模拟像ScreenUpdating = False 这样的 VBA 行为
  • 您可以使用__all__ attribute of your module 让人们更难导出私人数据。这是 Python 标准库的常用方法

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


【解决方案1】:

我将在 Python 地狱中燃烧,但没有一点乐趣的生活是什么。


重要免责声明

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

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

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)

【讨论】:

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

在模块内部,这完全可以通过一点黑魔法实现。

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 的模块中。您可以通过将tst 更改为__main__repl 中执行此操作。

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

【讨论】:

  • 我不确定我的 python 版本/实现是否有所不同,但对我来说,这仅在尝试访问受保护模块外部的变量时有效;即,如果我保护模块 tst 并在模块 tst 内将 Protect() 分配给名为 var 的变量两次,则不会引发异常。这与说明直接分配直接利用不可覆盖的全局 dict 的文档一致。
  • 我不记得我用哪个版本的 python 测试过。当时,我很惊讶它保护了变量不受本地更改的影响,但现在我无法复制它。值得注意的是tst.var = 5会抛出异常,而var = 5不会。
【解决方案3】:

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

正如 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

【讨论】:

    【解决方案4】:

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

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

    【讨论】:

    • 这是黑魔法!我完全希望能找到一堆答案,人们建议明确地使用带有覆盖的 setattr 的对象,没有考虑用自定义对象覆盖全局变量和局部变量,哇。不过,这一定会让 PyPy 哭泣。
    • @mad-physicist 在运行 python shell 时如何将其设置为默认值?我确实尝试过覆盖全局变量。当我不是在 shell 中而是在代码中运行 python 命令时,不确定我是否能够运行 python 可执行文件以一直运行上述覆盖。知道我该怎么做吗?
    • @Gary。 #1) 对我来说听起来像是代码味道。 #2) 只需运行驱动程序脚本开头显示的语句即可。
    • @mad-physicist 代码气味。不它不是。有用例。但是驱动脚本呢?我不明白。我想探索一下?司机应该是什么意思?我该怎么做?
    • @Gary。您可以子类化您的模块。例如,请参见此处:stackoverflow.com/q/4432376/2988730
    【解决方案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
    

    输出:

    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
    

    因此,&lt;&lt;= 运算符在属性中获取价值是视觉上更清晰的解决方案,它不会试图让用户犯一些反思性错误,例如:

    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
    

    【讨论】:

      【解决方案6】:

      你描述的方式是绝对不可能的。为名称赋值是 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 与getter 一起使用但不使用setter 类似于伪重载赋值。
      • getattr(self, "_locked", None) 而不是self.__dict__.get("_locked")
      • @VedranŠego 我听从了你的建议,但使用了False 而不是None。现在如果有人删除了_locked 成员变量,.get() 调用不会引发异常。
      • @steveha 它真的为你引发了异常吗? get 默认为None,不像getattr 确实会引发异常。
      • 啊,不,我没有看到它引发异常。不知何故,我忽略了您建议使用getattr() 而不是.__dict__.get()。我想最好使用getattr(),这就是它的用途。
      【解决方案7】:

      是的,有可能,你可以通过修改ast来处理__assign__

      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)吗?
      • 有几件事我不明白:1) 字符串'c' 是如何(以及为什么?)最终出现在v 参数中的__assign__ 方法?您的示例实际上显示了什么?这让我很困惑。 2)这什么时候有用? 3)这与问题有什么关系?为了与问题中编写的代码相对应,您不需要写b = c,而不是c = b吗?
      • OP 对您取消绑定名称的情况感兴趣,而不是您绑定它的位置。
      【解决方案8】:

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

      import copy
      global a
      
      class MyClass():
          def __init__(self):
                  a = 1000
                  # ...
      
          def __del__(self):
                  a = copy.copy(self)
      
      
      a = MyClass()
      a = 1
      

      【讨论】:

        【解决方案9】:

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

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

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

        【讨论】:

        • 单例是不够的,因为var = 1 不调用单例机制。
        • 明白。如果我不清楚,我很抱歉。单例会阻止创建对象的更多实例(例如Protect())。无法保护最初分配的名称(例如var)。
        • @Caruccio。不相关,但在 99% 的情况下,至少在 CPython 中,1 表现为单例。
        【解决方案10】:

        没有

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

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

        【讨论】:

        • 快到了!我试图重载模块的__dict__.__setattr__,但module.__dict__ 本身是只读的。另外,type(mymodule) == ,它是不可实例化的。
        【解决方案11】:

        不,因为分配是一个没有修改钩子的language intrinsic

        【讨论】:

        • 请放心,这在 Python 4.x 中不会发生。
        • 现在我很想写一个 PEP 来继承和替换当前范围。
        【解决方案12】:

        我认为这是不可能的。在我看来,对变量的赋值对它之前引用的对象没有任何作用:只是变量现在“指向”了不同的对象。

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

        【讨论】:

          猜你喜欢
          • 2013-09-17
          • 2014-10-16
          • 2021-03-11
          • 1970-01-01
          • 2014-06-16
          • 2010-10-20
          相关资源
          最近更新 更多