Exploring Fascinating Features of Python

Python is one of the most popular programming languages due to its simplicity, readability, and versatility. It’s used across various fields like web development, data science, machine learning, automation, and more.

Let’s dive into some of the most intriguing aspects of Python that you might not know about!

List Comprehensions

Python’s list comprehensions offer a concise way to generate lists. They allow developers to write powerful, compact expressions.

squares = [x**2 for x in range(10)]

This single line is equivalent to:

squares = []
for x in range(10):
    squares.append(x**2)

You can even include conditions:

# List of squares of even numbers
squares = [x**2 for x in range(10) if x % 2 == 0]

This feature is highly optimized and preferred for readable and concise code.

Generator Expressions

Generator expressions are similar to list comprehensions but use parentheses instead of square brackets. Here’s a deeper look into generator expressions:

(generator_expression for item in iterable if condition)

With Yield

# Generator function
def generate_squares(n):
    for x in range(n):
        yield x**2

Equivalent expression

squares_gen_expr = (x**2 for x in range(10))

Read about generators

The zip() Function

The zip() the function allows you to iterate over multiple iterables in parallel. It pairs up elements from the provided iterable:

names = ["Alice", "Bob", "Charlie"]
ages = [25, 30, 35]

for name, age in zip(names, ages):
    print(f"{name} is {age} years old.")

# Output:
# Alice is 25 years old.
# Bob is 30 years old.
# Charlie is 35 years old.

Different length

a = ("John", "Charles", "Mike","Vicky")
b = ("Jenny", "Christy", "Monica")

x = zip(a, b)


print(list(x)) 

#[('John', 'Jenny'), ('Charles', 'Christy'), ('Mike', 'Monica')]

It’s handy when you need to work with multiple lists or tuples and want to process corresponding elements together.

Python’s else in Loops

An often-overlooked feature of Python is the else clause that can be used in loops. The else block after a for or while loop will only be executed if the loop completes normally (i.e., no break statement is encountered).

for num in range(10):
    if num == 50:
        break
else:
    print("Loop completed without break") 

# Loop completed without break

Lambda, Map, Filter

Refer

Slicing (::)

In Python,:: is used to slice lists and other sequences. It allows you to specify a step value in addition to the start and stop indices. Here’s the general syntax:

sequence[start:stop:step]
  • start is the index where the slice begins (inclusive).
  • stop is the index where the slice ends (exclusive).
  • step is the stride or step size between each element in the slice.

If you leave out start and stop but include the step, it will slice the whole sequence with the specified step.

lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(lst[::2])  # Output: [0, 2, 4, 6, 8]
print(lst[1:7:2])  # Output: [1, 3, 5]
print(lst[::-1])  # Output: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
print(lst[::1])  # Output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(lst[::-2])  #[9, 7, 5, 3, 1]
print(lst[6:0:-2]) #[6, 4, 2]

Reversing a sequence:

reversed_sequence = sequence[::-1]

How it works?

  • When you omit start and stop, the slice covers the entire sequence.
  • By specifying a step of -1, you’re telling Python to move backwards through the sequence, starting from the end and ending at the beginning.

Example

text = "hello"
reversed_text = text[::-1]

print(reversed_text)  # Output: "olleh"
#Tuple Reversaltpl = (1, 2, 3, 4, 5)
reversed_tpl = tpl[::-1]

print(reversed_tpl)  # Output: (5, 4, 3, 2, 1)
# Interger reversal

n = 1234
print(int(str(n)[::-1])) #4321

Note

  • When start and stop are omitted, the slice defaults to covering the entire sequence.
  • Step of -1: The -1 step means the slice will take elements in reverse order.

Deep and shallow copy

Shallow Copy

A shallow copy creates a new object, but it does not create copies of objects contained within the original object. Instead, it references the same objects in memory.

Example 1

import copy

old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.copy(old_list)


print("Old list:", old_list , id(old_list))      # Old list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]] , 4715228096
print("New list:", new_list,id(new_list))   # New list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]] , 4715162880

print(id(old_list[0])) #4715227584
print(id(new_list[0])) #4715227584
  • Object old_list and new_list have different reference addresses 4715228096 and 4715162880
  • old_list[0] and new_list[0] have same reference address 4715227584

Example 2: Modifying the existing nested object, it will get reflected in both


old_list[1][1] = 'AA'

print("Old list:", old_list , id(old_list)) # Old list: [[1, 1, 1], [2, 'AA', 2], [3, 3, 3]] ,4715228096
print("New list:", new_list,id(new_list)) # New list: [[1, 1, 1], [2, 'AA', 2], [3, 3, 3]], 4715162880

Example 3: The addition of a new nested object, will not reflect in other object


old_list.append([4, 4, 4])

print("Old list:", old_list , id(old_list))
print("New list:", new_list,id(new_list))

Old list: [[1, 1, 1], [2, 'AA', 2], [3, 3, 3], [4, 4, 4]], 4715228096
New list: [[1, 1, 1], [2, 'AA', 2], [3, 3, 3]], 4715162880

Deep Copy

A deep copy creates a new object and recursively adds the copies of nested objects present in the original elements. The deep copy creates an independent copy of the original object and all its nested objects.

import copy

old_list = [[1, 1, 1], [2, 2, 2], [3, 3, 3]]
new_list = copy.deepcopy(old_list)

print("Old list:", old_list,id(old_list)) #Old list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]] 4715197120
print("New list:", new_list,id(new_list)) #New list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]] 4715263616


old_list[1][0] = 'BB'


print("Old list:", old_list,id(old_list))     #Old list: [[1, 1, 1], ['BB', 2, 2], [3, 3, 3]] 4715197120
print("New list:", new_list,id(new_list)) #New list: [[1, 1, 1], [2, 2, 2], [3, 3, 3]] 4715263616

new_list=old_list, Shallow or deep?

  • The statement new_list = old_list in Python is neither a shallow copy nor a deep copy.
  • Instead, it is a simple assignment where both new_list and old_list reference the same object in memory.
  • It’s not a copy of the object, it’s just an alias for the same object. It is just a reference assignment.

old_list = [[1, 2, 3], [4, 5, 6], [7, 8, 'a']]
new_list = old_list

new_list[2][2] = 9

print('Old List:', old_list,id(old_list))
print('New List:', new_list,id(new_list))

old_list.append([4, 4, 4])

old_list[1][1] = 'AA'
Old List: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 4715106240
New List: [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 4715106240
Old List: [[1, 2, 3], [4, 'AA', 6], [7, 8, 9], [4, 4, 4]] 4715106240
New List: [[1, 2, 3], [4, 'AA', 6], [7, 8, 9], [4, 4, 4]] 4715106240

Flatten a list of lists

Sum function

sum(iterable, start=0)
  • iterable: The sequence (like a list, tuple, etc.) of numbers or objects you want to sum.
  • start (optional): A starting value that is added to the sum. This defaults to 0.
numbers = [1, 2, 3, 4]
result = sum(numbers)
print(result) #10
result = sum(numbers, 10)
print(result) #20

Flattening

l1 = [[1, 2], [3, 4], [5, 6]]
result = sum(l1, [])
print(result) # [1, 2, 3, 4, 5, 6]
  • By default, start is set to 0, so if you pass a list of numbers, the sum starts from 0.
  • If the start is something else (like an empty list []), it affects how sum() behaves. When summing lists, it concatenates them instead of adding them numerically.
  • The sum() function works best for numeric types. If you try to sum non-numeric objects (like strings), it will raise a TypeError.

*args and **kwargs

“*” notation like this – *args OR **kwargs – as our function’s argument when we have doubts about the number of arguments we should pass in a function.

  • *args (Non-Keyword Arguments)
  • **kwargs (Keyword Arguments)

def myFun(**kwargs):
    for key, value in kwargs.items():
        print("%s == %s" % (key, value))


# Driver code
myFun(first='Back', mid='end', last='Mess')


def myFun(*argv):
    for arg in argv:
        print(arg)


myFun('Hello', 'Welcome', 'to', 'BackendMess')

Output

first == Back
mid == end
last == Mess
Hello
Welcome
to
BackendMess
def myFun(arg1, arg2, arg3):
    print("arg1:", arg1)
    print("arg2:", arg2)
    print("arg3:", arg3)


# Now we can use *args or **kwargs to
# pass arguments to this function :
args = ("Back", "end", "Mess")
myFun(*args)

kwargs = {"arg1": "Back", "arg2": "end", "arg3": "Mess"}
myFun(**kwargs)

Output

arg1: Back
arg2: end
arg3: Mess
arg1: Back
arg2: end
arg3: Mess

Unpacking and Extended Unpacking

# Basic unpacking
a, b, c = [1, 2, 3]
print(a, b, c)  # Output: 1 2 3

# Extended unpacking
a, *b, c = [1, 2, 3, 4, 5]
print(a, b, c)  # Output: 1 [2, 3, 4] 5

Conclusion

Python’s simplicity hides a wealth of interesting features that make it a powerful language for both beginners and experienced programmers. Features like dynamic typing, list comprehensions, lambda functions, generators, and f-strings offer elegant solutions to common programming tasks. As you explore Python further, you’ll discover even more nuances that make it an incredibly versatile and enjoyable language to work with!

Resource

Leave a Comment