Python Notes - OOPs - Udemy course
Oops:
- In python there is no such concept of access modifiers(like public,private,protected etc).By default all are variables inside of the class are public, we can add _(under score) prefix infront of name to inform developers it is non public i.e it should be accessed outside of the class.Technically developers can even access that outside of the class.
- Name mangling is also same when you want to access the variables inside a class. Here we will use 2 underscores before attribute and while calling we need to use(_className__attributeName).Even thecnically this can be used otuside of class but we should not use it.
A decorator is a function that takes a function as argument to extend its functionality without actually modifying it.
This is the typical syntax of a decorator function:
- def decorator_function(arg_function):
- def wrapper_function():
- # Code to extend the functionality
- arg_function()
- # Code to extend the functionality
- return wrapper_function
You can see a Python equivalent that illustrates why @property is implemented as a decorator in this article:
Methods and functions are not same in python. Blog
Difference between is and == in python:
In the previous video, you learned that, in the case of lists and tuples, the == operator compares the values, not if they are the same object. But take a look at this example:
- >>> class Dog:
- def __init__(self, age):
- self.age = age
- >>> a = Dog(5)
- >>> b = Dog(5)
- >>> a == b
- False
They comparison operator doesn't return True, even if their instance attributes have the same value. Why is this?
Objects created from user-defined classes have to meet two conditions for the expression obj1 == obj2 to evaluate to True.
They have to refer to the same object (
x is yhas to evaluate toTrue)The expression
hash(x) == hash(y)has to evaluate toTrue.
The hash() function maps the object to a unique integer. For more information on the hash() built-in function, please refer to this article.
According to the Python Documentation:
".... all objects compare unequal (except with themselves) ...
x == yimplies both thatx is yandhash(x) == hash(y)."- source
To find more information on this specific quote of the documentation, please refer to the article.
This is why comparing an object from a user-defined class with itself using the == operator returns True:
- >>> class Dog:
- def __init__(self, age):
- self.age = age
- >>> a = Dog(5)
- >>> b = Dog(5)
- >>> a == b
- False
- >>> a == a
- True
- >>> b == b
- True
- >>> b == a
- False
As you can see, the values returned by hash() are not equal for the two objects:
- >>> hash(a)
- -2143773140
- >>> hash(b)
- 3710489
So when the hash values are different, this expression hash(a) == hash(b) will be False and the expression a is b and hash(a) == hash(b) returns False, so a == b returns False
You have to be very careful with built-in methods because some of them mutate the original object.
For example:
- >>> a = [6, 2, 7, 1]
- >>> a.sort()
- >>> a
- [1, 2, 6, 7]
Here you can see (above) that the .sort() method mutated the original list.
To achieve this same functionality without mutating the original object, you should use the sorted() function.
- >>> a = [6, 2, 7, 1]
- >>> sorted(a)
- [1, 2, 6, 7]
- >>> a
- [6, 2, 7, 1]
As you can see, this function returns a sorted "version" of the list (a copy) without modifying the original list.
You can check if a method mutates the object in the official Python documentation. 👍
Now you will learn why you should avoid using mutable data types such as lists as default arguments:
Default arguments are initialized when the methods are initially processed, so there is only one copy of each default argument. They are not created when you call the method, they are created when the program starts to run.
1️⃣ Example
If you use a list as a default argument, the same list (reference) will be reused as the default argument for every method call.
Below, you can see how we use an empty list as the default argument for the clients parameter in __init__().
You would expect this to work normally, creating an empty list (a new object) for the default argument every time that you create an instance. But this is not what happens...
When we start adding elements to the list, you can see that the two instances were modified (please see the example below).
- >>> class WaitingList:
- def __init__(self, clients=[]): # The default argument is an empty list
- self.clients = clients
- def add_client(self, client):
- self.clients.append(client)
- # Create the instances
- >>> waiting_list1 = WaitingList()
- >>> waiting_list2 = WaitingList()
- # Add a client to the first waiting list
- >>> waiting_list1.add_client("Jake")
- # Both of them were modified!
- >>> waiting_list1.clients
- ['Jake']
- >>> waiting_list2.clients
- ['Jake']
What truly happens behind the scenes is that when this line is executed: self.clients.append(client), the new client is added to the same list! Not to a separate list that corresponds to each instance.
You can check that self.clients references the same list in the two instances with the id() function. Notice how the ids are equal.
- >>> class WaitingList:
- def __init__(self, clients=[]):
- self.clients = clients
- print("List id:", id(self.clients))
- def add_client(self, client):
- self.clients.append(client)
- >>> waiting_list1 = WaitingList()
- List id: 48967144
- >>> waiting_list2 = WaitingList()
- List id: 48967144
2️⃣ Solution
The solution to this problem is to avoid using the list directly as a default argument, and use this instead:
- class WaitingList:
- def __init__(self, clients=None):
- if clients == None:
- self.clients = []
- else:
- self.clients = clients
- def add_client(self, client):
- self.clients.append(client)
None is used as the default argument so you can omit the argument when you create the instance. If the value of clients is None, the attribute is initialized as an empty list. Otherwise, the value passed as argument is assigned.
And this will generate the behavior that you expect:
- >>> waiting_list1 = WaitingList()
- >>> waiting_list2 = WaitingList()
- >>> waiting_list1.add_client("Jake")
- >>> waiting_list1.clients
- ['Jake']
- >>> waiting_list2.clients
- []
Now the instances reference two separate lists and modifying one doesn't modify the other.
shallow copy vs deep copy : here
Comments
Post a Comment