1

Context

Suppose one has a list of algorithms, which each have a multiple run/parameter configurations. Next, one wrote a generic function def get_mdsa_configs(self) -> List[MDSA_config]: that returns an list of all the possible run configurations of an algorithm. However, as you can see it currently returns a list with MDSA_Config objects. This should be a generic object (a generic algorithm run configuration object). So to resolve that, one could return that object as a dict, such that it returns: def get_mdsa_configs(self) -> List[dict]:.

Doubt

However, I was curious, what should one ideally do if the return type indeed depends on the function input arguments. For example, what if I want to get the algorithm configuration of another algorithm (without returning a dict)?

I had the following thoughts on this:

  1. If your return type is a function of the input arguments, you are doing something wrong/sub-optimal.
  2. You can specify a list of possible return types instead of specifying exactly what will be returned.
  3. It may indeed occur that in some cases, you do not know in advance exactly what the return type will be.

Question

In Python, what is the best practice on specifying the return type of a function whose return type is a function of the input arguments?

Code

For completeness, attached it the code which led me to think about this question:

"""Contains the specification of and maximum values of the algorithm
settings."""


from typing import List


class MDSA_config:
    """Create a particular configuration for the MDSA algorithm."""

    def __init__(self, mdsa_config: dict) -> None:

        for property, value in mdsa_config.items():
            if property == "m_vals":
                # Verify type of parameters
                if not isinstance(value, int):
                    raise TypeError(
                        "m_val is not of type:int. Instead it is of "
                        + f"type:{type(value)}"
                    )

                # List of the algorithm parameters for a run settings dict.
                self.m_val = value
            else:
                raise KeyError(
                    f"Error, the key:{property} is not supported "
                    "for the MDSA configuration."
                )


#
class MDSA:
    """Specification of algorithm specification. Algorithm: Minimum Dominating
    Set Approximation by Alipour.

    Example usage: default_MDSA_alg=MDSA(m_vals=list(range(0, 4, 1)))
    """

    def __init__(
        self,
        m_vals: List[int],
    ) -> None:

        self.min_m_vals: int = 0
        self.max_m_vals: int = 3

        # Verify type of parameters.
        if not isinstance(m_vals, List):
            raise TypeError(
                "m_vals is not of type:List[int]. Instead it is of "
                + f"type:{type(m_vals)}"
            )

        # Verify values of parameters.
        for m_val in m_vals:
            if m_val < self.min_m_vals:
                raise ValueError(
                    "Error, the minimal supported value for "
                    + f"m_vals is:{self.min_m_vals}, yet we found:{m_vals}"
                )
            if m_val > self.max_m_vals:
                raise ValueError(
                    "Error, the maximum supported value for "
                    + f"m_vals is:{self.min_m_vals}, yet we found:{m_vals}"
                )

        # List of the algorithm parameters for a run settings dict.
        self.alg_parameters = {"m_vals": m_vals}

and the function, which also runs on other algorithms defined analog to the MDSA() object, is written as:

def get_algo_config_dicts(algo_spec:dict) -> List[dict]:
    """Returns a list of MDSA_config objects."""
    mdsa_configs = []

    keys = algo_spec["alg_parameters"].keys()
    print(f"keys={keys}")
    values = (algo_spec["alg_parameters"][key] for key in keys)
    print(f"values={values}")
    alg_settings = [
        dict(zip(keys, combination))
        for combination in itertools.product(*values)
    ]

    for algo_config in alg_settings:
        if algo_spec["name"] == "MDSA":
            mdsa_configs.append(MDSA_config(algo_config).__dict__)
        else:
            raise NameError(f"Algorithm:{algo_spec['name']} not yet "
            "supported.")
    return mdsa_configs

Which can be called with:

mdsa = MDSA(list(range(0, 4, 1)))
get_algo_configs(mdsa.__dict__)
4
  • 1
    "If your return type is a function of the input arguments, you are doing something wrong/sub-optimal." Probably this. What are you doing with the object of unknown type that is returned? Oct 31, 2022 at 11:48
  • Thank you, I combine it with other configuration settings to generate run configurations for an experiment. I think in this case I should consider the algorithm specification as dicts instead of objects (Like I did in the get_algo_config_dicts, that alleviates the concern.) Still I have them as objects such that it is easier for the users to create valid algorithm specification dicts.
    – a.t.
    Oct 31, 2022 at 12:00
  • You haven't defined a generic function, which is a function whose static type involves at least one TypeVar. What is supposed to be generic about get_algo_config_dicts/get_algo_configs?
    – Jasmijn
    Oct 31, 2022 at 22:00
  • @Jasmijn thank you for your explanation and question. Initially the get_algo_config_dicts was named get_mdsa_config_dicts, and it only worked on the MDSA algorithm. Then by making it return dicts, instead of the MDSA()/MDSA_config() object, I was able to return the same type (dict) each call and run it on all algorithm specifications analog to MDSA() and MDSA_config(). With generic I meant that aspect of compatibility with other algorithms as well. I was not aware there was a specific/formal definition of generic. Thank you, I updated the question accordingly!
    – a.t.
    Nov 1, 2022 at 8:58

1 Answer 1

3

This is a common situation in OO programming, even in less dynamically typed OO languages than Python.

Functions which can return different types of objects based on their input are called factory methods - I guess you have heard of that pattern. They usually produce objects having a common base class or common supertype. Sometimes, it can make sense to use the ubiquitous object as base (which, however, restricts the usage for the caller to things like putting the result into a container, check for equality or calculating a hash code).

Of course, in a language like Python, information about the (return) type can be passed implicity, utilizing duck typing.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.