"A class is a blueprint to create instances"
Instances are created based on a blueprint called a class. A class defines attributes as data and methods as functions associated with it. An instance variable contains data unique to each instance. When a method from a class is called, it automatically receives the first instance as an argument.
#Example of a class
class Employee:
def __init__(self, first, last, pay): # fisr, last, pay are the arguments
self.first = first # instance variable
self.last = last # instance variable
self.pay = pay
# creating instances
emp1 = Employee("Arthur","Klark","1000") # emp1 is an instance of the class,
emp2 = Employee("Brigitte", "Klark","2000")
#emp1.first = "food"
# Example of a class method - regular method
class Employee:
def __init__(self,first, last, pay):
self.first = first
self.last = last
self.pay = pay
def fullname(self):
return '{} {}'.format(self.first, self.last)
# creating instances
emp1 = Employee('Arthur', ' Klark', '1000')
print(emp1.fullname())
# or
Employee.fullname(emp1)
Arthur Klark
'Arthur Klark'
"Class variables are variables that are shared between all instances of a class"
Class variables are a type of variables that hold values or data that are shared among all instances or objects of a class. They are declared within the class but outside of any class methods or functions. Class variables are also known as static variables in some other programming languages.
They are not unique to each instance of the class, meaning that any changes made to a class variable will be reflected across all instances of the class. This can be useful for storing data that is common to all objects of the class, such as default values or constants.
For example, consider a class called 'Employee' that has a class variable 'raise_amount' set to 1.04. All instances of the 'Employee' class will have access to this variable and can modify its value. If one instance of the class changes the value of raise amount, this change will be visible to all other instances of the that class.
# Example class variable
class Employee:
raise_amount = 1.04
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
def fullname(self):
return ' {} {} '.format(self.first, self.last)
def apply_raise(self):
#self.pay = int(self.pay * 1.04)
self.pay = int(self.pay * self.raise_amount)
emp1 = Employee('Arthur','Klark',1000)
emp1.apply_raise()
print(emp1.pay)
1040
# print out the namespace of the instance
print(emp1.__dict__)
{'first': 'Arthur', 'last': 'Klark', 'pay': 1040}
# print out the namespace of the class - it does contain the 'raise_amount' attribute
print(Employee.__dict__)
{'__module__': '__main__', 'raise_amount': 1.04, '__init__': <function Employee.__init__ at 0x7fed003fc040>, 'fullname': <function Employee.fullname at 0x7fed003fc160>, 'apply_raise': <function Employee.apply_raise at 0x7fed003fc1f0>, '__dict__': <attribute '__dict__' of 'Employee' objects>, '__weakref__': <attribute '__weakref__' of 'Employee' objects>, '__doc__': None}
# to change the class variable do:
Employee.raise_amount = 1.04
print(Employee.raise_amount)
1.04
# changing the class variable through the instance will not change the variable for the class
emp1 = Employee('Arthur','Klark',1000)
emp1.raise_amount = 1.06
emp1.apply_raise()
print(emp1.pay)
1060
emp1 = Employee('Arthur','Klark',1000)
emp1.apply_raise()
print(emp1.pay)
1040
# Example Track the number of employees using class variables
class Employee:
num_of_emps = 0
raise_amount = 1.04
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
Employee.num_of_emps += 1 # on a class
def fullname(self):
return ' {} {}'.format(self.first, self.last)
def apply_raise(self):
self.pay = int( self.pay * self.raise_amount) # on an instance
emp1 = Employee('Arthur','Klark',1000)
#print(Employee.num_of_emps)
emp2 = Employee('Douglas','Kirk',2000)
print(Employee.num_of_emps)
2
"Classmethods receive the class as the first argument instead of an instance"
Class methods are defined using the @classmethod decorator and are used to define a method that operates on the class itself rather than an instance of the class. When a class method is called, the class is passed in as the first argument instead of an instance.
This is useful when you want to create a method that operates on the class itself rather than an instance of the class. Class methods can access and modify class-level data, and can also create new instances of the class if needed.
They can't access instance-level data, such as instance variables or instance methods. They are only able to access and modify class-level data and methods.
# Example
class Employee:
num_of_emps = 0
raise_amount = 1.04
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
Employee.num_of_emps += 1
def fullname(self):
return '{} {}'.format(self.first, self.last)
def apply_raise(self):
self.pay = int( self.pay * self.raise_amount)
@classmethod
def set_raise_amount( cls, amount ):
cls.raise_amount = amount
emp1 = Employee('Arthur','Klark',1000)
Employee.set_raise_amount(1.07)
print(Employee.raise_amount)
1.07
print(emp1.raise_amount)
1.07
# the instance can also run the classmethod and so change the class variable
emp1.set_raise_amount(1.08)
print(emp1.raise_amount)
print(Employee.raise_amount)
1.08 1.08
# extending the class
class Employee:
num_of_emps = 0
raise_amount = 1.04
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
Employee.num_of_emps += 1
def fullname(self):
return '{} {}'.format(self.first, self.last)
def apply_raise(self):
self.pay = int( self.pay * self.raise_amount)
@classmethod
def set_raise_amount( cls, amount ):
cls.raise_amount = amount
@classmethod
def from_string( cls, emp_str):
"""transforms a string input to desired form: 'Johnny-Mnemonic-100000"""
first, last, pay = emp_str.split('-')
return cls(first, last, int(pay))
emp_str = 'Johnny-Mnemonic-100000'
new_emp1 = Employee.from_string(emp_str)
new_emp1.fullname()
'Johnny Mnemonic'
new_emp1.apply_raise()
print(new_emp1.pay)
104000
In object-oriented programming, methods can be categorized as regular, class, or static methods. Regular methods require the instance to be passed as the first argument, whereas class methods require the class to be passed as the first argument. On the other hand, static methods don't automatically receive any argument and only take the argument that is explicitly provided during the method call.
# Example
import datetime
class DayCheck:
# def __init__(self, day):
# self.day = day
@staticmethod
def is_workday(day):
if day.weekday() == 5 or day.weekday() == 6:
return False
return True
d=datetime.date(2017,10,22)
#DayCheck(d).is_workday(d)
DayCheck.is_workday(d)
False
Inheritance is a mechanism in object-oriented programming that allows a new class (called the "subclass" or "derived class") to be based on an existing class (called the "parent class" or "base class"), inheriting its attributes and methods.
When a subclass inherits from a parent class, it can extend or modify the functionality of the parent class by adding new attributes or methods, or by overriding existing ones. However, the parent class itself remains unchanged, and any other subclasses that inherit from it will continue to use the original implementation of its attributes and methods unless they also override them.
# extending the class
class Employee:
num_of_emps = 0
raise_amount = 1.04
def __init__(self, first, last, pay):
self.first = first
self.last = last
self.pay = pay
Employee.num_of_emps += 1
def fullname(self):
return '{} {}'.format(self.first, self.last)
def apply_raise(self):
self.pay = int( self.pay * self.raise_amount)
@classmethod
def set_raise_amount( cls, amount ):
cls.raise_amount = amount
@classmethod
def from_string( cls, emp_str):
"""transforms a string input to desired form: 'Johnny-Mnemonic-100000"""
first, last, pay = emp_str.split('-')
return cls(first, last, int(pay))
@staticmethod
def is_workday(day):
if day.weekday() == 5 or day.weekday() == 6:
return False
return True
# define another class
class Developer(Employee):
raise_amount = 2
Employee('Arthur','Klark',1000).is_workday(d)
False
dev1 = Developer('Arthur','Klark',1000)
print(dev1.raise_amount)
2
print(dev1.pay)
1000
Developer.apply_raise(dev1)
print(dev1.pay)
2000
Adding more functionality to the original class:
# extending the functionality
class Developer(Employee):
raise_amount = 2
def __init__(self, first, last, pay, prog_lang):
# insted the lines above you can take the super method to take all attributes from the original class
super().__init__(first, last, pay)
# and just define the additional attribute
self.prog_lang = prog_lang
dev1 = Developer('Arthut', 'Klark', 1000, 'Python')
dev1.fullname()
'Arthut Klark'
print(dev1.prog_lang)
Python
Adding more classes that inharit from Employee class:
class Manager(Employee):
def __init__(self, first, last, pay, employees = None):
super().__init__(first, last, pay)
if employees is None:
self.employees = []
else:
self.employees = employees
# adding more regular methods
def add_emp(self, emp):
if emp not in self.employees:
self.employees.append(emp)
def print_emp(self):
for emp in self.employees:
print( '->>',emp.fullname())
dev1 = Developer('Arthur', 'Klark', 1000, 'python')
mng1 = Manager('Robert', 'Doyle', 500, [dev1])
mng1.print_emp()
->> Arthur Klark
mng1.fullname()
'Robert Doyle'
"When @property decorator is applied on a regular method of a class, that method will be accessible only as an attribute and not as a method"
Python provides decorators that can be used to modify the behavior of functions or methods in a class. One such decorator is "@property", which when applied to a regular method in a class, makes it accessible as an attribute instead of a method. This means that when an object of the class is accessed, the method with the @property decorator can be accessed like an attribute, without having to call it as a method. However, it is important to note that using the @property decorator in this way means that the method can no longer be called like a regular method, but rather can only be accessed like an attribute. It is a useful feature when the method is used to compute some property or value that depends on the object's state, as it simplifies the access to the property. It is important to keep in mind that the @property decorator is not suitable for all use cases and should be used with care.
#Example of a property decorator
class Employee:
def __init__(self, first, last):
self.first = first
self.last = last
def fullname_1(self):
return( '{} {}'.format(self.first, self.last))
@property
def fullname_2(self):
return( '{} {}'.format(self.first, self.last))
emp = Employee('Arthur','Klark')
print(emp.fullname_1())
#Notice the use of the print function and the Out[]
emp.fullname_2
Arthur Klark
'Arthur Klark'
In Python, an iterator is an object that provides a sequence of values one at a time. It is implemented as a class that defines two methods, iter() and next(). The iter() method returns the iterator object itself, while the next() method returns the next value in the sequence or raises the StopIteration exception if there are no more values. The use of iterators makes it possible to iterate over large data sets without having to load them all into memory at once.
# Example iterator class
# the class needs to contain the __iter__ and the __next__ methods in order for the class to work as an iterator
class SquaresIterator:
def __init__(self, max_root_value):
self.max_root_value = max_root_value
self.current_root_value = 0
def __iter__(self):
return self
def __next__(self):
if self.current_root_value >= self.max_root_value:
raise StopIteration
square_value = self.current_root_value **2
self.current_root_value += 1
return square_value
# usage
for a, b in enumerate(SquaresIterator(5)):
print('{} **2 = {}'.format(a, b))
# BUT this class is actually very easily implemented as a "generator function"
def make_numbers():
n = 0
while n < 5:
yield n
n +=1
# or
def make_squares(n):
for i in range(n):
yield i**2
yield 'end'
# use it as:
for i in make_numbers():
print(i**2)
# as a list comprehension
[i**2 for i in make_numbers()]
0 **2 = 0 1 **2 = 1 2 **2 = 4 3 **2 = 9 4 **2 = 16 0 1 4 9 16
[0, 1, 4, 9, 16]
make_squares(5)
<generator object make_squares at 0x7fed003ee5e0>
for i in make_squares(5):
print(i)
0 1 4 9 16 end
# BTW - the built-in enumerate function:
list(enumerate([5,6,7]))
[(0, 5), (1, 6), (2, 7)]
# your own enumerate function
def my_enumerate(sequence, start=0):
n = start
for elem in sequence:
yield n, elem
n += 1
list(my_enumerate([5,6,7]))
[(0, 5), (1, 6), (2, 7)]