我是一个大量使用继承的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文档会产生误导,因为很多重要参数都隐藏在{{ 1}}。
是否有某种方法可以将正确的签名(或者至少是正确签名的文档)从基类方法“转发”到派生类方法?
答案 0 :(得分:5)
我还没有找到一种方法来完美地创建具有相同签名的功能,但我认为我的实现的缺点并不严重。我提出的解决方案是功能装饰器。
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
dict传递给函数。 args
参数仅用于将varargs传递给函数。如果父函数没有varargs,args
将永远是一个空元组。
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
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
,然后返回动态创建的函数。
如果你不想继续&#34;继承&#34;参数x和y,使用
@copysig(parentfunc, 'x', 'y')
答案 1 :(得分:2)
考虑使用attrs module:
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)