Personal tools
You are here: Home Статьи Тонкости ООП при работе с перзистентными объектами
Document Actions

Тонкости ООП при работе с перзистентными объектами

by cray last modified 2007-10-09 20:25

В статье разбираются некоторые малоизвестные но чрезвычайно важные проблемы программирования: использованияе переменных различного назначения. Авторы выражают признательность А.Зарецкому за идею.

Тонкости ООП при работе с перзистентными объектами :

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

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

Немоного понятий и определений:

Переменная класса
это переменная, которая определена в классе, и до ее присваивания в объекте ее значение одинаково для всех экземпляров класса. После присваивания в конкретном объекте переменная класса будет перекрыта переменной объекта, хотя и сохранит первоначальное значение.

Важно: переменная класса существует _все_время_жизникласса.

Переменная объекта
доступна во всех методах объекта и является частью обьекта;

Примечание: в языке питон переменная объекта и класса обозначаются как :

                "self." <ИМЯ ПЕРЕМЕННОЙ>

Важно: переменная объекта существует _все_время_жизниобъекта.

Неперзистентная переменная объекта
существует только в Zope-питоне.

Примечание: в языке питон неперзистентная переменная объекта обозначается префиксом "_v_":

                "self._v_" <ИМЯ ПЕРЕМЕННОЙ>

Важно: неперзистентная переменная объекта существует только _в_течение_периода_активацииобъекта.

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

Важно: переменная существует все время существования локального контекста. Тут важно сделать такое лирическое отступление: локальных контекстов, соответствующих одному и тому же участку кода, может быть много. И локальных переменных, соответсвтенно, тоже. Все они имеют одно и то же имя, одинаково используются, но при этом имеют, в общем случае, разные значения и принадлежат разным _вычислительнымпроцессам.

Глобальная переменная
переменная, которая определена в программе глобально. Глобальные переменные существуют все время жизни программы и доступны всем.
Аргумент функции
в принципе, переменная, аналогичная локальной, если бы не некоторые особенности языка питон, которые делают ее сходной с переменной класса (ну или, собственной, с переменной функции).
Результат функции
это вообще синтаксическая фикция. Которой вроде бы даже уже и нет: результат отдан оператором return и достучатся его до конца присваивания нельзя. А в конце - это уже одна из обычных переменных.

Использование переменных:

Запомните такую табличку:

Переменные класса
используются для изменения поведения всего класса и всех его объектов;
Переменные объекта
используются для изменения поведения всего объекта;
Неперзистентные переменные объекта
используются только для сохранения данных, которые не могут быть перзистентными в силу своей природы (например, сокеты);
Локальные переменные
используются для сохранения промежуточных результатов вычислений;
Глобальные переменные
вменяемыми людьми не используются;
Аргументы и результаты функций
используются для передачи промежуточных результатов между различными функциями.
Любой шаг в сторону от этих правил
контрольный выстрел в голову: невыплаченная зарплата, ракета, пролетевшая мимо Земли, и ядерный реактор, размазанный на площади в несколько сотен гектар.

Понятие реентерабельности:

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

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

ПРИМЕР 1:

            class A(object) :

                def a1(self,a) :
                    b = a + 1
                    return b + 2

                def a2(self, a) :
                    self.b = a + 1
                    return self.b + 2

Здесь метод a1 - реентерабельный, а метод a2 - нет. Предоставляем читателю самостоятельно понять почему. В случае затруднений рекомендуем обратиться к очевидному признаку реентерабильности, расположенному абзацем выше.

Типичные примеры ошибок использования переменных:

Использование переменной объекта вместо локальной переменной :

Подробно см. пример 1. В дополнение к потере реентерабельности, в случае перзистент класса, каждый вызов метода a2 будет приводить к замыканию транзакции, росту базы данных, резкому снижению быстродействия и прочим неприятностям.

Использование переменной объекта вместо аргумента (или результата) метода:

ПРИМЕР 2:

                class A(object) :

                    def a1(self, a)
                        self.b = a + 1 # или более сложный процесс

                        return self.a2()

                    def a2(self) :
                        return self.b + 2

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

                class A(object) :

                    def a1(self,a)
                        b = a + 1
                        return self.a2(b)

                    def a2(self,b) :
                        return b + 2

Ну и, как нетрудно заметить, исправленный код стал реентерабельным. Такие ошибки иногда пытаются даже оправдать, говоря про множественные вызовы с одной и той же переменной, ну что ж: нужно понимать, стоимость записи в переменную класса, особенно для перзистент объекта, особенно для транзакционно-совместимой системы, высока настолько, что никакие оправдания дороговизны передачи дополнительного параметра между вызовами методов одного и того же объекта не срабатывают.

Использование переменной класса вместо переменной объекта :

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

ПРИМЕР 3:

                class A (object) :

                    ls = []

                    def b(self, x) :
                        self.y.append(x)

Что же не так в этом примере? Не так в нем то, что переменная ls будет изменятся (идентично) во всех экзмеплярах класса:

                a1 = A()
                a2 = A()
                for a in xrange(1,10) :
                    if a%2 :
                        a1.b(a)
                    else :
                        a2.b(a)

Кажется логичным получить в a1.ls список четных, а в a2.ls - нечетных чисел, однако результат будет иным: два одинаковых списка.

Более того, с точки зрения перзистентности: список не будет перзистентным, потому что перзистентны элементы объекта, но не класса.

Чтобы избежать такого, пример 3 нужно переписать следующим образом:

ПРИМЕР 4:

                class A (object) :

                    ls = []

                    def __init__(self) :
                        self.ls = []
                        super(A, self).__init__()

                    def b(self, x) :
                        self.ls.append(x)

Использование аргумента вместо локальной переменной :

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

ПРИМЕР 5:

                def a(a,b=[]) :
                    for c in xrange(0,a) :
                        if c % 2 :
                            b.append(c)
                    return b

Было нарушено простое правило: аргумент может использоваться только для получения значения, только на чтение. Ужас ситуации в том, что при вызовах вида :

                a(10)
                a(10)
                a(10)

Каждый раз будет получатся новое значение. Эта мистика и магия иногда оказывается полезной, но в подавляющем большинстве случаев это чистое ЗЛО.

А вот правильная версия:

ПРИМЕР 6:

                def a(a,b=[]) :
                    d = b[:]
                    for c in xrange(0,a) :
                        if c % 2 :
                            d.append(c)
                    return d

Как программировать без ошибок (краткий курс):

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

  1. Все нескалярные переменные (массивы, словари и т.п.) должны всегда инициализироваться в функции __init__.
  2. Промежуточные вычисления всегда должны сохранятся в локальных переменных, более того, за исключением случаев выноса инварианта, их не стоит сохранять вообще.
  3. Переменные класса не должны использоваться на запись никогда.
  4. Глобальные переменные не должны использоваться на запись никогда.
  5. Передача значений между функциями может производится только через параметры.
  6. Переменные объекта могут использоваться только для изменения состояния объекта, т.е., вообще говоря, при вычислениях они могут использоваться только как основание или результат вычислений.

Придется, видимо, дать прагматичное объясненение шестому пункту, опять-таки применительно к реалиям программирования под Zope3. Дело в том, что программируя под Zope3, говоря "объект", мы имеем в виду, в первую очередь, "контент-объект". Переменные объекта - это его содержимое, контент. Контент в вычислениях может использоваться тремя способами:

  • записать (создать) контент;
  • породить на его основе новый контент (отредактировать);
  • отобразить контент.

Заметьте: во всех этих случаях контент (т.е. переменные объекта) используются только в строго определенных участках вычислительного процесса: как источник данных или как хранилище результата.

Заключение:

Все элементы современных языков программирования были созданы "зачем-то", они не возникли из хаоса первобытного мира, в который вошел Homo Sapiens (тогда еще без второго sapiens) и начал решать:

  • О! Палка! Можно копать!
  • О! Камень! Можно бить!
  • О! Бревно! Можно плыть!

Современные элементы мироздания имеют изначальную цель и должны использоваться прежде всего для нее. И никаких:

  • О! "Алазань"! Можно сделать мангал!

Последствия будут трагичны. Использование продуктов интеллектуальной эволюции не по назначению - это практически всегда трагедия. Уж очешь силен в них целевой аспект.

Гений может сделать из примеров ошибок выше новую методологию программирования, и такие примеры известны. Но даже гений тратит на это годы труда. А все остальное время в этом НЕХАОТИЧНОМ мире, наполненном хищными ПРЕДМЕТАМИ, нужно тратить свои силы на то, чтобы не оказаться обезьяной с гранатой.

Примечание A: (Анатолий Зарецкий)

В свете изложенных выше соображений и особенно об использовании перзистент-переменных вместо локальных хочется привести обращение одного из наших разработчиков, который потратил много времени на борьбу с последствиями таких ошибок

Уважаемые коллеги! Каждый раз, когда вы присваиваете что-нибудь атрибуту persistent-объекта, Бог убивает котенка. Не делайте этого -- пожалейте котят!

Изменение persistent-объектов везде, кроме форм добавления/редактирования, должно вызывать самые черные подозрения. Есть, конечно, разные специализированные обработчики событий, но эти события обычно возникают тоже при добавлении/редактировании.

То же самое справедлив и для содержимого sql-баз.

Мой опыт позволяет предложить некое сомнительное приспособление для охоты на такие баги; прилагаемый файл ZODB.Connection.py.diff является патчем к lib/python/ZODB/Connection.py, с помощью которого можно отслеживать, где происходят изменения persistent-объектов.

Патч основан на внесении трассировки в код регистрации объекта в менеджере транзакции при первом его измении и приведет к появлению в логе zope при первом изменении объекта в транзакции будут появлятся примерно таких сообщений:

      ------
      2007-09-27T19:05:32 INFO ZODB.Connection transaction register content.news.news.News 0x7e6a
      Traceback (most recent call last):

      ... пропускаем много воды ...

        File "/usr/local/lib/Zope-3.3/lib/python/zope/tales/tales.py", line 696, in evaluate
          return expression(self)
         - .../references.pt
         - Line 2, Column 2
         - Expression: <PathExpr standard:u'view/getTagReferences'>
         - Names:
            {'args': (),
             'context': <content.news.news.News object at 0xbdfa52c>,
             'default': <object object at 0xb7fdc560>,
             'loop': {},
             'nothing': None,
             'options': {},
             'repeat': {},
             'request': <zc.resourcelibrary.publication.Request instance URL=...>,
             'template': <zope.app.pagetemplate.viewpagetemplatefile.ViewPageTemplateFile object at 0xb937d6c>,
             'usage': <zope.pagetemplate.pagetemplate.TemplateUsage object at 0xb1f890c>,
             'view': <zope.app.pagetemplate.simpleviewclass.SimpleViewClass from ... object at 0xb1f61ac>,
             'views': <zope.app.pagetemplate.viewpagetemplatefile.ViewMapper object at 0xb1f698c>}
        File "/usr/local/lib/Zope-3.3/lib/python/zope/tales/expressions.py", line 217, in __call__
          return self._eval(econtext)
        File "/usr/local/lib/Zope-3.3/lib/python/zope/tales/expressions.py", line 211, in _eval
          return ob()
        File ".../annotabledocument/references.py", line 49, in getTagReferences
          tags = ITagging(self.context).getTags()
        File "/usr/local/lib/Zope-3.3/lib/python/zope/app/component/hooks.py", line 96, in adapter_hook
          return siteinfo.adapter_hook(interface, object, name, default)
        File "/usr/local/lib/Zope-3.3/lib/python/zope/security/adapter.py", line 84, in __call__
          adapter = self.factory(*args)
        File ".../tagging.py", line 46, in __init__
          self.context._id = self._id

что означает, "присваивание self.context._id = self._id в таком-то месте вызвало изменение объекта класса content.news.news.News (oid 0x7e6a)". Любители программы grep могут ловить это сообщение в логе по подстроке "ZODB.Connection transaction register"

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

        $ cd Zope-3.3/lib/python
        $ patch -p0 < ZODB.Connection.py.diff

Работа патча основана на том, что при переходе объекта из "чистого" состояния в "измененное" вызывается метод register менеджера данных этого объекта, получающий сам объект как параметр. Поэтому из метода register можно "подсмотреть", кто трогает объект.

Более подробно о поведении persitent-объектов можно почитать в документации из persistent/interfaces.py (++apidoc++/Code/persistent/interfaces/index.html).


Powered by Plone CMS, the Open Source Content Management System