<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	>

<channel>
	<title>developers.org.ua &#187; Сергей Щетинин</title>
	<atom:link href="http://www.developers.org.ua/archives/author/mlk/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.developers.org.ua</link>
	<description>сообщество программистов</description>
	<pubDate>Fri, 21 Nov 2008 11:16:07 +0000</pubDate>
	<generator>http://wordpress.org/?v=2.6</generator>
	<language>en</language>
			<item>
		<title>Стиль удава: компиляция на лету</title>
		<link>http://www.developers.org.ua/archives/mlk/2008/07/23/python-kung-fu-p1-runtime-compilation/</link>
		<comments>http://www.developers.org.ua/archives/mlk/2008/07/23/python-kung-fu-p1-runtime-compilation/#comments</comments>
		<pubDate>Wed, 23 Jul 2008 06:11:17 +0000</pubDate>
		<dc:creator>Сергей Щетинин</dc:creator>
		
		<category><![CDATA[Python]]></category>

		<category><![CDATA[Разработка]]></category>

		<category><![CDATA[статьи]]></category>

		<category><![CDATA[CPython]]></category>

		<category><![CDATA[JIT]]></category>

		<category><![CDATA[_python-kung-fu]]></category>

		<guid isPermaLink="false">http://www.developers.org.ua/index.php?p=1402</guid>
		<description><![CDATA[Это первая статья <a href="http://www.developers.org.ua/archives/tag/_python-kung-fu/">серии посвященной Кунг-фу Python</a>. При неумелом использовании описываемых приемов можно запросто заехать себе пяткой в лоб, но настоящему программисту и это должно прийтись по душе, т.к. в конечном счете и такой опыт засчитывается в плюс.


Related posts:<ol><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/08/python-webdev-no-frameworks-p1/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 1)'>Python: Веб-разработка без фреймворков (часть 1)</a> <small>Каждый разработчик тщательно выбирает свой инструментарий и чем лучше он...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/10/python-webdev-no-frameworks-p2/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 2)'>Python: Веб-разработка без фреймворков (часть 2)</a> <small>В прошлой части я постарался рассказать о том, что чистый...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/14/python-webdev-no-frameworks-p3/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 3)'>Python: Веб-разработка без фреймворков (часть 3)</a> <small>Прочитав предыдущие статьи читатель, надеюсь, убедился, что средств PythonPaste и...</small></li></ol>

Related posts brought to you by <a href='http://mitcho.com/code/yarpp/'>Yet Another Related Posts Plugin</a>.]]></description>
			<content:encoded><![CDATA[<p>Это первая статья <a href="http://www.developers.org.ua/archives/tag/_python-kung-fu/">серии посвященной Кунг-фу Python</a>. При неумелом использовании описываемых приемов можно запросто заехать себе пяткой в лоб, но настоящему программисту и это должно прийтись по душе, т.к. в конечном счете и такой опыт засчитывается в плюс.</p>
<p>Я решил начать с чего попроще – runtime компиляции кода. Мы не будем писать JIT-компилятор в обычном понимании, а реализуем небольшую оптимизацию для вполне практичной библиотеки.</p>
<h3>Контекст задачи</h3>
<p>Для целей этой статьи будем считать, что нам позарез понадобилось снабжать наши функции проверкой типов аргументов. На самом деле, по ходу дела будет видно, что нетрудно расширить библиотеку для более полезных задач как приведение аргументов к типу или любые другие произвольные преобразования. Но сначала просто проверка типов.</p>
<pre><code>@accepts(float, float)
def float_div(a, b):
    return a / b</code></pre>
<p>Вот так мы будем её использовать. Это иллюстрация, сейчас нам главное саму библиотеку написать.</p>
<p>Какой эффект мы ожидаем от такого декоратора? Давайте сразу на коленке добавим документацию и тесты, сразу и прояснится:</p>
<pre><code>@accepts(float, float)
def float_div(a, b):
    &quot;&quot;&quot;
    A function that divides floats.
    Any other types of arguments will throw an exception

        &gt;&gt;&gt; float_div(1.0, 2.0)
        0.5
        &gt;&gt;&gt; float_div(1, 1.0)
        Traceback (most recent call last):
        ...
        TypeError: Expected &lt;type &#39;float&#39;&gt; for argument #1 got &lt;type &#39;int&#39;&gt;
    &quot;&quot;&quot;
    return a / b

if __name__ == &#39;__main__&#39;:
    import doctest
    doctest.run_docstring_examples(float_div, globals())</code></pre>
<p>Теперь должно стать очевидно, как реализовывать декоратор:</p>
<pre><code>class accepts_wrap(object):
    def __init__(self, func, argtypes):
        self.func = func
        self.__doc__ = func.__doc__
        self.argtypes = argtypes

    def __call__(self, *args):
        for i, (arg, cls) in enumerate(zip(args, self.argtypes)):
            if not isinstance(arg, cls):
                raise TypeError(&quot;Expected %r for argument #%d got %r&quot; % (cls, i+1, type(arg)))
        return self.func(*args)

def accepts(*argtypes):
    return lambda func: accepts_wrap(func, argtypes)</code></pre>
<h3>Постановка задачи</h3>
<p>Хоть ничего страшного в дополнительном коде нет, такие декораторы могут быть применены к весьма часто вызываемым функциям, и потому очень хотелось бы минимизировать возможные потери скорости. Это может быть оправдано, если мы действительно заметили замедление или если мы собираемся положить нашу библиотеку в основу многих разработок и суммарная экономия вычислительной мощности в итоге оправдает затраты времени на оптимизацию.</p>
<p>Также, будучи честным с самим собой, стоит признать, что писать такие нетривиальные вещи довольно интересно и потому, при наличии времени, можно реализовать что-нибудь эдакое в качестве практики, которая вполне может сильно пригодиться позже, когда оптимизация действительно понадобится и времени на усвоение приемов и ловушек такого подхода будет маловато. По сути, при должных навыках эти наши эксперименты можно переложить на генерацию машинного кода, что может очень пригодиться в определенных областях.</p>
<p>Так вот, мы попробуем минимизировать количество дополнительных команд исполняемых при каждом вызове обернутой функции путем генерации оптимального байт-кода для каждого экземпляра обертки.</p>
<h3>Модель исполнения</h3>
<p>Поскольку в данном случае нас волнует только CPython и его <a href="http://docs.python.org/lib/bytecodes.html">байт-код</a>, то кратко рассмотрим как он работает. При первой загрузке модуля его исходники разбираются в AST (Abstract Syntax Tree) по которому генерируется байт-код (который также кэшируется в .pyc файле). Байт-код python предназначен для исполнения <del>несложной</del> умеренно сложной стековой виртуальной машиной. Реализация внутреннего цикла CPython находится в файле <a href="http://svn.python.org/view/python/trunk/Python/ceval.c?view=auto">ceval.c</a> и ознакомиться с ним хотя бы поверхностно следует всякому интересующемуся тем, как Python работает внутри. Если вы собираетесь писать компиляцию на лету как в этой статье, то такое знакомство, хотя и не обязательно, но придется очень кстати.</p>
<h3>Инструментарий</h3>
<p>Python предоставляет доступ к разным этапам процесса загрузки и генерации байт-кода начиная с <a href="http://docs.python.org/lib/module-tokenize.html">разбития</a> на <a href="http://docs.python.org/lib/module-token.html">лексемы</a> и <a href="http://docs.python.org/lib/module-parser.html">построения синтаксических деревьев</a> и заканчивая <a href="http://docs.python.org/lib/node860.html">созданием</a> из этих структур <a href="http://docs.python.org/lib/bltin-code-objects.html">code-objects</a> и собственно <a href="http://docs.python.org/lib/module-new.html">функций, классов и модулей</a>, которые можно использовать непосредственно. Есть также возможность загружать и сохранять байткод самостоятельно, т.к. формат .pyc-файлов это знакомый всем <a href="http://docs.python.org/lib/module-marshal.html">marshal</a>. Еще очень полезен в разработке <a href="http://docs.python.org/lib/module-dis.html">декомпилятор байт-кода</a>, позволяющий посмотреть какие коды получаются из существующих исходников.</p>
<p>В зависимости от стоящих задач можно подойти к runtime-компиляции по-разному. Ведь если для AST / compileast можно ожидать поддержки в разных реализациях интерпретатора, то для собственно байт-кода этого сказать нельзя. Так что, возможно, для многих лучшим выбором окажется работа на другом уровне, однако в этой статье мы будем генерировать байт-код непосредственно. Для этого мы воспользуемся библиотекой <a href="http://pypi.python.org/pypi/BytecodeAssembler">BytecodeAssembler</a>.</p>
<p>Порождающий код, написанный с её использованием очень похож на ассемблер, но нужно понимать, что на самом деле это просто построение последовательности операций и четко различать какие операции выполняются, а какие добавляются в порожденный код. Это несложно, но требует некоторой адаптации. Например, вот как выглядит дизассемблированный код простенькой функции:</p>
<pre><code>&gt;&gt;&gt; def foo(x, y):
...     return x + y
...
&gt;&gt;&gt; from dis import dis
&gt;&gt;&gt; dis(foo)
  2           0 LOAD_FAST                0 (x)
              3 LOAD_FAST                1 (y)
              6 BINARY_ADD
              7 RETURN_VALUE</code></pre>
<p>А вот как можно породить такой же такой код:</p>
<pre><code>from peak.util.assembler import *
c = Code()
c.LOAD_FAST(&#39;x&#39;)
c.LOAD_FAST(&#39;y&#39;)
c.BINARY_ADD()
c.RETURN_VALUE()
c.co_argcount = 2
c.co_varnames = [&#39;x&#39;, &#39;y&#39;]
foo_code = c.code()</code></pre>
<h3>Новая реализация</h3>
<p>Дизассемблировав внутренний цикл нашей первой реализации accepts_wrap, получим кальку, по которой можно записать новый вариант:</p>
<pre><code>from peak.util.assembler import *
import new
import inspect

class accepts_wrap(object):
    def __init__(self, func, argtypes):
        self.func = func
        self.__doc__ = func.__doc__
        self.argtypes = argtypes
        self.compile()

    def compile(self):
        argnames = inspect.getargspec(self.func)[0]
        c = Code.from_function(self.func)
        c.LOAD_CONST(self.func)
        for i, cls in enumerate(self.argtypes):
            c.LOAD_FAST(argnames[i])
            c.LOAD_CONST(isinstance) # if not isinstance(arg, cls):
            c.LOAD_FAST(argnames[i])
            c.LOAD_CONST(cls)
            c.CALL_FUNCTION(2)

            skip = c.JUMP_IF_TRUE()
            c.LOAD_CONST(TypeError) # raise TypeError
            c.LOAD_CONST(&quot;Expected %r for argument #%d got %%r&quot; % (cls, i+1))
            c.LOAD_CONST(type)
            c.LOAD_FAST(argnames[i])
            c.CALL_FUNCTION(1)
            c.BINARY_MODULO()
            c.CALL_FUNCTION(1)
            c.RAISE_VARARGS(1)

            skip()
            c.POP_TOP()
        c.CALL_FUNCTION(len(argnames))
        c.RETURN_VALUE()
        self.wrapped_func = new.function(c.code(), {})

    def __call__(self, *args):
        return self.wrapped_func(*args)</code></pre>
<p>Не будем вдаваться в подробности лишь отметив, что многие операции были заменены на более дешевые. Например, isinstance загружается через LOAD_CONST а не LOAD_GLOBAL как в первом варианте. Также исчезла итерация по списку ожидаемых классов – при генерации байт-кода цикл разворачивается.</p>
<p>Для того чтобы понять, как всё работает, нужно иметь четкую ментальную модель стековой машины, потому что иначе загвоздки будут возникать на каждом шагу. Например, почему мы в первую очередь делаем c.LOAD_CONST(self.func)?</p>
<p>После вызова isinstance на вершине стека оказывается булевое значение, по которому мы проверяем валидность аргумента. В байт-коде ветвление, естественно, реализуется через JUMP. В BytecodeAssembler для прыжков на еще не объявленные лейблы есть удобное соглашение (см. в коде: skip = … создает отложенный лейбл, а skip() привязывает его к месту в коде).</p>
<p>Я думаю, вы заметили, что часть с созданием экземпляра TypeError оказалась громоздкой, она повторяет достаточно прямолинейный код, не требующий к тому же никакой оптимизации, нельзя ли записать это короче? Можно. При помощи конструкций в какой-то мере напоминающих AST, вот чем можно заменить этот код:</p>
<pre><code>c.LOAD_CONST(TypeError)
c.LOAD_CONST(&quot;Expected %r for argument #%d got %%r&quot; % (cls, i+1))
c(Call(Const(type), [Local(argnames[i])]))
c.BINARY_MODULO()
c.CALL_FUNCTION(1)
c.RAISE_VARARGS(1)</code></pre>
<p>Еще одна итерация и:</p>
<pre><code>error_msg = Suite([ Const(&quot;Expected %r for argument #%d got %%r&quot; % (cls, i+1)),
                    Call(Const(type), [Local(argnames[i])]),
                    Code.BINARY_MODULO])
c.LOAD_CONST(TypeError)
c(error_msg)
c.CALL_FUNCTION(1)
c.RAISE_VARARGS(1)</code></pre>
<p>И еще немного:</p>
<pre><code>error_msg = Suite([ Const(&quot;Expected %r for argument #%d got %%r&quot; % (cls, i+1)),
                    Call(Const(type), [Local(argnames[i])]),
                    Code.BINARY_MODULO])
c(Call(Const(TypeError), [error_msg]))
c.RAISE_VARARGS(1)</code></pre>
<p>Проделав аналогичные итерации с остальным циклом, получим следующее:</p>
<pre><code>        for i, cls in enumerate(self.argtypes):
            arg = Local(argnames[i])
            c(arg)
            c(Call(Const(isinstance), [arg, Const(cls)]))
            skip = c.JUMP_IF_TRUE()
            error_msg = Suite([ Const(&quot;Expected %r for argument #%d got %%r&quot; % (cls, i+1)),
                                Call(Const(type), [arg]),
                                Code.BINARY_MODULO])
            c(Call(Const(TypeError), [error_msg]))
            c.RAISE_VARARGS(1)
            skip()
            c.POP_TOP()</code></pre>
<h3>Расширяемость</h3>
<p>Далее, чтобы добиться расширяемости, реализуем тело цикла как новую конструкцию. </p>
<p>Для реализации process_arg есть <a href="http://peak.telecommunity.com/DevCenter/BytecodeAssembler#custom-code-generation">вспомогательные средства</a>, с ними он будет выглядеть следующим образом:</p>
<pre><code>@nodetype()
def process_arg(i, argname, spec, code=None):
    if code is None:
        return i, argname, spec
    else:
        arg = Local(argname)
        code(Call(Const(isinstance), [arg, Const(spec)]))
        skip = code.JUMP_IF_TRUE()
        error_msg = Suite([ Const(&quot;Expected %r for argument #%d got %%r&quot; % (spec, i+1)),
                            Call(Const(type), [arg]),
                            Code.BINARY_MODULO])
        code(Call(Const(TypeError), [error_msg]))
        code.RAISE_VARARGS(1)
        skip()
        code.POP_TOP()
        code(arg)</code></pre>
<p>Это в свою очередь позволяет нам сократить реализацию compile:</p>
<pre><code>    def compile(self):
        argnames = inspect.getargspec(self.func)[0]
        c = Code.from_function(self.func)
        code_args = [process_arg(i, argnames[i], cls) for i, cls in enumerate(self.argtypes)]
        c.return_(Call(Const(self.func), code_args))
        self.wrapped_func = new.function(c.code(), {})</code></pre>
<p>На самом деле можно сократить весь класс accepts_wrap, вот что от него осталось:</p>
<pre><code>def accepts(*argtypes):
    def decorate(func):
        argnames = inspect.getargspec(func)[0]
        code_args = [process_arg(i, argnames[i], cls) for i, cls in enumerate(argtypes)]
        c = Code.from_function(func)
        c.return_(Call(Const(func), code_args))
        wrapped = new.function(c.code(), {})
        return rewrap(func, wrapped) # preserve name / __doc__
    return decorate</code></pre>
<p>Вот <a href="http://pastebin.com/f3a50ac39">весь наш код на данный момент</a>, совсем немного. Благодаря тому, что обработка каждого аргумента вынесена в process_arg, несложно переопределять её для особых случаев. Например, на данный момент мы предполагаем что аргументами декоратора будут классы, можно же предположить, что ими могут также быть некие другие объекты, описывающие требования к аргументу. При условии, что мы умеем генерировать для этих проверок оптимальный код (эту задачу можно делегировать самим декларациям) мы получим свободно расширяемый предобработчкик аргументов. Естественно, в реальном проекте начинать надо с более традиционной реализации, но данная статья, я надеюсь, в какой-то мере демонстрирует каким образом возможно усиливать некоторые библиотеки частичной компиляцией на лету. </p>
<p>Само собой без замеров относительной скорости оптимизировать реализацию не рекомендуется. Также следует помнить, что генерация кода доступна не во всех окружениях и для максимальной совместимости нужно также поддерживать и тестировать обычную реализацию той же функциональности.</p>
<h3>Интересное про байт-код и интерпретатор CPython</h3>
<p>Для того чтобы комфортно писать генерацию байт-кода нужно ознакомиться с внутренностями интерпретатора, но поскольку из читателей наверняка этим займутся лишь немногие, перескажу несколько интересных фактов.</p>
<ul>
<li>Для получения атрибута нет специальной команды, компилятор генерирует последовательность команд <code>LOAD_CONST(getattr), LOAD_XXX(obj), LOAD_CONST(attrname), CALL_FUNCTION(2)</code></li>
<li>Локальные переменные во много раз быстрее глобальных. Поиск глобальной переменной это доступ к словарю, а для локальной переменной это  доступ к ячейке стека по заранее известному индексу.</li>
<li>Переменные общие для замыканий или объявленные уровнем выше реализованы через специальные структуры данных, для доступа к ним существуют отдельные команды.</li>
<li>На уровне интерпретатора реализовано предсказание последовательностей команд для ускорения наиболее часто встречающихся цепочек.</li>
</ul>
<p>Если есть какие-то вопросы, связанные с этой темой, спрашивайте в комментариях, попробую ответить.</p>
<style type="text/css">
pre { max-height: none; }
</style>
<br/><a href="http://www.developers.org.ua/archives/mlk/2008/07/23/python-kung-fu-p1-runtime-compilation/#ratings">Оценить статью на сайте</a> | <a href="http://www.developers.org.ua/archives/mlk/2008/07/23/python-kung-fu-p1-runtime-compilation/#comments">18 комментариев</a>]]></content:encoded>
			<wfw:commentRss>http://www.developers.org.ua/archives/mlk/2008/07/23/python-kung-fu-p1-runtime-compilation/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Python: Веб-разработка без фреймворков (часть 6)</title>
		<link>http://www.developers.org.ua/archives/mlk/2008/07/02/python-webdev-no-frameworks-p6/</link>
		<comments>http://www.developers.org.ua/archives/mlk/2008/07/02/python-webdev-no-frameworks-p6/#comments</comments>
		<pubDate>Wed, 02 Jul 2008 08:01:54 +0000</pubDate>
		<dc:creator>Сергей Щетинин</dc:creator>
		
		<category><![CDATA[Python]]></category>

		<category><![CDATA[Разработка]]></category>

		<category><![CDATA[статьи]]></category>

		<category><![CDATA[Python: Веб-разработка без фреймворков]]></category>

		<category><![CDATA[рефакторинг]]></category>

		<category><![CDATA[фреймворк]]></category>

		<guid isPermaLink="false">http://www.developers.org.ua/index.php?p=1374</guid>
		<description><![CDATA[В <a href="http://www.developers.org.ua/archives/mlk/2008/06/17/python-webdev-no-frameworks-p5/">прошлой статье</a> мы выяснили как может выглядеть независимая компонента, а в этой мы создадим ещу одну, чуть крупнее, и найдем способ связать её с остальным приложением.


Related posts:<ol><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/08/python-webdev-no-frameworks-p1/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 1)'>Python: Веб-разработка без фреймворков (часть 1)</a> <small>Каждый разработчик тщательно выбирает свой инструментарий и чем лучше он...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/10/python-webdev-no-frameworks-p2/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 2)'>Python: Веб-разработка без фреймворков (часть 2)</a> <small>В прошлой части я постарался рассказать о том, что чистый...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/05/20/python-webdev-no-frameworks-p4/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 4)'>Python: Веб-разработка без фреймворков (часть 4)</a> <small>В предыдущих статьях мы разобрались, как можно создавать веб-приложения на...</small></li></ol>

Related posts brought to you by <a href='http://mitcho.com/code/yarpp/'>Yet Another Related Posts Plugin</a>.]]></description>
			<content:encoded><![CDATA[<p>В <a href="http://www.developers.org.ua/archives/mlk/2008/06/17/python-webdev-no-frameworks-p5/">прошлой статье</a> мы выяснили как может выглядеть независимая компонента, а в этой мы создадим ещу одну, чуть крупнее, и найдем способ связать её с остальным приложением.</p>
<h3>Постановка задачи</h3>
<p>Допустим мы пишем веб-приложение, использующее какую-то JS-библиотеку, в нашем примере <a href="http://developer.yahoo.com/yui/">YUI</a>. Давайте попробуем инкапсулировать логику её подключения к странице.  Для таких задач есть целый ряд «решений» вроде <a href="http://toscawidgets.org/">ToscaWidgets</a>, но толку от них ноль, как это и принято среди всего имеющего в названии слово “widget”.</p>
<p>Фактически YUI может быть <a href="http://developer.yahoo.com/yui/articles/hosting/">размещена на серверах Yahoo</a>, на нашем сервере где-то рядом с приложением или где-то еще, мы будем поддерживать все эти варианты. Также удобно иметь возможность раздавать библиотеку прямо из дистрибутива (yui_x.x.x.zip) не распаковывая.</p>
<p>Приложение будет запрашивать у компоненты HTML код для подключения интересующих её модулей. У модулей есть debug и min версии, поэтому компонента должна учитывать и это. Мы будем местами срезать углы, например, у некоторых компонент в некоторых версиях есть дополнительный суффикс ‘beta’, но мы это будем игнорировать. Учесть это несложно, но в рамках статьи не оправдано. Точно также мы не будем выстраивать правильный порядок включения скриптов, зависимости, подключение CSS файлов их минификацию и прочие детали. В настоящей компоненте это всё следует реализовать – времени это займет минимум, а использовать её станет еще удобней и приятней. И всё же некоторые несущественные детали мы будем учитывать в нашей реализации для того чтобы было видно что это не требует никакой магии – всё отлично решается «в лоб».</p>
<h3>Подготовительные шаги</h3>
<p>Для начала давайте напишем небольшое приложение для тестирования компоненты:</p>
<pre><code>class App(object):
    def __init__(self, yui):
        self.yui = yui

    @webob_wrap
    def __call__(self, req):
        names = [name for name in req.path_info.split(&#39;/&#39;) if name]
        links = self.yui.js_links(names)
        return Response(links, content_type=&#39;text/plain&#39;)

if __name__ == &#39;__main__&#39;:
    from paste.httpserver import serve
    yui = YuiYahooHosted(version=&#39;2.5.2&#39;, debug=True)
    root = App(yui)
    serve(root)</code></pre>
<p>Конечно, для целей тестирования разумно вызывать непосредственно компоненту, но этот пример заодно показывает как она используется в реальном приложении. Для компоновки не требуется никаких трюков или специальных библиотек. На самом деле интересно, кому впервые пришла идея, что так писать недостаточно хорошо, и сколько человеко-часов было угроблено на хитросплетения под девизом «не повторяй себя»?</p>
<p>Итак, если мы захотим посмотреть в браузере как выглядит блок ссылок на скрипты history, animation и json мы откроем http://localhost:8080/history/animation/json итп.</p>
<p>В результате мы увидим результат подобный следующему:</p>
<pre><code>&lt;script type=&quot;text/javascript&quot; src=&quot;http://yui.yahooapis.com/2.5.2/build/history/history-min.js&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot; src=&quot;http://yui.yahooapis.com/2.5.2/build/animation/animation-min.js&quot;&gt;&lt;/script&gt;
&lt;script type=&quot;text/javascript&quot; src=&quot;http://yui.yahooapis.com/2.5.2/build/json/json-min.js&quot;&gt;&lt;/script&gt;</code></pre>
<p>В конечном счете эта строка результат вызова</p>
<pre><code>yui.js_links([&#39;history&#39;, &#39;animation&#39;, &#39;json&#39;])</code></pre>
<h3>Вариант для Yahoo CDN</h3>
<p>Начнем с реализации YuiYahooHosted:</p>
<pre><code>class YuiYahooHosted(object):
    prefix_template = &#39;http://yui.yahooapis.com/%s/build/&#39;
    prefix_cn_template = &#39;http://cn.yui.yahooapis.com/%s/build/&#39;
    js_link_template = &#39;&lt;script type=&quot;text/javascript&quot; src=&quot;%(prefix)s%(name)s/%(name)s%(suffix)s.js&quot;&gt;&lt;/script&gt;&#39;

    def __init__(self, version, debug=False, minified=False, china=False):
        if debug and minified:
            raise ValueError(&quot;Scripts can&#39;t be both minified and debuggable&quot;)

        self.version = version
        self.debug = debug

        self.minified = minified
        self.china = china

        if china:
            self.prefix = self.prefix_cn_template % version
        else:
            self.prefix = self.prefix_template % version

        if debug:
            self.suffix = &#39;-debug&#39;
        elif minified:
            self.suffix = &#39;-min&#39;
        else:
            self.suffix = &#39;&#39;

    def js_links(self, names):
        links = []
        subst = {&#39;prefix&#39;: self.prefix, &#39;suffix&#39;: self.suffix}
        for name in names:
            subst[&#39;name&#39;] = name
            links.append(self.js_link_template % subst)
        return &#39;\n&#39;.join(links)</code></pre>
<p>Трудно придумать что-то более очевидное. Понять что делает компонента очень просто, и это здорово даже если никто кроме вас никогда не будет читать ваш код. Но даже для простого кода документация не помешает.</p>
<pre><code>    &quot;&quot;&quot;
    Create a component that generates links to YUI scripts hosted by Yahoo!.

        Constructor Arguments:

            ``version``     YUI version (as of June 2008 the current version is 2.5.2)
            ``debug``       (False by default) Use debug versions of the scripts
            ``minified``    (False by default) Use minified versions of the scripts
            ``china``       (False by default) if True, generated links will point to China-based CDN

    ``js_links(names)``

        Takes a list of script names (for ex. ['cookie', 'history']) and returns HTML to include in your page.

    &quot;&quot;&quot;</code></pre>
<p>Большая часть этого текста потом будет перенесена в документацию общего интерфейса или суперкласса и именно там от неё будет больше всего толку – когда реализаций несколько, важно понимать что между ними общего и чем они отличаются, для таких целей код не может заменить документации.</p>
<h3>Рефакторинг и выделение общей функциональности</h3>
<p>Легко заметить, что основная часть кода может быть использована для других реализаций, поэтому вынесем её в новый класс:</p>
<pre><code>class YuiLinkGen(object):
    &quot;&quot;&quot;
    Create a component that generates links to YUI scripts.

        Constructor arguments:

            ``prefix``      Specifies prefix for the generated URIs (location of the YUI files)
            ``debug``       (False by default) Use debug versions of the scripts
            ``minify``      (False by default) Use minified versions of the scripts (debug flag takes precedence)
    &quot;&quot;&quot;

    js_link_template = &#39;&lt;script type=&quot;text/javascript&quot; src=&quot;%(prefix)s/%(name)s/%(name)s%(suffix)s.js&quot;&gt;&lt;/script&gt;&#39;

    def __init__(self, prefix, debug=False, minify=False):
        self.debug = debug
        self.minify = minify
        self.prefix = prefix

        if debug:
            self.suffix = &#39;-debug&#39;
        elif minify:
            self.suffix = &#39;-min&#39;
        else:
            self.suffix = &#39;&#39;

    def js_links(self, names):
        &quot;&quot;&quot;
        Takes a list of script names (for ex. [&#39;cookie&#39;, &#39;history&#39;]) and returns HTML to include in your page.
        &quot;&quot;&quot;
        links = []
        subst = {&#39;prefix&#39;: self.prefix, &#39;suffix&#39;: self.suffix}
        for name in names:
            subst[&#39;name&#39;] = name
            links.append(self.js_link_template % subst)
        return &#39;\n&#39;.join(links)</code></pre>
<p>Это не только базовый класс, он также может быть использован непосредственно, но самое интересное конечно будет в подклассах. Для начала посмотрим какой стала реализация для размещения на Yahoo.</p>
<pre><code>class YuiYahoo(YuiLinkGen):
    &quot;&quot;&quot;
    Create a component that generates links to YUI scripts hosted by Yahoo!.

        Contructor arguments:

            ``version``     YUI version (as of June 2008 the current version is 2.5.2)
            ``debug``       same meaning as in YuiLinkGen
            ``minify``      same, but True by default
            ``china``       (False by default) if True, generated links will point to China-based CDN

    &quot;&quot;&quot;
    prefix_template = &#39;http://yui.yahooapis.com/%s/build&#39;
    prefix_cn_template = &#39;http://cn.yui.yahooapis.com/%s/build&#39;

    def __init__(self, version, debug=False, minify=True, china=False):
        if china:
            prefix = self.prefix_cn_template % version
        else:
            prefix = self.prefix_template % version
        self.version = version
        self.china = china
        super(YuiYahoo, self).__init__(prefix=prefix, debug=debug, minify=minify)</code></pre>
<p>Что ж, пока что всё было просто, как насчет того чтобы совместить генерацию ссылок и собственно хостинг скриптов?</p>
<h3>Размещение на собственном сервере</h3>
<p>Поскольку задачи предоставления папки или zip-архива уже решены в Paste, то реализация выйдет на удивление короткой. Соотношение кода к документации, как положено, приближается к 1:1.</p>
<pre><code>class YuiSelfHosted(YuiLinkGen):
    &quot;&quot;&quot;
    This class implements a component that generates links to YUI scripts hosted by ourselves
    and can create the app used to serve the files from directory or distro zipfile.

        Constructors:

            ``from_directory()``

                ``prefix``      same meaning as in YuiLinkGen constructor
                ``dirpath``     filesystem path to &#39;build&#39; directory from YUI distro
                ``debug``       same as in YuiLinkGen
                ``minify``      asks to minify the served files. No -min suffixes are
                                generated and debug versions can be minified too.
                ``gzip``        (True by default) asks to gzip responses if possible.

            ``from_distro_zipfile()``

                Same as ``from_directory()``, but instead of ``dirpath`` argument it takes
                ``filename`` which would be the .zip file containing the YUI distribution to serve
                (as downloaded from YUI website).

        Attributes:

            ``yuiapp``  WSGI app to be mounted at ``prefix``

    &quot;&quot;&quot;

    def __init__(self, prefix, yuiapp, **kw):
        super(YuiSelfHosted, self).__init__(prefix, **kw)
        self.yuiapp = yuiapp

    @classmethod
    def from_distro_zipfile(cls, prefix, filename, **kw):
        from paste.fileapp import ArchiveStore
        app = ArchiveStore(filename)
        @webob_wrap
        def restrict_app(req):
            req.path_info = &#39;/yui/build&#39; + req.path_info
            return req.get_response(app)
        return cls.from_app(prefix, restrict_app, **kw)

   @classmethod
   def from_directory(cls, prefix, dirpath, **kw):
       from paste.urlparser import StaticURLParser
       app = StaticURLParser(dirpath)
       return cls.from_app(prefix, app, **kw)

    @classmethod
    def from_app(cls, prefix, yuiapp, debug=False, minify=False, gzip=True):
        return cls(prefix, cls.wrap_app(yuiapp, minify=minify, gzip=gzip), debug=debug)

    @classmethod
    def wrap_app(cls, app, minify, gzip):
        if minify:
            app = jsminify_middleware(app)
        if gzip:
            from paste.gzipper import middleware as gzip_middleware
            app = gzip_middleware(app)
        return app</code></pre>
<p>Опять, не прибегая ни к каким трюкам, получился качественный код. В этой реализации мы используем написанную в предыдущей статье мидлварь и вместо того чтобы для минификации добавлять суффикс –min к имени файла, мы минифицируем его самостоятельно. Это позволяет минифицировать также отладочные версии скриптов (полезно разве что для отладки скриптов удаленно размещенного приложения, что случается не часто), но главное наша минификация отрезает заголовки с копирайтом которые сохранены в файлах из дистрибутива – каждый байт на счету! А если серьезно, то собственная минификация пригодится на следующем этапе.</p>
<p>Мы также оборачиваем приложения в gzipper, но и это можно отключить передав конструктору gzip=False.</p>
<p>Обратите внимание на restrict_app из from_distro_zipfile, таким образом мы ограничиваем доступ папкой build из дистрибутива и облегчаем себе одну предстоящую задачу (о которой позже).</p>
<p>Собственно в интеграции генератора ссылок и самого WSGI приложения со скриптами нет ничего мудреного, у генератора есть атрибут yuiapp с приложением, которое скрипт конфигурации должен сделать доступным по префиксу указанному в конструкторе.</p>
<h3>Использование</h3>
<p>Поскольку мы отказались от использования специальных систем конфигурации, получившийся код готов к употреблению. App – приложение использующее значение аргумента своего конструктора как генератор ссылок на скрипты. Оно делает вызовы вроде <code>self.yui_linkgen.js_links([…])</code> и потому полностью совместимо со всеми нашими реализациями, никакие их внутренние отличия его не касаются.</p>
<h4>Уже размещенные скрипты</h4>
<pre><code>yui = YuiLinkGen(&#39;http://static.website.com/scripts/yui&#39;, minify=True)
application = App(yui)</code></pre>
<h4>На серверах Yahoo</h4>
<pre><code>yui = YuiYahoo(version=&#39;2.5.2&#39;)
application = App(yui)</code></pre>
<h4>Самостоятельно</h4>
<pre><code>yui = YuiSelfHosted.from_directory(&#39;/_yui&#39;, &#39;/home/web/checkouts/yui/build&#39;, minify=True)</code></pre>
<p>или</p>
<pre><code>yui = YuiSelfHosted.from_distro_zipfile(&#39;/_yui&#39;, &#39;yui_2.5.1.zip&#39;)

root = URLMap()
root[&#39;/_yui&#39;] = yui.yuiapp
root[&#39;/&#39;] = App(yui)

application = root</code></pre>
<h4>или в ходе тестирования:</h4>
<pre><code>serve(root)</code></pre>
<p>Если мы знаем домен по которому будет размещен root, то стоит добавить его к первому аргументу.</p>
<p>Обратите внимание, что однажды создав экземпляр YuiLinkGen, мы можем использовать его многократно, если у нас есть несколько приложений способных использовать такую компоненту разумно передавать им одну и ту же копию. Реализовать это не используя в конфигурации Python было бы затруднительно, к тому же не ясно: чего ради?</p>
<h3>Что-то новенькое</h3>
<p>Если в прошлой статье мы сумели сделать минификацию более удобной, то возможно нам удастся упростить склейку скриптов? Чем больше запросов браузер шлет к серверу тем обычно больше задержка при загрузке страницы, поэтому при переходе в продакшн толково сделанные (читай «не встречающиеся в природе») сайты склеивают свои скрипты в один файл, все CSS-файлы в другой и используют их в таком виде. Это уменьшает количество запросов к серверу, что в свою очередь уменьшает нагрузку на него, делает проверку на изменения гораздо более быстрой (важно при обновлении страницы пользователем), gzip на склеенных скриптах эффективнее чем на раздельных итд итп. Иногда для разных страниц нужно использовать разное подмножество скриптов и тогда у подхода описанного ниже обнаружатся и недостатки, но это особый случай и решать его также нужно отдельно. Для большинства случаев мы получим заметный выигрыш используя следующую стратегию.</p>
<p>Для начала мы сделаем middleware способную склеивать скрипты на лету. Мы хотим чтобы путь вида «/history/history;/json/json.js» был командой к тому чтобы вернуть склеенные /history/history.js и /json/json.js. Мы реализуем это как middleware а не новое приложение склеивающее файлы с диска для того чтобы воспользоваться, среди прочего, предоставлением файлов из архива. К этому, безусловно, нужно добавить кеширование, но в этой статье мы это опустим.</p>
<pre><code>@webob_middleware
def jsjoin_middleware(req, app):
    if req.path_info.endswith(&#39;.js&#39;) and req.method in [&#39;GET&#39;, &#39;HEAD&#39;]:# and not req.query_string
        parts = req.path_info[:-3].split(&#39;;&#39;)
        if len(parts) &gt; 1:
            subresponses = []
            for part in parts:
                subreq = req.copy()
                subreq.remove_conditional_headers()
                subreq.path_info = part + &#39;.js&#39;
                subres = subreq.get_response(app)
                if subres.content_type not in js_mimetypes or subres.status_int != 200:
                    return HTTPNotFound(comment=&quot;%s not found&quot; % subreq.url)
                subresponses.append(subres)
            r = Response(content_type=&#39;text/javascript&#39;, charset=&#39;UTF-8&#39;)
            bodies = []
            for subr in subresponses:
                if subr.charset:
                    bodies.append(subr.unicode_body or &#39;&#39;)
                else:
                    bodies.append(unicode(subr.body or &#39;&#39;))
            r.md5_etag()
            r.last_modified = max([subr.last_modified for subr in subresponses if subr.last_modified] or [None])
            r.conditional_response = True
            return r
    return req.get_response(app)</code></pre>
<p>Для наших нужд можно было бы опустить работу с юникодом и генерацию правильного last-modified, но я привожу эти фрагменты, чтобы не создать ложного впечатления о том насколько всё просто. Всё просто, но всё же нужно быть внимательным к деталям. По уму также можно не генерировать тело ответа целиком, а склеивать его по мере надобности в app_iter. Главным преимуществом этого была бы экономия в случае ответов 304 Not Modified, но, поскольку мы собираемся кешировать ответы, то генерация полного тела ответа – правильный подход.</p>
<h3>Генерация ссылок на склеенные скрипты</h3>
<p>Теперь нужно научить нашу реализацию генерировать ссылки на такие склеенные скрипты.</p>
<pre><code>class YuiMerge(YuiLinkGen):
    js_link_template = &#39;&lt;script type=&quot;text/javascript&quot; src=&quot;%s/%s.js&quot;&gt;&lt;/script&gt;&#39;
    js_part_template = &#39;%(name)s/%(name)s%(suffix)s&#39;

    def js_links(self, names):
        &quot;&quot;&quot;
        Same as YuiLinkGen.js_links but generates a link that will fetch all scripts as one file
        &quot;&quot;&quot;
        parts = []
        subst = {&#39;suffix&#39;: self.suffix}
        for name in names:
            subst[&#39;name&#39;] = name
            parts.append(self.js_part_template % subst)
        return self.js_link_template % (self.prefix, &#39;;/&#39;.join(parts))</code></pre>
<p>Это прямой наследник YuiLinkGen и может использоваться в тех же случаях. Например вместо блока ссылок вначале статьи он вернет </p>
<pre><code>&lt;script type=&quot;text/javascript&quot; src=&quot;/yui/history/history;/animation/animation;/json/json.js&quot;&gt;&lt;/script&gt;</code></pre>
<p>Может ли случиться что нам нужно генерировать такие ссылки не создавая соответствующего приложения для склейки? Это возможно, если такое приложение размещено на другом сервере или процессе. Для более частого случая, когда нам нужны обе части, пригодится следующий класс:</p>
<pre><code>class YuiHostedMerge(YuiMerge, YuiSelfHosted):
    @classmethod
    def wrap_app(cls, app, *args, **kw):
        app = jsjoin_middleware(app)
        return super(YuiHostedMerge, cls).wrap_app(app, *args, **kw)</code></pre>
<p>Обратите внимание на граф наследования, вернитесь к коду и прочитайте его весь еще раз. Заметьте, что YuiJoinHostedLinkGen унаследовал конструкторы from_directory и from_distro_zipfile с соответствующей им семантикой, но добавил поддержку склеивания. Я хочу еще раз подчеркнуть, что если вы пишете код в таком стиле, то вы используете или учитесь использовать те же навыки и архитектурные решения что и при разработке в любой другой области. Если вы умеете применять ООП к месту, если вы видите смысл в документировании кода, если имеете навык деления функциональности на компоненты, то это пригодится в любой области программирования. Нельзя применять особые критерии к веб-разработке: хранение запроса в глобальной переменной – в любом случае извращение, много кода ни о чем – плохой знак, если нет ясных стыков, на которых нужно писать документацию – плохи дела и т.д. Сделать хорошо – можно, но для этого нужно иметь свободу делать как угодно, и для этого WSGI бесценен.</p>
<h3>PS</h3>
<p>В конце прошлой статьи я предлагал написать реализацию декоратора webob_middleware (мы использовали его в этой статье) который превращал бы функцию с сигнатурой (Request, WSGI) → WSGI в конструктор соответствующей мидлвари. Сегодня было написано немало приличного кода, поэтому в качестве передышки предлагаю решение в одну строчку (вообще так писать не надо):</p>
<pre><code>webob_middleware = lambda mw: lambda app: webob_wrap(lambda req: mw(req, app))</code></pre>
<br/><a href="http://www.developers.org.ua/archives/mlk/2008/07/02/python-webdev-no-frameworks-p6/#ratings">Оценить статью на сайте</a> | <a href="http://www.developers.org.ua/archives/mlk/2008/07/02/python-webdev-no-frameworks-p6/#comments">5 комментариев</a>]]></content:encoded>
			<wfw:commentRss>http://www.developers.org.ua/archives/mlk/2008/07/02/python-webdev-no-frameworks-p6/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Python: Веб-разработка без фреймворков (часть 5)</title>
		<link>http://www.developers.org.ua/archives/mlk/2008/06/17/python-webdev-no-frameworks-p5/</link>
		<comments>http://www.developers.org.ua/archives/mlk/2008/06/17/python-webdev-no-frameworks-p5/#comments</comments>
		<pubDate>Tue, 17 Jun 2008 07:58:09 +0000</pubDate>
		<dc:creator>Сергей Щетинин</dc:creator>
		
		<category><![CDATA[Python]]></category>

		<category><![CDATA[Разработка]]></category>

		<category><![CDATA[статьи]]></category>

		<category><![CDATA[JSMin]]></category>

		<category><![CDATA[Middleware]]></category>

		<category><![CDATA[Python: Веб-разработка без фреймворков]]></category>

		<category><![CDATA[WSGI]]></category>

		<category><![CDATA[фреймворк]]></category>

		<guid isPermaLink="false">http://www.developers.org.ua/index.php?p=1347</guid>
		<description><![CDATA[Мы успели рассмотреть значительную часть инструментария, который нам понадобится для написания веб-приложений. Начиная с этой статьи я постараюсь показать как это всё выглядит на практике – как структурируется код, как совмещаются компоненты и т. п. Это не столько инструкция к действию, сколько демонстрация того, что нет необходимости в поддержке со стороны фреймворка. 


Related posts:<ol><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/08/python-webdev-no-frameworks-p1/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 1)'>Python: Веб-разработка без фреймворков (часть 1)</a> <small>Каждый разработчик тщательно выбирает свой инструментарий и чем лучше он...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/10/python-webdev-no-frameworks-p2/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 2)'>Python: Веб-разработка без фреймворков (часть 2)</a> <small>В прошлой части я постарался рассказать о том, что чистый...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/05/20/python-webdev-no-frameworks-p4/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 4)'>Python: Веб-разработка без фреймворков (часть 4)</a> <small>В предыдущих статьях мы разобрались, как можно создавать веб-приложения на...</small></li></ol>

Related posts brought to you by <a href='http://mitcho.com/code/yarpp/'>Yet Another Related Posts Plugin</a>.]]></description>
			<content:encoded><![CDATA[<p>В первых четырех статьях <a href="http://www.developers.org.ua/archives/tag/_python-webdev-no-frameworks/">этой серии</a> мы успели рассмотреть значительную часть инструментария, который нам понадобится для написания веб-приложений. Начиная с этой статьи я постараюсь показать как это всё выглядит на практике – как структурируется код, как совмещаются компоненты и т. п. Это не столько инструкция к действию, сколько демонстрация того, что нет необходимости в поддержке со стороны фреймворка. По мере усложнения вашего проекта код конечно будет меняться, но в каждый момент он будет лучше соответствовать имеющейся задаче чем какие-то заготовленные решения.</p>
<p>Я думаю трудно поспорить с тем, что модульность кода это хорошо, ведь независимость компонент позволяет более полно тестировать каждую из них, облегчает разработку в команде, совершенно очевидно, что рефакторить меньшими блоками гораздо удобнее и т.д. Но писать компонентный код нужно еще уметь.</p>
<h3>Middleware</h3>
<p>Стандарт какого-то интерфейса создает пространство для особого рода компонент – middleware. Это слово часто употребляют в смысле «адаптер» (совместно с которыми мидлварь часто используется), хотя это не совсем верно. Я стараюсь использовать его только в узком смысле – компонента предоставляющая и потребляющая один и тот же интерфейс. Например, load-balancer это middleware, кеширующая прослойка – тоже, а вот преобразователь XML-RPC ↔ SOAP уже под вопросом. В нашем случае middleware будет потреблять и предоставлять, конечно же, WSGI. Таким образом у нас есть возможность придать новые свойства любому существующему WSGI-приложению. Ряд уже готовых mw уже был упомянут в первой статье серии, и их <a href="http://www.wsgi.org/wsgi/Middleware_and_Utilities">еще огромное множество</a>, но мы рассмотрим, как написать такую компоненту с нуля.</p>
<p>Справедливость ради, стоит упомянуть, что некоторые фреймворки также используют middleware. Например, существует <a href="http://www.djangoproject.com/documentation/middleware/">Django middleware</a>, которое, естественно, работает только в своей песочнице и потому для всех остальных бесполезно.</p>
<h3>JSMin</h3>
<p>Начнем c примера попроще. Всем кто использовал в приложениях JavaScript известно, что, для ускорения загрузки, скрипты можно «минифицировать». Для этого есть <a href="http://www.google.com/search?q=jsmin">разные инструменты</a>, так что само по себе это не проблема. Неудобство в том, что иметь отдельные укороченные скрипты очень неудобно – чтобы их содержимое не устаревало нужно добавить специальный шаг в процесс деплоймента, да и может выйти, что какой-то из скриптов был забыт и прочие подобные неприятности. Для большинства случаев можно обойтись меньшей кровью написав мидлварь которая паковала бы скрипты на лету. На машине разработчика использовать её не стоит т.к. может понадобиться полистать используемые скрипты в браузере, но если вы усвоили идеи из <a href="http://www.developers.org.ua/archives/mlk/2008/05/20/python-webdev-no-frameworks-p4/">статьи про деплоймент</a>, то ничего сложного в этом не окажется.</p>
<p>Для упаковки скриптов есть <a href="http://crockford.com/javascript/jsmin.py.txt">готовый модуль</a>, поэтому задача состоит лишь в том, чтобы превратить его в мидлварь. Информации из предыдущих статей более чем достаточно, для того чтобы решить эту задачу, поэтому, прежде чем читать далее, попробуйте решить её самостоятельно.</p>
<p>Решением должна быть такая функция jsminify_middleware чтобы приложение, получающееся в следующем коде, возвращало все JS-скрипты минифицированными. Тут предполагается что ‘yui’ это папка с дистрибутивом <a href="http://developer.yahoo.com/yui/">YUI</a> (как вполне вероятный вариант применения).</p>
<pre><code>from paste.urlparser import StaticURLParser
yui_app = StaticURLParser('yui')
yui_app = jsminify_middleware(yui_app)</code></pre>
<h4>Решение</h4>
<p>Решение может быть например таким:</p>
<pre><code>js_mimetypes = frozenset(['text/javascript', 'application/x-javascript'])

def jsminify_middleware(app):
    @webob_wrap
    def middleware_app(req):
        r = req.get_response(app)
        if r.content_type in js_mimetypes and r.body:
            r.body = jsmin(r.body)
        return r
    return middleware_app</code></pre>
<p>Тут нет ничего нового. Мы просто создали замыкание (middleware_app), которое является WSGI-приложением, которое, вместо того чтобы генерировать ответ самостоятельно, поручает эту задачу обернутому приложению. Затем полученный от него ответ мы проверяем по типу содержимого и если там JS, то обрабатываем тело ответа jsmin.</p>
<p>Обращу внимание на ряд вещей:</p>
<ul>
<li>Если нижележащее приложение поддерживает If-None-Match или If-Modified-Since эта поддержка сохраняется.</li>
<li>Мы проверяем наличие тела ответа, т.к. ответ может иметь Content-Type, но быть пустым, например 304 Not Modified. У таких ответов .body is None.</li>
<li>Мы проверяем наличие тела ответа в последнюю очередь, т.к. доступ к атрибуту .body линеаризует ответ, а в случае, когда мы не собираемся дополнительно его обрабатывать, это привело бы к бессмысленной потере эффективности и трате памяти.</li>
<li>Мидлварь применима к любому приложению и потому может также минифицировать скрипты из архива или даже сгенерированные динамически. Мы также можем обернуть ей приложение написанное с использованием фреймворков – до внутренностей нам нет никакого дела.</li>
</ul>
<h3>Ошибки в реализации</h3>
<p>Но в этом решении есть как минимум две ошибки, ведь мы не учли некоторых возможностей HTTP. Попробуйте самостоятельно понять, в чем они заключаются.</p>
<p>Ошибки такие: мы не учитываем, что ответ может быть закодированным (content-encoding: gzip) или частичным (range). </p>
<h4>Content-Encoding</h4>
<p>Чтобы исправить первое, достаточно добавить r.decode_content(), впрочем, поскольку скрипты неплохо бы еще и сжать при передаче, давайте добавим и это. </p>
<pre><code>        if r.content_type in js_mimetypes and r.body:
            r.decode_content()
            r.body = jsmin(r.body)
            if 'gzip' in req.accept_encoding:
                r.encode_content()</code></pre>
<p>Как вариант последние две строки можно записать так:</p>
<pre><code>r.encode_content(req.accept_encoding.best_match([&#39;gzip&#39;, &#39;identity&#39;], &#39;identity&#39;))</code></pre>
<p>Эти варианты не идентичны. В том случае если user agent прислал заголовок <code>Accept-Encoding: gzip;q=0.5,identity;q=1</code> первый вариант запакует ответ, а второй прислушается к пожеланиям агента и запаковывать не станет. Конечно, если мы очень дорожим своим каналом, то у нас тоже может быть предпочтение по-поводу того в каком виде отдавать данные. В таком случае мы можем записать всё ту же строку следующим образом:</p>
<pre><code>r.encode_content(req.accept_encoding.best_match([(&#39;gzip&#39;, 1), (&#39;identity&#39;, 0.2)], &#39;identity&#39;))</code></pre>
<p>Таким образом мы указываем что отдавать скрипты в сжатом виде для нас предпочтительнее в пять раз. В таком случае на тот же запрос данные всё же будут запакованы, т.к. 1*0.5 &gt; 0.2*1. Но мы всё же оставляем клиенту шанс указать что незапакованные данные предпочтительнее если в запросе будет достаточно низкий q для gzip.</p>
<p>Безусловно, в данном случае, это всё изыски имеющие мало общего с реально стоящими задачами, но случаи бывают разные и возможность работать с HTTP-стандартом как положено порой оказывается очень ценной.</p>
<h4>Range</h4>
<p>Исправить ошибку с частичным ответом (Range) можно несколькими путями. Вполне приемлемый вариант оставить всё как есть – вряд ли когда либо случится так, что скрипт будут скачивать по частям. </p>
<p>Можно добавить условие “and not r.range” чтобы не обрабатывать такие ответы дополнительно. В таком случае разумно при упаковке изменять Etag ответа, чтобы при корректном (с If-Range) запросе на частичное содержимое нижележащее приложение замечало бы разницу с ожидаемым Etag и отдавало цельный ответ. Это позволит избежать маловероятной ситуации когда клиент начал скачивать скрипт в минифицированом виде, но из-за обрыва соединения вынужден был попробовать снова и послал запрос на недостающую часть. В таком случае помимо Range он должен бы указать заголовок If-Range со значением Etag из первого ответа. Если Etag пакованного и оригинального ответа не отличаются то в результате клиент получит нерабочего «мутанта» склеенного из несовместимых частей. Изменение Etag спасает ситуацию, но к сожалению такой подход не поможет если в If-Range будет указан не Etag а Last-Modified оригинального ответа, что тоже допустимо по стандарту.</p>
<p>Поэтому самый надежный способ – удалить Range и If-Range из запроса до того как передавать управление нижележащему приложению (req.range = None). Но из-за этого перестанут работать все запросы на частичное содержимое и потому такую мидлварь нужно будет применять осторожнее, чтобы ненароком не обернуть ей, например, какой-то большой файл для скачки.</p>
<h4>Nuke the entire site from orbit. It&#8217;s the only way to be sure.</h4>
<p>Для таких случаев также предусмотрен метод <code>Request.remove_conditional_headers</code> по умолчанию удаляющий из запроса не только Range но и Accept-Encoding (исправляя заодно нашу первую ошибку), If-None-Match и If-Modified-Since гарантируя таким образом что ответ будет полным и без Content-Encoding. Для нашей задачи это перебор, но знать о его существовании стоит.</p>
<h3>Домашнее задание</h3>
<p>В следующей статье мы будем развивать затронутые здесь идеи, а пока что пара задачек для читателей. Это конечно не <a href="http://www.developers.org.ua/archives/a4/2008/06/02/hard-interview-questions-1/">про войну гномов и магов</a>, но тоже задачи жизненные.</p>
<ul>
<li>Минификация и gzip архивация занимают время, развейте <a href="http://pastebin.com/f1124d595">имеющийся код</a> так, чтобы обработанные ответы кешировались, в том числе пакованые gzip. Укажите какие сделаны предположения и для оборачивания каких приложений это не подойдет. Тут есть целый ряд альтернативных подходов и правильных ответов тоже множество.</li>
<li>Создайте декоратор для удобства написания простых middleware, предполагаемое использование такое:
<pre><code>@webob_middleware
def jsminify_middleware(req, app):
    r = req.get_response(app)
    #...
    return r</code><code></code></pre>
</li>
</ul>
<p>Ответы можно давать в виде ссылок на ваш вариант на <a href="http://pastebin.com/f1124d595">pastebin</a>.</p>
<br/><a href="http://www.developers.org.ua/archives/mlk/2008/06/17/python-webdev-no-frameworks-p5/#ratings">Оценить статью на сайте</a> | <a href="http://www.developers.org.ua/archives/mlk/2008/06/17/python-webdev-no-frameworks-p5/#comments">4 комментариев</a>]]></content:encoded>
			<wfw:commentRss>http://www.developers.org.ua/archives/mlk/2008/06/17/python-webdev-no-frameworks-p5/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Python: Веб-разработка без фреймворков (ответ на критику)</title>
		<link>http://www.developers.org.ua/archives/mlk/2008/05/29/python-webdev-no-frameworks-response/</link>
		<comments>http://www.developers.org.ua/archives/mlk/2008/05/29/python-webdev-no-frameworks-response/#comments</comments>
		<pubDate>Thu, 29 May 2008 06:29:29 +0000</pubDate>
		<dc:creator>Сергей Щетинин</dc:creator>
		
		<category><![CDATA[Python]]></category>

		<category><![CDATA[Разработка]]></category>

		<category><![CDATA[статьи]]></category>

		<category><![CDATA[Django]]></category>

		<category><![CDATA[Python: Веб-разработка без фреймворков]]></category>

		<category><![CDATA[WebOb]]></category>

		<category><![CDATA[WSGI]]></category>

		<category><![CDATA[фреймворк]]></category>

		<guid isPermaLink="false">http://www.developers.org.ua/index.php?p=1310</guid>
		<description><![CDATA[Почти месяц назад в сети появился отзыв на эту серию статей, причем отзыв строго негативный. Я не придал этому значения, так как критика, на мой взгляд, оказалась направлена не на статьи, а на то, что автор в них увидел. Так что, я думаю, нужно всё-таки ответить.


Related posts:<ol><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/10/python-webdev-no-frameworks-p2/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 2)'>Python: Веб-разработка без фреймворков (часть 2)</a> <small>В прошлой части я постарался рассказать о том, что чистый...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/08/python-webdev-no-frameworks-p1/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 1)'>Python: Веб-разработка без фреймворков (часть 1)</a> <small>Каждый разработчик тщательно выбирает свой инструментарий и чем лучше он...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/14/python-webdev-no-frameworks-p3/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 3)'>Python: Веб-разработка без фреймворков (часть 3)</a> <small>Прочитав предыдущие статьи читатель, надеюсь, убедился, что средств PythonPaste и...</small></li></ol>

Related posts brought to you by <a href='http://mitcho.com/code/yarpp/'>Yet Another Related Posts Plugin</a>.]]></description>
			<content:encoded><![CDATA[<p>Почти месяц назад в сети появился <a href="http://softwaremaniacs.org/blog/2008/04/21/wsgi-framework/">отзыв</a> на эту серию статей, причем отзыв строго негативный. Я не придал этому значения, так как критика, на мой взгляд, оказалась направлена не на статьи, а на то, что автор в них увидел. Но, как выяснилось,  мало кто расценил это так же. Дошло до того, что один мой знакомый, устраиваясь на работу, упомянул разработку напрямую для WSGI, на что ему незамедлительно подсунули тот отзыв как бесспорный аргумент против такого подхода. Так что, я думаю, нужно всё-таки ответить.</p>
<blockquote><p>Комментарий Ивана Салагаева: Тут я хочу сказать, что от таких эффектов все равно нельзя защититься. Большинство людей любят не думать, что от чего зависит, а сразу ищут однозначных ответов. И уж тем более, если их пишет человек с авторитетом (который у меня, уж как получилось, есть). Моя статья, хоть и эмоционально написана, отвечает, как ты сам заметил, не конкретно на твой обзор WebOb, а использует его как повод для развития моей собственной точки зрения, которую я постарался обрисовать в начале. (WebOb важная часть, но статьи не только про него – примечание СЩ).</p></blockquote>
<p>Недоразумения можно было бы избежать, если бы Иван прежде чем писать отзыв уточнил правильно ли он понял к чему я веду. Чтобы не повторить эту ошибку со своей стороны, я, как вы уже заметили, попросил его прокомментировать эту статью до её публикации.</p>
<p>Начать стоит, пожалуй, с того, что я не агитирую никого бросать их любимый фреймворк, не призываю писать свой собственный, я вообще ничего не предлагаю и не требую. Статьи описывают библиотеки, которыми пользуюсь я сам и приводят примеры как выглядит в результате код, не более того. Статьи не предназначены для новичков, для того чтобы эффективно писать код в том стиле, как делаю это я, необходимо хорошо понимать как работают все задействованные уровни включая HTTP протокол, сам стандарт WSGI, диспатчинг, шаблоны, используемый ORM и сама база данных, объяснить всё это я могу лишь отчасти. Пожалуй нелишним будет опыт разработки на нескольких существующих web-фреймворках. Я начал программировать для веб на Python шесть лет назад, и хотя были перерывы, имею солидный опыт использования самых разных фреймворков, поэтому не надо думать, что я пишу код таким способом из-за того что страшусь использовать существующие. </p>
<h3>Велосипеды</h3>
<p>Насколько я понимаю, основная претензия к описываемому подходу состоит в том, что якобы предлагается заново реализовывать тот самый код, который делает фреймворки раздутыми, либо же, что рано или поздно это придется сделать. На самом деле это не так.</p>
<p>Для того чтобы внести ясность попробую объяснить различие между фреймворком и библиотеками. Framework в переводе с английского это буквально каркас, т.е. ваш код является вторичным по отношению к библиотечному коду. Не вы выбираете где и что использовать, а расширяете и переопределяете уже имеющуюся функциональность. Более четким критерием отличия фреймворка будет повсеместное использования инверсии контроля, будь это декларативный код или callbacks<sup><a id="footnote1-link" href="#footnote1">[1]</a></sup>.</p>
<p>Я исхожу из того, что этого нужно избегать насколько только возможно. Это вопрос личного предпочтения – если вам нравится писать код в колбеках, если это вам не мешает, то, вероятно, у нас совершенно разные стили программирования и мой опыт вам ни к чему. Если же вы улавливаете преимущества более свободного структурирования кода, то, возможно, программировать при помощи библиотек окажется удобней и продуктивней<sup><a id="footnote2-link" href="#footnote2">[2]</a></sup>.</p>
<p>Таким образом, надеюсь, понятно, что я не считаю нужным переписывать уже реализованный функционал, но точно также я считаю неуместным раскорячивать свой код для того чтобы подстроиться под фреймворк. Цель этих статей – показать что в этом просто нет необходимости.</p>
<p>Если верить предположению Ивана, то у меня должен был бы образоваться собственный фреймворк, однако этого не произошло – помимо сторонних библиотек проекты повторно используют минимум кода, от силы наберется пару килобайт. Весь остальной код в проектах выполняет задачи специфичные для именно для них. Трудно поверить, но на самом деле меньше стало не только используемых библиотек, но и самого кода.</p>
<p>Также Иван предположил, что такой подход продиктован «чистотой» WSGI, но ничто не может быть дальше от правды. Как раз поиском решений для всего и сразу страдают фреймворки. Они часто исходят из какой-то идеологии (например MVC). Очень часто пытаются спрятать сложность веб-приложения за избранными абстракциями итд итп. Благодаря же WebOb получается укротить эту сложность, оставив притом всю доступную мощь на кончиках пальцев. Что приводит нас к следующему пункту.</p>
<h3>«… в качестве клея компонентов&#8230; WSGI! Нет, серьезно – WSGI!!!»</h3>
<p>Объясню, почему в статьях всё крутится вокруг WSGI. Поскольку я стараюсь максимально использовать существующие библиотеки, поскольку я стараюсь не терять возможностей на разных уровнях приложения, то я привязываюсь к WSGI. Если мы хотим использовать разные библиотеки совместно мы вынуждены использовать WSGI. Посмотрите на URLMap, посмотрите на различные middleware, без предоставления и использования WSGI они были бы невозможны. Из-за того что фреймворки идут другим путем и выбирают другую, предположительно лучшую, модель представления запросов и ответов именно для них так часто приходится изобретать велосипед, ведь нужно вписаться в идеологию фреймворка. В таких случаях обычно говорят о важности интеграции родных компонент и прочую лабуду, однако правда гораздо проще – просто приходится делать заново или что-то перекручивать<sup><a id="footnote3-link" href="#footnote3">[3]</a></sup>.</p>
<p>Я знаю <a href="http://dirtsimple.org/2007/02/wsgi-middleware-considered-harmful.html">мнение PJE</a> о том, что middleware злоупотребляют и я с ним полностью согласен. Ядро его недовольства он сформулировал так: «If your application requires that API to be present, then <strong>it&#8217;s not middleware any more!</strong>». Совершенно верно, однако есть ряд случаев когда middleware единственно верное решение, например <a href="http://pythonpaste.org/module-paste.gzipper.html">paste.gzipper</a>. Это те случаи когда оборачиваемые приложения не знают и не должны знать что они обернуты, к такому использованию прослоек претензий быть не может.</p>
<p>Не нужно думать что такие рассуждения идут вразрез с мейнстримом в веб-разработке. Дело лишь в том, что и распространенность поддержки WSGI и, скажем, выпуск WebOb произошли относительно недавно. Поэтому неизбежная инерция продолжает двигать разработку фреймворков – уже есть программисты с такими навыками, уже есть туча проектов требующих поддержки, кроме того всегда будет приток новичков для которых необходимо ограничивать сложность разработки (но ценой доступной мощности), также никуда не исчезнут желающие сделать лучше чем все предшественники. Также обратите внимание на возраст разных фреймворков, большинство уходит корнями в такую древность, что никаких WSGI, никаких вспомогательных библиотек просто не было. Более новые библиотеки зачастую были созданы с учетом ошибок и находок из фреймворков, с использованием современных средств языка и нет ничего странного что использовать их порой оказывается удобней.</p>
<p>Фреймворки были, есть и будут, это совершенно нормально. Более того, если вам они нравятся – пользуйтесь, если вы новичок – пользуйтесь итд. Моя задача лишь поделиться тем, что есть жизнь в веб-разработке и без них. На моем опыте выходит, что фреймворки неудобней на порядок. Пробовать применять мои рекомендации на опыте или нет в любом случае ваш выбор, но не надо спешить заочно утверждать что это ущербный подход.</p>
<h3>Диспатчинг</h3>
<p>Мне, без дураков, больше нравится писать серию if или, скажем return self.appmap.get(req.path_info_pop()). Если ваш пуризм (или менеджер проекта) требует чтобы это было сделано иначе – делайте, для этого, <a href="http://www.wsgi.org/wsgi/Middleware_and_Utilities">среди прочего</a>, есть например <a href="http://routes.groovie.org/">Routes</a>. Не нужно ни писать заново, ни платить цену использования фреймворка.</p>
<p>Вообще, многих программистов похоже пугает писать код, как только они пишут код приложения, они думают «О Боже! Почему этого не делает за меня фреймворк!?». Конечно, размышляя таким образом, получится, что диспатчинг вручную – зло, и срочно надо писать свою библиотеку или хвататься за готовую. Если же не спешить, и не побояться сделать самому решение именно имеющегося вопроса (подчеркиваю – не универсальное решение), то может оказаться, что паника была преждевременной, и кода вышло меньше, код вышел понятней, в будущем менять его проще. Так выходит у меня. Извините.</p>
<h3>Остальное</h3>
<p><em>«Что произойдет, если клиент пришлет невалидную utf-8 строку?»</em> (см. <a href="http://www.developers.org.ua/archives/mlk/2008/04/14/python-webdev-no-frameworks-p3/">третью статью</a> в серии). Произойдет то что и должно произойти – клиент получит ошибку (нет, ничего не упадет, снова извините)<sup><a id="footnote4-link" href="#footnote4">[4]</a></sup>. Какие такие «ваши данные» выжмет Django из невалидной строки я не знаю, но мне такие и не надо. Может вам нравится иначе, но это, по крайней мере, соответствует <a href="http://www.python.org/dev/peps/pep-0020/">PyZen</a> и здравому смыслу.</p>
<p>Кстати, если посмотреть на <a href="http://www.python.org/dev/peps/pep-0020/">The Zen of Python</a> в целом (или его <a href="http://ru.wikipedia.org/wiki/Python#.D0.A4.D0.B8.D0.BB.D0.BE.D1.81.D0.BE.D1.84.D0.B8.D1.8F">русский перевод</a>), то почти по каждому пункту веб-фреймворки идут против него, такое моё мнение. Это не аргумент против, просто наблюдение<sup><a id="footnote5-link" href="#footnote5">[5]</a></sup>.</p>
<hr />
<ol>
<li id="footnote1"><a href="#footnote1-link">^</a> ИС: Кстати&#8230; Джанго, как немногие знают, очень близко подходит к этой границе: в нем очень много просто библиотечного кода, который не является каркасом. Да, Джанго, несомненно фреймворк, потому что предоставляет архитектуру обработки запроса с разбором url&#8217;ов и вызовом пользовательской view. Но это, пожалуй, и все. Шаблоны, обработка форм, ORM &#8212; это фактически внешние библиотеки, которые пользователь зовет сам, и тогда, когда ему надо. Единственное, чем тут помогает Джанго &#8212; примерами в документации.<br />
Тот же Pylons в этом смысле существенно глубже находится в зоне &#8220;фреймворк&#8221;.</li>
<li id="footnote2"><a href="#footnote2-link">^</a> Мне действительно нравятся те места, где Джанго строит &#8220;собственный путь&#8221; по той простой причине, что он совпадает с тем, что сделал бы я сам. А это значит, что он экономит мне время.<br />
Но в целом, я полностью согласен с тем, что это вопрос личного стиля.</li>
<li id="footnote3"><a href="#footnote3-link">^</a> А я тогда объясню, почему я так резко реагирую :-). Дело в том, что я удивительно часто встречаюсь с точкой зрения, что WSGI &#8212; это такая великая магия, и если на сайте фреймворка на первой странице написано это слово, то фреймворк автоматически считается дико гибким и мощным. Моя цель &#8212; объяснить, что WSGI &#8212; это простой протокол, который очень удобно использовать в качестве базы, но который сам по себе никакой архитектуры не строит. В WSGI нет, например, понятия каскада приложений. Его так использует тот же Paste, но это не само по себе происходит.<br />
И снова, это я отвечаю не на то, что ты пишешь, а разъясняю, что имел в виду я сам. (С последним полностью согласен – СЩ)</li>
<li id="footnote4"><a href="#footnote4-link">^</a> Тут под &#8220;упадет&#8221; я имел в виду не то, что приложение остановится на сервере. Я имел в виду как раз 500 ошибку. Это, я так понимаю, просто разница в жаргоне. (Насколько я понял, просто был выбран неудачный пример для критики – СЩ)</li>
<li id="footnote5"><a href="#footnote5-link">^</a> О, вот это точно флеймообразующий абзац :-). 9-й пункт в Дзен (&#8221;Although practicality beats purity&#8221;) дает возможность трактовать все что угодно как угодно :-). Поэтому я обычно на Дзен ссылаюсь только в шуточной манере. (А как же остальные пункты! – СЩ)</li>
</ol>
<br/><a href="http://www.developers.org.ua/archives/mlk/2008/05/29/python-webdev-no-frameworks-response/#ratings">Оценить статью на сайте</a> | <a href="http://www.developers.org.ua/archives/mlk/2008/05/29/python-webdev-no-frameworks-response/#comments">34 комментариев</a>]]></content:encoded>
			<wfw:commentRss>http://www.developers.org.ua/archives/mlk/2008/05/29/python-webdev-no-frameworks-response/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Python: Веб-разработка без фреймворков (часть 4)</title>
		<link>http://www.developers.org.ua/archives/mlk/2008/05/20/python-webdev-no-frameworks-p4/</link>
		<comments>http://www.developers.org.ua/archives/mlk/2008/05/20/python-webdev-no-frameworks-p4/#comments</comments>
		<pubDate>Tue, 20 May 2008 05:15:41 +0000</pubDate>
		<dc:creator>Сергей Щетинин</dc:creator>
		
		<category><![CDATA[Python]]></category>

		<category><![CDATA[Разработка]]></category>

		<category><![CDATA[статьи]]></category>

		<category><![CDATA[Python: Веб-разработка без фреймворков]]></category>

		<category><![CDATA[WSGI]]></category>

		<category><![CDATA[фреймворк]]></category>

		<guid isPermaLink="false">http://www.developers.org.ua/index.php?p=1281</guid>
		<description><![CDATA[В предыдущих статьях мы разобрались, как можно создавать веб-приложения на Python используя лишь необходимые средства. Следующим этапом будет развертывание приложения на сервере и связанная с этим задача конфигурации его компонент (deployment).


Related posts:<ol><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/08/python-webdev-no-frameworks-p1/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 1)'>Python: Веб-разработка без фреймворков (часть 1)</a> <small>Каждый разработчик тщательно выбирает свой инструментарий и чем лучше он...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/10/python-webdev-no-frameworks-p2/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 2)'>Python: Веб-разработка без фреймворков (часть 2)</a> <small>В прошлой части я постарался рассказать о том, что чистый...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/05/29/python-webdev-no-frameworks-response/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (ответ на критику)'>Python: Веб-разработка без фреймворков (ответ на критику)</a> <small>Почти месяц назад в сети появился отзыв на эту серию...</small></li></ol>

Related posts brought to you by <a href='http://mitcho.com/code/yarpp/'>Yet Another Related Posts Plugin</a>.]]></description>
			<content:encoded><![CDATA[<p>В <a href="http://www.developers.org.ua/archives/mlk/2008/04/08/python-webdev-no-frameworks-p1/">предыдущих</a> <a href="http://www.developers.org.ua/archives/mlk/2008/04/10/python-webdev-no-frameworks-p2/">статьях</a> мы <a href="http://www.developers.org.ua/archives/mlk/2008/04/14/python-webdev-no-frameworks-p3/">разобрались</a>, как можно создавать веб-приложения на Python используя лишь необходимые средства. Следующим этапом будет развертывание приложения на сервере и связанная с этим задача конфигурации его компонент (deployment).</p>
<p>Сама задача <a href="http://www.developers.org.ua/archives/mlk/2008/04/08/python-webdev-no-frameworks-p1/">WSGI стандарта</a> – установить интерфейс, через который HTTP сервер будет общаться с веб-приложением, так что не приходится беспокоиться поддерживается ли нравящийся нам вариант связки или сервер. Выбор большой:</p>
<ul>
<li>Реализация HTTP сервера непосредственно на Python
<ul>
<li><a href="http://www.python.org/doc/lib/module-wsgiref.simpleserver.html">wsgiref</a> (однопотоковый)</li>
<li><a href="http://pythonpaste.org/module-paste.httpserver.html">paste.httpserver</a></li>
<li><a href="http://www.cherrypy.org/browser/trunk/cherrypy/wsgiserver/__init__.py">cherrypy.wsgiserver</a></li>
<li><a href="http://twistedmatrix.com/trac/browser/trunk/twisted/web2/wsgi.py#L29">Twisted</a> (<a href="http://unpythonic.blogspot.com/2007/07/running-wsgi-application-inside-twisted.html">пример использования</a>)</li>
<li>В дополнение нередко применяются <a href="http://www.google.com/search?q=reverse+proxy">reverse proxy</a> (Apache, lighttpd, Squid)</li>
</ul>
</li>
<li><a href="http://trac.saddi.com/flup/wiki/FlupServers">CGI</a></li>
<li><a href="http://trac.saddi.com/flup/wiki/FlupServers">FastCGI</a></li>
<li><a href="http://trac.saddi.com/flup/wiki/FlupServers">SCGI</a></li>
<li><a href="http://trac.saddi.com/flup/wiki/FlupServers">AJP</a></li>
<li><a href="http://modpython.org/FAQ/faqw.py?req=show&#038;file=faq03.029.htp">Apache + mod_python</a></li>
<li><a href="http://www.modwsgi.org/">Apache + mod_wsgi</a></li>
</ul>
<p>В зависимости от вашего выбора связка выполняется или просто или очень просто, так что выбирать следует исходя из того в каком окружении будет работать ваше приложение и с каким сервером вы наиболее знакомы. Я перепробовал много вариантов, но когда появился mod_wsgi опробовал и перешел на его использование во всех случаях когда только возможно. </p>
<h3><a href="http://www.modwsgi.org/">mod_wsgi</a></h3>
<p>Как ясно из названия, это модуль Apache написанный специально для связки непосредственно с WSGI приложениями. Я думаю для многих важно, что в нем есть базовая поддержка хостинга пользовательских скриптов в shared-hosting окружениях. На самом деле одной из ведущих мотиваций при написании этого замечательного модуля было сделать хостинг веб-приложений на Python доступнее, а сами приложения в таких условиях быстрее. mod_wsgi показал себя отлично в реальных условиях, работает стабильно, быстро и безошибочно, потому можно рассчитывать, что он получит распространение среди хостеров. Также он <a href="http://packages.debian.org/unstable/python/libapache2-mod-wsgi">на пути к включению</a> в основные репозитории Debian.</p>
<p>Приблизительная модель работы этого модуля такова:</p>
<ul>
<li>Есть один или более именованных пулов python-процессов обрабатывающих запросы. Обратите внимание, что, среди прочего, благодаря этому снимаются какие-либо вопросы к поддержке параллельных вычислений в Python</li>
<li>Конфигурация сервера позволяет определять политику наполнения пулов процессами (минимальное и максимальное их количество, количество потоков в каждом, таймауты и прочее)</li>
<li>У каждого пула может быть свой python-path. Таким образом, <a href="http://code.google.com/p/modwsgi/wiki/VirtualEnvironments">можно создавать независимые окружения</a> (при помощи <a href="http://pypi.python.org/pypi/virtualenv">virtualenv</a>) не привязанные к версиям библиотек установленных на системе глобально или в других окружениях. Такие окружения также могут принадлежать непривилигерованым пользователям</li>
<li>Есть довольно гибкий механизм для определения какой группой процессов будет обработан тот или иной запрос. На это могут влиять довольно высокоуровневые переменные вроде имени пользователя  (при условии, что аутентификация была проведена самим Apache)</li>
<li>Для крайне нагруженных серверов можно использовать менее гибкий, зато еще более быстрый вариант связки – встраивание интерпретатора и исполнение приложения непосредственно в процессе Apache.</li>
</ul>
<p>Каким же именно образом в конфигурации сервера указывается WSGI приложения и их конфигурация? Пожалуй <a href="http://code.google.com/p/modwsgi/wiki/ConfigurationGuidelines">самым простым из возможных</a> – указанием Python-скрипта по исполнению которого в его пространстве имен окажется переменная application значение которой и будет использовано как WSGI-приложение. Скрипт может иметь любое имя, но принято давать таким файлам расширение .wsgi. Как правило, такой скрипт будет импортировать все необходимые компоненты и конфигурировать их для данного размещения. Может показаться, что это какой-то недостаточно мощный или непродуманный подход, но опыт показывает, что это оптимальное решение. Для того чтобы стало понятней почему я так считаю, давайте кратко рассмотрим альтернативный подход.</p>
<h3>PasteDeploy</h3>
<p><a href="http://pythonpaste.org/deploy/">PasteDeploy</a> – это, в первую очередь, средство конфигурации и компоновки WSGI приложений, а также выбора и конфигурации связки с веб-сервером. Предназначено оно, среди прочего, для конечного пользователя / администратора не обязательно знакомого с Python, поэтому конфигурация хранится в .ini файлах. Эту конфигурацию можно запускать, мониторить и в целом использовать как UNIX-демон благодаря команде <a href="http://pythonpaste.org/script/#running-the-server">paster</a> из <a href="http://pythonpaste.org/script/">PasteScript</a>. На первый взгляд это хорошее решение, но практическое его использование раскрывает ряд недостатков.</p>
<p>Я не буду вдаваться в подробности его применения, так как моя цель раскритиковать такой подход, а не научить им пользоваться. Вкратце конфигурация содержит отдельные секции для каждого используемого приложения, фильтра (middleware) и сервера. Используемые приложения указываются в виде URI в специальных схемах. Наиболее используемая <a href="http://pythonpaste.org/deploy/#egg-uris">URI-схема egg</a>: указывает на <a href="http://peak.telecommunity.com/DevCenter/setuptools#entry-points">setuptools entry point</a>, который является заводом (<a href="http://c2.com/cgi/wiki?AbstractFactoryPattern">factory</a>) по производству искомых приложений <a href="http://pythonpaste.org/deploy/#defining-factories">из передаваемой конфигурации</a>. Также есть схема для использования других конфигурационных файлов (<a href="http://pythonpaste.org/deploy/#config-uris">config:</a>). </p>
<p>Я не буду перечислять преимущества системы, сосредоточившись на недостатках, потому что они не так очевидны без опыта использования.</p>
<ul>
<li>Для того чтобы сделать свое приложение доступным  необходимо:
<ul>
<li>определить функцию или класс производящие приложения из конфигурации. Тут открываются разные детали:
<ul>
<li>предусмотренных переменных конфигурации всегда оказывается мало и со временем factory нужно подправлять;</li>
<li>конфигурация это всегда словарь где и ключи и значения – строки, таким образом возникают неопределенности если вам нужны юникод-значения;</li>
<li>если в конфигурации неплохо бы применить список или еще какое значение типом посложнее, то придется изобретать кодирование такого значения для ini формата;</li>
</ul>
</li>
<li>затем нужно создать entry point в setup.py для вашего пакета,</li>
<li>задокументировать это,</li>
<li>сослаться на него в конфигурации.</li>
</ul>
</li>
<li>Для использования приложения надо знать имя его entry point, как именно его конфигурировать итп. Таким образом возникает необходимость в еще одном наборе документации к приложению. Впрочем, чаще всё равно приходится читать исходники.</li>
<li>Также есть сложности с повторным использованием блоков конфигурации, например, если нужно использовать одну и ту же базу данных в различных приложениях. Минимальная поддержка есть, но нельзя скажем параметризовать включаемый конфигурационный файл. Также, например, не смотря на то, что мы можем использовать одну и ту же БД в разных приложениях, они будут использовать раздельные пулы соединений.</li>
</ul>
<p>Хотя часть этих проблем может быть решена конфигурацией в другом виде, например XML, большая часть проблем неизбежна при использовании отдельного языка для конфигурации приложений. Если же для конфигурации использовать Python-скрипты, то все упомянутые проблемы решаются сами собой – повторное использование параметризованных блоков конфигурации превращается ни во что иное как определение и вызовы функций. Не нужны никакие уловки для построения более сложных структур данных. Использование точек входа становится необязательным. И именно так работают wsgi-скрипты для mod_wsgi. Такое решение, кажущееся на первый взгляд кустарным, оказывается гораздо более мощным и удобным в использовании, чем любая возможная альтернатива. Мы теряем возможности PasteScript по управлению демонами сервера приложений, но это нас не беспокоит, так как теперь они управляются совместно с Apache, для их перезапуска применяется привычное sudo service apache2 force-reload. </p>
<p>Что до конфигурации приложений непрограммистами, то править файл с синтаксисом Python ничуть не сложнее чем аналогичный INI.</p>
<h3>Запуск WSGI-скриптов без Apache</h3>
<p>Что ж, с развертыванием на сервере более-менее ясно, а как быть с разработкой? Будем поднимать локальный сервер, настраивать его, перезапускать всякий раз когда вносим в код  изменения? Я считаю что не стоит, и потому привожу свой модуль для исполнения wsgi-скриптов (это сокращенный, но рабочий вариант):</p>
<pre><code>import sys
import os
from time import sleep
from traceback import print_exc

from paste.httpserver import serve
from paste.evalexception import EvalException
from paste.debug.prints import PrintDebugMiddleware
from paste import reloader

def run_script(script):
    if not os.path.isfile(script):
        print script, "does not exist"
        sys.exit(1)
    reloader.install()
    reloader.watch_file(script)

    script_locals = {}
    execfile(script, {&#39;__file__&#39;: script}, script_locals)
    app = script_locals[&#39;application&#39;]
    app = EvalException(app)
    app = PrintDebugMiddleware(app)
    serve(app)

if __name__ == &#39;__main__&#39;:
    try:
        run_script(sys.argv[0])
    except SystemExit, exc:
        raise exc
    except:
        print_exc()
        print &#39;-&#39; * 20, &#39;Restarting in 5 secs..&#39;, &#39;-&#39; * 20
        sleep(5)
        sys.exit(3)</code></pre>
<p>Сохраним этот файл как run_wsgiscript.py там, где он будет доступен импортированию. Лучше оформить его как пакет, но самым простым вариантом будет разместить его в site-packages.</p>
<p>Такой скрипт загружает WSGI приложение образом аналогичным mod_wsgi, но в дополнение он оборачивает его парой отладочных middleware и запускает HTTP-сервер (по умолчанию на 8080-м порту). Также, благодаря paste.reloader, раз в секунду будет проверяться наличие изменений в самом скрипте и загруженных им модулях. Если такие изменения появятся, то скрипт выйдет с кодом ошибки 3; подразумевается, что внешний, вызывающий его скрипт воспринимает это как сигнал к перезапуску, о чем чуть дальше. Если при загрузке скрипта произошла ошибка (обычно это происходит если во внесенных изменениях есть синтаксическая ошибка), то на консоль будет выведен трейсбек и наш модуль выдержит пятисекундную паузу перед перезапуском (чтобы дать спокойно прочитать где была ошибка).</p>
<p>Последняя недостающая часть нашей системы – это внешний скрипт который перезапукает наш модуль. Я разрабатываю под Windows и пользуюсь таким (файл run-wsgi.bat):</p>
<pre><code>@echo off
:repeat
    python -m run_wsgiscript %1
if %errorlevel% == 3 goto repeat</code></pre>
<p>Если всё еще сохранилась неясность почему и как это сделано, просто посмотрите <a href="http://pythonpaste.org/module-paste.reloader.html">краткую документацию paste.reloader</a> – мы просто применили эту систему к загрузке wsgi скриптов.</p>
<p>Если кто захочет оформить это всё в виде пакета&nbsp;– сообщите, я тоже поучаствую.</p>
<h3>SciTE</h3>
<p>Тем, кто, как и я, использует в работе <a href="http://www.scintilla.org/SciTE.html">SciTE</a>, будет удобно добавить в его конфигурацию следующие строки, которые добавят wsgi -скриптам корректную подсветку и позволят запускать их по F5:</p>
<pre><code>file.patterns.py=*.py;*.pyw;*.wsgi
command.go.*.wsgi=run-wsgi.bat "$(FilePath)"
command.go.subsystem.*.wsgi=2</code></pre>
<h3>WSGI-скрипт</h3>
<p>На всякий случай, упреждая возможные вопросы, приведу пример скрипта. Поскольку wsgi-скрипт это полноценный питоновский файл, то мы можем определить WSGI приложение прямо в нем. Т.е. любой из примеров приведенных в предыдущих статьях можно использовать таким образом. Только теперь там, где мы вызывали paste.httpserver.serve(APP) нужно писать  application = APP. Например вот наш самый первый пример в виде WSGI-скрипта (добавилась последняя строка кода):</p>
<pre><code>import cgi
def hello_app(environ, start_response):
    start_response(&#39;200 OK&#39;, [(&#39;Content-type&#39;, &#39;text/plain&#39;)])
    yield &quot;Hello, &quot;
    form = cgi.FieldStorage(environ=environ)
    name = form.getfirst(&#39;name&#39;, &#39;stranger&#39;)
    yield name

application = hello_app</code></pre>
<p>В Apache тоже ничего мудрить не надо, для начала достаточно такого:</p>
<pre><code>WSGIScriptAlias /path-to-app /home/user/public/hello.wsgi</code></pre>
<p>Не думаю что статья отвечает на все вопросы, но моей задачей было, в первую очередь, показать что в этом вопросе нет ничего страшного и отослать к документации тех компонент которые могут пригодиться. </p>
<p>Следующая запланированная статья будет о создании WSGI middleware.</p>
<br/><a href="http://www.developers.org.ua/archives/mlk/2008/05/20/python-webdev-no-frameworks-p4/#ratings">Оценить статью на сайте</a> | <a href="http://www.developers.org.ua/archives/mlk/2008/05/20/python-webdev-no-frameworks-p4/#comments">18 комментариев</a>]]></content:encoded>
			<wfw:commentRss>http://www.developers.org.ua/archives/mlk/2008/05/20/python-webdev-no-frameworks-p4/feed/</wfw:commentRss>
		</item>
		<item>
		<title>Python: Веб-разработка без фреймворков (часть 3)</title>
		<link>http://www.developers.org.ua/archives/mlk/2008/04/14/python-webdev-no-frameworks-p3/</link>
		<comments>http://www.developers.org.ua/archives/mlk/2008/04/14/python-webdev-no-frameworks-p3/#comments</comments>
		<pubDate>Mon, 14 Apr 2008 14:01:18 +0000</pubDate>
		<dc:creator>Сергей Щетинин</dc:creator>
		
		<category><![CDATA[Python]]></category>

		<category><![CDATA[Разработка]]></category>

		<category><![CDATA[статьи]]></category>

		<category><![CDATA[Python: Веб-разработка без фреймворков]]></category>

		<category><![CDATA[WebOb]]></category>

		<category><![CDATA[WSGI]]></category>

		<guid isPermaLink="false">http://www.developers.org.ua/archives/mlk/2008/04/14/python-webdev-no-frameworks-p3/</guid>
		<description><![CDATA[Прочитав предыдущие статьи читатель, надеюсь, убедился, что средств PythonPaste и WebOb более чем достаточно для разбора запроса, композиции приложений и выполнения множества стандартных задач. Далее мы будем рассматривать вопросы генерации ответов, развертывания на сервере и выбора различных вспомогательных библиотек и инструментов.


Related posts:<ol><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/10/python-webdev-no-frameworks-p2/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 2)'>Python: Веб-разработка без фреймворков (часть 2)</a> <small>В прошлой части я постарался рассказать о том, что чистый...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/05/29/python-webdev-no-frameworks-response/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (ответ на критику)'>Python: Веб-разработка без фреймворков (ответ на критику)</a> <small>Почти месяц назад в сети появился отзыв на эту серию...</small></li><li><a href='http://www.developers.org.ua/archives/mlk/2008/04/08/python-webdev-no-frameworks-p1/' rel='bookmark' title='Permanent Link: Python: Веб-разработка без фреймворков (часть 1)'>Python: Веб-разработка без фреймворков (часть 1)</a> <small>Каждый разработчик тщательно выбирает свой инструментарий и чем лучше он...</small></li></ol>

Related posts brought to you by <a href='http://mitcho.com/code/yarpp/'>Yet Another Related Posts Plugin</a>.]]></description>
			<content:encoded><![CDATA[<p>Прочитав <a href="http://www.developers.org.ua/archives/mlk/2008/04/08/python-webdev-no-frameworks-p1/">предыдущие</a> <a href="http://www.developers.org.ua/archives/mlk/2008/04/10/python-webdev-no-frameworks-p2/">статьи</a> читатель, надеюсь, убедился, что средств <a href="http://pythonpaste.org/">PythonPaste</a> и <a href="http://pythonpaste.org/webob/">WebOb</a> более чем достаточно для разбора запроса, композиции приложений и выполнения множества стандартных задач. Далее мы будем рассматривать вопросы генерации ответов, развертывания на сервере и выбора различных вспомогательных библиотек и инструментов.</p>
<p>До сих пор в примерах мы почти не занимались собственно ответами, перекладывая эту задачу на какие-то уже существующие приложения. Как уже было упомянуто в <a href="http