вторник, 21 апреля 2009 г.

использование "глобальных" переменных в Python

Маленький пример кода из документации Pylons:
from pylons import url
print url.current() # prints /foo/bar if this is request for /foo/bar
Чем является объект url? Переменной (глобальной) в pylons/__init__.py? А может это модуль url.py у которого есть функция current? С точки зрения Питона разница небольшая - и то и то есть ключ в каком-то словаре имен (Namespaces are one honking great idea).

Так почему тогда это так "напрягает" некоторых разработчиков?

Да-да, все мы читали про Singletons are evil и глобальные переменные это тоже "плохо". Но почему? Точнее не так - в питоне вы используете кучу глобальных переменных, которые "появляются" через ключевое слово import. E.g.: import urlllib; urllib.open(). Чем же import pylons; pylons.request.environ() хуже?

Псевдоглобальные объекты, типа pylons.request, удобны тем, что они доступны через простой и понятный интерфейс, который любой программист на Python хорошо знает (import). Простота - это хорошо.

Да, для создания таких объектов приходится использовать thread-locals, ну и что? Это implementation detail.

Какая альтернатива? Если "глобальные" переменные не используются, тогда мы должны каким-то образом "доставлять" нужные объекты в нужные участки программы. Вариант один - передавать их по стеку вызовов.

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

Плохо это тем, что т.к. абстракции типа request или database нужны много где, код в итоге загромождается и усложняется из-за необходисти "носить" с собой эти объекты, которые могут понадобится на 2-3 уровня ниже по стеку.

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

И что же у меня получается? Код без использования "глобальных" переменных - "правильнее", а с ними - "удобнее". Прямо как Pascal vs. C. ;)

Может, действительно pylons.url и прочие - зло, а я - ленивый программист? Хотя лень у нас недостатком вроде и не считается...

update: в комментариях очень познавательная информация про библиотеку Contextual, которая решает проблему глобально-доступных объектов, но не использует thread locals.

8 коммент.:

Андрей Светлов комментирует...

В умелых руках "глобальные объекты" - очень к месту. В Пилоне применены, как мне кажется, в тему.
Другой вопрос: разработчик должен подходить к созданию своих синглетонов очень ответственно. Т.е. "зарезать" все варианты их неправильного использования. Что делается далеко не всегда. Чаще же не делается совсем.

Я последние полгода использую Contextual - http://pypi.python.org/pypi/Contextual
Очень удобная и весьма строгая абстракция. Так что весьма доволен. Юниттесты, к слову, вместе с ним тоже пишуться на ура :)

max комментирует...

Спасибо за хинт! Помню Щетинин о ней рассказывал, но тогда я не воспринял ее в нужном "контексте". :)

Хотя описание не очень понятное - ругается thread locals но при этом утверждается, что в тредах оно работает как надо. глянул исходник - оно использует thread.get_ident() в качестве ключа в словаре состояний. сам словарь локами на запись не защищен. я не понимаю как оно работает thread safe. ну да я не PJE и даже не GvR. ;)

Андрей Светлов комментирует...

get_indent хватает. Элементарные операции со словарем атомарные и занимают одну инструкцию в байт-коде. Поэтому - thread-safe. Хоть и неочевидный.

mlk комментирует...

Там где собсно get_ident() там есс-но по кажому ключу только один поток работает, никаких локов не надо в любом случае. Вопросы появляются если один и тот же контекст (или наследники одного контекста) активны нескольких потоках и активируется какое-то значение, а это thread-safe за счет .setdefault. Также см.

mlk комментирует...

Насчет использования -- запрос, url итп в сервисы или глобальные переменные совать я считаю неверно, а вот конфигурацию БД, сессии алхимии итп -- в самый раз. Автоматическая активация очень к месту будет. В частности то что я писал про разные варианты подключения YUI -- я собирался потом показать как такие "один интерфейс / много реализаций" хорошо переносятся в context.Service.

mlk комментирует...

== Думал blogger вместо превью проглотил мой первый каммент, ан нет, вот он в другом табе :) Частично дублирует, но есть и то что обломился писать заново, потому отправляю и его. ==

Contextual рвет threadlocals по нескольким параметрам. Из простого:
* Если один и тот же контекст используется в разных потоках. (Например если WSGI app_iter дергается в разных потоках тредпула по мере надобности)
* Пока не вошли в дочерний контекст переопределять значение запрещено -- это рубит много багов.
* Ну и там куча всего про что без Contextual мечтать не приходится.

Thread-safe главным образом за счет правильно использованных .setdefault -- инициализация сервиса теоретически может произойти несколько раз, но в использование попадет всегда только один экземпляр.

Хранить запрос в глоб. переменной / сервисе считаю неправильно. Для конкретного приложения сделать иногда можно, но не на уровне фреймворка. А вот БД / сессии алхимии оформить сервисами сам бог велел.

Есть еще фичи с зависимостями значений хранимых в контексте про которые я в доках ничего не видел, но которые еще помогли бы автоматизировать транзакции на каждый запрос и прочее подобное. (См в сырцах определение глубины на которую сохранять вычисленное значение).

max комментирует...

Спасибо за объяснение! Действительно полезная штучка.

mlk комментирует...

Угум :)

Отправить комментарий