是否有可能重载 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-pa...

问题描述:

是否有一个魔术方法可以重载赋值运算符,例如__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 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)

解决方案 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.getrecursionlimitsys.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
相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2738  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1676  
  在全球化的浪潮下,企业的业务范围不断拓展,跨文化协作变得愈发普遍。不同文化背景的团队成员在合作过程中,由于语言、价值观、工作习惯等方面的差异,往往会面临诸多沟通挑战。而产品生命周期管理(PLM)系统作为企业管理产品全生命周期的重要工具,如何有效支持跨文化协作成为了关键问题。通过合理运用沟通策略,PLM系统能够在跨文化团...
plm是什么软件   5  
  PLM(产品生命周期管理)系统在企业的产品研发、生产与管理过程中扮演着至关重要的角色,其中文档版本控制是确保产品数据准确性、完整性和可追溯性的关键环节。有效的文档版本控制能够避免因版本混乱导致的错误、重复工作以及沟通不畅等问题,提升企业整体的运营效率和产品质量。接下来,我们将深入探讨 PLM 系统实现文档版本控制的 6...
plm是什么意思   6  
  PLM(产品生命周期管理)项目管理旨在通过有效整合流程、数据和人员,优化产品从概念到退役的整个生命周期。在这个过程中,敏捷测试成为确保产品质量、加速交付的关键环节。敏捷测试强调快速反馈、持续改进以及与开发的紧密协作,对传统的测试流程提出了新的挑战与机遇。通过对测试流程的优化,能够更好地适应PLM项目的动态变化,提升产品...
plm管理系统   4  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用