是否可以重载Python分配?


76

有没有一种魔术方法可以重载赋值运算符,例如__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
用例:人们将使用我的服务API编写小脚本,并且我想防止他们更改内部数据并将此更改传播到下一个脚本。
Caruccio 2012年

5
Python明确避免承诺防止恶意或无知的编码器被访问。其他语言使您可以避免由于无知而导致的一些程序员错误,但是人们具有不可思议的能力来围绕它们进行编码。
msw 2012年

您可以使用exec in dd是一些字典来执行该代码。如果代码在模块级别,则每个作业都应发送回字典。您可以在执行后恢复值/检查值是否更改,或者拦截字典分配,即用另一个对象替换变量字典。
Ant6n 2014年

哦,不,因此不可能像ScreenUpdating = False模块级别那样模拟VBA行为
Winand

您可以使用__all__模块属性使人们更难以导出私有数据。这是Python标准库的共同做法

Answers:


71

您描述它的方式绝对不可能。分配名称是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,它控制是否允许分配。您可以将其解锁以更新值。


4
使用@property吸气剂,但没有setter方法是类似的方式,以伪过载分配。
jtpereyda

2
getattr(self, "_locked", None)代替self.__dict__.get("_locked")
VedranŠego18年

@VedranŠego我遵循了您的建议,但使用False代替None。现在,如果有人删除_locked成员变量,则.get()调用不会引发异常。
steveha

1
@steveha它真的为您带来了例外吗?get默认为None,不同的getattr是确实会引发异常。
VedranŠego18年

啊,不,我没有看到它引发异常。我以某种方式忽略了您建议使用getattr()而不是.__dict__.get()。我想最好使用getattr(),这就是它的用途。
steveha


8

我认为不可能。从我的角度来看,对变量的赋值不会对其之前引用的对象产生任何影响:只是变量现在“指向”另一个对象。

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


6

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

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__


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,该REPL几乎可以正常运行,但是拒绝任何设置变量的尝试val。您也可以使用execfile(filename, myg)。请注意,这不能防止恶意代码。


这是黑魔法!我完全希望能找到一堆答案,人们建议使用显式的setattr显式地使用对象,而不是考虑使用自定义的对象来覆盖全局变量和局部变量。这必须使PyPy哭泣。
约瑟夫·加文

3

不,没有

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

如果您希望重新绑定的名称实际上是某个其他实体的属性,即myobj.var,则可以防止为该实体的属性/属性分配值。但是我认为那不是您从示例中得到的。


1
快好了!我试图重载模块的,__dict__.__setattr__但是module.__dict__它本身是只读的。另外,type(mymodule)== <type'module'>,并且它不可实例化。
卡鲁乔

2

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

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

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


单例是不够的,因为var = 1不调用单例机制。
卡鲁乔

明白了 如果不清楚,我深表歉意。单例将阻止Protect()创建对象(例如)的其他实例。没有办法保护最初分配的名称(例如var)。
贾森主义

@Caruccio。无关,但至少在CPython中99%的时间中1表现为单例。
疯狂物理学家

2

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

import copy
global a

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

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


a = MyClass()
a = 1

2

是的,有可能,您可以__assign__通过Modify处理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)吗?
zezollo

1
有几件事我不理解:1)字符串如何(以及为什么?)'c'最终出现在方法的v参数中__assign__?您的示例实际上显示了什么?这让我感到困惑。2)什么时候有用?3)这与问题有什么关系?它对应的写在问题的代码,你会不会需要写b = c,不是c = b
HelloGoodbye

2

通常,我发现的最佳方法是重写__ilshift__为setter和__rlshift__getter,由属性装饰器复制。它几乎是最后要解析的运算符,只是(|&^)并且逻辑较低。它很少使用(__lrshift__较少,但是可以考虑)。

在使用PyPi Assign软件包时,只能控制前向分配,因此操作员的实际“力量”较低。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

因此<<=,在属性中获取价值的操作员是更直观的解决方案,它不是在尝试让用户犯一些反射性错误,例如:

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.