I was wondering if the actual version from fastai (2024-10-30 ) had integrated something from here.
As this is above me, I asked … you know.
Here are the answers.
A: OegeDijk
B: current fastcore as of 2024-10-30
BTW, it took me a while to find some good documentation about @delegates
================
What are the differences between
A
def delegates(to=None, keep=False):
"Decorator: replace `**kwargs` in signature with params from `to`"
def _f(f):
from_f = f.__init__ if to is None else f
sig = inspect.signature(from_f)
sigd = dict(sig.parameters)
k = sigd.pop('kwargs')
if to is None:
for base_cls in f.__bases__:
to_f = base_cls.__init__
s2 = {k:v for k,v in inspect.signature(to_f).parameters.items()
if v.default != inspect.Parameter.empty and k not in sigd}
sigd.update(s2)
else:
to_f = to
s2 = {k:v for k,v in inspect.signature(to_f).parameters.items()
if v.default != inspect.Parameter.empty and k not in sigd}
sigd.update(s2)
if keep: sigd['kwargs'] = k
from_f.__signature__ = sig.replace(parameters=sigd.values())
return f
return _f
B
def delegates(to:FunctionType=None, # Delegatee
keep=False, # Keep `kwargs` in decorated function?
but:list=None): # Exclude these parameters from signature
"Decorator: replace `**kwargs` in signature with params from `to`"
if but is None: but = []
def _f(f):
if to is None: to_f,from_f = f.__base__.__init__,f.__init__
else: to_f,from_f = to.__init__ if isinstance(to,type) else to,f
from_f = getattr(from_f,'__func__',from_f)
to_f = getattr(to_f,'__func__',to_f)
if hasattr(from_f,'__delwrap__'): return f
sig = inspect.signature(from_f)
sigd = dict(sig.parameters)
k = sigd.pop('kwargs')
s2 = {k:v.replace(kind=inspect.Parameter.KEYWORD_ONLY) for k,v in inspect.signature(to_f).parameters.items()
if v.default != inspect.Parameter.empty and k not in sigd and k not in but}
anno = {k:v for k,v in getattr(to_f, "__annotations__", {}).items() if k not in sigd and k not in but}
sigd.update(s2)
if keep: sigd['kwargs'] = k
else: from_f.__delwrap__ = to_f
from_f.__signature__ = sig.replace(parameters=sigd.values())
if hasattr(from_f, '__annotations__'): from_f.__annotations__.update(anno)
return f
return _f
================
These two versions of the delegates
decorator serve a similar purpose — to enhance a function’s signature by updating its parameters to match those of another function (the “delegatee”). However, they differ in their design, feature set, and some implementation details. Here are the main differences between the two:
1. Parameter Flexibility
- Version A:
- Has two parameters:
to
, which is the function to delegate parameters from, and keep
, which specifies whether to retain kwargs
in the decorated function’s signature.
- Version B:
- Has three parameters:
to
, keep
, and but
.
to
and keep
have the same purpose as in Version A.
but
is an additional parameter that allows specific parameters to be excluded from the delegation. This makes Version B more flexible in scenarios where you want to delegate only a subset of the delegatee’s parameters.
2. Type Hinting
- Version A:
- Does not use type hints in the function signature.
- Version B:
- Type-hints the
to
parameter to expect a FunctionType
, making it clearer that to
is expected to be a callable. This helps with readability and can aid in linting and type-checking.
3. Delegatee (to
) Resolution
- Version A:
- Resolves
to
by checking if it’s None
and, if so, attempts to use the __init__
method of any base classes of f
.
- Version B:
- Allows for greater flexibility in specifying the delegatee. If
to
is provided as a class, it uses the class’s __init__
method. If it’s a callable, it uses to
directly. This makes the decorator more robust in handling both functions and classes.
4. Parameter Annotations
- Version A:
- Does not handle or copy parameter annotations from the delegatee.
- Version B:
- Copies parameter annotations from the delegatee function to the decorated function. This is achieved by updating
from_f.__annotations__
with anno
, which collects the annotations from to_f
.
- This feature enhances code clarity and maintains documentation and type information on the decorated function.
5. Keyword-Only Parameter Conversion
- Version A:
- Does not convert any parameters to keyword-only.
- Version B:
- Explicitly converts delegated parameters to keyword-only parameters in the decorated function’s signature by using
inspect.Parameter.KEYWORD_ONLY
.
- This can make the function signature more explicit, enforcing that these parameters must be specified as keyword arguments, which can improve code readability and prevent accidental positional use of delegated parameters.
6. Attributes and Caching
- Version A:
- Does not cache or add any custom attributes to the decorated function.
- Version B:
- Uses the
__delwrap__
attribute to cache the delegatee function (to_f
) on the decorated function (from_f
).
- This attribute acts as a flag to prevent re-wrapping the function if it’s already wrapped, providing additional safety when stacking multiple decorators or avoiding redundant wrapping.
7. Signature Update Mechanism
- Both Versions:
- Both modify the signature of the function
from_f
using from_f.__signature__ = sig.replace(parameters=sigd.values())
.
- Version B:
- Additionally manages and updates the
__annotations__
attribute to include any annotations from to_f
, ensuring type hints are correctly copied over.
Summary of Key Differences:
- Feature: Version B is more feature-rich, adding the ability to exclude specific parameters, carry over annotations, convert parameters to keyword-only, and handle caching with
__delwrap__
.
- Code Complexity: Version B is more complex, but this is primarily to support additional flexibility and maintain metadata like annotations and keyword-only parameters.
- Use Case Suitability: If you need basic delegation, Version A suffices. However, if you require exclusions (
but
), annotations, or keyword-only parameters, Version B is more suitable.
In conclusion, Version B is a more versatile and robust implementation of the delegates
decorator, while Version A provides a simpler, more lightweight version suitable for basic delegation needs.
================
The creator of version A says However it doesn’t work with multiple inheritance.
referring to version B. Is he right?
================
The creator of Version A is correct in noting that Version B may not work as expected with multiple inheritance in certain cases, depending on how it’s used. Here’s why:
Issue with Multiple Inheritance
In Python, when you use multiple inheritance, the order in which base class methods are resolved follows the Method Resolution Order (MRO). If a decorator like delegates
tries to access the __init__
method from a base class without explicitly handling multiple inheritance, it can lead to unexpected behaviors. Version B assumes a single base class’s __init__
or a single to
function when delegating parameters, which doesn’t account for the complexity of multiple inheritance scenarios.
Here’s where it gets specific with Version B:
- When
to
is None
, Version B defaults to setting to_f
to f.__base__.__init__
. The __base__
attribute in Python refers to the first base class only, and it doesn’t account for other base classes in cases of multiple inheritance.
- In a multiple inheritance context,
__base__
will not necessarily represent the correct __init__
method according to MRO. As a result, it may not capture parameters from all base class __init__
methods or may simply choose the wrong one.
Why Version A Might Handle This Better
In Version A:
- The decorator iterates over all base classes (
for base_cls in f.__bases__:
) when to
is None
, allowing it to pick parameters from all base class __init__
methods.
- This approach aligns with multiple inheritance, as it doesn’t restrict delegation to a single
__init__
and instead considers the parameters of each base class in f.__bases__
.
Example Case Illustrating the Problem
Consider a scenario with multiple inheritance:
class A:
def __init__(self, a=1, **kwargs): pass
class B:
def __init__(self, b=2, **kwargs): pass
class C(A, B):
@delegates()
def __init__(self, c=3, **kwargs):
super().__init__(**kwargs)
- With Version A:
__init__
in C
can inherit both a
and b
parameters from A
and B
because it iterates through C.__bases__
, finding both A.__init__
and B.__init__
.
- With Version B:
__init__
in C
will only inherit parameters from A
(since it only references __base__.__init__
), completely ignoring B.__init__
.
Possible Solution for Version B
To make Version B compatible with multiple inheritance, you could modify it to iterate over all base classes rather than relying solely on __base__
. However, handling this gracefully would also require managing potential conflicts or overlaps in parameter names from multiple base classes.
Conclusion
Yes, the creator of Version A is correct. Version B, in its current form, does not fully support multiple inheritance due to its reliance on a single __base__
class. Version A, by iterating through all base classes, better accommodates parameters from multiple inheritance hierarchies.