Main point of OOP: code reuse. Inheritance search look for an attribute: from bottom to top of the object tree and from left to right. Difference between a class object and an instance object: both are namespace (packages of variables that appear as attributes); difference is that classes creates instances. First argument in a class's method: self by convention; receives the instance object; designed to process or change objects. init method use for: constructor method; it is passed the new instance implicitly and any argument explicitly; most commonly used operator overloading method. Specify a class's superclasses: listing them in parentheses in the class statement after the new class's name.
Class coding basics: Classes generate multiple instance objects; Classes are customized by inheritance; Classes can intercept python operators.
How are classes related to modules?: classes are statements not entire files; module is like a single instance class (without inheritance). How and where are class attributes created?: assigning attributes to a class object. How and where are instance attributes created?: assigning attributes to a class object. How is operator overloading coded in a class?: begin and end with double underscores; runs them automatically when an instance appear in the corresponding operation. When might you want to support operator overloading?: for mimic the built-in's. Key concepts to understand OOP. self and init.
A more realistic example: Making instance; Adding behavior methods; Operator overloading; Customizing behavior by subclassing; Customizing constructors, too; Using introspection tools; Storing objects in a database; Future directions. We took objects created by our classes and made them persistent by storing them on a shelve object db.
Importance of move processing into methods instead of hardcoding: only change one (code maintenance, avoid redundancy), so the method can be run on any instance (encapsulation). Why customize by subclassing rather than copying the original and modifying?: extend our prior work with custom stuff. Why call back to a superclass to run default actions, instead of coding and modifying its code in a subclass?: if a subclass needs to perform default actions coded in a superclass method then call back it (avoid redundancy). Why is it better to use tools like dict that allow objects to be processed generically than to write more custom code for each type of class?: repr not be updated each time a new attribute is added to instances in an init constructor.
Class coding details: The class statement; Methods; Inheritance; Namespaces; Documentation strings; Classes versus modules.
What is an abstract superclass?: class that callas a method but does not inherit or define it; this if often used as a way to generalize classes when behavior cannot be predicted until a more specific sub-class. Why might a class need to manually call the init method in a superclass?. Augmenting an inherited method: to augment you have to redefine it in a subclass, but call back to the superclass's of the method; pass the self instance to the superclass's on the method manually (subclass.method(self,...)). How does a class's local scope differ from that of a function: Like modules, the class local scope morphs into an attribute namespace after the class statement is run.
Operator overloading: Indexing and slicing; Iterable objects; Index iteration; Membership; Attributes access; String representation; Right-side and in-place uses; call expressions; Comparisons; Boolean tests; Object destruction.
> Operator overloading methods to support iteration in your classes. Operators overloading methods handle printing and in what context?. How to intercept slice operations in a class?. How can you catch in-place addition in a class?. When should you provide operator overloading?.
Design with classes: Python and OOP; OOP and inheritance; OOP and composition; OOP and delegation; Pseudoprivate class attributes; Methods are objects; Multiple inheritance; Other design-related topics.
Multiple inheritance: class inherits from more than one superclass; mixing together multiples packages of class-based code. Delegation: wrap an object in a proxy class. Composition: controller class embeds and directs a number of objects and provides an interface all its own. Bound methods: combine an instance and a method function; you can call them without passing in an instance object (original instance is still available). Pseudoprivate attributes: name begin with '__' are used to localize names to the enclosing class
Advance class topics: Extending built-in types; New style class model;/changes; /extensions; /methods; Decorators and metaclasses; The super built-in function; Class gotchas.
What are function and class decorators?: function decorators are generally used to manage a function or method, or add to it a layer of logic that is run each time the function or method is called (can be used to log, count calls, check argument types, declare static methods, class methods and properties); class decorators similar but manage whole objects and their interfaces instead of a function call.
> Code a new-style class:: search the diamond pattern of multiple inheritance trees differently. Difference between classic a new-style classes. Normal and static methods. slots and super valid to use?.
#%%ho
sequence = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
def multiply_by_2(item):
return item * 2
mapped = map(multiply_by_2, sequence)
list(mapped)
def multipley(args):
total = 1
for arg in args:
total = total * arg
return total
multipley(args=sequence)
list(map(lambda item: item * 2, sequence))
#%%
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self): # for print function to work with this class object
return f"{self.name} is {self.age} years old."
bob = Person('Bob', 35)
print(bob)
#%%
# class test with instance method, classmethod and staticmethod
class ClassTest():
def instance_method(self): # instance method
print(f"This is an instance method {self}")
@classmethod
def class_method(cls): # cls is the class itself
print(f"This is a class method {cls}")
@staticmethod
def static_method(): # no access to the class or instance
print("This is a static method")
test = ClassTest()
ClassTest().class_method()
ClassTest().instance_method()
ClassTest().static_method()
# create a class with a cuple of class methods + static methods
class Book():
TYPES = ('hardcover', 'paperback')
def __init__(self, name, book_type, weight): # constructor method for the class Book with 3 arguments (name, book_type, weight)
self.name = name
self.book_type = book_type
self.weight = weight
def __repr__(self):
return f"<Book {self.name}, {self.book_type}, weighing {self.weight}g>"
@classmethod
def hardcover(cls, name, page_weight): # cls is the class itself (Book)
return cls(name, cls.TYPES[0], page_weight + 100)
@classmethod
def paperback(cls, name, page_weight): # cls is the class itself (Book)
return cls(name, cls.TYPES[1], page_weight)
@staticmethod # no access to the class or instance (Book) or self
def book_type_weight(weight):
return weight * 0.5
Book.hardcover('Harry Potter', 1500)
#%% Inheritance
class Device(): # parent class (superclass) with 2 methods (on and off) and 2 attributes (name and connected_by)
def __init__(self, name, connected_by):
self.name = name
self.connected_by = connected_by
self.connected = True
def __str__(self):
return f"Device {self.name!r} ({self.connected_by})"
def disconnect(self): # instance method
self.connected = False
print("Disconnected.")
printer = Device("Printer", "USB")
print(printer)
printer.disconnect()
class Printer(Device): # Printer class inherits from Device class (Printer is a Device)
def __init__(self, name, connected_by, capacity): # constructor method for the class Printer with 3 arguments (name, connected_by, capacity)
super().__init__(name, connected_by) # super() is the parent class (Device) and it's constructor method (Device.__init__) is called with the arguments (name, connected_by)
self.capacity = capacity # self.capacity is an attribute of the class Printer
self.remaining_pages = capacity # self.remaining_pages is an attribute of the class Printer
def __str__(self):
return f"{super().__str__()} ({self.remaining_pages} pages remaining)" # super().__str__() calls the __str__ method of the parent class (Device) and returns the string representation of the parent class (Device)
def print(self, pages):
if not self.connected:
print("Your printer is not connected!")
return
print(f"Printing {pages} pages.")
self.remaining_pages -= pages # self.remaining_pages is an attribute of the class Printer and it's value is reduced by the number of pages printed
printer = Printer("Printer", "USB", 500)
printer.print(20)
printer.disconnect()
#%% Composition
class BookShelf(): # parent class (superclass) with 1 method (__init__) and 1 attribute (number_of_books)
def __init__(self, *books): # constructor method for the class BookShelf with 1 argument (*books) which is a tuple of books (Book objects)
self.books = books
def __str__(self):
return f"BookShelf with {len(self.books)} books."
class Book(): # parent class (superclass) with 2 methods (__init__) and 2 attributes (name and book_type)
def __init__(self, name):
self.name = name
def __str__(self):
return f"Book {self.name}"
book = Book("Harry Potter")
book2 = Book("Python 101")
shelf = BookShelf(book, book2)
#%% First class functions
def divide(dividend, divisor): # function divide
if divisor == 0:
raise ZeroDivisionError("Divisor cannot be 0.") # raise an exception
return dividend / divisor
def calculate(*values, operator): # function calculate with 2 arguments (*values and operator)
return operator(*values)
result = calculate(20, 4, operator=divide) # operator=divide is a keyword argument and divide is a function
print(result)
# Usefull functions for lists
def search(sequence, expected, finder):
for elem in sequence:
if finder(elem) == expected:
return elem
raise RuntimeError(f"Could not find an element with {expected}.")
friends = [
{"name": "Rolf Smith", "age": 24},
{"name": "Adam Wool", "age": 30},
{"name": "Anne Pun", "age": 27}
]
def get_friend_name(friend):
return friend["name"]
print(search(friends, "Rolf Smith", get_friend_name))
print(search(friends, "Adam Wool", lambda friend: friend["name"])) # lambda function is used instead of get_friend_name function
#%% Simple decorator
import functools
def make_secure(func): # function make_secure with 1 argument (func) which is a function get_admin_password
@functools.wraps(func) # functools.wraps is used to preserve the metadata of the function get_admin_password
def secure_function(): # secure_function is a wrapper function for the function func
if user["access_level"] == "admin":
return func() # func() is the function func
else:
return f"No admin permissions for {user['username']}."
return secure_function # return the function secure_function
@make_secure # make_secure is a decorator for the function get_admin_password
def get_admin_password(): # function get_admin_password
return "1234"
# get_admin_password = make_secure(get_admin_password) # get_admin_password is a function and it's value is the function secure_function which is returned by the function make_secure with the argument get_admin_password
user = {"username": "jose", "access_level": "admin"}
print(get_admin_password()) # secure_function() is called
print(get_admin_password.__name__) # secure_function is the name of the function get_admin_password
#%% Decorator functions with parameters
def make_secure(func): # function make_secure with 1 argument (func) which is a function get_admin_password
@functools.wraps(func) # functools.wraps is used to preserve the metadata of the function get_admin_password
def secure_function(*args, **kwargs): # secure_function is a wrapper function for the function func
if user["access_level"] == "admin":
return func(*args, **kwargs) # func() is the function func
else:
return f"No admin permissions for {user['username']}."
return secure_function # return the function secure_function
@make_secure # make_secure is a decorator for the function get_admin_password
def get_admin_password(panel): # function get_admin_password
if panel == "admin":
return "1234"
elif panel == "billing":
return "super_secure_password"
print(get_admin_password("billing")) # secure_function() is called
#%% Decorator with parameters
user = {"username": "jose", "access_level": "guest"}
def make_secure(access_level): # function make_secure with 1 argument (access_level)
def decorator(func): # create a decotator. Function decorator with 1 argument (func) which is a function get_admin_password
@functools.wraps(func) # functools.wraps is used to preserve the metadata of the function get_admin_password
def secure_function(*args, **kwargs):
if user["access_level"] == access_level:
return func(*args, **kwargs)
else:
return f"No {access_level} permissions for {user['username']}."
return secure_function
return decorator
@make_secure("admin") # make_secure is a decorator for the function get_admin_password
def get_admin_password(): # function get_admin_password
return "1234"
@make_secure("guest") # make_secure is a decorator for the function get_dashboard_password
def get_dashboard_password(): # function get_dashboard_password
return "user_password"
print(get_admin_password()) # secure_function() is called
print(get_dashboard_password()) # secure_function() is called
#%% Custom errors
class TooManyPagesReadError(ValueError): # class TooManyPagesReadError is a subclass of the class ValueError
pass
class Book():
# init method with name, page_count and pages_read attributes
def __init__(self, name, page_count, pages_read):
self.name = name
self.page_count = page_count
self.pages_read = 0
def __repr__(self):
return f"<Book {self.name}, read {self.pages_read} pages out of {self.page_count}>" # return a string representation of the object
def read(self, pages:int): # method read with 1 argument (pages)
if self.pages_read + pages > self.page_count: # if the number of pages read is greater than the number of pages in the book
# raise ValueError(f"You tried to read {self.pages_read + pages} pages, but this book only has {self.page_count} pages.") # raise an exception
raise TooManyPagesReadError(f"You tried to read {self.pages_read + pages} pages, but this book only has {self.page_count} pages.") # raise an exception with a custom error
python101 = Book("Python 101", 50, 0) # create an object python101 from the class Book
try:
python101.read(35) # call the method read with 1 argument (35)
python101.read(50) # call the method read with 1 argument (50)
except TooManyPagesReadError as e:
print(e)
#%% Decorators simple example
def decorator_function(original_function): # function decorator_function with 1 argument (original_function)
def wrapper_function(*args, **kargs): # function wrapper_function
print(f"wrapper executed this before {original_function.__name__}") # print a string
return original_function(*args, **kargs) # return the function original_function
return wrapper_function
class decorator_class(object): # class decorator_class
def __init__(self, original_function): # init method with 1 argument (original_function)
self.original_function = original_function # attribute original_function
def __call__(self, *args, **kwargs): # method __call__ with 2 arguments (args and kwargs)
print(f"call method executed this before {self.original_function.__name__}")
return self.original_function(*args, **kwargs)
def display(): # function display
print("display function ran")
# Using function decorator ------------------------------------------
@decorator_function # decorator_function is a decorator for the function display
def displayother(): # function display
print("display function ran another function")
@decorator_function # decorator_function is a decorator for the function display
def display_info(name, age): # function display_info with 2 arguments (name, age)
print(f"display_info ran with arguments ({name}, {age})")
# Using class decorator ------------------------------------------
@decorator_class # decorator_function is a decorator for the function display
def displayother(): # function display
print("display function ran another function")
@decorator_class # decorator_function is a decorator for the function display
def display_info(name, age): # function display_info with 2 arguments (name, age)
print(f"display_info ran with arguments ({name}, {age})")
decorated_display = decorator_function(display)() # decorated_display is a function and it's value is the function wrapper_function which is returned by the function decorator_function with the argument display
displayother() # call the function displayother which is decorated by the decorator decorator_function
display_info('John', 25) # call the function display_info which is decorated by the decorator decorator_function
#%% Practical example of decorators
import functools
def my_logger(orig_func): # function my_logger with 1 argument (orig_func)
import logging # import the module logging
# set the basic configuration of the logging module
logging.basicConfig(
filename=f'{orig_func.__name__}.log', level=logging.INFO) # set the filename and the level of logging
# functools.wraps is used to preserve the metadata of the function orig_func
@functools.wraps(orig_func)
def wrapper(*args, **kwargs): # function wrapper with 2 arguments (args and kwargs)
# log a message
logging.info(f'Ran with args: {args}, and kwargs: {kwargs}')
return orig_func(*args, **kwargs) # return the function orig_func
return wrapper
def my_timer(orig_func): # function my_timer with 1 argument (orig_func)
import time
# functools.wraps is used to preserve the metadata of the function orig_func
@functools.wraps(orig_func)
def wrapper(*args, **kwargs): # function wrapper with 2 arguments (args and kwargs)
t1 = time.time() # get the current time
result = orig_func(*args, **kwargs) # return the function orig_func
t2 = time.time() - t1 # get the current time and subtract the time t1
print(f'{orig_func.__name__} ran in: {t2} sec') # print a string
return result # return the result of the function orig_func
return wrapper
@my_logger
def display_info(name, age): # function display_info with 2 arguments (name, age)
print(f"display_info ran with arguments ({name}, {age})")
import time
@my_timer # decorator my_timer is a decorator for the function display_info_sleep
def display_info_sleep(name, age): # function display_info with 2 arguments (name, age)
time.sleep(1)
print(f"display_info ran with arguments ({name}, {age})")
display_info('John', 25) # call the function display_info which is decorated by the decorator my_logger
display_info_sleep('Wick', 40)
#%% Change decorators together
from functools import wraps
def my_logger(orig_func): # function my_logger with 1 argument (orig_func)
import logging # import the module logging
# set the basic configuration of the logging module
logging.basicConfig(
filename=f'{orig_func.__name__}.log', level=logging.INFO) # set the filename and the level of logging
# functools.wraps is used to preserve the metadata of the function orig_func if not used the function display_info will have the name wrapper
@wraps(orig_func)
def wrapper(*args, **kwargs): # function wrapper with 2 arguments (args and kwargs)
# log a message
logging.info(f'Ran with args: {args}, and kwargs: {kwargs}')
return orig_func(*args, **kwargs) # return the function orig_func
return wrapper
def my_time(original_function): # function my_time with 1 argument (original_function)
import time
@wraps(original_function)
def wrapper(*args, **kwargs): # function wrapper with 2 arguments (args and kwargs)
t1 = time.time() # get the current time
result = original_function(*args, **kwargs) # return the function original_function
t2 = time.time() - t1 # get the current time and subtract the time t1
print(f'{original_function.__name__} ran in: {t2} sec') # print a string
return result # return the result of the function original_function
return wrapper
@my_logger
@my_time
def display_info(name, age): # function display_info with 2 arguments (name, age)
time.sleep(1)
print(f"display_info ran with arguments ({name}, {age})")
display_info('John', 25) # call the function display_info which is decorated by the decorator my_logger