Did you know? math.sin__name__ prints 'sin'
Check out the previous part: Core Python: Part 2
Dictionary:
Some languages call it hash and as we know a dictionary that is used to translate words from one language to another, basically has a key-value pair that has the same syntax in programming languages as well.
Dictionaries are mutable
my_dict = {"a": 1, "b": 2} # Adding a new key-value pair my_dict["c"] = 3 # Updating the value of an existing key my_dict["b"] = 10 # Removing a key-value pair using del del my_dict["b"] # Removing and returning a value using pop removed_value = my_dict.pop("a")
Dictionary keys are hashable objects
Hashable objects in Python include integers, floats, strings, tuples (if their elements are hashable), and some built-in types like frozensets. Lists, dictionaries, and other mutable objects are not hashable because their values can change after creation. Hence keys are immutable and unique (that is if we define the same key with two different values, the latest value will be retained).
Why do we use hashable objects as keys?
Because they allow the interpreter to quickly determine the storage location of an object.# Keys are integers (hashable) hashable_dict = {1: "one", 2: "two", 3: "three"} # Lists are not hashable (raises TypeError) unhashable_dict = {[1, 2]: "list"} # creating a dictionary with dict ordinal = dict([(1, 'First'), (2, 'Second'), (3, 'Third')])
Dictionary Methods
Dictionaries are iterable and hence can be used in loops. Along with this, we will learn about get(), keys(), values(), and items() methods. As we saw about KeyError in part 1, get() is used to avoid that as it simply prints None if the key does not exist in a dictionary if no optional message is provided.
keys(), values() and items() methods return an iterable object without copying to a list as this is faster and saves memory. Since they are not list, they cannot be indexed or assigned but can be converted to list using list(). Same applies for items() and values() methods as well.
my_dict = {"a": 1, "b": 2, "c": 3} # Using for loop to iterate over keys print("Keys:") for key in my_dict: print(key, my_dict[key]) # Output: a 1, b 2, c 3 # keys() method my_dict.keys() # dict_keys(['a', 'b', 'c']) my_dict.keys()[0] # TypeError: 'dict_keys' object is not subscriptable list(my_dict.keys())[0] # Output: a # Using keys() method to iterate over keys for key in my_dict.keys(): print(key) # Output: a, b, c # Using items() method to iterate over key-value pairs print("\nItems() method:") for key, value in my_dict.items(): print(key, value) # Output: a 1, b 2, c 3 # Using values() method to iterate over values print("\nValues() method:") for value in my_dict.values(): print(value) # Output: 1, 2, 3 # Using get() method to access values print("\nUsing get() method:") print(my_dict.get("b")) # Output: 2 print(my_dict.get("x")) # Output: None print(my_dict.get("x", "Not found")) # Output: Not found
Keyword arguments
When we do not know how many arguments a function is going to receive, we use *args that collect extra positional arguments in a tuple.
**kwargs allows a function to accept an arbitrary number of keyword arguments. It collects all the extra keyword arguments passed to the function into a dictionary.
def my_function(x, *args): # 1 is assigned to x and remaining arguments to args for arg in args: print(arg) my_function(1, 2, 3) # Output: 2, 3 def my_function(st, country, **kwargs): # Berlin is assigned to st and Germany is assigned to country and the rest to kwargs as dictionary elements for key, value in kwargs.items(): print(f"{key}: {value}") my_function("Berlin", country="Germany" name="Alice", age=30, city="Wonderland") # Output: name: Alice, age: 30, city: Wonderland
defaultdict
It is not an in-built function but should be imported from the collections module of Python. It adds an additional benefit to the dictionary i.e. it helps to avoid KeyError and create dictionaries with default values for keys that do not yet exist.
from collections import defaultdict # Create a defaultdict with a default value of int (0) my_dict = defaultdict(int) my_dict["a"] = 5 my_dict["b"] = 10 print(my_dict["a"]) # Output: 5 print(my_dict["b"]) # Output: 10 print(my_dict["c"]) # Output: 0 (default value for nonexistent key) # Create a defaultdict with a default value of list (empty list) my_dict = defaultdict(list) my_dict["a"].append(5) my_dict["b"].extend([10, 20]) print(my_dict["a"]) # Output: [5] print(my_dict["b"]) # Output: [10, 20] print(my_dict["c"]) # Output: [] (default value for nonexistent key)
Before beginning with sets, let's learn about one more error - AttributeError.
An AttributeError in Python is raised when you try to access an attribute (a property or method) of an object that doesn't exist for that particular object.
import math class MyClass: def __init__(self): self.value = 10 obj = MyClass() print(obj.val) # Raises AttributeError because 'val' attribute doesn't exist print(math.sine(30)) # Raises AttributeError because it's math.sin(), not math.sine()
Sets:
Why sets?
Iterable and mutable but the elements of sets are hashable.
Unique, hence remove the duplicate elements
Test membership of an element using in and not in operator
Supports the len() function
Determines all the Set Theory terms like Union, Intersection, Difference, Symmetric Difference, Cardinality, Subset, Proper Subset, Disjoint Sets
Why not sets?
They cannot be indexed or sliced and cannot be used as dictionary keys.
my_set = {1, 2, 3, 4, 5}
# Trying to access elements by index or slice
print(my_set[0]) # Raises TypeError: 'set' object is not subscriptable
print(my_set[1:3]) # Raises TypeError: 'set' object is not subscriptable
Note: Sets determine the uniqueness by value and not by type. [1 == 1.0 is True], here though 1 and 1.0 are of integer and float types their values are equal hence set removes one of them if we try to add both to the set.
s = {2, 9}
s.add(1)
s.add(1.0)
print(s) # Output: {1, 2, 9} 1.0 is not added as 1 already exists
Methods of sets
# 1. add - Adds the specified element to the set if it is not already present.
my_set = {1, 2, 3}
my_set.add(4)
# 2. remove - Removes the specified element from the set. Raises a KeyError if the element is not found.
my_set.remove(2)
# 3. discard - Removes the specified element from the set if it is present. Does not raise an error and does nothing if the element is not found.
my_set.discard(2)
# 4. pop - Removes and returns an arbitrary element from the set
popped_element = my_set.pop()
# 5. clear - Removes all elements from the set, leaving it empty.
my_set.clear()
frozenset:
As we learned, sets are mutable and cannot be used as keys in dictionaries there is a frozenset object that is immutable and hashable and hence can be used as dictionary keys and members of the set.
# Creating a frozenset
my_frozenset = frozenset([1, 2, 3])
# Accessing elements of a frozenset
for element in my_frozenset:
print(element)
# Trying to modify a frozenset (raises an error)
my_frozenset.add(4)
# Raises AttributeError: 'frozenset' object has no attribute 'add'
# because frozenset is immutable
Note: The main difference between immutable and hashable objects is
immutability refers to an object's inability to change its value after
creation, while hashability refers to an object's ability to be used as a
key in a dictionary or an element in a set. In Python, all immutable
objects are hashable, meaning that they can be used as keys in dictionaries
or elements in sets. The reason for this is that hashability is closely
tied to immutability; objects that cannot change their state after creation
are suitable for being hashed. Therefore, there are no examples of
immutable objects that are not hashable in Python's built-in data types.