1. Instance-level data#
Let’s see how we can distinguish Instance-level data and Class-level data.
The following code has
name
andsalary
as attributes, with specific values for each new instance of the class. They are calledinstance attributes
Instance-level Data |
Notes |
---|---|
class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary
emp1 = Employee("Poor Guy", 30000)
emp2 = Employee("You my friend", 100000)
print(emp1.name + ': ' + str(emp1.salary))
print(emp2.name + ': ' + str(emp2.salary))
|
|
2. Class-level data#
But now, we want to store some data that is shared among all the instances.
For example, we can introduce a minimal salary across the entire organization. In other words, the data should not differ among object instances. So we can define
class attributes
in the body ofclass
. Thatclass attribute
will serve as aglobal variable
within a class.
Class-level Data |
Notes |
---|---|
class Employee:
# Define a `class attribute`
MIN_SALARY = 30000
def __init__(self, name, salary):
self.name = name
#-------------------------#
# Use class name to access
# class attribute
#------------------------#
if salary >= Employee.MIN_SALARY:
self.salary = salary
else:
self.salary = Employee.MIN_SALARY
emp1 = Employee("Poor Guy", 40000)
emp2 = Employee("You my friend", 120000)
print(emp1.name + ': ' + str(emp1.MIN_SALARY))
print(emp2.name + ': ' + str(emp2.MIN_SALARY))
|
|
The benefit of using
class attributes
is to have global constants that are related to class, for examplemin
andmax
values for attributes, or commonly used values and constants, e.g.pi
.
3. Class Methods#
Regular
methods
are already shared between instances. The same code gets executed for everyinstance
. The only difference is the data that is fed into it.It is possible to define methods bound to class rather than an instance. But these methods will not be able to use any
instance-level
data.To define a
class-method
, we have to start with aclassmethod
decorator, followed by a method definition. The only difference is that now the first argument is notself
, butcls
, referring to the class, just like the self argument was a reference to a particular instance.
class MyClass:
@classmethod # --> use decorator to declare a class method
def my_method(cls,argument_1,argument_2): # --> `cls` argument refers to the class
MyClass.my_method(argument_1,argument_2) # --> class-dot-method syntax
A class can only have one
__init__
method, but there might be multiple ways to initialize an object.For example, if you want to create an
Employee
object from data stored in a file, we can’t use a method. This would require an instance, and there isn’t one yet. So, we can create aclass-method
that accepts a file name, reads the first line from the file that contains the name of the employee, and returns an object instance.
3.1. Example - Class Method 1#
class Employee:
MIN_SALARY = 30000
def __init__(self, name, salary=30000):
self.name = name
if salary >= Employee.MIN_SALARY:
self.salary = salary
else:
self.salary = Employee.MIN_SALARY
@classmethod
def from_file(cls,filename):
with open(filename,"r") as f:
name = f.readline()
return cls(name)
emp1 = Employee("fernando", 500000)
print(type(emp1))
print(emp1.name, str(emp1.salary))
# TODO - Find a way to load a file from here
#emp2 = Employee.from_file("")
#print(type(emp2))
#print(emp2.name, str(emp2.salary))
<class '__main__.Employee'>
fernando 500000
3.2. Example - Class Method 2#
Let’s create a
constructor
=__init__
that creates BetterDate objects given the values foryear
month
, andday
. But we can also create aBetterDate
objects fromstrings
anddatetime
.
from datetime import datetime
class BetterDate:
# Constructor
def __init__(self, year, month, day):
self.year, self.month, self.day = year, month, day
# Define a class method from_str
@classmethod
def from_str(cls, datestr):
# Split the string at "-" and convert each part to integer
parts = datestr.split("-")
year, month, day = int(parts[0]), int(parts[1]), int(parts[2])
# Return the class instance
return cls(year, month, day)
@classmethod
def from_datetime(cls, datetime):
return cls(datetime.year, datetime.month, datetime.day)
# Constructor able to create `BetterDate` objects froms strings
bd = BetterDate.from_str('2050-01-01')
print('!' + '-'*70)
print("BetterDate objects from strings:")
print(bd.year, bd.month, bd.day)
print(bd.__dict__)
print('!'+ '-'*70)
# BetterDate objets given values of year, month, day
bd2 = BetterDate(year=2022, month = 2, day =22)
print("`BetterDate` objects from year, month, day:")
print(bd2.year, bd2.month, bd2.day)
print(bd2.__dict__)
print('!' + '-'*70)
# BetterDate objects from datetime objects
today = datetime.today()
bd3 = BetterDate.from_datetime(today)
print("`BetterDate` objects from datetime:")
print(bd3.year, bd3.month, bd3.day)
print(bd3.__dict__)
print('!' + '-'*70)
!———————————————————————-
BetterDate
objects from strings
:
2050 1 1
{‘year’: 2050, ‘month’: 1, ‘day’: 1}
!———————————————————————-
BetterDate
objects from year
, month
, day
:
2022 2 22
{‘year’: 2022, ‘month’: 2, ‘day’: 22}
!———————————————————————-
BetterDate
objects from datetime
:
2022 4 19
{‘year’: 2022, ‘month’: 4, ‘day’: 19}
!———————————————————————-
4. Inheritance#
Inheritance allows us to define a class that inherits all the methods and attributes from another class.
The code below has a class
Sensor
with attributesname
,location
, andrecord_date
that pass from the creation of an object and an attributedata
as an empty dictionary to store data. The methodadd_data
containst
anddata
as input parameters to take in timestamp and data arrays. Within this method, we also assignt
anddata
to theself.data
attribute withtime
anddata
as the[`keys`]
. The methodclear_data
is created to delete the data.
4.1. Example - Inheritance 1#
import numpy as np
class Sensor():
def __init__(self, name, location, record_date):
self.name = name
self.location = location
self.record_date = record_date
self.data = {}
def add_data(self, t, data):
self.data['time'] = t
self.data['data'] = data
print(f'We have {len(data)} points saved')
def clear_data(self):
self.data = {}
print('Data cleared!')
if __name__ == "__main__":
sensor1 = Sensor('sensor1', 'Berkeley', '2019-01-01')
data = np.random.randint(-10, 10, 10)
sensor1.add_data(np.arange(10), data)
print(sensor1.data)
Say we have a new type of sensor: an accelerometer. It shares the same attributes and the methods as
Sensor
class, but it also has different attributes or methods need to be appended or modified from the original class.This new class will inherit from the
Sensor
class with all the attributes and methods. Let us create this new classAccelerometer
, and add a new methods,show_type
, to report what kind of sensor it is.
# New Child Class
class Accelerometer(Sensor): # --> Sensor is a `superclass`
def show_type(self): # --> New method
print('I am an accelerometer!')
acc = Accelerometer('acc1', 'Oakland', '2019-02-01')
acc.show_type()
print(acc.name, acc.location, acc.record_date)
print('!' + '-'*70)
data = np.random.randint(-10, 10, 10)
acc.add_data(np.arange(10), data)
print('!' + '-'*70)
print(acc.data)
The
Sensor
class is the parent class (or superclass) and is more general. It passes all the characteristics to the child classAccelerometer
.
4.2. Example - Inheritance 2#
class Employee:
MIN_SALARY = 30000
def __init__(self, name, salary=MIN_SALARY):
self.name = name
if salary >= Employee.MIN_SALARY:
self.salary = salary
else:
self.salary = Employee.MIN_SALARY
def give_raise(self, amount):
self.salary += amount
# MODIFY Manager class and add a display method
class Manager(Employee):
def display(self):
print("Manager " + self.name )
mng = Manager("Debbie Lashko", 86500)
print(mng.name)
# Call mng.display()
mng.display()
4.3. The super().__init__
#
Let’s now create a new class called
NewSensor
that inherits fromSensor
class, but with updated the attributes by adding a new attributebrand
.To do the above, we can use
super().__init__(self,)
:
class NewSensor(Sensor):
def __init__(self, name, location, record_date, brand):
super().__init__(name, location, record_date) #--> super().__init__ == Sensor().__init__
self.brand = brand
new_sensor = NewSensor('OK', 'SF', '2019-03-01', 'XYZ')
print(new_sensor.brand)
The above is a good way to keep our code reusable.
5. Encapsulation#
It describes the idea of restricting access to methods and attributes in a class.
Prevents data being modified by accident.
We can create
private methods
orprivate attributes
using underscore as prefix, i.e single_
or double__
.
5.1. Example - Encapsulation 1#
class Sensor():
def __init__(self, name, location):
self.name = name
self._location = location
self.__version = '1.0'
# a getter function
def get_version(self):
print(f'The sensor version is {self.__version}')
# a setter function
def set_version(self, version):
self.__version = version
if __name__ == "__main__":
sensor1 = Sensor('Acc', 'Berkeley')
print(sensor1.name)
print(sensor1._location)
print(sensor1.__version)
!———————————————————————-
Acc
Berkeley
!———————————————————————-
Error
AttributeError Traceback (most recent call last)
/tmp/ipykernel_165/1597133680.py in
19 print(sensor1.name)
20 print(sensor1._location)
—> 21 print(sensor1.__version)
AttributeError: ‘Sensor’ object has no attribute ‘__version’
!———————————————————————-
See the notes below:
Attribute |
Notes |
---|---|
|
It should not be accessed directly. |
|
This can not be accessed or modified directly. |
To get access to the double underscore attributes, we need to use
getter
andsetter
functions to access it internally.
sensor1.get_version()
sensor1.set_version('2.0')
sensor1.get_version()
The sensor version is 1.0
The sensor version is 2.0
6. Polymorphism#
Polymorphism allow us to use a single interface with different underlying forms such as data type or classes.
class Cat:
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"I am a cat. My name is {self.name}. I am {self.age} years old.")
def make_sound(self):
print("Meow")
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
def info(self):
print(f"I am a dog. My name is {self.name}. I am {self.age} years old.")
def make_sound(self):
print("Bark")
cat1 = Cat("Kitty", 2.5)
dog1 = Dog("Fluffy", 4)
for animal in (cat1, dog1):
animal.make_sound()
animal.info()
animal.make_sound()
Note
See that both classes Cat
and Dog
share a similar structure and have same method names
info()
and make_sound()
.