What is a “Python craftsman”?

I’ve always felt that programming is something of a “craft,” because elegant and efficient code is as enjoyable as the perfect craft.

In the process of crafting code, there are big projects: what architecture to use, what design patterns to use. There are also more minor details, such as when to use Exceptions or how to name variables. The truly great code is made up of countless great details.

This “Python Craftsman” series is my little experiment. It focuses on sharing some of the “small” aspects of Python programming. I hope I can help every programmer on the road.

Series of articles:

  • Python Craftsman: Use variables to improve code quality
  • Python craftsman: Techniques for writing conditional branching code

The preface

Writing conditional branching code is an integral part of the coding process.

As a road metaphor, the code in the real world is never a straight highway, but more like a map of a city with countless fork roads. We coders are like drivers, and we need to tell our program whether to turn left or right at the next corner.

It is important to write good conditional branching code, because poor, complex branching is very confusing and can degrade code quality. So, this article will focus on some of the things you should do when writing branching code in Python.

Branching code in Python

Python supports the most common if/else conditional branch statements, though it lacks the switch/case statements common in other programming languages.

In addition, Python provides else branches for for/while loops and try/except statements, which can come in handy in special situations.

I’m going to talk about best practices, common techniques, and common pitfalls for writing good conditional branching code.

Best practices

1. Avoid multi-layer branch nesting

If this article could be reduced to a single sentence, it would be “do everything possible to avoid branch nesting.”

Deep branch nesting is one of the most common mistakes many novice programmers make. If a novice JavaScript programmer writes many layers of branch nesting, you might see layer after layer of braces: if {if {if {… }}} instead! It is commonly known as the Nested if Statement Hell.

But because Python uses indentation instead of {}, deeply nested branches can have more serious consequences than in other languages. For example, too many indentation levels can easily cause code to exceed PEP8’s word-per-line limit. Let’s look at this code:

Def buy_fruit(nerd, store): """ def buy_fruit(nerd, store): """ 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!" )Copy the code

The biggest problem with the above code is that it translates the original conditional branching requirement too directly, resulting in only a dozen lines of code with three levels of nested branching.

Such code is poorly readable and maintainable. But we can optimize this code with a very simple trick: “End early” :

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)Copy the code

Premature termination refers to the use of a return or raise statement in a function to terminate the function in a branch before it is terminated. For example, in the new buy_fruit function, when the branch condition is not met, we simply throw an exception to end the branch. Such code has no nested branches and is more direct and readable.

Encapsulate overly complex logical judgments

If the expression in the conditional branch is too complex and there are too many not/and/ OR, then the readability of the code is impaired, as in this code:

# If the activity is still open and the remaining quota is more than 10, all genders are female, If activity.is_active and activity.remaining > 10 and \ user.is_active and (user.sex == 'female' or user.level > 3): user.add_coins(10000) returnCopy the code

For such code, we can simplify the code by encapsulating specific branch logic into functions or methods:

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

In fact, after rewriting the code, the previous comment text can actually be removed. Because this code is already self-explanatory. What kind of users meet the activity criteria? This question should be answered by the specific match_activity_condition() method.

Hint:Proper encapsulation not only directly improves the readability of the code, but in fact is necessary if the above activity logic occurs more than once in the code. Otherwise, repeating the code would greatly damage the maintainability of the logic.

3. Watch out for duplicate code under different branches

Duplicate code is the natural enemy of code quality, and conditional branch statements can easily become a disaster area of duplicate code. So when we write conditional branching statements, we need to be careful not to produce unnecessary duplicate code.

Let’s take a look at this example:

# create a new user profile for a new user, otherwise update the old profile if user.no_profile_exists: Create_user_profile (username=user.username, email=user.email, age=user.age, address=user.address, 0 points=0, created=now(),) else: update_user_profile( username=user.username, email=user.email, age=user.age, address=user.address, updated=now(), )Copy the code

In the above code, we can see at a glance that under different branches, the program calls different functions and does different things. But, because of the repetitive code, it’s hard to tell the difference.

In fact, thanks to Python’s dynamic nature, we can easily rewrite the above code to make it significantly more readable:

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
)
Copy the code

When you’re writing branch code, pay extra attention to duplicate code blocks generated by branches, and don’t hesitate to kill them if you can easily.

4. Use ternary expressions with caution

Ternary expressions are a syntax supported by Python since version 2.5. Until then, the Python community had thought that ternary expressions were unnecessary and that we needed to simulate them using x and a or B.

The truth is that, in many cases, code that uses plain if/else statements is actually more readable. The blind pursuit of ternary expressions can easily tempt you to write complex, unreadable code.

So, remember to use ternary expressions for simple branches of logic.

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

For most cases, use plain if/else statements.

Common techniques

1. Use de Morgan’s Law

When making branching judgments, we sometimes write code like this:

If not user.has_logged_in or not user.is_from_chrome: return "our service is only available for chrome logged in user"Copy the code

When you first see the code, do you have to think for a moment to understand what it wants? This is because there are two not and one OR in the logical expression above. We humans are just not good at dealing with too many “negatives” and “or” logic.

This is where de Morgan’s law comes in. In layman’s terms, de Morgan’s law states that not A or not B is equivalent to not (A and B). With this transformation, the above code can be rewritten to look like this:

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

So, is the code a lot easier to read? Remember de Morgan’s law, a lot of times it’s useful for simplifying code logic in conditional branches.

2. “Boolean true and false” for custom objects

We often say that in Python, “Everything is an object.” In fact, not only is everything an object, but there are a number of magical methods (called user-defined methods in the documentation) that can be used to define the behavior of objects. We can influence code execution in magical ways that we can’t in other languages.

For example, all Objects in Python have their own “Boolean true/false” :

  • Objects whose Boolean value is false:None.0.False.[].(a).{}.set().frozenset(). .
  • Object whose Boolean value is true: not0The numerical,True, non-empty sequences, tuples, ordinary user class instances,… .

With the built-in bool() function, you can easily check whether an object is true or false. Python also uses this value for conditional branching:

>>> bool(object())
True
Copy the code

The point is, although all user class instances have true booleans. But Python provides a way to change this behavior: customize the class’s __bool__ magic method (__nonzero__ in Python 2.x). When a class defines a __bool__ method, its return value is treated as the Boolean value of the class instance.

Also, __bool__ is not the only way to influence whether an instance Boolean is true or false. If the class does not define a __Bool__ method, Python will also try to call the __len__ method (that is, the len function on any sequence object) to determine whether the instance is true or false by the result being zero.

So what does this feature do? 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!" )Copy the code

In the above code, the users._users length is used to determine if the UserCollection has content. In fact, the above branch can be made simpler by adding the __len__ magic method to the UserCollection:

class UserCollection: def __init__(self, users): self._users = users def __len__(self): Return len(self._users) users = UserCollection([piglei, Raymond]) # If users: print("There's some users in collection!" )Copy the code

By defining the magic methods __len__ and __Bool__, we can make the code more pythonic by giving the class itself control over the Boolean true and false values it wants to represent.

3. Use all()/any() in the conditional judgment.

The all() and any() functions are great for use in conditional judgment. These two functions take an iterable and return a Boolean value where:

  • all(seq): only whenseqReturns when all objects in theTrueOtherwise returnFalse
  • any(seq): as long asseqReturns if any of the objects in theTrueOtherwise returnFalse

Suppose 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 TrueCopy the code

Using the all() built-in function and a simple generator expression, the above code could be written like this:

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

Simple and efficient, with no loss of usability.

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

Let’s look at this 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 # if first_thing_successed if first_thing_successed: return do_the_second_thing()Copy the code

In the case of do_stuff, we want the second function call to proceed only if do_the_first_thing() is called successfully (that is, without throwing any exceptions). To do this, we need to define an additional variable, first_thing_successed, as a marker.

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()
Copy the code

After the else branch is appended to the end of the try block, the do_the_second_thing() branch is executed only after all statements under the try have executed normally (i.e., no exceptions, no returns, no breaks, etc.).

Similarly, the For /while loop in Python supports the addition of else branches, which indicate that code under the else branch is executed only when the iterator used by the loop is normally exhausted, or when the condition variable used by the while loop becomes False.

Common pitfalls

1. Comparison with None

In Python, there are two ways to compare variables: == and is, which have fundamentally different meanings:

  • = =: indicates what the two point tovalueAre consistent
  • is: indicates whether they refer to the same content in memory, i.eid(x)Whether is equal to theid(y)

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

Otherwise, the following situation may occur:

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

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

So, when you want to determine if a variable is None, use is instead of ==.

2. Pay attention to the priority of and and or

Take a look at these two expressions and guess if they have the same value.

>>> (True or False) and False
>>> True or False and False
Copy the code

The answer is: no, the values are False and True respectively, did you guess correctly?

The crux of the matter is that the and operator takes precedence over or. So the second expression above is actually True or (False and False) in Python’s view. So the result is True instead of False.

When writing expressions that contain more than one AND and OR, pay extra attention to the precedence of and and OR. Even if the execution priority is exactly what you need, you can add extra parentheses to make the code clearer.

conclusion

That’s the second article in the Python Craftsman series. I don’t know if the content of the article is to your taste.

Branching statements within code are inevitable, so when writing code, we need to pay special attention to its readability to avoid confusing others who see the code.

After reading the article, do you have anything to tease? Let me know in the comments.