Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Как устроен namedtuple

Anna | 16.06.2014 | нет комментариев
Мы в Буруках любим не только людей и цифры. Мы также без утомились совершенствуемся во владении нашим основным инструментом, языком Python. Ссылка для тех, кто хочет совершенствоваться с нами. В этой статье-переводе автор разбирает устройство namedtuple и по ходу рассказывает об одной из основных доктрин языка.

Пару дней назад я был на пути в Сан-Франциско. Интернета в самолёте не было, следственно я читал исходники стандартной библиотеки Python 2.7. Реализация namedtuple показалась мне исключительно увлекательной, вероятно, потому, что на деле всё значительно проще, чем я думал прежде.

Вот тут лежат исходники. Если вы никогда прежде не знали о namedtuple, то рекомендую ознакомиться с этой функцией.

Код

################################################################################
### namedtuple
################################################################################

Ого! Заметный заголовок, правда?

В начале, как и полагается, определение функции, и пример отменного доктеста.

def namedtuple(typename, field_names, verbose=False, rename=False):
    """Возвращает новейший подкласс кортежа с именованными полями.

    >>> Point = namedtuple('Point', 'x y')
    >>> Point.__doc__                   # докстринг нового класса
    'Point(x, y)'
    >>> p = Point(11, y=22)             # создание экземпляра с позиционными и именованными доводами
    >>> p[0]   p[1]                     # индексируется как обыкновенный кортеж
    33
    >>> x, y = p                        # распаковывается как обыкновенный кортеж
    >>> x, y
    (11, 22)
    >>> p.x   p.y                       # поля доступны по имени
    33
    >>> d = p._asdict()                 # конвертируется в словарь
    >>> d['x']
    11
    >>> Point(**d)                      # создаётся из словаря
    Point(x=11, y=22)
    >>> p._replace(x=100)               # обновляет значение именованного поля
    Point(x=100, y=22)

    """

Потом начинаются разборки с доводами. Обратите внимание на применение basestring в вызове isinstance— так мы определим, что трудимся со строкой, если тип объекта unicode либо str (это верно работает в Python < 3.0).

    # Парсим и валидируем имена полей. Валидация служит двум целям:
    # генерация информативных сообщений об ошибках
    # и предотвращение атак внедрением в образцы.
    if isinstance(field_names, basestring):
        field_names = field_names.replace(',', ' ').split() # имена поделены пробелами и/или запятыми

Если установлен признак rename, то все неправильные наименования полей будут переименованы соответственно их позициям.

    field_names = tuple(map(str, field_names))
    if rename:
        names = list(field_names)
        seen = set()
        for i, name in enumerate(names):
            if (not all(c.isalnum() or c=='_' for c in name) or _iskeyword(name)
                or not name or name[0].isdigit() or name.startswith('_')
                or name in seen):
                names[i] = '_%d' % i
            seen.add(name)
        field_names = tuple(names)

Обратите внимание на генераторное выражение, обёрнутое в all(). Такая запись, all(bool_expr(x) for x in things), — весьма комфортный метод описать желаемый итог в одном выражении.

    for name in (typename,)   field_names:
        if not all(c.isalnum() or c=='_' for c in name):
            raise ValueError(
                'Type names and field names can only contain alphanumeric characters and underscores: %r' % name
            )
        if _iskeyword(name):
            raise ValueError('Type names and field names cannot be a keyword: %r' % name)
        if name[0].isdigit():
            raise ValueError('Type names and field names cannot start with a number: %r' % name)

Проверочка на повторения имён:

    seen_names = set()
    for name in field_names:
        if name.startswith('_') and not rename:
            raise ValueError('Field names cannot start with an underscore: %r' % name)
        if name in seen_names:
            raise ValueError('Encountered duplicate field name: %r' % name)
        seen_names.add(name)

А сейчас начинается реальное развлечение. (Я уверен, что создание типа данных во время исполнения — это радостно). Подготавливаем различным образом имена полей для внедрения в образец кода. Увлекательно применение текстового представления кортежа и нотации срезов для определения argtxt.

    # Сделаем и заполним образец класса
    numfields = len(field_names)
    argtxt = repr(field_names).replace("'", "")[1:-1]   # представление кортежа без кавычек и скобок
    reprtxt = ', '.join('%s=%%r' % name for name in field_names)

И вот что на самом деле творится под капотом namedtuple. Эта строка впоследствие превратится в Python-код.

    template = '''class %(typename)s(tuple):
        '%(typename)s(%(argtxt)s)' n
        __slots__ = () n
        _fields = %(field_names)r n

        def __new__(_cls, %(argtxt)s):
            'Create new instance of %(typename)s(%(argtxt)s)'
            return _tuple.__new__(_cls, (%(argtxt)s)) n

        @classmethod
        def _make(cls, iterable, new=tuple.__new__, len=len):
            'Make a new %(typename)s object from a sequence or iterable'
            result = new(cls, iterable)
            if len(result) != %(numfields)d:
                raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result))
            return result n

        def __repr__(self):
            'Return a nicely formatted representation string'
            return '%(typename)s(%(reprtxt)s)' %% self n

        def _asdict(self):
            'Return a new OrderedDict which maps field names to their values'
            return OrderedDict(zip(self._fields, self)) n

        __dict__ = property(_asdict) n

        def _replace(_self, **kwds):
            'Return a new %(typename)s object replacing specified fields with new values'
            result = _self._make(map(kwds.pop, %(field_names)r, _self))
            if kwds:
                raise ValueError('Got unexpected field names: %%r' %% kwds.keys())
            return result n

        def __getnewargs__(self):
            'Return self as a plain tuple.  Used by copy and pickle.'
            return tuple(self) nn

        ''' % locals()

Собственно, это и есть образец нашего нового класса.

Применение locals() для строковой интерполяции мне кажется дюже комфортным. Питону не хватает примитивный интерполяции локальных переменных. В Groovy и CoffeeScript, скажем, дозволено написать что-то как бы "{name} is {some_value}". Но я думаю, что данный Python-вариант абсолютно сойдёт: "{name} is {some_value}".format(**locals()).

Вы, вероятно, подметили, что __slots__ определяется как пустой кортеж. Питон в таком случае не использует для экземпляров словари в качестве пространств имён, что немножко экономит источники. Вследствие неизменяемости, которая наследуется от родительского класса (tuple), и неосуществимости добавлять новые признаки (потому что __slots__ = ()), экземпляры namedtuple-типов являются объектами-значениями, что разрешает применять их, скажем, в качестве ключей в словарях.

Идём дальше. На всякое имя создаётся качество только для чтения. _itemgetter — это itemgetter из модуляoperator, тот, что возвращает функцию одного довода, что как раз подходит для свойства.

    for i, name in enumerate(field_names):
        template  = "        %s = _property(_itemgetter(%d), doc='Alias for field number %d')n" % (name, i, i)
    if verbose:
        print template

Выходит, у нас есть колоссальная строчка с питонячьим кодом. Что с ней делать? Выполнение в ограниченном пространстве имён кажется умным. Посмотрите, как здесь применяется exec ... in:

    # Выполним полученный код во временном пространстве имён.
    # Не забываем о поддержке трассировщиков, определяем значение
    # frame.f_globals['__name__']
    namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename,
                     OrderedDict=OrderedDict, _property=property, _tuple=tuple)
    try:
        exec template in namespace
    except SyntaxError, e:
        raise SyntaxError(e.message   ':n'   template)
    result = namespace[typename]

Дюже хитроумно! Идея выполнить строку кода в изолированном пространстве имён, а после этого вытянуть из него новейший тип непривычна для меня. За подробностями об exec идём в пост Армина Ронахера.

Дальше немножко магии, Дабы определить __module__ нового класса как модуль, тот, что вызвал namedtuple:

    try:
        result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
    except (AttributeError, ValueError):
        pass

и на этом всё!

    return result

Легко, не так ли?

Мысли о реализации

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

for name in (typename,)   field_names:
    if not all(c.isalnum() or c=='_' for c in name):
        raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: %r' % name)
    if _iskeyword(name):
        raise ValueError('Type names and field names cannot be a keyword: %r' % name)
    if name[0].isdigit():
        raise ValueError('Type names and field names cannot start with a number: %r' % name)

дозволено было бы написать

for name in (typename,)   field_names:
    try:
        exec ("%s = True" % name) in {}
    except (SyntaxError, NameError):
        raise ValueError('Invalid field name: %r' % name)

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

Между нами только find

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

И вообще. Постигайте вероятности инструментов, которыми пользуетесь, не занимайтесь велосипедостроением!

Источник: programmingmaster.ru
Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB