0

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)

An old man in the sea.
  • 1,169
  • 1
  • 13
  • 30
  • 1
    Can you clarify what are you trying to do with `eval(param_1_param_2_)`? And you don't seem to be using the param_1 and param_2 from your function. – Carlos Bergillos May 07 '22 at 11:06
  • What do you mean by branching out ? can you give a real example or a more complete code ? as @CarlosBergillos what is the point of eval ? have you tried using decorators ( the python way of altering functions ) ? – Ibraheem Alyan May 07 '22 at 11:41
  • Only use eval if you absolutely trust the input source. – OTheDev May 07 '22 at 11:43

2 Answers2

2

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(): Uses importlib.import_module to fetch the attribute (function, class, variable, etc.) directly
  • typing.Protocol to type the expected function signature. Via PEP 554's Callback protocol section.
  • Default fallbacks
  • Handles functions (callables) directly as well
  • Moved val to the last param of function. 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.

tony
  • 870
  • 7
  • 16
  • Your answer seems very interesting. I'll study it, and I'll get back to you (possibly later in the week). Thanks ;) – An old man in the sea. May 07 '22 at 12:31
  • 1
    Thanks, it worked as expected. Just a question: Why import 'Callable', just to use 'callable'? nowhere do you use type hinting with Callable... ;) – An old man in the sea. May 09 '22 at 22:07
  • @Anoldmaninthesea. Corrected that, [`typing.Callable`](https://docs.python.org/3/library/typing.html#callable) isn't needed since a more specific callback annotation is used: [`typing.Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol). Also I fixed `FN_MAP` by adding a [`typing.Union`](https://docs.python.org/3/library/typing.html#typing.Union). – tony May 10 '22 at 14:00
0

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))
aquaplane
  • 127
  • 4
  • Thanks for your answer! However, this will create the same problem I was trying to avoid which is creating a huge sequence of if. The parameters can take dozens of values... – An old man in the sea. May 07 '22 at 12:51