Core Python: Part 4

Syntactic Sugar

Core Python: Part 4

Check out the previous part: Core Python: Part 3

As we already know there are multiple solutions for the same problem in any programming language, and there are multiple ways to write the program as well. Today we will discuss some ways that make the code look cleaner and easier to write as they are concise.

  1. Augmented assignment

     a = 1
     a += 1 # augmented assignment that is equivalent to
     a = a + 1 
    
     b = [1, 2, 3]
     b[-1] # negative indexing, more convenient than
     b[len(b) - 1]
    
  2. Comparison and Assignment

     x = y = z = -1 # assigning more than one variable to the same object
     # equivalent to
     x = -1
     y = -1
     z = -1
    
     # Multiple assigments
     a, b, c = 1, 2, 3
     # equivalent to
     a = 1
     b = 2
     c = 3
    
     # Comparison chaining
     if a == b == 3:
     print('a and b both equal 3')
     if -1 < x < 1:
     print('x is between -1 and 1')
    
  3. Conditional Assignment

     import math
     y = math.sin(x)/x if x else 1
     # equivalent to
     try:
     y = math.sin(x)/x
     except ZeroDivisionError:
     y = 1
    
  4. List Comprehension

     xlist = [1, 2, 3, 4, 5, 6]
     x2list = [x**2 for x in xlist]
    
     # equivalent to
     x2list = []
     for x in xlist:
         x2list.append(x**2)
    
     # list comprehension in conditional statements
     x2list = [x**2 for x in xlist if x % 2]
     # squares the odd integers and cubes the even integers in xlist
     x3list = [x**2 if x % 2 else x**3 for x in xlist]
    
  5. lambda Functions

    It is an anonymous function.

     # A lambda function that adds two numbers
     add = lambda x, y: x + y
     print(add(3, 5))  # Output: 8
    
     # A lambda function that squares a number
     square = lambda x: x ** 2
     print(square(4))  # Output: 16
    
     flist = [lambda x: 1, lambda x: x, lambda x: x**2, lambda x: x**3]
     flist[3](5) # flist[3] is x**3
     # Output: 125
     flist[2](4) # flist[2] is x**2
     # Output: 16
    
  6. with Statement

    The with statement in Python is used for context management, particularly when working with resources that need to be acquired and released properly. It simplifies the management of resources like files, network connections, and database connections by automatically handling the setup and cleanup operations.

     # Working with Files
     with open("myfile.txt", "r") as file:
         content = file.read()
     # File is automatically closed here
    
     # Working with a Database Connection
     import sqlite3
    
     with sqlite3.connect("mydb.db") as conn:
         cursor = conn.cursor()
         cursor.execute("SELECT * FROM users")
     # Connection is automatically closed here
    
     # Custom Context Managers
     # using classes that define __enter__() and __exit__() methods. 
     class MyContext:
         def __enter__(self):
             print("Entering context")
             return self
    
         def __exit__(self, exc_type, exc_value, traceback):
             print("Exiting context")
    
     with MyContext() as ctx:
         print("Inside the context")
    
     # Output:
     # Entering context
     # Inside the context
     # Exiting context
    
    1. map, filter

      Though list comprehension is a better way than map and filter, it is necessary to understand how map and filter work.

       numbers = [1, 2, 3, 4, 5]
       squared = map(lambda x: x ** 2, numbers)
       squared_list = list(squared)  # Convert the iterator to a list
       print(squared_list)  # Output: [1, 4, 9, 16, 25]
      
       numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
       even_numbers = filter(lambda x: x % 2 == 0, numbers)
       even_list = list(even_numbers)  # Convert the iterator to a list
       print(even_list)  # Output: [2, 4, 6, 8, 10]
      
  7. Generators

    Generators allow us to declare a function that can be used to iterate over a sequence of values, one at a time, without having to store the entire sequence in memory. The generator function has yield instead of return and cannot be called like a regular function. They are memory-efficient since they generate values on-the-fly, allowing you to work with large or infinite sequences of data without running out of memory. They can improve performance, especially when you're processing data lazily and don't need to generate all values at once.

     def squares_generator(n):
         for i in range(n):
             yield i ** 2
    
     squares = squares_generator(5)
     for square in squares:
         print(square)
    
     # generator comprehension syntax
     squares = (x**2 for x in range(5))
     for square in squares:
         print(square)
    
  8. Walrus Operator

    := is known as the walrus operator that resembles as you can see in the above picture, the walrus animal's eye and teeth. We usually store a computed value for example len(string) in a variable so that we can use the variable many times instead of computing again and again. But var = len(string) does not return any value. Here comes the importance of the walrus operator that returns the value. Hence we can save a line of code.

     s = 'A string with too many characters'
     # computing len(s) 2 times
     if len(s) > 10:
         print(f's has {len(s)} characters. The maximum is 10.')
    
     # To avoid this, we assign it to a variable
     slen = len(s)
     if slen > 10:
         print(f's has {slen} characters. The maximum is 10.')
    
     # Walrus operator
     if (slen := len(s)) > 10: # := returns len(s) saved a line of code
         print(f's has {slen} characters. The maximum is 10.')
    
     # application of walrus in list comprehension
     # without walrus operator
     filtered_values = [f(x) for x in values if f(x) >= 0]
     # with walrus operator
     filtered_values = [val for x in values if (val := f(x)) >= 0]
     # here, computing f(x) two times is expensive, hence val:= saves this
    
    1. Generator Expressions

      Generator expressions are similar to list comprehensions but with parentheses instead of square brackets.

       g = (x**2 for x in range(3))
       g # Output: <generator object <genexpr> at 0x7f4c45a786c0>
       next(g) # 0
       next(g) # 1
       next(g) # 4
       # The generator object keeps track of where it is in the sequence, so 
       # the for loop picks up where next left off. Once the generator is 
       # exhausted, it continues to raise StopIteration
       next(g) # StopIteration
      
       # Generator expressions are often used with functions like sum, max, 
       # and min:
       sum(x**2 for x in range(5)) # 30
      
    2. Counters

      Counter is a natural way to represent a multiset.

       from collections import Counter
       count = Counter('parrot')
       count
       # Output: Counter({'r': 2, 't': 1, 'o': 1, 'p': 1, 'a': 1})
       # Counters don’t raise an exception unlike dictionary if you access an 
       # element that doesn’t appear. Instead, they return 0:
       count['d'] # 0
      

Check out the next part: Core Python: Part 5