Effective and Ineffective Python Coding Techniques

Hello, Habr! I bring to your attention a translation of the article Good and Bad Practices of Coding in Python by Duomly.



Python is a high-level programming language that focuses on readability. It is developed, maintained and often used in accordance with The Zen of Python or PEP 20.



This article shows some examples of good and bad coding techniques in Python that you are likely to come across.



Using Unpacking to Write Compact Code



Packaging and unpacking are powerful Python tools. You can use decompression to assign values ​​to variables:



>>> a, b = 2, 'my-string' >>> a 2 >>> b 'my-string'
      
      





You can use this to write perhaps the most compact code that swaps the values ​​of variables



 >>> a, b = b, a >>> a 'my-string' >>> b 2
      
      





This is amazing!



Unpacking can also be used to assign values ​​to multiple variables in more complex cases. For example, you can write like this:



 >>> x = (1, 2, 4, 8, 16) >>> a = x[0] >>> b = x[1] >>> c = x[2] >>> d = x[3] >>> e = x[4] >>> a, b, c, d, e (1, 2, 4, 8, 16)
      
      





But instead, you can also use a more concise and perhaps more readable way:



 >>> a, b, c, d, e = x >>> a, b, c, d, e (1, 2, 4, 8, 16)
      
      





That's cool, right? But you can write even cooler:



 >>> a, *y, e = x >>> a, e, y (1, 16, [2, 4, 8])
      
      





The trick is that the variable with * collects values ​​not assigned to other variables.



Using Chaining to Write Compact Code



Python allows you to use chains of comparison operators. Thus, you do not need to check if two or more comparisons are true:



 >>> x = 4 >>> x >= 2 and x <= 8 True
      
      





Instead, you can use a more compact form of writing:



 >>> 2 <= x <= 8 True >>> 2 <= x <= 3 False
      
      





Python also supports chaining variable values. So, if you want to assign the same value to several variables at the same time, you can do it in a simple way:



 >>> x = 2 >>> y = 2 >>> z = 2
      
      





A more compact way is to use unpacking:



 >>> x, y, z = 2, 2, 2
      
      





However, things look even cooler if you use chaining:



 >>> x = y = z = 2 >>> x, y, z (2, 2, 2)
      
      





Be careful when the values ​​are different! All variables refer to the same value.



Check for None



None is not a unique object in Python. It has analogues, for example, null in C-like languages.



You can check whether a variable refers to None using the comparison operators == and ! = :



 >>> x, y = 2, None >>> x == None False >>> y == None True >>> x != None True >>> y != None False
      
      





However, using is and is not preferred:



 >>> x is None False >>> y is None True >>> x is not None True >>> y is not None False
      
      





In addition, it is better to use the construction x is not None , but not less readable alternative ( x is None ).



Sequences and Mappings



You can implement loops in Python in several ways. Python offers several built-in classes to simplify their implementation.



Almost always, you can use a range to get a loop that returns integers:



 >>> x = [1, 2, 4, 8, 16] >>> for i in range(len(x)): ... print(x[i]) ... 1 2 4 8 16
      
      





However, there is a better way to do this:



 >>> for item in x: ... print(item) ... 1 2 4 8 16
      
      





But what if you want to start the loop in the reverse order? Of course, you can use the range again:



 >>> for i in range(len(x)-1, -1, -1): ... print(x[i]) ... 16 8 4 2 1
      
      





But flipping the sequence is a more compact way:



 >>> for item in x[::-1]: ... print(item) ... 16 8 4 2 1
      
      





The pythonic way is to use the reverse sequence to get a loop that will return the elements of the sequence in reverse order:



 >>> for item in reversed(x): ... print(item) ... 16 8 4 2 1
      
      





Sometimes both sequence elements and their corresponding indices are needed:



 >>> for i in range(len(x)): ... print(i, x[i]) ... 0 1 1 2 2 4 3 8 4 16
      
      





It is better to use an enumeration to get another loop that returns the values ​​of indices and elements:



 >>> for i, item in enumerate(x): ... print(i, item) ... 0 1 1 2 2 4 3 8 4 16
      
      





That's cool. But what if it is necessary to sort out two or more sequences? Of course, you can use the range again:



 >>> y = 'abcde' >>> for i in range(len(x)): ... print(x[i], y[i]) ... 1 a 2 b 4 c 8 d 16 e
      
      





In this case, Python also offers a better solution. You can apply zip :



 >>> for item in zip(x, y): ... print(item) ... (1, 'a') (2, 'b') (4, 'c') (8, 'd') (16, 'e')
      
      





You can also combine this method with unpacking:



 >>> for x_item, y_item in zip(x, y): ... print(x_item, y_item) ... 1 a 2 b 4 c 8 d 16 e
      
      





Please keep in mind that a range can be quite useful. Nevertheless, there are cases (as, for example, above) where it is more optimal to use alternatives.

Enumerating a dictionary gives its keys:



 >>> z = {'a': 0, 'b': 1} >>> for k in z: ... print(k, z[k]) ... a 0 b 1
      
      





However, you can use the .items () method and get the keys and their corresponding values:



 >>> for k, v in z.items(): ... print(k, v) ... a 0 b 1
      
      





You can also use the .keys () and .values ​​() methods to iterate over keys and values, respectively.



Comparison with zero



When you have numerical data and you need to check if the numbers are equal to zero, you can (but not always need) use the comparison operators == and ! = :



 >>> x = (1, 2, 0, 3, 0, 4) >>> for item in x: ... if item != 0: ... print(item) ... 1 2 3 4
      
      





The method suggested by Python is to interpret zero as False , and all other numbers as True :



 >>> bool(0) False >>> bool(-1), bool(1), bool(20), bool(28.4) (True, True, True, True)
      
      





With that in mind, you can use if item instead of if item! = 0 :



 >>> for item in x: ... if item: ... print(item) ... 1 2 3 4
      
      





You can follow the same logic and use if not item instead of if item == 0 .



Avoiding mutable optional arguments



Python has a very flexible system for providing arguments to functions and methods. Optional arguments are part of this. But be careful: you usually don't want to use mutable optional arguments. Consider the following example:



 >>> def f(value, seq=[]): ... seq.append(value) ... return seq
      
      





At first glance, it looks like this if you do not specify seq , f () adds value to the empty list and returns something like [value]:



 >>> f(value=2) [2]
      
      





Looks great, right? Not! Consider the following examples:



 >>> f(value=4) [2, 4] >>> f(value=8) [2, 4, 8] >>> f(value=16) [2, 4, 8, 16]
      
      





Surprised? If so, then you are not alone.



It seems that every time a function is called, the same instance of an optional argument (in this case, a list) is provided. Maybe sometimes this will be what you need. However, it is much more likely that you will need to avoid this. One way is as follows:



 >>> def f(value, seq=None): ... if seq is None: ... seq = [] ... seq.append(value) ... return seq
      
      





More compact version:



 >>> def f(value, seq=None): ... if not seq: ... seq = [] ... seq.append(value) ... return seq
      
      





Now we get another conclusion:



 >>> f(value=2) [2] >>> f(value=4) [4] >>> f(value=8) [8] >>> f(value=16) [16]
      
      





In most cases, this is what you need.



Avoiding Classic Getter and Setter



Python allows you to define getter and setter methods in the same way as C ++ and Java:



 >>> class C: ... def get_x(self): ... return self.__x ... def set_x(self, value): ... self.__x = value
      
      





Here is how you can use them:



 >>> c = C() >>> c.set_x(2) >>> c.get_x() 2
      
      





In some cases, this is the best way. However, it is often better to use properties, especially in simple cases:



 >>> class C: ... @property ... def x(self): ... return self.__x ... @x.setter ... def x(self, value): ... self.__x = value
      
      





Properties are considered more Pythonic than classic getters and setters. You can use them in the same way as in C #, that is, in the same way as regular data attributes:



 >>> c = C() >>> cx = 2 >>> cx 2
      
      





So, in general, it is better to use properties when possible.



Avoid access to protected class members



In Python, there are no private class members as such. However, if you write (_) at the beginning of the element name, then access to change it outside the class will be denied.



For example, consider the code:



 >>> class C: ... def __init__(self, *args): ... self.x, self._y, self.__z = args ... >>> c = C(1, 2, 4)
      
      





Class C instances have three data elements: .x, .y, and ._Cz. If a member name begins with a double underscore (dunder), it becomes changed. That is why instead of the ._z element, the ._Cz.



Now you can access or change directly .x:



 >>> cx # OK 1
      
      





You can also access or change ._y outside its class, but this is considered a bad manners:



 >>> c._y # Possible, but a bad practice! 2
      
      





You cannot access .z because the variable is changed, but you can access or change ._Cz:



 >>> c.__z # Error! Traceback (most recent call last): File "", line 1, in AttributeError: 'C' object has no attribute '__z' >>> c._C__z # Possible, but even worse! 4 >>>
      
      





You must avoid this. The author of the class probably starts the names with an underscore to emphasize that it is better not to use this element outside its class.



Using Context Managers to Free Resources



Sometimes you need to write code to manage resources properly. This often happens when working with files, database connections, or other objects with unmanaged resources. For example, you can open a file and process it:



 >>> my_file = open('filename.csv', 'w') >>> # do something with `my_file`
      
      





To manage memory correctly, you need to close this file after completion of work:



 >>> my_file = open('filename.csv', 'w') >>> # do something with `my_file and` >>> my_file.close()
      
      





Doing it this way is better than not doing it at all. But what if an exception occurs during processing of your file? Then my_file.close () is never executed. You can handle this with exception handling or with contextual managers. The second way means you put your code in a with block:



 >>> with open('filename.csv', 'w') as my_file: ... # do something with `my_file`
      
      





Using the with block means that the special .enter () and .exit () methods are called even in cases of exceptions. These methods should take care of resources.

You can achieve particularly robust constructs by combining context managers and exception handling.



Stylistic tips



Python code should be elegant, concise, and readable.



The main resource on how to write beautiful Python code is the Style Guide for Python Code or PEP 8. You should read it if you want to write Python code.



conclusions



This article gives some tips on how to write more compact, readable code. In short, it shows how to make the code more Pythonic. In addition, PEP 8 provides the Style Guide for Python Code, and PEP 20 introduces the principles of the Python language.



Enjoy writing useful and beautiful code!



Thanks for reading.



All Articles