python как создать переменные частного класса с помощью setattr или exec?

Я только что столкнулся с ситуацией, когда псевдо - имена членов частного класса не искажаются при использовании 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 ответов


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

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

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

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

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

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


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

  • __X получает символ подчеркивания и имя класса. Е. Г. если это Test, искалеченный attr _Test__X.
  • единственное исключение, если имя класса начинается с подчеркивания, это отшелушится. Е. Г. если класс __Test, искалеченный attr все еще _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)

я знаю, что это несколько "жестко кодирует" имя mangling, но оно, по крайней мере, изолировано от одной функции. Затем его можно использовать для искажения строк для 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

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

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

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

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


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

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)