Type casting (multiple parameters of the same type)

Viewed 121

I have the following method declaration:

def method(alpha, size=10, logged_in=False, interactions=False):
    
    if not isinstance(logged_in, bool):
        logged_in = str(logged_in).lower() in ['true']
    
    if not isinstance(interactions, bool):
        interactions = str(interactions).lower() in ['true']

The if statements are needed because, for example, interactions would be falsely set to True if I pass interaction="False" as a parameter (so, I pass a String). While the above does the trick, I wonder if there is a more elegant way to achieve it (note that I have a lot of boolean parameters, beside logged_in and interactions, and I also have some int and float parameters. How could decorators be used here? Could one decorator be used for all bool parameters, and how could I make a general one (i.e., supporting bool, float, int, string)? I found the answer to this question potentially useful, but still wanted to check if there are better ways.

2 Answers

You could try something like this.

def bool_fixer(func):
    table = {"true": True, "false": False}
    def wrapper(*args, **kwargs):
        for key, val in kwargs.items():
            if isinstance(val, str) and val.lower() in table:
                kwargs[key] = table[val.lower()]
        return func(*args, **kwargs)
    return wrapper

@bool_fixer
def method(...):
    do something

after the decorator does its magic then all of the string "true" and "false" will turn into their python bool equivalent and you can skip all the checking in the method function.

If you want it to cover ints and floats too:

import re
def converter(func):
    patterns = {r"\d*": int, r"\d*?\.\d*": float}
    table = {"true": True, "false": False}
    def wrapper(*args, **kwargs):
        for key, val in kwargs.items():
            if isinstance(val, str):
                if val.lower() in table:
                    kwargs[key] = table[val.lower()]
                    continue
                for pattern in patterns:
                    if re.match(pattern, val):
                        kwargs[key] = patterns[pattern](val)
        return func(*args, **kwargs)
    return wrapper

or

def converter(func):
    table = {"true": True, "false": False}
    def wrapper(*args, **kwargs):
        for key, val in kwargs.items():
            if isinstance(val, str):
                if val.lower() in table:
                    kwargs[key] = table[val.lower()]
                elif val.isdigit():
                    kwargs[key] = int(val)
                elif re.match(r"\d*?\.\d*", val):
                    kwargs[key] = float(val)
        return func(*args, **kwargs)
    return wrapper

This decorator should reliably handle bools, ints and floats. As written this will modify keyword arguments that are str type. It first checks to see if the str is the words "[Tt]rue" or "[Ff]alse". The next test is checking to see if the str is an integer (a simple cast test works). Int is checked first because the formatting is more particular than float, since int must be only numbers, but float can contain decimal points. Finally, it tries a cast to float. If all of these casts fail, then we assume it is an actual str that shouldn't be tampered with.

def cast_arguments(method):
    """decorator"""

    def wrapper(*args, **kwargs):
        for key, value in kwargs.items():

            # change special strings to bools
            if isinstance(value, str):
                if value.lower() == "true":
                    kwargs[key] = True
                elif value.lower() == "false":
                    kwargs[key] = False
                else:
                    try:
                        # change ints to int (int cannot have ".")
                        kwargs[key] = int(value)
                    except ValueError:
                        try:
                            # change floats to float (can have ".")
                            kwargs[key] = float(value)
                        except ValueError:
                            # this is an actual string
                            pass

        return method(*args, **kwargs)
    return wrapper


class Test:
    @cast_arguments
    def method(self, logged_in=False, interactions=False, other=False):
        print("logged_in: ", logged_in, type(logged_in))
        print("interactions: ", interactions, type(interactions))
        print("other: ", other, type(other))


c = Test()
c.method(logged_in="1", interactions="True", other="3.14")

output:

$ python test.py
logged_in:  1 <class 'int'>
interactions:  True <class 'bool'>
other:  3.14 <class 'float'>
Related