Chapter 6: Classes and Objects in Python¶
Python, as an object-oriented programming language, provides a powerful approach to manage and organize your code through the use of classes and objects. This chapter will guide you through the fundamentals of object-oriented programming (OOP) in Python, introducing you to key concepts such as classes
, objects
, inheritance
, encapsulation
, polymorphism
, and enumeration
.
1. Introduction to Classes and Objects¶
In Python, a class can be thought of as a blueprint
for creating objects. Objects are instances of classes and can hold data (attributes
) and operations (methods
) related to that data.
1.1 Defining a Class and Creating Objects¶
Constructor
: Python use constructor to return an object of a class__init__()
class MyClass:
"""A simple example class"""
class_variable = 123
def __init__(self, value): # Constructor
self.instance_variable = value
def a_method(self):
return f'Class Variable: {MyClass.class_variable}, Instance Variable: {self.instance_variable}'
# Creating an object of MyClass
my_object = MyClass(456)
print(my_object.a_method())
Class Variable: 123, Instance Variable: 456
class Person:
hair = 'black'
def __init__(self,name = 'Charlie',age = 8):
# Add two instance parameter to Person class
self.name = name
self.age = age
# Define a say function
def say(self,content):
print(content)
1.2 Generate Objects and Use it¶
p = Person()
print(p.name,p.age)
Charlie 8
p.name = 'Nio'
p.say('It is easy to learn python programming!')
print(p.name,p.age)
It is easy to learn python programming! Nio 8
1.3 Dynamic add attributes and methods¶
p.skills = ['programming','swimming']
print(p.skills)
# Delete parameters within a object
del p.name
print(p.name) # AttributeError
['programming', 'swimming']
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[6], line 5 3 # Delete parameters within a object 4 del p.name ----> 5 print(p.name) # AttributeError AttributeError: 'Person' object has no attribute 'name'
def info(self):
print("---info function---",self)
# use info function
p.foo = info
p.foo(p)
---info function--- <__main__.Person object at 0x0000022B91A83750>
p.bar = lambda self: print("---lambda ---",self)
p.bar(p)
---lambda --- <__main__.Person object at 0x0000022B91A83750>
bind the self
as the first parameter to the dynamic added function
MethodType
:- The
MethodType
function is used to convert the functionintro_func
into a method of an instancep
. - The
MethodType
function essentially binds the first parameter of the function (self
) to the instancep
, so when the method is called,p
is automatically passed as the first argument.
- The
def intro_func(self,content):
print("I am a person, infomation are %s" % content)
from types import MethodType
p.intro = MethodType(intro_func,p)
# the first parameter are fixed as 'p', do not need to pass again
p.intro("What a good life")
I am a person, infomation are What a good life
1.4 Class variables(static) and instance variables¶
If you want to assign value to instance variables, you could do like following
class Inventory:
# two class variables
item = 'Mouse'
dpi = 2000
# define a instance function
def change(self,item,dpi):
self.item = item
self.dpi = dpi
iv = Inventory()
iv.change('Razer',3600)
print(iv.item)
print(iv.dpi)
print(Inventory.item)
print(Inventory.dpi)
Razer 3600 Mouse 2000
In this case, if you change the value of class variables, the value of instance variables would not be changed
Inventory.item = 'logitech'
Inventory.dpi = 5400
print(iv.item)
print(iv.dpi)
Razer 3600
2. Class Methods vs. Static Methods¶
Class Methods
- Class methods are methods that are bound to the class itself, not to an instance of the class.
- The first parameter of a class method is typically named
cls
, which represents the class itself. - Class methods can access and modify
class attributes
, but they cannot access or modifyinstance attributes
(unless an instance is explicitly passed). - Class methods are marked with the
@classmethod
decorator.
Static Methods ((usually not needed))
- Static methods are functions defined within a class, but they are not associated with instances or the class itself.
- Static methods do not receive an implicit first argument (
self
orcls
). - Static methods cannot access or modify class or instance attributes directly.
- Static methods are marked with the
@staticmethod
decorator.
class Bird:
# using @classmethod to illustrate it is a class method
@classmethod
def fly(cls):
print('Class method fly: ', cls)
# using @staticmethod to illustrate it is a static method
@staticmethod
def info(p):
print('Static method info: ', p)
Bird.fly() # Class method fly: <class '__main__.Bird'>
Bird.info('crazyit') # Static method info: crazyit
b = Bird()
b.fly() # Class method fly: <class '__main__.Bird'>
b.info('fkit') # Static method info: fkit
Class method fly: <class '__main__.Bird'> Static method info: crazyit Class method fly: <class '__main__.Bird'> Static method info: fkit
- A
class method
normally works as afactory method
and returns an instance of the class with supplied arguments. - However, it doesn't always have to work as a factory class and return an instance. You can create an instance in the class method, do whatever you need, and don’t have to return it
class Cellphone:
def __init__(self, brand, number):
self.brand = brand
self.number = number
def get_number(self):
return self.number
@staticmethod
def get_emergency_number():
return "911"
@classmethod
def iphone(cls, number):
_iphone = cls("Apple", number)
print("An iPhone is created.")
return _iphone
iphone = Cellphone.iphone("1112223333")
# An iPhone is created.
iphone.get_number()
# "1112223333"
iphone.get_emergency_number()
An iPhone is created.
'911'
3. Function Decorators¶
Function decorators in Python are a way to modify the behavior of a function without changing its source code directly.
They are higher-order functions that take a function as input, add some functionality to it, and return a new function with the added functionality.
3.1 How Decorators Work¶
When you use @decorator_function
to decorate decorated_function
, the following happens:
- The
decorated_function
is passed as an argument to thedecorator_function
. - The
decorator_function
performs some additional operations and returns a new function object. - The new function object returned by the
decorator_function
is assigned to the name of thedecorated_function
.
3.2 Example: Authority Checking Decorator¶
def auth(fn):
def auth_fn(*args):
print("Authority checking")
# Call the input function
fn(*args)
return auth_fn
@auth
def test(a, b):
print(f"Perform test function, parameter a: {a}, parameter b: {b}")
test(20, 15) # Output: Authority checking
# Perform test function, parameter a: 20, parameter b: 15
Authority checking Perform test function, parameter a: 20, parameter b: 15
3.3 @property
¶
In the code snippet above, there is a function called get_number
which returns the number of a Cellphone instance. We can optimize this method a bit and return a formatted phone number:
class Cellphone:
def __init__(self, brand, number):
self.brand = brand
self.number = number
def get_number(self):
_number = "-".join([self.number[:3], self.number[3:6], self.number[6:]])
return _number
cellphone = Cellphone("Samsung", "1112223333")
print(cellphone.get_number())
# 111-222-3333
111-222-3333
- As we see, in this example, when we try to get the number of a cellphone, we don’t return it directly but do some formatting before returning it.
- This is a perfect case for using the
@property
decorator.- In Python, with the
@property
decorator, you can usegetter
andsetter
to manage the attributes of your class instances very conveniently.
- In Python, with the
- The above code can be re-written with
@propery
like this:
class Cellphone:
def __init__(self, brand, number):
self.brand = brand
self.number = number
@property
def number(self):
_number = "-".join([self._number[:3], self._number[3:6],self._number[6:]])
return _number
@number.setter
def number(self, number):
if len(number) != 10:
raise ValueError("Invalid phone number.")
self._number = number
cellphone = Cellphone("Samsung", "1112223333")
print(cellphone.number)
# 111-222-3333
111-222-3333
- if only use
@property
, then this variable only readable but not writeable - add
@xxx.setter
to add the writeable properties
class Cell:
@property
def state(self):
return self._state
@state.setter
def state(self,value):
if 'alive' in value.lower():
self._state = 'alive'
else:
self._state = 'dead'
@property
def is_dead(self):
return not self._state.lower() == 'alive'
c = Cell()
c.state = 'Alive'
print(c.state)
print(c.is_dead)
alive False
4. Namespace¶
In python, every class has their own namespace
global_fn = lambda p: print('lambda expression, p is:',p)
class Category:
cate_fn = lambda p: print('lambda expression, p is:',p)
global_fn('Python')
c = Category()
# When crate a object, python would combine the first parameter automatically
c.cate_fn()
lambda expression, p is: Python lambda expression, p is: <__main__.Category object at 0x0000022B91F9ACD0>
class User:
def __init__(self, name, age):
self.name = name
self.age = age
def __hide__(self):
print('demonstrate hide function')
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if len(value) < 3 or len(value) > 8:
raise ValueError('The length of username must between 3 to 8!')
self._name = value
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if value < 15 or value > 75:
raise ValueError('The age of user must between 15 to 75!')
self._age = value
u = User('jk',14)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[3], line 1 ----> 1 u = User('jk',14) Cell In[2], line 3, in User.__init__(self, name, age) 2 def __init__(self, name, age): ----> 3 self.name = name 4 self.age = age Cell In[2], line 16, in User.name(self, value) 13 @name.setter 14 def name(self, value): 15 if len(value) < 3 or len(value) > 8: ---> 16 raise ValueError('The length of username must between 3 to 8!') 17 self._name = value ValueError: The length of username must between 3 to 8!
u = User('Betty',24)
print(u.name)
print(u.age)
Betty 24
u.__hide__()
demonstrate hide function
5.2 Inheritance¶
In python, one subclass could inherit from multiple superclass
Basic inheritance¶
class Fruit:
def info(self):
print('This fruit has %g gram' %self.weight)
class Apple(Fruit):
def taste(self):
print('This is a food!')
a = Apple()
a.weight = 5.6
a.info()
a.taste()
This fruit has 5.6 gram This is a food!
Multiple superclas¶
- It is not recommended since it would lead to some unknown error.
class Fruit:
def info(self):
print('This fruit has %g gram' %self.weight)
class Food:
def taste(self):
print('This is a food!')
class Apple(Fruit,Food):
pass
a = Apple()
a.weight = 5.6
a.info()
a.taste()
This fruit has 5.6 gram This is a food!
Overwrite the function from superclass¶
class Bird:
def fly(self):
print('I am flying!')
class Ostrich(Bird):
def fly(self):
print('I could only running.')
os1 = Ostrich()
os1.fly()
I could only running.
If you want to use the function in superclass which has been overwritten, you could use the following statement
class BaseClass:
def foo(self):
print('foo function in the superclass')
class SubClass(BaseClass):
def foo(self):
print('foo function in the subclass')
def bar(self):
print('in the bar function')
self.foo()
BaseClass.foo(self)
sc = SubClass()
sc.bar()
in the bar function foo function in the subclass foo function in the superclass
Use the super
function to call the constructor of the superclass¶
- If you want to create a
subclass
from two or more superclass, you have to overwrite the constructor - In the constructor of the subclass, use s
uper()
to call the constructor from superclass
help(super)
Help on class super in module builtins: class super(object) | super() -> same as super(__class__, <first argument>) | super(type) -> unbound super object | super(type, obj) -> bound super object; requires isinstance(obj, type) | super(type, type2) -> bound super object; requires issubclass(type2, type) | Typical use to call a cooperative superclass method: | class C(B): | def meth(self, arg): | super().meth(arg) | This works for class methods too: | class C(B): | @classmethod | def cmeth(cls, arg): | super().cmeth(arg) | | Methods defined here: | | __get__(self, instance, owner=None, /) | Return an attribute of instance, which is of type owner. | | __getattribute__(self, name, /) | Return getattr(self, name). | | __init__(self, /, *args, **kwargs) | Initialize self. See help(type(self)) for accurate signature. | | __repr__(self, /) | Return repr(self). | | ---------------------------------------------------------------------- | Static methods defined here: | | __new__(*args, **kwargs) from builtins.type | Create and return a new object. See help(type) for accurate signature. | | ---------------------------------------------------------------------- | Data descriptors defined here: | | __self__ | the instance invoking super(); may be None | | __self_class__ | the type of the instance invoking super(); may be None | | __thisclass__ | the class invoking super()
class Employee:
def __init__(self,salary):
self.salary = salary
def work(self):
print('This man is working, with %s salary' %self.salary)
class Customer:
def __init__(self,favorite,address):
self.favorite = favorite
self.address = address
def info(self):
print('I am a customer, I favorite thing is %s, and my address is %s' %(self.favorite, self.address))
class Manager(Employee,Customer):
def __init__(self,salary,favorite,address):
print('--Constructor of Manager---')
super().__init__(salary)
Customer.__init__(self,favorite,address)
m = Manager(125000,'Python','Texas')
m.work()
m.info()
--Constructor of Manager--- This man is working, with 125000 salary I am a customer, I favorite thing is Python, and my address is Texas
6. Polymorphism¶
Polymorphism means the ability to take various forms.
In Python, Polymorphism allows us to define methods in the child class with the same name as defined in their parent class.
class Bird:
def move(self,field):
print('Birds fly in the %s' %field)
class Dog:
def move(self,field):
print('Dogs run on the %s' %field)
x = Bird()
x.move('sky')
x = Dog()
x.move('ground')
Birds fly in the sky Dogs run on the ground
It is very useful when two or more classes correlated with each other
class Canvas:
def draw_pic(self,shape):
print('---Start Drawing---')
shape.draw(self)
class Rectangle:
def draw(self,canvas):
print('Draw a rectangle on %s' %canvas)
class Triangle:
def draw(self,canvas):
print('Draw a triangle on %s' %canvas)
class Circle:
def draw(self,canvas):
print('Draw a circle on %s' %canvas)
c = Canvas()
c.draw_pic(Rectangle())
c.draw_pic(Triangle())
c.draw_pic(Circle())
---Start Drawing--- Draw a rectangle on <__main__.Canvas object at 0x000001C4C1206E50> ---Start Drawing--- Draw a triangle on <__main__.Canvas object at 0x000001C4C1206E50> ---Start Drawing--- Draw a circle on <__main__.Canvas object at 0x000001C4C1206E50>
7. Check the type of instance and class¶
issubclass(cls,class_or_tuple)
isinstance(obj,class_or_tuple)
hello = 'Hello'
print(isinstance(hello,str))
print(isinstance(hello,object))
print(issubclass(str,object))
True True True
Every class is the subclass of object
print(issubclass(Rectangle,object))
print(issubclass(list,object))
True True
- Python provide
__bases__
to check all the superclasses of the input class - Python provide
__subclasses__ ()
to check all the subclasses of the input class
print(Rectangle.__bases__)
print(list.__subclasses__())
(<class 'object'>,) [<class 'functools._HashedSeq'>, <class 'traceback.StackSummary'>, <class 'socketserver._Threads'>, <class 'logging.config.ConvertingList'>, <class 'traitlets.config.loader.DeferredConfigList'>, <class 'dateutil.parser._parser._ymd'>, <class 'email.header._Accumulator'>, <class 'importlib.metadata.DeprecatedList'>, <class 'IPython.utils.text.SList'>, <class 'prompt_toolkit.document._ImmutableLineList'>, <class 'prompt_toolkit.formatted_text.base.FormattedText'>, <class 'xml.dom.minicompat.NodeList'>, <class 'prompt_toolkit.layout.utils._ExplodedList'>, <class 'parso.parser.Stack'>, <class '_pydevd_frame_eval.vendored.bytecode.bytecode._BaseBytecodeList'>, <class '_pydevd_frame_eval.vendored.bytecode.bytecode._InstrList'>]
class Student:
gender = None
phone = None
address = None
email = None
def __init__(self,name,age):
self.name = name
self.age = age
@property
def email(self):
return self.__email
@email.setter
def email(self,value):
if('@' not in value):
raise ValueError('It is not a email address')
self.__email = value
def eat(self,things):
print('Student %s is eating %s' %(self.name,things))
def sleep(self):
print('Student %s is sleeping!')
a = Student('Anna',14)
b = Student('Betty',15)
c = Student('Carrolin',15)
d = Student('Dick',16)
a.eat('burger')
Student Anna is eating burger
a.email = '12345678@gmail.com'
a.address = 'NewYork'
b.email = '987654321@utdallas.edu'
b.address = 'Texas'
c.email = '7539514682@yahoo.com'
c.address = 'Chicago'
d.email = '147896352@163.com'
c.address = 'Ohio'
a.email = '12345678'
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[20], line 1 ----> 1 a.email = '12345678' Cell In[17], line 18, in Student.email(self, value) 15 @email.setter 16 def email(self,value): 17 if('@' not in value): ---> 18 raise ValueError('It is not a email address') 19 self.__email = value ValueError: It is not a email address
8.2 Create a address book list of the students instance¶
Could do the query through name, email, and address variables.
add_book = [a,b,c,d]
def Student_query(q_variable,q_property,add_book):
variable_lst = ['name','email','address']
if q_variable not in variable_lst:
raise ValueError('Please use one of name,email and address to do the query!')
for student in add_book:
if getattr(student,q_variable) == q_property:
print('Find it, the student named %s' %student.name)
return student
break
print('Sorry, we could not find it')
Student_query('address','Ohio',add_book)
Find it, the student named Carrolin
<__main__.Student at 0x1c4c0cf1810>
Student_query('address','Ohio1',add_book)
Sorry, we could not find it