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.
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]
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')
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
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]
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
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
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]
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)
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
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
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