Ist es möglich, die Python-Zuweisung zu überladen?

76

Gibt es eine magische Methode, die den Zuweisungsoperator überladen kann, wie __assign__(self, new_value)?

Ich möchte eine erneute Bindung für eine Instanz verbieten:

class Protect():
  def __assign__(self, value):
    raise Exception("This is an ex-parrot")

var = Protect()  # once assigned...
var = 1          # this should raise Exception()

Ist es möglich? Ist es verrückt? Soll ich Medizin nehmen?

Caruccio
quelle
1
Anwendungsfall: Benutzer werden mithilfe meiner Service-API kleine Skripts schreiben, und ich möchte verhindern, dass sie interne Daten ändern, und diese Änderung an das nächste Skript weitergeben.
Caruccio
5
Python vermeidet ausdrücklich das Versprechen, dass ein böswilliger oder ignoranter Codierer am Zugriff gehindert wird. In anderen Sprachen können Sie Programmierfehler aufgrund von Unwissenheit vermeiden, aber die Leute haben eine unheimliche Fähigkeit, um sie herum zu programmieren.
Msw
Sie können diesen Code ausführen, indem Sie exec in dd als Wörterbuch verwenden. Wenn sich der Code auf Modulebene befindet, sollte jede Zuweisung an das Wörterbuch zurückgesendet werden. Sie können entweder Ihre Werte nach der Ausführung wiederherstellen / prüfen, ob sich die Werte geändert haben, oder die Wörterbuchzuweisung abfangen, dh das Variablenwörterbuch durch ein anderes Objekt ersetzen.
Ant6n
Oh nein, es ist also unmöglich, das VBA-Verhalten wie ScreenUpdating = Falseauf Modulebene zu simulieren
Winand
Sie können das __all__Attribut Ihres Moduls verwenden , um den Export privater Daten zu erschweren. Dies ist ein gängiger Ansatz für die Python Standard Library
Ben

Antworten:

71

Die Art, wie Sie es beschreiben, ist absolut nicht möglich. Die Zuweisung zu einem Namen ist ein grundlegendes Merkmal von Python, und es wurden keine Hooks bereitgestellt, um sein Verhalten zu ändern.

Die Zuweisung zu einem Mitglied in einer Klasseninstanz kann jedoch durch Überschreiben nach Ihren Wünschen gesteuert werden .__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

Beachten Sie, dass es eine Mitgliedsvariable gibt _locked, die steuert, ob die Zuweisung zulässig ist. Sie können es entsperren, um den Wert zu aktualisieren.

steveha
quelle
4
Die Verwendung @propertymit einem Getter, aber ohne Setter, ähnelt der Pseudo-Überlastungszuweisung.
jtpereyda
2
getattr(self, "_locked", None)statt self.__dict__.get("_locked").
Vedran Šego
@ VedranŠego Ich bin deinem Vorschlag gefolgt, habe aber Falsestattdessen verwendet None. Wenn nun jemand die _lockedMitgliedsvariable löscht, löst der .get()Aufruf keine Ausnahme aus.
Steveha
1
@steveha Hat es tatsächlich eine Ausnahme für dich ausgelöst? getStandardeinstellung ist None, anders als getattrdies tatsächlich eine Ausnahme auslösen würde.
Vedran Šego
Ah, nein, ich habe nicht gesehen, dass es eine Ausnahme auslöst. Irgendwie habe ich übersehen, dass Sie vorgeschlagen haben, getattr()eher zu verwenden als .__dict__.get(). Ich denke, es ist besser zu benutzen getattr(), dafür ist es.
Steveha
27

Nein, da die Zuweisung eine Sprache ist, die keinen Modifikations-Hook hat.

msw
quelle
3
Seien Sie versichert, dass dies in Python 4.x nicht passieren wird.
Sven Marnach
6
Jetzt bin ich versucht, ein PEP zu schreiben, um den aktuellen Bereich zu unterklassifizieren und zu ersetzen.
Zigg
8

Ich denke nicht, dass es möglich ist. So wie ich es sehe, hat die Zuweisung zu einer Variablen nichts mit dem Objekt zu tun, auf das zuvor Bezug genommen wurde: Es ist nur so, dass die Variable jetzt auf ein anderes Objekt "zeigt".

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!

Sie können das gewünschte Verhalten jedoch nachahmen, indem Sie ein Wrapper-Objekt mit __setitem__()oder __setattr__()Methoden erstellen , die eine Ausnahme auslösen, und das "unveränderliche" Material darin behalten.

Lev Levitsky
quelle
6

Innerhalb eines Moduls ist dies durch ein bisschen dunkle Magie absolut möglich.

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

Beachten Sie, dass Sie auch innerhalb des Moduls nicht in die geschützte Variable schreiben können, sobald die Klassenänderung erfolgt. Im obigen Beispiel wird davon ausgegangen, dass sich der Code in einem Modul mit dem Namen befindet tst. Sie können dies in der repldurch Ändern tstzu tun __main__.

Perkins
quelle
4

Mit dem Namespace der obersten Ebene ist dies nicht möglich. Wenn du rennst

var = 1

Es speichert den Schlüssel varund den Wert 1im globalen Wörterbuch. Es ist ungefähr gleichbedeutend mit einem Anruf globals().__setitem__('var', 1). Das Problem ist, dass Sie das globale Wörterbuch in einem laufenden Skript nicht ersetzen können (Sie können es wahrscheinlich tun, indem Sie mit dem Stapel herumspielen, aber das ist keine gute Idee). Sie können jedoch Code in einem sekundären Namespace ausführen und ein benutzerdefiniertes Wörterbuch für dessen Globals bereitstellen.

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

Dadurch wird eine REPL ausgelöst, die fast normal funktioniert, jedoch alle Versuche, die Variable festzulegen, ablehnt val. Sie könnten auch verwenden execfile(filename, myg). Beachten Sie, dass dies nicht vor bösartigem Code schützt.

Perkins
quelle
Das ist dunkle Magie! Ich hatte voll und ganz damit gerechnet, nur eine Reihe von Antworten zu finden, bei denen Leute vorschlagen, ein Objekt explizit mit einem überschriebenen Setattr zu verwenden, und nicht daran gedacht haben, Globals und Einheimische mit einem benutzerdefinierten Objekt zu überschreiben, wow. Dies muss PyPy allerdings zum Weinen bringen.
Joseph Garvin
3

Nein, gibt es nicht

Denken Sie darüber nach, in Ihrem Beispiel binden Sie den Namen var erneut an einen neuen Wert. Sie berühren die Instanz von Protect nicht wirklich.

Wenn der Name, den Sie neu binden möchten, tatsächlich eine Eigenschaft einer anderen Entität ist, z. B. myobj.var, können Sie verhindern, dass der Eigenschaft / dem Attribut der Entität ein Wert zugewiesen wird. Aber ich nehme an, das ist nicht das, was Sie von Ihrem Beispiel wollen.

Tim Hoffman
quelle
1
Fast dort! Ich habe versucht, das Modul zu überlasten, __dict__.__setattr__aber es module.__dict__ist schreibgeschützt. Geben Sie außerdem (mymodule) == <type 'module'> ein, und es ist nicht instanziierbar.
Caruccio
2

Im globalen Namespace ist dies nicht möglich, Sie können jedoch die erweiterte Python-Metaprogrammierung nutzen, um zu verhindern, dass mehrere Instanzen eines ProtectObjekts erstellt werden. Das Singleton-Muster ist ein gutes Beispiel dafür.

Im Fall eines Singleton würden Sie sicherstellen, dass das Objekt nach der Instanziierung auch dann bestehen bleibt, wenn die ursprüngliche Variable, die auf die Instanz verweist, neu zugewiesen wird. Alle nachfolgenden Instanzen würden lediglich einen Verweis auf dasselbe Objekt zurückgeben.

Trotz dieses Musters können Sie niemals verhindern, dass ein globaler Variablenname selbst neu zugewiesen wird.

Nathanismus
quelle
Ein Singleton reicht nicht aus, da var = 1der Singleton-Mechanismus nicht aufgerufen wird.
Caruccio
Verstanden. Ich entschuldige mich, wenn ich nicht klar war. Ein Singleton würde verhindern, dass weitere Instanzen eines Objekts (z. B. Protect()) erstellt werden. Es gibt keine Möglichkeit, den ursprünglich zugewiesenen Namen (z var. B. ) zu schützen .
Nathanismus
@Caruccio. Unabhängig, aber in 99% der Fälle, zumindest in CPython, verhält sich 1 wie ein Singleton.
Verrückter Physiker
2

Eine hässliche Lösung besteht darin, den Destruktor neu zuzuweisen. Aber es ist keine echte Überlastungszuweisung.

import copy
global a

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

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


a = MyClass()
a = 1
ecn
quelle
2

Ja, es ist möglich, dass Sie __assign__über Ändern bearbeiten können ast.

pip install assign

Test mit:

class T():
    def __assign__(self, v):
        print('called with %s' % v)
b = T()
c = b

Sie erhalten

>>> import magic
>>> import test
called with c

Das Projekt ist bei https://github.com/RyanKung/assign Und das Einfachere:https://gist.github.com/RyanKung/4830d6c8474e6bcefa4edd13f122b4df

Ryan Kung
quelle
Es gibt etwas, das ich nicht verstehe ... Sollte es nicht sein print('called with %s' % self)?
Zezollo
1
Es gibt einige Dinge, die ich nicht verstehe: 1) Wie (und warum?) Endet die Zeichenfolge 'c'im vArgument für die __assign__Methode? Was zeigt Ihr Beispiel eigentlich? Das verwirrt mich. 2) Wann wäre das sinnvoll? 3) Wie hängt das mit der Frage zusammen? Müssen Sie nicht schreiben b = c, damit es dem in der Frage geschriebenen Code entspricht c = b?
HelloGoodbye
2

Im Allgemeinen ist der beste Ansatz, den ich gefunden habe, das Überschreiben __ilshift__als Setter und __rlshift__als Getter, der vom Immobiliendekorateur dupliziert wird. Es ist fast der letzte Operator, der nur (| & ^) aufgelöst wird und logisch niedriger ist. Es wird selten verwendet ( __lrshift__ist weniger, kann aber berücksichtigt werden).

Bei Verwendung des PyPi-Zuweisungspakets kann nur die Vorwärtszuweisung gesteuert werden, sodass die tatsächliche "Stärke" des Bedieners geringer ist. Beispiel für ein PyPi-Zuweisungspaket:

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

Ausgabe:

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'

Das gleiche gilt für die Verschiebung:

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

Ausgabe:

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

Der <<=Bediener, der Wert auf eine Immobilie legt, ist die visuell sauberere Lösung und versucht nicht, einige reflektierende Fehler zu machen, wie:

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
Timofey Kazantsev
quelle