2008-01-11

Введение в питон.txt

  2008-01-11 20:32

Краткое введение в питон : Это введение расчитано на подготовленных читателей, которые знают и умеют программировать на языке питон, и единственная цель, которую оно преследует - напомнить о тех приемах программирования, которые могут быть полезны при изучении и программировании под Zope. Это давало богатые, хотя и неполные возможности к повторному использованию кода и фактически весь Zope2 был построен на этой основе: если была потребность в классе, обладающем ранее разработанной функциональностью, то его реализация сводилась к наследованию от базовых классов и незначительной последующей доработке. В Zope3 на смену такому подходу пришло использование компонент, которое оказалось значительно более гибким, но, тем не менее, множественное наследование занимает важное место в практике программирования под Zope3. Python исторически предлагает две разновидности ООП: классы старого и нового стиля. Все нижеследующее изложение дается для классов ...

Краткое введение в питон :

Это введение расчитано на подготовленных читателей, которые знают и умеют программировать на языке питон, и единственная цель, которую оно преследует - напомнить о тех приемах программирования, которые могут быть полезны при изучении и программировании под Zope.

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

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

Python исторически предлагает две разновидности ООП: классы старого и нового стиля. Все нижеследующее изложение дается для классов _нового_ стиля, которые являются основой программирования под Zope3.

Чтобы создать класс нового стиля, нужно наследовать от класса object (или от его подкласса):

        class NewStyle(object) : 
            # класс нового стиля

        class OldStyle : 
            # класс старого стиля

        class OldStyle2(OldStyle) : 
            # класс старого стиля

        class NewStyle2(NewStyle,OldStyle2) : 
            # класс нового стиля

Обратите внимание, что перекрытие имен в классах старого и нового стиля происходит по разному. Поэтому, строго говоря, функционирование OldStyle2 как самостоятельного класса будет отличаться от функционирования OldStyle2 как части класса нового стиля NewStyle2, хотя этот пример и слишком прост чтобы это продемонстрировать. Тем не менее, такой разнобой может привести к некоторой фрустрации разработчиков, поэтому рекомендуется пользоваться _только_ классами нового стиля.

В классах старого стиля наследование происходит "привычным" образом: просмотром дерева наследования в порядке корень-левое-правое (КЛП). В классах нового стиля наследование включает в себя сортировку базовых классов и удаление их повторов. Это различие практически незаметно в простых, повседневных примерах, но может оказать заметное влияние при массовом множественном наследовании. Подробно об этом можно прочитать здесь .

ООП программирование в python обладает некоторыми особенностями.

Использование super :

Полезная возможность ООП программирования - вызов методов суперкласса. Вот как это делается в python, для классов нового стиля:

            class A(object) : 

                def do(self,*kv) :

                    print kv

            class B(A) : 

                def do(self,*kv) :

                    print kv 

                    return super(B,self),do()

Для классов старого стиля реализация super тоже возможна, но лучше ими просто не пользоваться. Впрочем, желающие могут попробовать реализовать super как интересное упражнение [1].

Искусственное создание класса нового стиля:

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

            type(<CLASSNAME>,<BASECLASSES>,<CLASSVARIABLE>)

здесь:

CLASSNAME
имя класса, которое вы хотите использовать для нового класса. Имейте в виду, что если вы потом хотите на этот класс ссылаться (импортировать его или типа того), он должен быть расположен внутри некоторого модуля и его имя должно быть соответственно скорректировано;
BASECLASSES
кортеж с базовыми классами. Как правило, есть один базовый класс _всегда_, а все другие передаются откуда-то еще (см. пример ниже);
CLASSVARIABLE
словарь переменных класса.

Вот такая простая с виду конструкция может создать класс. Теперь рассмотрим пример, похожий на реальную жизнь.

Этот небольшой исходник демонстрирует то, как делать классы на основе существующего вызовом специальной фабрики классов.

Сначала идет определение базового класса:

            class NothingToDo(object) :
                """ This is simple class which do nothing """
                a = 1
                b = 2

Далее идет определение специальной глобальной переменной, посредством которой новые классы будут размещаться в пространство модуля:

            namespace = vars()               

Неизвестно сколько будет работать эта подозрительная фича, но лет пять уже работает.

Теперь определим функцию, создающую и регистрирующую классы:

            def SomeThing(name,bases=(),**kw) :
                klass = namespace[name] = type(name,tuple(bases)+(NothingToDo,),kw)
                return klass

Теперь задействуем ее, чтобы сгенерировать разные тестовые классы:

            SomeThing("Number0",number=0)

            SomeThing("Number1",number=1)

            SomeThing("DictNumber2",(dict,),number=3)

Сохраним все это в:

            klasssample.py

И теперь попробуем воспользоваться. Запустим python :

            $ python
            Python 2.4.4 (#1, May 15 2007, 20:49:37)
            [GCC 4.1.1 20070105 (ALT Linux, build 4.1.1-alt11)] on linux2
            Type "help", "copyright", "credits" or "license" for more information.
            >>> import klasssample
            >>> dir(klasssample)
            ['DictNumber2', 'NothingToDo', 'Number0', 'Number1', 'SomeThing', 
            '__builtins__', '__doc__', '__file__', '__name__', 'namespace']
            >>> klasssample.Number0
            <class 'klasssample.Number0'>
            >>> n0=klasssample.Number0()
            >>> n0
            <klasssample.Number0 object at 0xb7cd36ec>
            >>> klasssample.Number1
            <class 'klasssample.Number1'>
            >>> n1=klasssample.Number1()
            >>> n1
            <klasssample.Number1 object at 0xb7cd374c>
            >>> klasssample.DictNumber2
            <class 'klasssample.DictNumber2'>
            >>> n2=klasssample.DictNumber2()
            >>> n2
            {}
            >>> n0.number
            0
            >>> n1.number
            1
            >>> n2.number
            3
            >>> n2['aaa'] = 1
            >>> n1['aaa'] = 1
            TypeError: object does not support item assignment
            >>> n0.a
            1
            >>> n1.a
            1
            >>> n2.a
            1
            >>>                  

Как нетрудно заметить по результатам нашего тестирования, все классы внутри модуля были созданы, они имеют соответствующие имена, каждый из них имеет переменные, унаследованные от базового класса и уникальную переменную number, содержащую номер класса. Наконец, только один из них (klasssample.DictNumber2) является также и словарем.

Использование property :

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

            class C(object): 

                def getx(self): return self.__x 

                def setx(self, value): self.__x = value 

                def delx(self): del self.__x 

                x = property(getx, setx, delx, "I'm the 'x' property.")

Этот пример всегда можно увидеть, написав help(property) в подсказке интерпретатора python :).

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

Использование _getframe() :

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

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

            class A(object) :

                a = [1]

                b = 2

            class B(object) :

                a = [2]

                b = 3

            def create_reset(klass) : 

                def reset(self) :
                    self.a = klass.a 
                    self.b = klass.b

                klass.reset = reset

            create_reset(A)

            create_reset(B)

После выполнения кода, у классов A и B будет специальный метод reset, позволяющий вернуть атрибутам начальное значение (заметим, что метод reset может быть реализован и более прямым способом, это всего лишь иллюстрация, такую реализацию оставим как упражнение [2]).

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

            <ИМЯ АТРИБУТА> ": " <ЗНАЧЕНИЕ>

Такой тип может быть реализован как подкласс типа property :

            class Strproperty(property) : 

                def __init__(self,name) :
                    self.name = name
                    super(Strproperty,self).__init__(self.get,self.set)

                def get(self,ob) : 
                    return "%s: %s" % (self.name,str(getattr(ob,"__%s" % self.name,None)))

                def set(self,ob,value) : 

                    return setattr(ob,"__%s" % self.name,value)

Тогда класс с атрибутами и вышеописанным поведением создается следующим образом:

            class A(object) : 

                aa = strproperty('aa') 

                bb = strproperty('bb')

Есть некоторое неудобство в том, что имя атрибута указывается в двух местах. Чтобы обойти это, используем _getframe() :

            from sys import _getframe

            def strproperty(name) : 

                ns = _getframe(1).f_locals 

                ns[name] = Strproperty(name)

Теперь класс A описывается проще:

            class A(object) : 

                strproperty('aa') 

                strproperty('bb')

Эта, казалась бы, несущественная деталь способна сберечь немало нервов, а кроме того, ее описание потребовало рассказа о нескольких исключительно полезных, но трудно формализуемых идеях программирования на python.

Возможность перекрытия getattr, динамическое создание атрибутов :

Поведение экземпляра класса можно изменить, перекрыв некоторые методы, наследуемые (или подразумеваемые) от класса object. Наиболее часто перекрытию подвергается методы __getattr__ и __setattr__, используемые для получения и присваивания значения атрибутам экземпляра класса. Таким образом, можно получить два решения:

  1. Получить класс, атрибуты экземпляра которого при обращении к ним вычисляются, либо обращение к атрибутам изменяет внутреннее состояние экземпляра класса. На сегодняшний день такое решение редко имеет практический смысл, так как его реализация на основе property несет в себе меньше неожиданных проблем:
  2. Построение универсального proxy-объекта.

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

proxy

proxy

Реализация с использованием __getattr__ выглядит, например, так:

            class Proxy(object) : 

                def __init__(self,ob) :
                    self.__ob_ = ob

                def __getattr__(self,name) : 

                    return getattr(self.__ob_,name)

Обратите внимание - этот Proxy ничего не делает. Доработаем его так, чтобы результатом обращения к любому атрибуту всегда было строковое значение:

            class Proxy(object) : def __init__(self,ob) :
                    self.__ob_ = ob

                def __getattr__(self,name) : 

                    attr = getattr(self.__ob_,name)

                    if callable(attr) : 

                        return attr

                    return str(attr)

Обратите внимание на проверку значения на "вызываемость": методы класса -- это такие же атрибуты, как и все остальное: их можно создавать, удалять, изменять и получать значение. Эта проверка позволяет вызывать методы проксируемого класса. В качестве самостоятельного упражнения [3], рекомендуем найти способ реализации такого Proxy, у которого результат вызова методов также преобразуется в строку.

Особенность обработки getattr - метод вызывается __после__ попытки получения атрибута, если эта попытка была неудачной. Иными словами:

            class A(object) : 

                a = 1 

                b = 2

            a = A() 

            p = Proxy(a)

            # p.a - строка, равна '1' 

            # p.b - строка, равно '2'

            p.b = []

            # p.b - пустой список [] 

            # a.b - целое, равно 2

Эта особенность имеет важное практическое значение, так как позволяет реализовать __кеширование__ значений проксируемых атрибутов:

            class Proxy(object) : 

                def __init__(self,ob) :

                    self.__ob_ = ob

                def __getattr__(self,name) : 

                    attr = getattr(self.__ob_,name)

                    setattr(self,name,attr)

                    if callable(attr) : 

                        return attr

                    return str(attr)

Этот прокси будет обращаться к проксируемому объекту только однажды для каждого атрибута. Если такая особенность getattr не нужна (или мешает), то можно использовать функцию __getattribute__, вызываемую до попытки обращения к атрибуту.

Реализация Proxy может включать в себя перекрытие методов __setattr__, __delattr__ и ряда других. Смысл их ясен из названия, а попытки реализации оставим для самостоятельных упражнений (*4).

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

.. [1] Упражнение: Написать super для класса старого стиля;

.. [2] Упражнение: Сделайте свою реализацию метода reset;

.. [3] Упражнение: Сделать такую реализацию прокси, у которой результат вызова метода также преобразуется в строку;

Официальный сайт Zope3 Московская группа изучения реактивного движения The Dream Bot Site noooxml