Python fields gathering

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

Одной из достаточно часто встречающихся задач является “сборка полей” - сбор существующих членов класса, которые служат как поля данных.

def CommonAPIStructure(APIStructure):
    int_field = 0
    str_field = u"10"
    bool_field = False

Вот только далеко не все понимают как собрать список полей, а главное – как собрать их верно.

Для получения членов класса в Python можно воспользоваться двумя путями: self.__class__.__dict__, dir(self.__class__).

Почему и что такое self.__class__? Каждый объект в Python имеет ссылку на свой класс, хранимую в поле __class__. Мы захотим хранить где-нибудь дефолтное значение, да и валидатор, и, возможно, что-нибудь ещё. Но вот проблема, если мы будем хранить всё это в поле класса, то в экземпляре оно будет перезаписано:

def CommonAPIStructure(APIStructure):
    int_field = INTValidator(default=0)

struct = CommonAPIStructure()
struct.int_field = 100

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

if var_name not in self.__dict__:
    return self.__class__.__dict__[var_name]

Для большей справки, читайте Python Data model.

dir

If the object is a module object, the list contains the names of the module’s attributes. If the object is a type or class object, the list contains the names of its attributes, and recursively of the attributes of its bases. Otherwise, the list contains the object’s attributes’ names, the names of its class’s attributes, and recursively of the attributes of its class’s base classes.

Этим всё сказано. Рекурсивно проходит все под-классы и выдаёт полный список всех полей всех подклассов и класса самого. Однако, dir генерирует только список имён полей - просто list.

>>> class Test(object):
...    int_field = 0
>>> print dir(Test)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'int_field']

Казалось бы, не очень удобно, ведь нам важно получить доступ к значениям - именно там будет храниться нужная нам информация. Но не стоит делать поспешных выводов.

__dict__

Здесь нет никакой магии. Вообще. Мы получаем отсюда то, что класс содержит. Без купюр. Однако, Python не будет иметь возможности среагировать и препроцессировать то, что мы получим.

>>> class Test(object):
...    int_field = 0
>>> print Test.__dict__
{'__dict__': <attribute '__dict__' of 'Test' objects>, '__module__': '__main__', 'int_field': 0, '__weakref__': <attribute '__weakref__' of 'Test' objects>, '__doc__': None}

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

Подводные камни

Дело в том, что Python предоставляет нам полный доступ не только к тому, чем мы оперируем, но и что творится за сценой. Достаточно просто понимать где данная грань расположена. Где? Вот тут:

__ __

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

Не просто так я вспоминаю об этом, конечно. Через __dict__ мы получае сырые данные элементов объекта.

One of many

Рассмотрим один из примеров подводных камней: А вы знаете что статичные и классовые методы(@staticmethod, @classmethod) в Python реализованы через дескрипторы и ваши функции создаются в методах __get__ этих самих дескрипторов? А вы знаете что такое дескриптор, в контексте Python? Если нет, то почитайте вот тут.

Ну раз вы знаете что такое дескрипторы, то вы должны понимать какое зло работать с ними, не зная о том, что они есть. Python нас защищает от ошибок, но если мы выбираем путь Нео(Джедай будет мудрее, он таким путём не пойдёт), через __dict__, то случитнся… проблема. Если вам нужно орудовать содержимым дестрипнора, то через __dict__ мы получим только… сам дескриптор. О чём я? А вот о чём:

class Test(object):
    int_field = 0
    @staticmethod
    def callable_a(a):
        print a
a = getattr(Test, "callable_a")
b = Test.__dict__["callable_a"]
print "callable_a high-level -", callable(a), type(a)
print "callable_a low-level -", callable(b), type(b)

callable_a high-level - True <type 'function'>
callable_a low-level - False <type 'staticmethod'>

В классе Test мы имеем статичную функцию callable_a и мы пытаемся получить её через getattr(функцию высокого абстрактного уровня модели данных Python) и __dict__. Как видно из сего примера, через getattr мы получаем то, что ожидали, а вот через __dict__ - объект, который даже не является callable! И верно, ведь у дескриптора нет метода __call__, а так как мы обратились к членам класса низкоуровневым методом, мы получили… этот самый дескриптор.

Возможно, у вас в голове появится мысль: “О чём думал Гвидо?!”, но я бы хотел вас немного остановить и заставить подумать.

Python - боевой инструмент, для разработки живых систем, с огромными возможностями и большой свободой. Свобода накладывает ответственность. Python - не игрушка. И это хорошо, ведь на игрушечной машинке далеко не уехать.

Итог

Я не буду приводить ни плюсов, ни минусов обоих методов получения членов объекта. Вся правда в том, что у них разная цель, разное применение и если не понимать как это работает, то вы наломаете дров. А так, используйте dir & getattr и ващи волосы будут приятные и шелковистые!

Пара слов о модуле inspect и его функции getmembers

Не скажу точно, но когда-то давно один небольшой проект наткнулся на проблему при использовании inspect.getmembers. Проблема решилась использованием dir и getattr прямо. Фактически, inspect.getmembers делает то же самое, что и можно делать руками:

def getmembers(object, predicate=None):
    """Return all members of an object as (name, value) pairs sorted by name.
    Optionally, only return members that satisfy a given predicate."""
    results = []
    for key in dir(object):
        value = getattr(object, key)
        if not predicate or predicate(value):
            results.append((key, value))
    results.sort()
    return results

Что использовать? Выбор за вами, разницы не должно быть, если функция имплементирована везде одинаково.

Written on January 3, 2013