1. Instance-level data#
Let’s see how we can distinguish Instance-level data and Class-level data.
The following code has
nameandsalaryas 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 attributesin the body ofclass. Thatclass attributewill serve as aglobal variablewithin 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 attributesis to have global constants that are related to class, for exampleminandmaxvalues for attributes, or commonly used values and constants, e.g.pi.
3. Class Methods#
Regular
methodsare 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-leveldata.To define a
class-method, we have to start with aclassmethoddecorator, 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
Employeeobject 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-methodthat 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 foryearmonth, andday. But we can also create aBetterDateobjects fromstringsanddatetime.
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
Sensorwith attributesname,location, andrecord_datethat pass from the creation of an object and an attributedataas an empty dictionary to store data. The methodadd_datacontainstanddataas input parameters to take in timestamp and data arrays. Within this method, we also assigntanddatato theself.dataattribute withtimeanddataas the[`keys`]. The methodclear_datais 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
Sensorclass, 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
Sensorclass 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
Sensorclass 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
NewSensorthat inherits fromSensorclass, 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 methodsorprivate attributesusing 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
getterandsetterfunctions 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().