11

我是一个大量使用继承的 Python 项目的维护者。有一个反模式给我们带来了一些问题并且使阅读变得困难,我正在寻找一种解决它的好方法。

问题是将非常长的参数列表从派生类转发到基类 - 主要但不总是在构造函数中。

考虑这个人为的例子:

class Base(object):
    def __init__(self, a=1, b=2, c=3, d=4, e=5, f=6, g=7):
       self.a = a
       # etc

class DerivedA(Base):
    def __init__(self, a=1, b=2, c=300, d=4, e=5, f=6, g=700, z=0):
        super().__init__(a=a, b=b, c=c, d=d, e=e, f=f, g=g)
        self.z = z

class DerivedB(Base):
    def __init__(self, z=0, c=300, g=700, **kwds):
        super().__init__(c=c, g=g, **kwds)
        self.z = z

此时,一切看起来都像是DerivedA——长长的参数列表,所有这些都显式地传递给基类。

不幸的是,在过去的几年中,我们遇到了一些问题,包括忘记传递参数并获取默认值,以及没有注意到派生类中的一个默认参数与默认默认值不同。

它还使代码不必要地庞大,因此难以阅读。

DerivedB更好并解决了这些问题,但有一个新问题是派生类中方法的 Python 帮助/sphinx HTML 文档具有误导性,因为许多重要参数隐藏在**kwds.

有没有办法将正确的签名——或者至少是正确签名的文档——从基类方法“转发”到派生类方法?

4

2 回答 2

7

我还没有找到一种方法来完美地创建具有相同签名的函数,但我认为我的实现的缺点并不太严重。我想出的解决方案是函数装饰器。

使用示例:

class Base(object):
    def __init__(self, a=1, b=2, c=3, d=4, e=5, f=6, g=7):
       self.a = a
       # etc

class DerivedA(Base):
    @copysig(Base.__init__)
    def __init__(self, args, kwargs, z=0):
        super().__init__(*args, **kwargs)
        self.z = z

所有命名的继承参数都将通过kwargsdict 传递给函数。该args参数仅用于将可变参数传递给函数。如果父函数没有可变参数,args则始终是一个空元组。

已知问题和限制:

  • 在python2中不起作用!(为什么你还在使用 python 2?)
  • 并非装饰函数的所有属性都被完美保留。例如,function.__code__.co_filename将设置为"<string>".
  • 如果修饰函数抛出异常,异常回溯中会出现一个可见的附加函数调用,例如:

    >>> f2() Traceback (most recent call last): File "", line 1, in File "", line 3, in f2 File "untitled.py", line 178, in f2 raise ValueError() ValueError

  • 如果一个方法被修饰,第一个参数必须称为“self”。

执行

import inspect

def copysig(from_func, *args_to_remove):
    def wrap(func):
        #add and remove parameters
        oldsig= inspect.signature(from_func)
        oldsig= _remove_args(oldsig, args_to_remove)
        newsig= _add_args(oldsig, func)

        #write some code for a function that we can exec
        #the function will have the correct signature and forward its arguments to the real function
        code= '''
def {name}{signature}:
    {func}({args})
'''.format(name=func.__name__,
            signature=newsig,
            func='_'+func.__name__,
            args=_forward_args(oldsig, newsig))
        globs= {'_'+func.__name__: func}
        exec(code, globs)
        newfunc= globs[func.__name__]

        #copy as many attributes as possible
        newfunc.__doc__= func.__doc__
        newfunc.__module__= func.__module__
        #~ newfunc.__closure__= func.__closure__
        #~ newfunc.__code__.co_filename= func.__code__.co_filename
        #~ newfunc.__code__.co_firstlineno= func.__code__.co_firstlineno
        return newfunc
    return wrap

def _collectargs(sig):
    """
    Writes code that gathers all parameters into "self" (if present), "args" and "kwargs"
    """
    arglist= list(sig.parameters.values())

    #check if the first parameter is "self"
    selfarg= ''
    if arglist:
        arg= arglist[0]
        if arg.name=='self':
            selfarg= 'self, '
            del arglist[0]

    #all named parameters will be passed as kwargs. args is only used for varargs.
    args= 'tuple(), '
    kwargs= ''
    kwarg= ''
    for arg in arglist:
        if arg.kind in (arg.POSITIONAL_ONLY,arg.POSITIONAL_OR_KEYWORD,arg.KEYWORD_ONLY):
            kwargs+= '("{0}",{0}), '.format(arg.name)
        elif arg.kind==arg.VAR_POSITIONAL:
            #~ assert not args
            args= arg.name+', '
        elif arg.kind==arg.VAR_KEYWORD:
            assert not kwarg
            kwarg= 'list({}.items())+'.format(arg.name)
        else:
            assert False, arg.kind
    kwargs= 'dict({}[{}])'.format(kwarg, kwargs[:-2])

    return '{}{}{}'.format(selfarg, args, kwargs)

def _forward_args(args_to_collect, sig):
    collect= _collectargs(args_to_collect)

    collected= {arg.name for arg in args_to_collect.parameters.values()}
    args= ''
    for arg in sig.parameters.values():
        if arg.name in collected:
            continue

        if arg.kind==arg.VAR_POSITIONAL:
            args+= '*{}, '.format(arg.name)
        elif arg.kind==arg.VAR_KEYWORD:
            args+= '**{}, '.format(arg.name)
        else:
            args+= '{0}={0}, '.format(arg.name)
    args= args[:-2]

    code= '{}, {}'.format(collect, args) if args else collect
    return code

def _remove_args(signature, args_to_remove):
    """
    Removes named parameters from a signature.
    """
    args_to_remove= set(args_to_remove)
    varargs_removed= False
    args= []
    for arg in signature.parameters.values():
        if arg.name in args_to_remove:
            if arg.kind==arg.VAR_POSITIONAL:
                varargs_removed= True
            continue

        if varargs_removed and arg.kind==arg.KEYWORD_ONLY:#if varargs have been removed, there are no more keyword-only parameters
            arg= arg.replace(kind=arg.POSITIONAL_OR_KEYWORD)

        args.append(arg)

    return signature.replace(parameters=args)

def _add_args(sig, func):
    """
    Merges a signature and a function into a signature that accepts ALL the parameters.
    """
    funcsig= inspect.signature(func)

    #find out where we want to insert the new parameters
    #parameters with a default value will be inserted before *args (if any)
    #if parameters with a default value exist, parameters with no default value will be inserted as keyword-only AFTER *args
    vararg= None
    kwarg= None
    insert_index_default= None
    insert_index_nodefault= None
    default_found= False
    args= list(sig.parameters.values())
    for index,arg in enumerate(args):
        if arg.kind==arg.VAR_POSITIONAL:
            vararg= arg
            insert_index_default= index
            if default_found:
                insert_index_nodefault= index+1
            else:
                insert_index_nodefault= index
        elif arg.kind==arg.VAR_KEYWORD:
            kwarg= arg
            if insert_index_default is None:
                insert_index_default= insert_index_nodefault= index
        else:
            if arg.default!=arg.empty:
                default_found= True

    if insert_index_default is None:
        insert_index_default= insert_index_nodefault= len(args)

    #find the new parameters
    #skip the first two parameters (args and kwargs)
    newargs= list(funcsig.parameters.values())
    if not newargs:
        raise Exception('The decorated function must accept at least 2 parameters')
    #if the first parameter is called "self", ignore the first 3 parameters
    if newargs[0].name=='self':
        del newargs[0]
    if len(newargs)<2:
        raise Exception('The decorated function must accept at least 2 parameters')
    newargs= newargs[2:]

    #add the new parameters
    if newargs:
        new_vararg= None
        for arg in newargs:
            if arg.kind==arg.VAR_POSITIONAL:
                if vararg is None:
                    new_vararg= arg
                else:
                    raise Exception('Cannot add varargs to a function that already has varargs')
            elif arg.kind==arg.VAR_KEYWORD:
                if kwarg is None:
                    args.append(arg)
                else:
                    raise Exception('Cannot add kwargs to a function that already has kwargs')
            else:
                #we can insert it as a positional parameter if it has a default value OR no other parameter has a default value
                if arg.default!=arg.empty or not default_found:
                    #do NOT change the parameter kind here. Leave it as it was, so that the order of varargs and keyword-only parameters is preserved.
                    args.insert(insert_index_default, arg)
                    insert_index_nodefault+= 1
                    insert_index_default+= 1
                else:
                    arg= arg.replace(kind=arg.KEYWORD_ONLY)
                    args.insert(insert_index_nodefault, arg)
                    if insert_index_default==insert_index_nodefault:
                        insert_index_default+= 1
                    insert_index_nodefault+= 1

        #if varargs need to be added, insert them before keyword-only arguments
        if new_vararg is not None:
            for i,arg in enumerate(args):
                if arg.kind not in (arg.POSITIONAL_ONLY,arg.POSITIONAL_OR_KEYWORD):
                    break
            else:
                i+= 1
            args.insert(i, new_vararg)

    return inspect.Signature(args, return_annotation=funcsig.return_annotation)

简短说明:

装饰器创建表单的字符串

def functionname(arg1, arg2, ...):
    real_function((arg1, arg2), {'arg3':arg3, 'arg4':arg4}, z=z)

然后execs 它并返回动态创建的函数。

附加的功能:

如果您不想“继承”参数 x 和 y,请使用

@copysig(parentfunc, 'x', 'y')
于 2017-02-26T22:17:22.000 回答
4

考虑使用attrs 模块

import attr

@attr.s
class Base(object):
    a = attr.ib(1)
    b = attr.ib(2)
    c = attr.ib(3)
    d = attr.ib(4)
    e = attr.ib(5)
    f = attr.ib(6)
    g = attr.ib(7)

@attr.s
class DerivedA(Base):
    z = attr.ib(0)

der_a = DerivedA()
print(der_a.a, der_a.z)
于 2017-03-04T11:41:39.213 回答