ГЛАВНАЯ      ДОКУМЕНТАЦИЯ      СТАТЬИ      ПРОГРАММЫ      ССЫЛКИ      ФОРУМ      ДРУГОЕ   

Перегрузка и специализация функций и классов в Python

        Один из недостатков языка Python -- отсутствие перегрузки функций. Особенно остро это ощущается при написании классов, которым хочется сделать несколько конструкторов для различных ситуаций. Конечно, этот недостаток является прямым следствием отсутствия строгой типизации, и может быть исправлен определением функции с переменным количеством аргументов и анализом их количества и типов внутри функции, но такой код выглядит существенно хуже.
        Реализация перегрузки функций с учетом гибкости Python не является особенно сложной задачей -- в простейшем случае создается объект, содержащий словарь с записями вида кортеж-типов-аргументов:функция и перегруженный оператор вызова функции.
#-------file simple_overload.py:------------
class overload:
   def __init__(self): self.D={}
   def __call__(self,*L) : return apply( self.D[ tuple(map(type,L)) ],L )
   def registry(self, f, *L) : self.D[L]=f
#...

def f1() : print 1
def f2(a) : print 'int',a
def f3(a) : print 'float',a

f=overload()
f.registry(f1) 
f.registry(f2,int) 
f.registry(f3,float) 
#-------------------------------
$python -i simple_overload1.py
>>>f()
1
>>>f(2)
int 2
>>>f(3.)
float 3.0
        Но такой подход имеет ряд недостатков -- неясный, избыточный синтаксис (перегрузка функций в C++ выглядит куда более прозрачно, в частности желательно что бы типы аргументов задавались где то в районе заголовка функции), засорение пространства имен (для каждой перегруженной функции приходится вводить свое имя), для перегрузки методов классов экземпляр класса overload приходится в итоге «заворачивать» в lambda-функцию (иначе не происходит связывания):
class A:
   ...
   _f=overload()
   ...
   f=lambda *L : apply(_f,L)
   ...
        Каждый из перечисленных недостатков может иметь самые различные решения, но вариант приведенный ниже на мой взгляд один из самых изящных.
        Итак, необходимо:
         * создать некоторый объект, содержащий таблицу перегруженных функций, желательно уже «завернутый» в lambda-функцию;
         * создание новых перегруженных функций не должно приводить к засорению пространства имен;
         * регистрация функций должна происходить автоматически, типы аргументов должны задаваться где то в районе начала функции;
         * полученный в итоге объект должен быть «завернут» в lambda-функцию.
        Самый простой (для пользователя) вариант регистрации новых функций в таблице объекта реализующего перегрузку -- это ее отсутствие. Python очень лаконичный язык, и было бы обидно писать лишние строчки, тем более, что иногда их можно просто забыть. Такой вариант регистрации возможен, и основан на «не убиваемом» объекте. Сборщик мусора дает уничтожаемому объекту «последний шанс» вызывая его деструктор -- если в деструкторе будут созданы новые ссылки на объект, объект не будет уничтожен. Таким образом, если мы создадим в локальном пространстве имен объект с таблицей перегружаемых функций, то при создании новой одноименной перегружаемой функции для объекта будет вызываться деструтор, в котором может быть осуществлена регистрация уничтожившей его функции, а объект восстановлен в локальном пространстве имен для вызова или регистрации следующей перегружаемой функции. При этом решается проблема засорения пространства имен. Объект может быть изначально «завернут» в lambda-функцию и восстанавливаться в таком же «завернутом» виде -- lambda-функция является единственным «держателем» ссылки на объект, и при ее уничтожении будет вызваться деструктор объекта. Последняя сложность -- как при таком способе регистрации передать объекту кортеж типов аргументов регистрируемой функции. Простейший вариант -- поместить кортеж в строку документации функции, допустим это будет первая строка документации. Строка документации задается сразу после заголовка, что делает синтаксис более наглядным, и кроме того плохая документация -- лучше чем никакая, а такой подход обеспечивает обязательное документирование для всех перегружаемых функций.
#-------file overload.py:------------
import sys
#-----------------------------------------------------------------------------
class _overload:
    def __init__(self, name): self.name, self.D = name, {}
    def __del__(self):
        try: raise Exception('call for get last stack frame')
        except : fr = sys.exc_info()[2].tb_frame.f_back
        if not fr : return
        f = fr.f_locals[self.name]
        L = eval(f.__doc__.split('\n')[0])
        shift = int( len(L) and L[0] == self )
        self.D[ L[shift:] ] = f
        fr.f_locals[self.name] = lambda *L : self(L, shift)
    def __call__(self, L, shift):
        f = self.D.get(tuple( map(type, L[shift:]) ))
        if f : return apply(f,L)
#-----------------------------------------------------------------------------
def overload(*L):
    try: raise Exception('call for get last stack frame')
    except : locals_dict = sys.exc_info()[2].tb_frame.f_back.f_locals
    for i in L : locals_dict[i] = _overload(i)
#-----------------------------------------------------------------------------
EOF
...
#-------file test.py:------------
from overload import *

overload('f')
def f() :
    '()'
    print 'f()'
def f(a):
    '(int,)'
    print 'f(int %s)'%a
def f(a):
    '(float,)'
    print 'f(float %s)'%a

f()
f(1)
f(10.)

class A:
    overload('__init__')
    def __init__(self):
        '(self,)'
        print 'A()'
    def __init__(self,a):
        '(self,int)'
        print 'A(int %s)'%a
    def __init__(self,a):
        '(self,float)'
        print 'A(float %s)'%a

A()
A(1)
A(10.)
#----------------------------------
EOF
...

$python test.py
f()
f(int 1)
f(float 10.0)
A()
A(int 1)
A(float 10.0)
        Для манипуляций с пространством имен из деструктора уничтожаемого объекта используется получение доступа к стеку при перехвате исключения. Для перегрузки методов класса первый аргумент в кортеже типов должен задаваться как self -- тогда поле shift устанавливается равным 1 и тип первого аргумента не будет проверяться. Следует учитывать, что при работе в интерактивном режиме результаты последней операции сохраняются в переменной _, поэтому после определения каждой новой функции нужно набирать какую нибудь команду, что бы изменить значение переменной _ и вызвать уничтожение объекта.
        Пока что наш код не производит автоматического приведения типов аргументов, хотя в ряде случаев это желательно. Кроме того, при вызове в случае отсутствия соответствующей функции ничего не происходит, а надо бы генерировать исключение. Добавим класс исключения OverloadError и изменим функцию _overload.__call__:
#-------file overload.py:------------
...
#-----------------------------------------------------------------------------
class OverloadError(Exception):
    def __init__(self,name,L,D,RL) : 
        self.name, self.L, self.D, self.RL = name, L, D, RL
    def __str__(self):
        if self.RL : 
            return "Overload failed --- several %s%s call functions found:"%\
           (self.name,self.L)+\
           ("\n\t"+self.name+"%s")*len(self.RL)%\
           tuple(map(lambda i:i[0].__doc__,self.RL))
        return "Overload failed --- no matching function for call to '%s%s'.\
	 Candidates are :"%\
               (self.name,self.L)+("\n\t"+self.name+"%s")*len(self.D)%\
               tuple(map(lambda i:i.__doc__,self.D.values()))
#-----------------------------------------------------------------------------
...
class _overload:
...
    def __call__(self, L, shift):
        f=self.D.get(tuple( map(type, L[shift:]) ))
        if f : return apply(f,L)
        RL=[]
        for d,f in self.D.items():
            try: 
	RL.append( [f]+list(L[:shift])+map( lambda i,j:i(j), d, L[shift:] ) )
            except :
	pass
        if len(RL)!=1 : 
	raise OverloadError( self.name, L, self.D, RL )
        return apply(RL[0][0], RL[0][1:])
...
        Если при вызове функции в таблице не находится записи в точности соответствующей типам аргументов, начинается перебор всех записей, причем для каждой записи производится попытка приведения аргументов к соответствующим типам. Если для записи все аргументы удалось привести (не было возбуждено ни одного исключения), запись попадает в список RL в виде списка, на нулевой позиции идет функция а дальше приведенные аргументы. Если список RL пуст значит не удалось найти ни одной подходящий функции, если больше одной записи значит возникла неоднозначность. В обоих случаях возбуждается исключение OverloadError.
        Такой подход существенно расширяет возможности перегрузки функций. Тип рассматривается как функция, приводящая аргумент к какому то новому виду или возбуждающая исключение. Добавим несколько функций:
#-------file overload.py:------------
...
#-----------------------------------------------------------------------------
ANY=lambda x:x
def _convert(x,L):
    for i in L :
        try: return i(x)
        except : pass
    raise ""
TO=lambda *L: lambda x : _convert(x,L)
IN=lambda *L: lambda x : filter( lambda i : type(x) is i,L )[0](x)
def _value(x,v):
    if x==v : return x
    raise ""
VAL=lambda l: lambda x: _value(x,l)
#-----------------------------------------------------------------------------
        ANY означает что аргумент может иметь любой тип.
        TO(T1,T2,T3,...) последовательно пытается привести аргумент к типам T1,T2,T3,... и возвращает первый удачный результат.
        IN(T1,T2,T3,...) проходит проверку только если тип аргумента соответствует одному из типов списка T1,T2,T3,....
        VAL обеспечивает перегрузку по значению аргумента, (некоторый аналог перегрузки параметризованных функций в C++ , или точнее частичная специализация функций, которая в C++ существует только для параметризованных классов) -- аргумент и значение по котрому производится перегрузка могут иметь разные типы, главное что бы результат их сравнения был равен True.
        ANY, TO, IN, VAL могут быть скомбинированы произвольным образом, при необходимости можно предложить еще более изощренные функции. Однако следует помнить, что например использование ANY может привести к возникновению неоднозначности. Можно усовершенствовать алгоритм выбора перегруженной функции, например разрешая неоднозначности при помощи балльной системы, однако мы оставим это на будущее.
        Оказывается наш механизм перегрузки работает только с аргументами базовых типов, и совершенно не поддерживает аргументы пользовательского типа. Во первых придется поправить разбор строки документации в деструкторе -- разбор должен происходить в глобальном пространстве имен перегружаемой функции. Во вторых придется опять изменить _overload.__call__:
#-------file overload.py:------------
...
#-----------------------------------------------------------------------------
from types import ClassType 
def _overload_call(T,x):
    if type(T) != ClassType : return T(x)
    if issubclass(T, x.__class__) : return x
    raise ""
#-----------------------------------------------------------------------------
class _overload:
    def __del__(self):
        ...
        L = eval(f.__doc__.split('\n')[0], fr.f_globals, locals() )
...
    def __call__(self, L, shift):
        f=self.D.get(tuple( map(lambda i : i.__class__, L[shift:]) ))
        if f : return apply(f,L)
        RL=[]
        for d,f in self.D.items():
            try: 
	RL.append( [f]+list(L[:shift])+map( _overload_call, d, L[shift:] ) )
            except : 
	pass
        if len(RL) !=1 : 
	raise OverloadError( self.name, L, self.D, RL )
        return apply(RL[0][0], RL[0][1:])
        Вместо вызова type(x) придется использовать x.__class__. Классы нельзя рассматривать как функции приведения типов, для сравнения классов используется встроенная функция issubclass, поэтому вместо lambda i,j:i(j) пришлось ввести функцию _overload_call.
        Но в Python таким методом перегружать можно не только функции но и классы. Объект класса допустимо рассматривать как функцию, при вызове создающую и возвращающую экземпляр класса. Классы могут быть перегружены (и частично специализированы при помощи VAL) вперемешку с функциями, причем перегрузка может быть вложенной (то есть у классов могут быть перегружены некоторые методы и т.д.).
#-------file test.py:------------
from overload import *

overload('f')
def f():
    '()'
    print 'f()'
def f(a):
    '(int,)'
    print 'f(int %s)'%a
def f(a):
    '(float,)'
    print 'f(float %s)'%a
class f:
    '(int,ANY)'
    overload('__init__')
    def __init__(self,a,b):
        '(self,VAL(1),str)'
        print 1,a,b
    def __init__(self,a,b):
        '(self,VAL(2),list)'
        print 2,a,b
    def __init__(self,a,b):
        '(self,VAL(3),ANY)'
        print 2,a,b
f()
f(1)
f(10.)
f(1,"class f")
f(2,"class f")
f(3,"class f")
#---------------------------------
EOF
$python test.py
f()
f(int 1)
f(float 10.0)
1 1 class f
2 2 ['c', 'l', 'a', 's', 's', ' ', 'f']
3 3 class f
        И последний штрих -- хорошо бы собирать в строке документации каждого экземпляра класса _overload все строки документации зарегистрированных в нем функций. Для этого введем строку документации класса _overload, добавим в конструктор одну и в деструктор две строки кода:
#-------file overload.py:------------
...
class _overload:
    "Overload function "
    def __init__(self, name):
        self.name, self.D = name, {}
        self.__doc__ += name+'(...)'
    def __del__(self):
        ...
        self.__doc__+= '\n\t'+self.name+f.__doc__
        fr.f_locals[self.name].__doc__ = self.__doc__
        Предложенный вариант перегрузки не поддерживает функции с именованными аргументами и функции с произвольным количеством аргументов, но при необходимости может быть расширен соответствующим образом. Перегрузка таких функций вызывает скорее идеологические проблемы -- слишком высока вероятность возникновения неоднозначностей, а алгоритм их разрешения становится чересчур запутанным.
        Благодаря потрясающей гибкости, перегрузка и частичная специализация классов и функций в Python может быть развита гораздо сильнее чем в C++ , несмотря на отсутствие строгой типизации. В Python программист ограничен лишь собственной фантазией, что и делает этот язык столь привлекательным.
test.py
overload.py

Источники:
        alpha.sec.ru/~aiv/

автор: А.В.Иванов   
ПОМОЩЬ САЙТУ :
sms.Є®ЇЁ«Є  *PythonUA*
Для чего Вы используете Python?
Admin( 46 )
Web( 61 )
GUI( 37 )
Embedding ( 16 )
Другое( 34 )
Какими продуктами Вы пользовались?
Zope( 15 )
Plone( 1 )
TG( 7 )
Django( 15 )
Twisted( 5 )
Другими( 10 )
ДРУЗЬЯ:
LUG.DN.UA
D-FENS.ORG.UA
SLAV0NIC.XSS.RU
CETUS.COM.UA
ENTDEV.ORG
[Python Powered]
Rambler's Top100
Copyright © 2006 python.com.ua