gpt4 book ai didi

python - 完美转发——Python

转载 作者:太空狗 更新时间:2023-10-29 21:00:25 25 4
gpt4 key购买 nike

我是一个大量使用继承的 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

是否有某种方法可以将正确的签名(或至少是正确签名的文档)从基类方法“转发”到派生类方法?

最佳答案

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

使用示例:

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

所有命名的继承参数将通过 kwargs 传递给函数字典。 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)

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

附加功能:

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

@copysig(parentfunc, 'x', 'y')

关于python - 完美转发——Python,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42471083/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com