Login With Github

Best Practices And Tips For Python Conditional Statements

Preface

A few days ago, I wrote an article to give some tips for Python variables and code quality.

Writing the conditional statements is an integral part of the coding process.

This article will focus on the tips when writing conditional statements in Python.

If you are a python beginner and are not familiar with conditional statements in python, read the python conditional statements tutorial first.

Conditional Statements In Python

Python supports the most common if/else statements, but the switch/case statements which are common in other programming languages are not supported in Python.

In addition, Python provides else statements for the for/while loops and try/except statements.

The following will go from the three aspects of best practices, common tips, and common traps to talk about how to write good conditional branching statements.

Best Practices

1. Avoid multi-layer branch nesting

If this article can only be reduced to one sentence, then the sentence must be "Avoid branch nesting as possible as you can."

Too deep branch nesting is one of the most common mistakes that many novice programmers make. If a newbie JavaScript programmer writes the multi-layer branch nesting, then there might be multiple curly braces, layer after layer: if { if { if { ... }}}, which is commonly known as "Nested If Statement Hell."

However, Python uses indentation instead of {}, so too deep nested branches will produce more serious consequences than in other languages. For example, too many levels of indentations can easily make the code exceed the limit of words per line specified in PEP8. Let's take a look at the following code:

def buy_fruit(nerd, store):
    """Go to the fruit store to buy apples.

    - First, check if the store is open.
    - If there are apples, buy 1
    - If the money is not enough, go home to take more money.
    """
    if store.is_open():
        if store.has_stocks("apple"):
            if nerd.can_afford(store.price("apple", amount=1)):
                nerd.buy(store, "apple", amount=1)
                return
            else:
                nerd.go_home_and_get_money()
                return buy_fruit(nerd, store)
        else:
            raise MadAtNoFruit("no apple in store!")
    else:
        raise MadAtNoFruit("store is closed!")

The biggest problem with the above code is that it follows the original conditional branching directly, resulting in just a dozen lines of code containing three nested branches.

Such kind of code has poor readability and maintainability. However, we can use a very simple trick "End the function in advance" to optimize the code:

def buy_fruit(nerd, store):
    if not store.is_open():
        raise MadAtNoFruit("store is closed!")

    if not store.has_stocks("apple"):
        raise MadAtNoFruit("no apple in store!")

    if nerd.can_afford(store.price("apple", amount=1)):
        nerd.buy(store, "apple", amount=1)
        return
    else:
        nerd.go_home_and_get_money()
        return buy_fruit(nerd, store)

"End the function in advance" means using a statement such as return or raise to terminate the function in the branch ahead of time. For example, in the new buy_fruit function, when the branch condition is not met, we simply throw an exception and end the code branch. Such kind of code has no nested branches, so it's more straightforward and easier to read.

2. Encapsulate those logical judgments that are too complicated

If an expression in a conditional branch is too complicated (for example, there are too many not/and/or), then the readability of the code will be greatly reduced, such as the following code:

# If the activity is still active and the remaining quota of the activity is greater than 10, then
# 10,000 coins will be issued for all the active users whose genders are female, or levels are greater than 3.
if activity.is_active and activity.remaining > 10 and \
        user.is_active and (user.sex == 'female' or user.level > 3):
    user.add_coins(10000)
    return

We can consider encapsulating the specific branching logic into functions or methods to simplify the code:

if activity.allow_new_user() and user.match_activity_condition():
    user.add_coins(10000)
    return

In fact, the previous comment can be removed after rewriting the code, because the latter piece of the code has been self-documenting. As for the specific problem "What kind of users meet the activity conditions?", it should be solved through the specific match_activity_condition() method.

Hint: Proper encapsulation will improve the code readability directly. In fact, if the above judgment logic for the activity appears more than once in the code, encapsulation is even more necessary. Otherwise, repeating the code will greatly destroy the maintainability of the logic.

3. Pay attention to the duplicate code under different branches

The duplicate code is the enemy of code quality, and conditional statements are very easy to become the hardest hit areas for duplicate code. Therefore, we need to pay attention not to produce unnecessary duplicate code when writing conditional branch statements.

Let's look at the following example:

# Create new user profiles for new users, otherwise update the old data.
if user.no_profile_exists:
    create_user_profile(
        username=user.username,
        email=user.email,
        age=user.age,
        address=user.address,
        # For new users, set the users' points to 0.
        points=0,
        created=now(),
    )
else:
    update_user_profile(
        username=user.username,
        email=user.email,
        age=user.age,
        address=user.address,
        updated=now(),
    )

In the above code, we can see that under different branches, the program calls different functions and does different things. However, because of the existence of the duplicate code, it is difficult to distinguish the difference between the two easily.

In fact, thanks to the dynamic feature of Python, we can simply rewrite the above code to make the readability significantly improved:

if user.no_profile_exists:
    profile_func = create_user_profile
    extra_args = {'points': 0, 'created': now()}
else:
    profile_func = update_user_profile
    extra_args = {'updated': now()}

profile_func(
    username=user.username,
    email=user.email,
    age=user.age,
    address=user.address,
    **extra_args
)

When you're writing branches, pay attention to the duplicate code blocks generated by the branches. If you can remove them simply, don't hesitate.

4. Use ternary expressions with caution

The ternary expression is a syntax that is only supported after Python 2.5. Python community once thought that ternary expressions were unnecessary, and we needed to simulate it using x and a or b.

The fact is that, in many cases, the code that uses if/else statements is more readable. Pursuing ternary expressions blindly can entice you easily to write out complex and poorly readable code.

So, remember that ternary expressions only can be used to handle simple logical branches.

language = "python" if you.favor("dynamic") else "golang"

Use the common if/else statements for the most cases.

Common Tips

1. "De Morgan's Law"

Sometimes when executing branch judgments we may write code like this:

# Refuse to provide the service if the user is not logged in or the user is not using chrome.
if not user.has_logged_in or not user.is_from_chrome:
    return "our service is only available for chrome logged in user"

Didn't you need to think about it for a while to understand what the code wanted to do, when you first saw the code? This is because there are 2 not and 1 or in the above logical expression. We humans are not good at dealing with too many "Not" and "Or" logical relationships.

Thus De Morgan's Law comes. In layman's terms, De Morgan's law is making not A or not B equivalent to not (A and B). So you can rewrite the above code like this:

if not (user.has_logged_in and user.is_from_chrome):
    return "our service is only open for chrome logged in user"

The code is much easier to read now, isn't it? De Morgan's Law is very useful for simplifying the logic of code in conditional branches in many cases.

2. Boolean values of custom objects

We often say that, in Python, "everything is an object." In fact, we can also use a lot of magic methods (which are called user-defined methods in documents) to customize the various behaviors of objects.

For example, all objects in Python have their own Boolean values:

  • Objects whose Boolean values are false:None, 0, False, [], (), {}, set(), frozenset(), ... ...
  • Objects whose Boolean values ​​are true: non-0 values, True, non-empty sequences, tuples, general user class instances, ... ...

You can check the Boolean value of an object easily with the built-in function bool(). And this value is also used for the conditional branch judgment in Python:

>>> bool(object())
True

It's important to know that although the Boolean values ​​for all user class instances are true, Python provides a way to change the behavior: the magic method __bool__ of custom classes (It is __nonzero__ in Python 2.X). After the __bool__ method is defined for a class, its return value will be treated as a Boolean value of the class instance.

In addition, __bool__ is not the only way to work on the boolean value of an instance. If you don't define the __bool__ method for the class, Python will also try to call the __len__ method (which means it will call the len function on any sequence object) to determine whether the instance is true or false by checking the result is 0 or not.

Let's take a look at the following code:

class UserCollection(object):

    def __init__(self, users):
        self._users = users


users = UserCollection([piglei, raymond])

if len(users._users) > 0:
    print("There's some users in collection!")

In the above code, the length of users._users is used to determine whether the UserCollection contains anything. In fact, you can add the __len__ magic method to the UserCollection to make the above branch simpler:

class UserCollection:

    def __init__(self, users):
        self._users = users

    def __len__(self):
        return len(self._users)


users = UserCollection([piglei, raymond])

# The UserCollection object itself can be used for Boolean judgment after defining the __len__method.
if users:
    print("There's some users in collection!")

We can let the class itself express its Boolean value by defining the magic methods __len__ and __bool__, making the code more pythonic.

3. Use all() / any() in conditional statements

The two functions all() and any() are very suitable for conditional statements. Each of the two functions accepts an iterable object and returns a Boolean value:

  • all(seq): Return True only if all objects in seq are True, otherwise return False
  • any(seq): Return True as long as any object in seq is True, otherwise return False

Let's say we have the following code:

def all_numbers_gt_10(numbers):
    """Return True only if all numbers in the sequence are greater than 10.
    """
    if not numbers:
        return False

    for n in numbers:
        if n <= 10:
            return False
    return True

If you use the built-in function all() with a simple generator expression, the above code can be written like this:

def all_numbers_gt_10_2(numbers):
    return bool(numbers) and all(n > 10 for n in numbers)

Isn't it become simpler and more efficient?

4. Use the else statements in try/while/for

Let's take a look at the following function:

def do_stuff():
    first_thing_successed = False
    try:
        do_the_first_thing()
        first_thing_successed = True
    except Exception as e:
        print("Error while calling do_some_thing")
        return

    # It'll do the second thing only when first_thing completes successfully.
    if first_thing_successed:
        return do_the_second_thing()

In the function do_stuff, the second function will be called only after do_the_first_thing() has been successfully called (no exceptions are thrown). So, we need to define an extra variable first_thing_successed as a tag.

In fact, we can achieve the same effect in a simpler way:

def do_stuff():
    try:
        do_the_first_thing()
    except Exception as e:
        print("Error while calling do_some_thing")
        return
    else:
        return do_the_second_thing()

After the else branch is appended to the try statement block, the do_the_second_thing() under the branch will only be executed after all statements under the try have been executed normally (no exceptions, no return, break, etc.).

Similarly, the for/while loop in Python also supports adding the else branch, which means that the code under the else branch will be executed only when the iteration objects used by the loop are exhausted normally, or the condition variable used by the while loop becomes False.

Common Traps

1. Comparison with the None

In Python, there are two ways to compare variables: == and is, and there are fundamental differences between the two:

  • ==: Indicate whether the two values ​​pointed to are consistent
  • is: Indicate whether the contents in memory pointed to are the same (whether id(x) is equal to id(y) ).

None is a singleton object in Python. If you want to judge if a variable is None, you should use is instead of == because only is can indicate strictly whether a variable is None.

Otherwise, there may occur the following situations:

>>> class Foo(object):
...     def __eq__(self, other):
...         return True
...
>>> foo = Foo()
>>> foo == None
True

In the above code, the Foo class satisfies the condition == None easily by customizing the magic method __eq__ .

So, use is instead of == when determining whether a variable is None.

2. The precedence for and and or

Take a look at the two expressions below, and guess their values ​​are the same?

>>> (True or False) and False
>>> True or False and False

The answer is: no. Their values ​​are False and True respectively.

The key to the problem is that the and operator takes precedence over or. So actually the second expression above is True or (False and False) in Python. So the result is True.

Pay extra attention to the precedence of and and or when writing expressions that contain multiple and and or. Even if the execution priority is exactly what you need, you can also add extra parentheses to make the code clearer.

Conclusion

The branching statements are inevitable when writing code, so we need to pay special attention to its readability.

Note

In fact, x and a or b won't always give you the correct result. Only when the Boolean values ​​of both a and b are true, this expression can work properly, which is determined by the short-circuit characteristics of the logic operations. You can try running True and None or 0 on the command line, and the result is 0 instead of None.

0 Comment

temp