'Branching out a function
I've built a function that tries to extract some information from a string.
Before: function (string)
Now, I want to refactor that function by receiving two extra params, call them param_1, param_2.
Now: function(string, param_1:str, param_2:str)
The function is imported to the namespace where the string and parameters reside, and I can only know the exact values of param_1, param_2 at runtime, even though they belong to a long list, which is known in advance.
However, I was thinking that instead of just doing function (string, param_1, param_2), and then branching out with a lot of elif statements
if param_1 == val_11 and param_2 == val_12:
<some_code>
elif param_1 == val_21 and param_2 == val_22:
<some_different_code>
elif (...)
I could just do something like:
def function(string, param_1:str, param_2:str):
new_function = eval(param_1_param_2_<old_function_name>)
return new_function(string)
And then define separately each param_1_param_2_<old_function_name>.
- Is there a more pythonic way of solving my original problem?
- From a software engineering perspective / clean code, should I do something else instead?
Edit: The objective is to extract information from the string. Let's assume the info is dates in a document. Depending on the document type (param_2), and on the author (param_1), the way a date is formatted will differ. The focus of the question is not on better machine learning models, or functions like dateparser (but if you do have a suggestion, leave a comment :) ), but how to 'branch out' the original function.
extract_dates(string, author, doc_type)
Solution 1:[1]
You could use a dictionary mapping of import strings for an easy "registry" of callbacks.
That way you can check the keys, and if needed, have a fallback.
import importlib
from typing import Dict, Protocol, Union
class FnMapProtocol(Protocol):
def __call__(self, val: str):
"""Callback for FN_MAP."""
pass
def default_callback(val: str):
# You could pass or even raise an error depending on the situation
pass
def modern_history_callback(val: str):
pass
FN_MAP: Dict[str, Dict[str, Union[FnMapProtocol, str]]] = {
"HISTORY": {
"MEDIEVAL": "path.to.module.medieval_history_callback",
"MODERN": modern_history_callback,
"DEFAULT": "path.to.module.default_history_callback",
}
}
def get_fn(import_string: str) -> FnMapProtocol:
# Break off the module attribute from module
mod_name, attr_name = import_string.rsplit(".", 1)
# import module
module = importlib.import_module(mod_name)
return getattr(module, attr_name)
def function(param_1: str, param_2: str, val: str):
import_string = default_callback
if param_1 in FN_MAP:
if param_2 in FN_MAP[param_1]:
import_string = FN_MAP[param_1][param_2]
elif "DEFAULT" in FN_MAP[param_1]:
import_string = FN_MAP[param_1]["DEFAULT"]
# check if it's an import string or a callback
if isinstance(import_string, str):
fn = get_fn(import_string)
elif callable(import_string):
fn = import_string
return fn(val)
- Dictionary mapping lookup to look up by key
get_fn(): Usesimportlib.import_moduleto fetch the attribute (function, class, variable, etc.) directlytyping.Protocolto type the expected function signature. Via PEP 554's Callback protocol section.- Default fallbacks
- Handles functions (
callables) directly as well - Moved
valto the last param offunction. The reason why is you may want to expand the input arguments and perhaps add*args, **kwargs. - Not tested by hand, just a pattern - it's one way you could control the flow with import strings.
P.S. I created an article on import strings a while back that may prove helpful: How Django uses deferred imports to scale.
P.P.S. See Werkzeug's import_string and find_modules (usage) as well as Django's and its django.utils.module_loading functions.
Solution 2:[2]
Depending on exactly what you are trying to do, it might be possible to use decorators. A lot of pythonistas would probably suggest that eval() is a dangerous function, given potential security concerns.
Altering a related example from How do I pass extra arguments to a Python decorator?, this should give you a cleaner and more pythonic design.
import functools
def my_decorator(test_func=None,param_1=None, param_2=None):
if test_func is None:
return functools.partial(my_decorator, param_1=param_1, param_2=param_2)
@functools.wraps(test_func)
def f(*args, **kwargs):
if param_1 is not None:
print(f'param_1 is {param_1}')
if param_2 is not None:
print(f'param_2 is {param_2}')
if param_1 is not None and param_2 is not None:
print(f'sum of param_1 and param_2 is {param_1 + param_2}')
return test_func(*args, **kwargs)
return f
#new decorator
my_decorator_2 = my_decorator(param_1=2)
@my_decorator(param_1=2)
def pow2(i):
return i**2
@my_decorator(param_2=1)
def pow3(i):
return i**3
@my_decorator_2(param_1=1, param_2=2)
def pow4(i):
return i**4
print(pow2(2))
print(pow3(2))
print(pow4(2))
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | aquaplane |
