How to reduce Cognitive Complexity in this Python method

Question:

I am faced with a challenge.

I have an Python method implemented and the SonarLint plugin of my PyCharm warns me with the message: "Refactor this function to reduce its Cognitive Complexity from 19 to the 15 allowed." but I can’t see how to reduce the complexity.

My Python method is:

def position(key):
    if key == 'a':
        return 0
    elif key == 'b':
        return 1
    elif key == 'c':
        return 2
    elif key == 'd':
        return 3
    elif key == 'e':
        return 4
    elif key == 'f':
        return 5
    elif key == 'g':
        return 6
    elif key == 'h':
        return 7
    elif key == 'i':
        return 8
    elif key == 'j':
        return 9
    elif key == 'k':
        return 10
    elif key == 'l':
        return 11
    elif key == 'm':
        return 12
    elif key == 'n':
        return 13
    elif key == 'ñ':
        return 14
    elif key == 'o':
        return 15
    elif key == 'p':
        return 16
    elif key == 'q':
        return 17
    else:
        logger.info('error')

And the warning of SonarLint is:

enter image description here

And if I click on show issue locations it gives me the explanation of how the Cognitive Complexity is calculated:
enter image description here

I can’t see how to reduce the complex of this function. I know that I can implement another method with the same behaviour using things like the ascii code, but it’s not the point of this question.

The summary of the question is how can I follow the suggestion of SonarLint, I mean, how can I reduce the Cognitive Complexity from 19 to the 15 of this particular method.

Something I’ve noticed is that if I remove elif statements until I have only 14 characters cases, the warning magically disappears.

Answers:

I’ve found what’s happening. The Plugin SonarLint has a maximum number of Cognitive Complexity, as you can see in this capture:

enter image description here

So SonarLint doesn’t say you that you can simplify your method, it tells you that this method is more complex than the prefixed limit that SonarLint has setted.

This is the reason because if I delete elif until I reach the magic number of 15 for the Cognitive Complexity the warning magically disappears.

¡¡WARNING!! It’s not recommendable to increase the SonarLint limit of Cognitive Complexity. The most advisable option is to refactor your method and find another way to do the same.

In my case I’ve implemented the next method instead (there are another shorter solutions, but because I have to use spanish alphabet I decided to use what for mi is the most readable solution):

 def position(key: str) -> (int, None):
    char_map = {
        'a': 0,
        'b': 1,
        'c': 2,
        'd': 3,
        'e': 4,
        'f': 5,
        'g': 6,
        'h': 7,
        'i': 8,
        'j': 9,
        'k': 10,
        'l': 11,
        'm': 12,
        'n': 13,
        'ñ': 14,
        'o': 15,
        'p': 16,
        'q': 17,
        'r': 18,
        's': 19,
        't': 20,
        'u': 21,
        'v': 22,
        'w': 23,
        'x': 24,
        'y': 25,
        'z': 26
    }

    try:
        output = char_map[key]
    except KeyError:
        logger.error('KeyError in position() with the key: ' + str(key))
        return None

    return output

You can refactor this to look some thing like this:

def position(key):
     values ="abcdefghijklmnñopq"
     try:
         return values.index(key)
     except Exception as e:
         print(e) #you can use logger here if you want
>>> position("a")
0
>>> position("z")
substring not found
Answered By: Bendik Knapstad

(Edit: this approach breaks because of the "ñ" character, but I’ll leave it visible in case someone wants to use the regular alphabet.)


You could use ord to turn the key into a number, and enforce a valid range of keys without having to specify "abcdefghijklmnop" like some other answers suggest.

Hardcoded range:


def position(key:str) -> int:
    value = ord(key) - ord("a")
    if 0 <= value <= ord("p") - ord("a"):
        return value
    else:
        logger.info('error')

Variable range with default values set to "a" and "p":

def position(key:str; min_key="a", max_key="p") -> int:
    value = ord(key) - ord(min_key)
    if 0 <= value <= ord(max_key) - ord(min_key):
        return value
    else:
        logger.info('error')
Answered By: Guimoute

What SonarLint is detecting is a "code smell" that suggests you might be able to write your code in a more efficient and understandable way. It doesn’t mean it’s an actual problem, just that you might want to take a closer look. That’s what linters are for, after all.

In this case, it’s right. If I see a tower of if statements, I have to read them all to make sure the code is doing what I think it’s doing. In fact, I suspect that it’s doing something non-obvious because otherwise, why write it like that? (And also, it’s very easy to make a mistake when writing the code, which can be very hard to find and debug.) Even using a dictionary, which would be an improvement here, can have this problem.

In fact, because it’s a tower of ifs, I didn’t notice for two weeks that there was an ñ in there! And here I am updating my answer to account for it.

In general, a better way is to exploit the pattern in the data. In the following code it is easy to see that we are translating characters to integers, and that a is zero and the subsequent letters use the next numbers in sequence. You don’t have to look at what each letter translates to. It is much easier to read and understand.

def position(key, offset=ord("a")):
    pos = ord(key) - offset
    if 0 <= pos <= 17:
        return pos
    logger.info("error")

Unfortunately, a version that deals with the ñ is slightly more complicated.

def position(key, offset=ord("a")):
    if key = "ñ":
        return 14
    pos = ord(key) - offset
    if 0 <= pos <= 13:
        return pos
    if 14 <= pos <= 16:
        return pos + 1
    logger.info("error")

This may or may not be an improvement on a dictionary lookup, but at least it is clear at a glance that ñ receives special treatment.

Since code is written once but may be read many many times, it pays to optimize for readability, even (usually) over performance. Remember, other people will be reading your code… and "you in six months" counts as "other people."

P.S. A possible upside of the original code is that if you need to figure out what number each letter translates to, or vice versa, you can see that a lot more easily than in a pattern-exploiting version. If that’s important to you, especially if the mapping is not obvious, using a dictionary is still usually a better solution.

Answered By: kindall
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.