Можно ли динамически добавлять атрибуты к встроенным объектам Python в Python?

Мне нужно динамически добавить атрибут (содержащий кортеж или объект) к объектам python. Это работает для классов Python, написанных мной, но не для встроенных классов.

Рассмотрим следующую программу:

import numpy as np

class My_Class():
    pass

my_obj = My_Class()
my_obj2 = My_Class()

my_obj.__my_hidden_field = (1,1)
my_obj2.__my_hidden_field = (2,1)

print(my_obj.__my_hidden_field, my_obj2.__my_hidden_field)

Это правильно печатает (1, 1) (2, 1). Однако следующая программа не работает.

X  = np.random.random(size=(2,3))


X.__my_hidden_field = (3,1) 
setattr(X, '__my_hidden_field', (3,1))

Обе приведенные выше строки выдают следующую ошибку # AttributeError: 'numpy.ndarray' object has no attribute '__my_hidden_field'

Теперь причина, найденная в этих вопросах (т.е. назначение атрибута встроенному объекту, Невозможно установить атрибуты класса объекта, python: динамическое добавление атрибутов во встроенный класс) - это Python не позволяет динамически добавлять атрибуты к встроенным объектам.

Выдержка из ответа: https://stackoverflow.com/a/22103924/8413477

Это запрещено намеренно, чтобы предотвратить случайные фатальные изменения встроенных типов (фатальные для частей кода, о которых вы никогда не задумывались). Кроме того, это делается для того, чтобы изменения не влияли на разные интерпретаторы, находящиеся в адресном пространстве, поскольку встроенные типы (в отличие от определяемых пользователем классов) являются общими для всех таких интерпретаторов.

Однако все ответы довольно старые, и мне очень нужно сделать это для моего исследовательского проекта.

Однако есть модуль, который позволяет добавлять методы в класс: https://pypi.org/project/forbiddenfruit/

Однако он не позволяет добавлять объекты/атрибуты к каждому объекту.

Любая помощь ?


person Mr. Nobody    schedule 31.01.2019    source источник
comment
Почему бы вам не подкласс?   -  person iz_    schedule 31.01.2019
comment
Можно добавить атрибуты к некоторым объектам, но нельзя добавить их к другим объектам, независимо от того, встроены они или нет.   -  person zvone    schedule 31.01.2019
comment
Я пытаюсь написать трассировщик для анализа кода Python. Я хочу, чтобы мои инструменты были максимально прозрачными. и создание подклассов может / не может решить проблему, о которой я еще не думал. Даже если это сработает, это будет иметь меньший приоритет! @Tomothy32   -  person Mr. Nobody    schedule 31.01.2019
comment
В качестве обходного пути, как насчет использования словаря с id(X) в качестве ключа и значения скрытого поля в качестве значения словаря?   -  person Michael Butscher    schedule 31.01.2019
comment
Кстати, запретный плод — это segfault, ожидающий своего появления. Он перебирает путь к dict типа без реального понимания последствий этого, и он не обрабатывает ни одной из ловушек, таких как кеш атрибутов типа.   -  person user2357112 supports Monica    schedule 31.01.2019
comment
Вот простой пример, который в настоящее время вызывает ошибку сегментации на repl.it. (Также обратите внимание на молчаливый неверный результат на втором печать, прежде чем третья печать полностью все разрушит.)   -  person user2357112 supports Monica    schedule 31.01.2019
comment
@gilch: Нет, потому что это еще одна вещь, с которой запретный плод не справляется.   -  person user2357112 supports Monica    schedule 31.01.2019


Ответы (3)


Вероятно, вам нужен weakref.WeakKeyDictionary. Из документа,

Это можно использовать для связывания дополнительных данных с объектом, принадлежащим другим частям приложения, без добавления атрибутов к этим объектам.

Подобно атрибуту и ​​в отличие от обычного словаря, это позволяет объектам собирать мусор, когда на него нет других ссылок.

Вы бы искали поле с

my_hidden_field[X]

вместо

X._my_hidden_field

Два предостережения: во-первых, поскольку слабый ключ может быть удален в любое время без предупреждения, вам не следует перебирать WeakKeyDictionary. Однако поиск объекта, на который у вас есть ссылка, в порядке. И, во-вторых, вы не можете сделать слабую ссылку для типа объекта, написанного на C, у которого нет слота для него (верно для многих встроенных функций), или типа, написанного на Python, который не допускает атрибут __weakref__ (обычно из-за до __slots__).

Если это проблема, вы можете просто использовать обычный словарь для этих типов, но вам придется очистить его самостоятельно.

person gilch    schedule 31.01.2019
comment
Это не совсем __weakref__, что необходимо - типы, написанные на C, которые поддерживают слабые ссылки, обычно не будут иметь атрибута __weakref__, но у них будет зарезервировано место для поддержки слабых ссылок. Вы можете проверить, зарезервировано ли для объекта такое пространство, проверив, имеет ли его тип ненулевое значение __weakrefoffset__. - person user2357112 supports Monica; 31.01.2019
comment
С функциями, являющимися одним из примеров. - person gilch; 31.01.2019

Быстрый ответ

Можно ли в Python динамически добавлять атрибуты к встроенным объектам Python?

Нет, причины, о которых вы читали в ссылках, которые вы разместили, остались прежними. Но я придумал рецепт, который, как мне кажется, может стать отправной точкой для твоего трейсера.

Инструментарий с использованием подклассов в сочетании с AST

Прочитав много об этом, я придумал рецепт, который, возможно, не является полным решением, но, похоже, вы можете начать отсюда.

Преимущество этого рецепта в том, что он не использует сторонние библиотеки, все достигается с помощью стандартных (Python 3.5, 3.6, 3.7) библиотек.

Целевой код.

Этот рецепт сделает такой код инструментальным (здесь выполняется простое инструментирование, это всего лишь пример концепции) и выполненным.

# target/target.py

d = {1: 2}
d.update({3: 4})
print(d)                 # Should print "{1: 2, 3: 4}"
print(d.hidden_field)    # Should print "(0, 0)"

Подклассы

Во-первых, мы должны добавить hidden_field ко всему, что мы хотим (этот рецепт был протестирован только со словарями).

Следующий код получает значение, определяет его тип/класс и подкласс, чтобы добавить упомянутый hidden_field.

def instrument_node(value):
    VarType = type(value)
    class AnalyserHelper(VarType):
        def __init__(self, *args, **kwargs):
            self.hidden_field = (0, 0)
            super(AnalyserHelper, self).__init__(*args, **kwargs)
    return AnalyserHelper(value)

с этим на месте вы можете:

d = {1: 2}
d = instrument_node(d) 
d.update({3: 4})
print(d)                 # Do print "{1: 2, 3: 4}"
print(d.hidden_field)    # Do print "(0, 0)"

На данный момент мы уже знаем способ "добавить инструментовку во встроенный словарь" но здесь нет прозрачности.

Измените АСТ.

Следующий шаг — «скрыть» вызов instrument_node, и мы сделаем это с помощью ast. Модуль Python.

Ниже приведен преобразователь узла AST, который берет любой найденный словарь и заключает его в вызов instrument_node:

class AnalyserNodeTransformer(ast.NodeTransformer):
    """Wraps all dicts in a call to instrument_node()"""
    def visit_Dict(self, node):
        return ast.Call(func=ast.Name(id='instrument_node', ctx=ast.Load()),
                        args=[node], keywords=[])
        return node

Собираем все вместе.

С помощью этих инструментов вы можете написать скрипт, который:

  1. Прочитайте целевой код.
  2. Разобрать программу.
  3. Примените изменения AST.
  4. Скомпилируйте это.
  5. И выполнить его.
import ast
import os
from ast_transformer import AnalyserNodeTransformer

# instrument_node need to be in the namespace here.
from ast_transformer import instrument_node

if __name__ == "__main__":

    target_path = os.path.join(os.path.dirname(__file__), 'target/target.py')

    with open(target_path, 'r') as program:
        # Read and parse the target script.
        tree = ast.parse(program.read())
        # Make transformations.
        tree = AnalyserNodeTransformer().visit(tree)
        # Fix locations.
        ast.fix_missing_locations(tree)
        # Compile and execute.
        compiled = compile(tree, filename='target.py', mode='exec')
        exec(compiled)

Это возьмет наш целевой код, обернет каждый словарь instrument_node() и выполнит результат такого изменения.

Результат выполнения этого против нашего целевого кода,

# target/target.py

d = {1: 2}
d.update({3: 4})
print(d)                 # Will print "{1: 2, 3: 4}"
print(d.hidden_field)    # Will print "(0, 0)"

is:

>>> {1: 2, 3: 4} 
>>> (0, 0)

Рабочий пример

Вы можете клонировать рабочий пример здесь.

person Raydel Miranda    schedule 31.01.2019
comment
Я не мог заставить класс-оболочку AnalyzeHelper работать для классов numpy.ndarray. Как заставить эту технику работать для встроенных/сторонних классов?? - person Mr. Nobody; 01.02.2019
comment
Что ж, я предполагаю, что для того, чтобы объявить numpy.ndarray, вам нужно сделать вызов, подобный numpy.ndarray(<initial data here>), вы должны прочитать в ast и посмотреть, как вы можете изменить узлы, которые представляют этот вызов. Дело в том, что AST — хороший инструмент для достижения желаемого уровня прозрачности. - person Raydel Miranda; 01.02.2019

Да, это возможно, это одна из самых крутых вещей в питоне, в питоне все классы создаются typeклассом

Вы можете подробно прочитать здесь, но вам нужно сделать следующее:

In [58]: My_Class = type("My_Class", (My_Class,), {"__my_hidden_field__": X})

In [59]: My_Class.__my_hidden_field__
Out[59]:
array([[0.73998002, 0.68213825, 0.41621582],
       [0.05936479, 0.14348496, 0.61119082]])



* Отредактировано, потому что отсутствовало наследование, вам нужно передать исходный класс в качестве второго аргумента (в кортеже), чтобы он обновлялся, иначе он просто перезаписывает класс)

person Jose Angel Sanchez    schedule 31.01.2019
comment
это не будет работать для встроенных классов. также мне нужно добавить информацию к объектам, а не к классам - person Mr. Nobody; 31.01.2019