Как создать переменные частного класса, используя setattr или exec? - PullRequest
8 голосов
/ 16 октября 2011

Я только что столкнулся с ситуацией, когда псевдо -приватные имена членов класса не искажаются при использовании setattr или exec.

In [1]: class T:
   ...:     def __init__(self, **kwargs):
   ...:         self.__x = 1
   ...:         for k, v in kwargs.items():
   ...:             setattr(self, "__%s" % k, v)
   ...:         
In [2]: T(y=2).__dict__
Out[2]: {'_T__x': 1, '__y': 2}

Я тоже пробовал exec("self.__%s = %s" % (k, v)) с тем же результатом:

In [1]: class T:
   ...:     def __init__(self, **kwargs):
   ...:         self.__x = 1
   ...:         for k, v in kwargs.items():
   ...:             exec("self.__%s = %s" % (k, v))
   ...:         
In [2]: T(z=3).__dict__
Out[2]: {'_T__x': 1, '__z': 3}

Выполнение self.__dict__["_%s__%s" % (self.__class__.__name__, k)] = v будет работать, но __dict__ является атрибутом только для чтения.

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


Лучший способ сформулировать мой вопрос:

Что делает python «под капотом», когда он встречает установленный атрибут двойного подчеркивания (self.__x)? Есть ли магическая функция, которая используется для калечащих операций?

Ответы [ 3 ]

6 голосов
/ 17 октября 2011

Я полагаю, что Python выполняет искажение закрытых атрибутов во время компиляции ... в частности, это происходит на этапе, когда он только что проанализировал источник в абстрактном синтаксическом дереве и отображает его в виде байтового кода.Это единственный раз, когда виртуальная машина знает имя класса, в пределах (лексической) области которого определена функция.Затем он искажает псевдо-приватные атрибуты и переменные и оставляет все остальное без изменений.Это имеет несколько последствий ...

  • Строковые константы, в частности, не искажены, поэтому ваш setattr(self, "__X", x) остается один.

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

  • Насколько я знаю, не существует простого способа определить (во время выполнения), какой класс был определен функциейin ... По крайней мере, без большого количества вызовов inspect, которые полагаются на отражение источника для сравнения номеров строк между источниками функции и класса.Даже если этот подход не является надежным на 100%, существуют пограничные случаи, которые могут привести к ошибочным результатам.

  • Процесс на самом деле довольно деликатен в отношении искажения - если вы попытаетесь получить доступ к __X атрибут для объекта, который не является экземпляром класса, в котором лексически определена функция, он все равно будет искажать его для этого класса ... позволяя вам хранить атрибуты частного класса в экземплярах других объектов!(Я бы почти утверждал, что этот последний пункт - это особенность, а не ошибка)

Таким образом, искажение переменной должно быть выполнено вручную, так что вы вычисляете, что искаженный атрибутдолжно быть для того, чтобы позвонить setattr.


Что касается самого искажения, оно выполняется функцией _Py_Mangle , которая использует следующую логику:

  • __X получает подчеркивание иимя класса добавлено.Например, если это Test, искаженный атрибут равен _Test__X.
  • Единственное исключение - если имя класса начинается с подчеркивания, оно удаляется.Например, если класс __Test, искаженный атрибут все еще _Test__X.
  • Конечные подчеркивания в имени класса не удалены.

Чтобы обернуть все это в функцию ...

def mangle_attr(source, attr):
    # return public attrs unchanged
    if not attr.startswith("__") or attr.endswith("__") or '.' in attr:
        return attr
    # if source is an object, get the class
    if not hasattr(source, "__bases__"):
        source = source.__class__
    # mangle attr
    return "_%s%s" % (source.__name__.lstrip("_"), attr)

Я знаю, что это несколько "жесткие коды" искажения имени, но оно по крайней мере изолировано от одной функции,Затем его можно использовать для манипулирования строками для setattr:

# you should then be able to use this w/in the code...
setattr(self, mangle_attr(self, "__X"), value)

# note that would set the private attr for type(self),
# if you wanted to set the private attr of a specific class,
# you'd have to choose it explicitly...
setattr(self, mangle_attr(somecls, "__X"), value)

Альтернативно, следующая реализация mangle_attr использует eval, так что она всегда использует текущую логику искажения Python (хотя я неЯ думаю, что логика, изложенная выше, никогда не менялась) ...

_mangle_template = """
class {cls}:
    @staticmethod
    def mangle():
        {attr} = 1
cls = {cls}
"""

def mangle_attr(source, attr):
    # if source is an object, get the class
    if not hasattr(source, "__bases__"):
        source = source.__class__
    # mangle attr
    tmp = {}
    code = _mangle_template.format(cls=source.__name__, attr=attr)
    eval(compile(code, '', 'exec'), {}, tmp); 
    return tmp['cls'].mangle.__code__.co_varnames[0]

# NOTE: the '__code__' attr above needs to be 'func_code' for python 2.5 and older
4 голосов
/ 17 октября 2011

Обращаясь к этому:

Что делает питон «под капотом», когда он сталкивается с двойным атрибут подчеркивания (self.__x) устанавливается? Есть ли волшебная функция что используется для калечащих операций?

AFAIK, это в основном особый случай в компиляторе. Так что, как только он в байт-коде, имя уже искажено; Интерпретатор никогда не видит неупорядоченное имя вообще и не подозревал о необходимости какой-либо специальной обработки. Вот почему ссылки через setattr, exec или при поиске строки в __dict__ не работают; компилятор видит все эти строки как строки и не знает, что они имеют какое-либо отношение к доступу к атрибутам, поэтому он пропускает их без изменений. Интерпретатор ничего не знает о искажениях имен, поэтому он просто использует их напрямую.

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

2 голосов
/ 16 октября 2011

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

class T(object):

    def __init__(self, **kwds):
        for k, v in kwds.items():
            d = {}
            cls_name = self.__class__.__name__

            eval(compile(
                'class dummy: pass\n'
                'class {0}: __{1} = 0'.format(cls_name, k), '', 'exec'), d)

            d1, d2 = d['dummy'].__dict__, d[cls_name].__dict__
            k = next(k for k in d2 if k not in d1)

            setattr(self, k, v)

>>> t = T(x=1, y=2, z=3)
>>> t._T__x, t._T__y, t._T__z
(1, 2, 3)
...