Чи можна перевантажити призначення Python?


77

Чи існує магічний метод, який може перевантажити оператор присвоєння, наприклад __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, і я хочу перешкодити їм змінювати внутрішні дані та розповсюджувати цю зміну на наступний сценарій.
Каруччо

5
Python явно уникає обіцянок, що зловмисному чи необізнаному кодеру буде заборонено доступ. Інші мови дозволяють уникнути помилок програміста через незнання, але люди мають дивовижну здатність кодувати навколо себе.
msw

ви можете виконати цей код, використовуючи exec in dде d - якийсь словник. якщо код знаходиться на рівні модуля, кожне завдання має бути відправлене назад у словник. Ви можете або відновити свої значення після виконання / перевірити, чи змінилися значення, або перехопити призначення словника, тобто замінити словник змінних на інший об'єкт.
Ant6n

О ні, отже, неможливо імітувати поведінку VBA, як ScreenUpdating = Falseна рівні модуля
Winand

Ви можете використовувати __all__атрибут свого модуля, щоб ускладнити експорт приватних даних. Це загальний підхід для стандартної бібліотеки Python
Бен,

Відповіді:


73

Те, як ви це описуєте, абсолютно неможливо. Присвоєння імені є фундаментальною особливістю 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з геттером, але жодним сеттером - це подібний спосіб призначення псевдоперевантаження.
jtpereyda

2
getattr(self, "_locked", None)замість self.__dict__.get("_locked").
Ведран Шего,

@ VedranŠego Я виконав вашу пропозицію, але використав Falseзамість None. Тепер, якщо хтось видаляє _lockedзмінну-учасника, .get()виклик не спричинить виняток.
steveha

1
@steveha Це насправді викликало для вас виняток? getза замовчуванням None, на відміну від getattrяких справді може бути виняток.
Ведран Шего,

А, ні, я не бачив, що це викликає виняток. Якось я пропустив, що ви пропонуєте getattr()скоріше використовувати , ніж .__dict__.get(). Думаю, краще використовувати getattr(), для цього він і призначений.
steveha

27

Ні, оскільки присвоєння є внутрішньою мовою, яка не має гачка модифікації.


4
Будьте впевнені, цього не станеться в Python 4.x.
Sven Marnach

7
Зараз у мене виникає спокуса написати PEP для підкласу та заміни поточного обсягу.
zigg

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, який майже працює нормально, але відмовляється від будь-яких спроб встановити змінну val. Ви також можете використовувати execfile(filename, myg). Зауважте, це не захищає від зловмисного коду.


Це темна магія! Я повністю очікував, що просто знайду купу відповідей, де люди пропонують використовувати об’єкт явно із заміненим setattr, не думали про те, щоб замінити глобальні та місцеві жителі за допомогою спеціального об’єкта, вау. Однак це повинно змусити PyPy плакати.
Джозеф Гарвін,

3

Ні, немає

Подумайте, у вашому прикладі ви перев'язуєте ім'я var до нового значення. Ви насправді не торкаєтесь екземпляра Protect.

Якщо ім'я, яке ви хочете повторно прив'язати, насправді є властивістю якоїсь іншої сутності, тобто myobj.var, тоді ви можете запобігти присвоєнню значення властивості / атрибуту сутності. Але я припускаю, що це не те, що ви хочете від вашого прикладу.


1
Майже там! Я намагався перевантажити модуль, __dict__.__setattr__але module.__dict__сам для читання. Крім того, введіть (mymodule) == <type 'module'>, і це неможливо встановити.
Каруччо

2

У глобальному просторі імен це неможливо, але ви можете скористатися передовим метапрограмуванням Python, щоб запобігти створенню декількох екземплярів Protectоб'єкта. Шаблон Singleton є хорошим прикладом цього.

У випадку Singleton ви гарантуєте, що після створення екземпляра, навіть якщо вихідна змінна, що посилається на екземпляр, буде перепризначена, що об'єкт буде зберігатися. Будь-які наступні екземпляри просто повернуть посилання на той самий об'єкт.

Незважаючи на цей шаблон, ви ніколи не зможете запобігти перепризначенню самого імені глобальної змінної.


Синглтона недостатньо, оскільки var = 1не викликає синглтон-механізм.
Каруччо

Зрозуміло. Прошу вибачення, якщо я не зрозумів. Синглтон не дозволить Protect()створювати подальші екземпляри об’єкта (наприклад ). Немає способу захистити спочатку призначене ім'я (наприклад var).
ятанізм

@Caruccio. Не пов'язані між собою, але 99% випадків, принаймні в CPython, 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__за допомогою модифікації 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__як сеттера та __rlshift__як геттера, що дублюється декоратором властивостей. Це майже останній оператор, який розв'язується лише (| & ^), а логічні нижче. Він використовується рідко ( __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'

Те саме зі зміною:

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.