Understanding the Issue with Calling a Function using Only kwargs
In Python, when calling a function, we often encounter situations where some parameters are required while others are optional. In such cases, it’s common to have a mix of positional and keyword arguments (args
and kwargs
) to accommodate both types of parameters.
However, in the context of this question, there’s an interesting issue related to how Python handles functions with specific argument requirements. Specifically, we’re dealing with a situation where a function expects both positional and keyword-only arguments (kwargs) but only maps kwargs when they’re passed as positional args.
Understanding Positional vs Keyword-Only Arguments
In Python 3.x, all non-keyword arguments are positional by default. This means that if you pass positional arguments to a function, those arguments take precedence over any provided keyword arguments.
On the other hand, keyword-only arguments can only be accessed within the function definition itself and not from outside the function’s scope.
The TA-Lib Problem: Mapping kwargs
In this specific scenario, we’re working with a TA-lib wrapper that utilizes Python’s abstract Function
class to invoke functions. The problem arises when trying to map keyword arguments (kwargs
) passed to the wrapper class.
Given the provided code snippet and explanation of how the abstract.Function
works, let’s break down the issue:
def Function(function_name, *args, **kwargs):
func_name = function_name.upper()
if func_name not in _func_obj_mapping:
raise Exception('%s not supported by TA-LIB.' % func_name)
return _Function(
func_name,
# The `_function` attribute holds the underlying function object.
_func_obj_mapping[func_name],
*args, **kwargs
)
When we call abstract.Function(function_name)
with positional arguments and keyword arguments (kwargs
), Python attempts to map the non-keyword arguments (positional) first.
The Problem: Mapping Only Expected Args
However, as pointed out in the question, the TA-lib wrapper class doesn’t seem to be properly mapping the optional parameters passed via kwargs
. This is where things get confusing.
In the provided code snippet, we have:
def augment(self, df, concat=False):
fn_result = self.ta_lib_function(self.ta_lib_kwargs) # Problem here
...
Here, self.ta_lib_function
expects a single argument (fn_result
). However, inside this function call, self.ta_lib_kwargs
is passed as if it’s a positional argument.
This issue arises because of how Python handles positional and keyword-only arguments. In this case, since fn_result
is not a keyword-only argument, it takes precedence over the optional parameters in kwargs
.
The Solution: Use Default Argument Values
To solve this problem, we need to rethink our approach. Instead of relying on default values for positional arguments, let’s use keyword-only defaults.
Here’s an updated version of the Function
class:
def Function(function_name, *, fn_result, *args, **kwargs):
func_name = function_name.upper()
if func_name not in _func_obj_mapping:
raise Exception('%s not supported by TA-LIB.' % func_name)
return _Function(
func_name,
# The `_function` attribute holds the underlying function object.
_func_obj_mapping[func_name],
fn_result,
*args, **kwargs
)
Now, fn_result
is a keyword-only argument, and we can safely map any provided optional parameters (kwargs
) to it.
Adapting the TA-Lib Wrapper Class
With this updated approach in mind, let’s revisit our TA-lib wrapper class:
import pandas as pd
from talib import abstract
class TALibAugments():
"""
Augment with TA-lib functions
Parameter documentation: https://mrjbq7.github.io/ta-lib/
Function Sample: real = MA(close, timeperiod=30, matype=0)
"""
def __init__(self, function_name, out_cols, **kwargs):
self.ta_lib_function = abstract.Function(function_name, fn_result=None, *args, **kwargs)
# The `fn_result` keyword-only argument will be mapped to `_Function`.
# It's now safe to use `kwargs` as positional arguments.
self.out_cols = out_cols
def augment(self, df, concat=False):
# Now we can safely map optional parameters (`kwargs`) to `fn_result`.
fn_result = self.ta_lib_function(fn_result=None, *self.out_cols, **{**self.out_cols, **self.ta_lib_kwargs})
df_out = None
if len(self.out_cols) == 1:
df_out = pd.DataFrame(fn_result, index=df.index, columns=self.out_cols)
else:
df_out = pd.DataFrame()
for i in range(0, len(fn_result)):
df_out[self.out_cols[i]] = pd.Series(fn_result[i], index=df.index)
return df_out
Now that we’ve addressed the issue with keyword-only arguments, our TA-lib wrapper class should work as expected.
Conclusion
In this blog post, we delved into a fascinating topic related to Python’s function calls and positional vs keyword-only arguments. We also explored how to handle functions with specific argument requirements using default values for positional arguments.
By applying these concepts and adapting the provided code snippet accordingly, we’ve created a more robust TA-lib wrapper class that correctly maps optional parameters passed via kwargs
.
Last modified on 2023-11-27