Python Short Reference

Data types, functions, data structures, and classes
python
Author

Felix

Published

October 25, 2021

Complex Data Types

Copying and References

# copying with the copy library
import copy
first_str = "Will"
second_str = copy.copy(first_str)
print(first_str)
print(second_str)
Will
Will
# changing elements via index
names = ["Jan", "Felix", "Ralph"]
second_names = names
second_names[1] = "Maria"
print(names)
print(second_names)
['Jan', 'Maria', 'Ralph']
['Jan', 'Maria', 'Ralph']
# using copy so index does not change the value in the copied element
third_names = copy.copy(names)
third_names[0] = "Julia"
print(names)
print(third_names)
['Jan', 'Maria', 'Ralph']
['Julia', 'Maria', 'Ralph']

Dictionaries and Sets

Dictionaries

# creating a dict
bike_owners = {"James":"Ducati Monster 1200",
               "Jacob":"Ducati Scrambler 1100"}
# printing an element
bike_owners["Jacob"]
# using int keys
int_dict = {1:45, 2:55, 3:65}
int_dict[1]
45
# print all keys
int_dict.keys()
dict_keys([1, 2, 3])
# mixed keys dictionaries
mixed_dict = {False: "Daniel",
              "Aria":[1,2,3],
              "Jacob":True}
mixed_dict
{False: 'Daniel', 'Aria': [1, 2, 3], 'Jacob': True}
# deleting elements
del int_dict[3]
int_dict
{1: 45, 2: 55}
# Complex data types
fruits = {
    "Banana":[50,60,75,99],
    "Apple":[48,86,47,25],
    "Strawberries":[70,80,60,65]
}
print(fruits["Banana"])
print(fruits["Banana"][3])
fruits["Banana"][3]=50
print(fruits["Banana"][3])
[50, 60, 75, 99]
99
50
# length function for number of elements
print(len(fruits))
# sorting a dict
print(sorted(fruits))
# return key value pairs
print(fruits.items())
3
['Apple', 'Banana', 'Strawberries']
dict_items([('Banana', [50, 60, 75, 50]), ('Apple', [48, 86, 47, 25]), ('Strawberries', [70, 80, 60, 65])])

Sets

# creating sets
set_string = {"Emma", "Olivia", "Ava", "Mia"}
print(set_string) # no intrinsic ordering
empty_set = set()
mixed_set = {"Emmma", 5, 1.5, True,(1,2,3,4)}
student_set = {"Emma", "Marc", "Janine", "Emma"}
print(student_set) # duplicates are eliminated
student_set.add("Felix")
print(len(student_set))
print(max(student_set))
student_set.remove("Felix")
# working with numbers
number1={1,2,3,4,5}
number2={4,5,6,7,8}
number3={7,8,9,10,11}
print(number1.union(number2))
print(number1.difference(number2))
print(number1.isdisjoint(number2))
print(number1.isdisjoint(number3))
{1, 2, 3, 4, 5, 6, 7, 8}
{1, 2, 3}
False
True

Lists

empty_list = []
list_str=["Toyota","VW","BMW","Mercedes"]
list_bool=[True, False, False, True]
list_str[1]
'VW'
print(list_str[len(list_str)-1])
print(list_str[-1])
Mercedes
Mercedes
list_str[0] = "Hyundai"
list_str
['Hyundai', 'VW', 'BMW', 'Mercedes']
list_str += ['Dacia', 'Ford']
list_str
['Hyundai', 'VW', 'BMW', 'Mercedes', 'Dacia', 'Ford']
list_str.sort()
list_str.reverse()
list_str.pop()
'BMW'
new_list = list_str.copy()
list_str.clear()
list_str
del list_str
list_2 = sorted(new_list)
list_2
['Dacia', 'Ford', 'Hyundai', 'Mercedes', 'VW']
print(new_list[0:2])
print(new_list[:-1])
['VW', 'Mercedes']
"Dacia" in new_list
True
# Nested Lists
car_matrix = [["Hennessey Venom GT", 1244],
              ["SSC Ultimate Aero", 1287],
              ["Zenvo ST1", 1100]]
car_matrix
print(len(car_matrix))
print(len(car_matrix[1]))
print(car_matrix[1][0])
3
2
SSC Ultimate Aero

Functions

Functions in general

# defining a function
country = "USA"
def some_fn():
    print("Country: ", country)
# calling a function
some_fn()
Country:  USA
def some_fn():
    global country # interpret the global variable
    country = "Bangladesh"
    print(country)
some_fn()
print(country) # global variable has changed
Bangladesh
Bangladesh
# passing by reference, because not reassigned and complex data type
fruits_list = ["Apple", "Grapes", "Mango", "Bananas"]
def change_list(fruits_list):
    fruits_list[0] = "Kiwi"
    fruits_list = ["Kiwi"] # does nothing on the outside
    print("Inside the function: ", fruits_list)

change_list(fruits_list)
print()
print("Outside the function: ", fruits_list)
Inside the function:  ['Kiwi']

Outside the function:  ['Kiwi', 'Grapes', 'Mango', 'Bananas']
# using python functions
import math
print(math.pi)
3.141592653589793
# multiple arguments
def calculate(*args, fn): # * -> multiple arguments, unpack the args tuple
    return fn(*args)

def diameter_circle_fn(r):
    pass

calculate(10, fn=diameter_circle_fn)
def area_rectangle_fn(length, breadth):
    return length * breadth

def calculate(*args, fn): # * -> multiple arguments, unpack the args tuple
    return fn(*args)

calculate(20, 40, fn=area_rectangle_fn)
800

Lambdas

# lambdas - functions without names
def square(x):
    return x * x
result = square(5)
cube_of = lambda x: x * x * x
result = cube_of(3)
result
27
add = lambda x, y: x + y
result = add(5, 10)
result
15
# all in one
(lambda x: x + 2)(10)
12
# lambda filter
num_list = [1,5,6,7,11,78,99,34,105,214]
filter(lambda x: x > 10, num_list)
<filter at 0x2220e96dd30>
greater_than_10_list = list(filter(lambda x: x > 10, num_list))
greater_than_10_list
[11, 78, 99, 34, 105, 214]

Functions advanced

Recursion

# Recursion - Invoking Functions - Invoking a function from within itself
def hello(name):
    print("Hello", name)
    hello(name)
#hello("Ron") - endless loop
# system wide recursion limit
import sys
sys.getrecursionlimit()
3000

Generators

# Generators - create a sequence that can be iterated over
def generator():
    print("One!")
    yield 1 # control execution flow
    print("Two!")
    yield 2
    print("Three!")
    yield 3
g = generator() # no code execution
g
<generator object generator at 0x0000013378275970>
next(g)
One!
1
def generate_even_numbers(limit):
    for i in range(0, limit, 2):
        yield i
g = generate_even_numbers(7)
next(g)
0
next(g)
2

Closures

# Closures - Function in a function
def nested_hello_fn():
    def hello():
        print("Hello Cathy!")
    hello()
nested_hello_fn()
Hello Cathy!
def get_hello_fn(): # every closure has its own local state (do not share local variables)
    def hello():
        print("Hello Cathy!")
    return hello
hello_fn = get_hello_fn()
hello_fn()
Hello Cathy!

Decorators

import random

def print_message():
    print("Yohoo! Decorators are cool!")

def make_highlighted(func):
    annotations = ["-", "*", "+"]
    annotate = random.choice(annotations)
    print(annotate * 50)
    func()
    print(annotate * 50)
make_highlighted(print_message)

@make_highlighted # that'S how they are really used
def print_a_message():
    print("Now you'll see how decorators are used")
print_a_message
--------------------------------------------------
Yohoo! Decorators are cool!
--------------------------------------------------
--------------------------------------------------
Now you'll see how decorators are used
--------------------------------------------------
def plus_highlight(func):
    def highlight():
        print("+"*50)
        func()
        print("+"*50)
    return highlight

def asterisk_highlight(func):
    def highlight():
        print("*"*50)
        func()
        print("*"*50)
    return highlight

@plus_highlight
@asterisk_highlight
def hello():
    print("hello!")
hello()
++++++++++++++++++++++++++++++++++++++++++++++++++
**************************************************
hello!
**************************************************
++++++++++++++++++++++++++++++++++++++++++++++++++

Data Structures

Queues

#Built-in queue
from queue import Queue
olympics = Queue(5)
olympics
<queue.Queue at 0x1934fae89d0>
olympics.put("United Statues(USA)")
olympics.put("Great Britain(GBR)")
print(olympics.empty())
print(olympics.full())
print(olympics.qsize())
olympics.put("China(CHN)")
olympics.put("Russia(RUS)")
olympics.put("Germany(GER)")
False
False
2
print(olympics.full())
olympics.get()
True
'United Statues(USA)'

Stacks

stack = []
stack.append("United States")
stack.append("Great Britain")
stack.append("China")
stack
['United States', 'Great Britain', 'China']
stack.pop()
'China'

Linked Lists

class Node:
    def __init__(self, dataval=None, nextval=None):
        self.dataval = dataval
        self.nextval = nextval

    def __repr__(self):
        return repr(self.dataval)

class LinkedList:
    def __init__(self):
        self.head = None

    def __repr__(self): # O(N)
        nodes = []
        curr = self.head

        while curr:
            nodes.append(repr(curr))
            curr = curr.nextval

        return "[" + "->".join(nodes) + "]"

    def prepend(self, dataval): # O(1)
        self.head = Node(dataval=dataval, nextval = self.head)

    def append(self, dataval):
        if not self.head:
            self.head = Node(dataval=dataval)
            return

        curr = self.head

        while curr.nextval:
            curr = curr.nextval

        curr.nextval = Node(dataval=dataval)

    def add_after(self, middle_dataval, dataval):
        if middle_dataval is None:
            print("Data to insert after not specified")
            return

        curr = self.head

        while curr and curr.dataval != middle_dataval:
            curr = curr.nextval

        new_node = Node(dataval = dataval)

        new_node.nextval = curr.nextval
        curr.nextval = new_node

    def find(self, data):
        curr = self.head
        while curr and curr.dataval != data:
            curr = curr.nextval

        return curr

    def remove(self, data):
        curr = self.head
        prev = None

        while curr and curr.dataval != data:
            prev = curr
            curr = curr.nextval

            if prev is None:
                self.head = curr.nextval
            elif curr:
                prev.nextval = curr.nextval
                curr.nextval = None

    def reverse(self):
        curr = self.head

        prev_node = None
        next_node = None

        while curr:
            nextval = curr.nextval
            curr.nextval = prev_node

            prev_node = curr

            curr = nextval

        self.head = prev_node
numbers = LinkedList()
numbers.append("two")
numbers.append("three")
numbers.prepend("one")
numbers
['one'->'two'->'three']

Classes and Inheritance

Basics

# Initializing
# special methods are marked with __methodname__
class Student:
    def __init__(self, name): # can be anything, but self is standard
        self.name = name # self refers to the current instance
        self.mail = name + "." + "@xyz.com"
class Competition:

    # class variable
    raise_amount = 1.04

    def __init__(self, name, prize):

        self.name = name
        self.prize = prize

    def raise_prize(self):
        self.prize = self.prize * Competition.raise_amount
debate = Competition('Debate', 500)

print(debate.raise_amount)
1.04

Private attributes

# instance and class variables are public by default
# hack for private attributes:
class Dog:
    def __init__(self, name, breed):
        self.__name = name
        self.__breed = breed

    def print_details(self):
        print('My name is %s and I am a %s' % (self.__name, self.__breed))

d1 = Dog("Moje", "Golden Retriever")
d1.print_details()
My name is Moje and I am a Golden Retriever
d1.__name = "Oba"
d1.print_details() # doesn't update
My name is Moje and I am a Golden Retriever
d1._Dog__breed = "Husky" # makes it harder to change, but can be changed
d1.print_details()
My name is Moje and I am a Husky

Special Methods

class Competition:

    def __init__(self, name, country, prize):
        self.__name = name
        self.__country = country
        self.__prize = prize

    def get_name_country(self):
        return '{} {}'.format(self.__name, self.__country, self.__prize)

    def __repr__(self): # representation for print function
        return "Competition: {} held in {}, prize: {}".format(self.__name, self.__country,
                                                               self.__prize)

    def __str__(self):
        return "'{} - {}'".format(self.get_name_country(), self.__prize)

archery = Competition("Archery", "Germany", 8000)
repr(archery)
'Competition: Archery held in Germany, prize: 8000'
str(archery) # looks for special method __str__()
"'Archery Germany - 8000'"
int.__add__(1, 2)
3

Properties

# Properties with Decorators
class Wrestler:
    def __init__(self, name):
        self.__name = name

    @property # this is the method for accessing
    def name(self):
        print("getter method called")
        return self.__name

    @name.setter # this is the method for setting new vals
    def name(self, value):
        print("setter method called")
        self.__name = value

    @name.deleter
    def name(self):
        del self.__name

w = Wrestler("Kart")
w.name
getter method called
'Kart'

Class Methods / Static Methods

class Competition:

    __raise_amount = 1.04 # class variable

    def __init__(self, name, country, prize):
        self.__name = name
        self.__country = country
        self.__prize = prize

    def raise_prize(self):
        self.__prize = self.__prize * self.__raise_amount

    def get_name_country(self):
        return '{} {}'.format(self.__name, self.__country, self.__prize)

    def __repr__(self): # representation for print function
        return "Competition: {} held in {}, prize: {}".format(self.__name, self.__country,
                                                               self.__prize)
    def __str__(self):
        return "'{} - {}'".format(self.get_name_country(), self.__prize)

    @classmethod
    def get_raise_amount(cls):
        return cls.__raise_amount

    @classmethod
    def set_raise_amount(cls, amount):
        cls.__raise_amount = amount
c1 = Competition("Running", "Germany", 50000)
c1.set_raise_amount(2)
c1.get_raise_amount()
2
Competition.get_raise_amount()
2
class Rectangle:

    @staticmethod # cannot access class variables
    def area(x,y):
        return x * y

Rectangle.area(5,5)
25

Abstract Methods

from abc import ABC, abstractmethod

class Hominidae(ABC):
    @abstractmethod # how to do it
    def diet(self):
        pass

    @abstractmethod
    def walk(self):
        pass

    def behavior(self):
        print("Blabla")

Inheritance

class Shape:
    def __init__(self, shape_type, color="Red"): # optional
        self.__type = shape_type
        self.__color = color

    def get_type(self):
        return self.__type

    def get_color(self):
        return self.__color

    def get_area(self):
        pass

    def get_perimeter(self):
        pass

class Circle(Shape):
    pass

circle = Circle("circle")

type(circle)
__main__.Circle
class Square(Shape):

    def __init__(self):
        Shape.__init__(self, "square")

square = Square()
square.get_type()
'square'
issubclass(Circle, Shape)
True
# Multiple and Multilevel Inheritance
class Father:
    def height(self):
        print("I have inherited my height from my father")

class Mother:
    def intelligence(self):
        print("I have inherited my intelligence from my mother")

class Child(Father, Mother):
    def experience(self):
        print("My experience are all my own")

c = Child()
c.height()
I have inherited my height from my father
c.intelligence()
I have inherited my intelligence from my mother

Creational Design Patterns

Singletons

  • a class of which only a single instance can exist
  • Ensure a class is instantiated only once, and provide a global point of access to it
class Logger:
    __instance = None

    def __init__(self):
        raise RuntimeError('Call get_instance() instead')

    @classmethod
    def get_instance(cls):
        if cls.__instance is None :
            print('No instance exists, creating a new one')
            cls.__instance = cls.__new__(cls)
        else:
            print('A previously created instance exists, returning that same one')
        return cls.__instance

logger1 = Logger.get_instance()
logger1

Pythonic implementation - new is always called first -> it creates the instance - cls is reference to not yet existing instance - init is used to initialize the existing instance -> self is a reference to the instance

class PythonicLogger:
    __instance = None

    def __init__(self):
        print('Object initialized')
        # put your custom code here
        # is called every time - could be expensive

    def __new__(cls):
        if cls.__instance is None:
            print('No instance exists, creating a new one')
            cls.__instance = super(PythonicLogger, cls).__new__(cls)
        else:
            print('A previously created instance exists, returning that same one')
        return cls.__instance
class SuperLogger:
    __instance = None

    def __new__(cls):
        if cls.__instance is None:
            print('No instance exists, creating a new one')
            cls.__instance = super(SuperLogger, cls).__new__(cls)
            # Place all initialization code here
        else:
            print('A previously created instance exists, returning that same one')
        return cls.__instance

Factory & Abstract Factory Design Patterns

  • Separate the creation of objects from their use
  • Class creation or Object creation
  • Class-creation patterns use inheritance
  • Object-creation patterns use delegation
  • Factory method is specified in base class, implemented in derived classes
  • Creates an instance of several derived classes
  • Define an interface for creating an object, but let subclasses decide which class to instantiate
  • Used to postpone instantiation - responsibility passes from base class to derived classes
  • Factory method -> create an instance of any of many derived classes
  • Abstract F. pattern -> create an instance of any one of many families of derived classes
  • Abstract Factory: create instances of several families of classes encapsulate platform dependencies
# Factory Method
class Product:
    def __init__(self, name, price):
        self.__name = name
        self.__price = price

    def get_price(self):
        return self.__price
class MacBookAir(Product):

    def __init__(self, memory, os):
        Product.__init__(self, 'MacBookAir', 1031)

        self.__memory = memory
        self.__os = os


class AppleIPad(Product):

    def __init__(self, generation):
        Product.__init__(self, 'AppleIPad', 529)

        self.__generation = generation

class AppleIWatch(Product):

    def __init__(self):
        Product.__init__(self, 'AppleIWatch', 264)
class ProductFactory:

    @staticmethod
    def create(item_name, *args):

        if item_name == 'MacBookAir':
            return MacBookAir(*args)
        elif item_name == 'AppleIPad':
            return AppleIPad(*args)
        elif item_name == 'AppleIWatch':
            return AppleIWatch(*args)

air = ProductFactory.create('MacBookAir', '16GB', 'Sierra')
ipad = ProductFactory.create('AppleIPad', '2nd')
iwatch = ProductFactory.create('AppleIWatch')
iwatch
<__main__.AppleIWatch at 0x193517d2610>

Abstract Factory

An abstract factory is a factory that returns factories. A normal factory can be used to create sets of related objects. An abstract factory returns factories. Thus, an abstract factory is used to return factories that can be used to create sets of related objects.

import abc
class Toy(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def show(self):
        pass

class Color(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def show_color(self):
        pass
class Car(Toy):
    def show(self):
        print("Remote controlled car")

class ActionFigure(Toy):
    def show(self):
        print("Captain America action figure")

class ConstructionToy(Toy):
    def show(self):
        print("Lego")

class Red(Color):
    def show_color(self):
        print("red")

class Green(Color):
    def show_color(self):
        print("green")

class Blue(Color):
    def show_color(self):
        print("blue")

car = Car()

red = Red()

red.show_color(), car.show()
red
Remote controlled car
(None, None)
class AbstractFactory(metaclass=abc.ABCMeta):

    @abc.abstractmethod
    def get_color(self):
        pass

    @abc.abstractmethod
    def get_toy(self):
        pass
# Create concrete classes implementing the same interface.
# create Factory classes extending AbstractFactory
class ColorfulToysFactory(AbstractFactory):

    def get_toy(self, toy_type):
        if toy_type == None:
            return None

        if toy_type == "car":
            return Car()
        elif toy_type == "action figure":
            return ActionFigure()
        elif toy_type == "construction toy":
            return ConstructionToy()

        return None

    def get_color(self, color_type):
        if color_type == None:
            return None

        if color_type == "red":
            return Red()
        elif color_type == "green":
            return Green()
        elif color_type == "blue":
            return Blue()

        return None
# Use the FactoryProducer to get AbstractFactory in order to get factories of
# concrete classes by passing an information such as type
RED_CAR = 'red_car'
BLUE_LEGO = 'blue_lego'
GREEN_ACTION_FIGURE = 'green_action_figure'

class ColorfulToysProducer:

    __colorful_toys_factory = ColorfulToysFactory()

    @classmethod
    def get_toy_and_color(cls, choice):
        toy = None
        color = None

        if choice == RED_CAR:
            toy = cls.__colorful_toys_factory.get_toy('car')
            color = cls.__colorful_toys_factory.get_color('red')

        elif choice == BLUE_LEGO:
            toy = cls.__colorful_toys_factory.get_toy('construction toy')
            color = cls.__colorful_toys_factory.get_color('blue')

        elif choice == GREEN_ACTION_FIGURE:
            toy = cls.__colorful_toys_factory.get_toy('action figure')
            color = cls.__colorful_toys_factory.get_color('green')

        return toy, color

toy, color = ColorfulToysProducer.get_toy_and_color(RED_CAR)
toy, color
(<__main__.Car at 0x193517f9280>, <__main__.Red at 0x193517f9dc0>)
toy, color = ColorfulToysProducer.get_toy_and_color(BLUE_LEGO)
toy, color
(<__main__.ConstructionToy at 0x1934fb85fd0>, <__main__.Blue at 0x1934fb855b0>)

Builder Pattern

Builder Pattern - Separate the construction of an object from representation - allow same construction process for many representations - parse a complex representation, create different objects - Consider a SQL query builder class - allows step-by-step creation of a SQL query - Query is a complex entity with many different parts - Applications might build once, run multiple times - Separates object construction from its representation - parse a complex construction process into simple constituent operations

class Mobile:

    def __init__(self,
                 name,
                 weight,
                 screen_size,
                 ram,
                 os,
                 camera_mp,
                 battery):

        self.name = name
        self.weight = weight
        self.screen_size = screen_size
        self.ram = ram
        self.os = os
        self.camera_mp = camera_mp
        self.battery = battery

    def show(self):
        print("name:", self.name)
        print("weight:", self.weight)
        print("screen_size:", self.screen_size)
        print("ram:", self.ram)
        print("os:", self.os)
        print("camera_mp:", self.camera_mp)
        print("battery:", self.battery)
samsung_s10 = Mobile(name="Samsung S10",
                     weight = "157g",
                     screen_size = "6.1 inch",
                     ram = "8GB",
                     os = "android 9.0",
                     camera_mp = "12 megapixel",
                     battery = "3400 mAh")
samsung_s10.show()
name: Samsung S10
weight: 157g
screen_size: 6.1 inch
ram: 8GB
os: android 9.0
camera_mp: 12 megapixel
battery: 3400 mAh
  • to get rid of the long list of parameters we can have the features in the main program but directly setting attributes in the client program is wrong, it goes against “encapsulate what varies principle”
  • this is prone to errors and maintenance unfriendly
class Mobile():

    def __init__(self):

        self.name = None
        self.weight = None
        self.screen_size = None
        self.ram = None
        self.os = None
        self.camera_mp = None
        self.battery = None

    def show(self):
        print("name:", self.name)
        print("weight:", self.weight)
        print("screen_size:", self.screen_size)
        print("ram:", self.ram)
        print("os:", self.os)
        print("camera_mp:", self.camera_mp)
        print("battery:", self.battery)

s10 = Mobile()
s10.name = "Samsung S10"
s10.screen_size = "6.1 inch",
s10.os = "android 9.0",
s10.camera_mp = "12 megapixel",
s10.battery = "3400 mAh"
s10.show()
# now the features have been encapsulated in a separate class called MyMobile
# the build method instantiates a new mobile object and encapsulates setting
# of attributes

class MyMobileBuilder():

    def __init__(self):
        self.__mobile = Mobile()

    def get_mobile(self):
        return self.__mobile

    def build_name(self, name):
        self.__mobile.name = name

    def build_memory(self, ram):
        self.__mobile.ram = ram

    def build_camera(self, camera_mp):
        self.__mobile.camera_mp = camera_mp

    def build_otherfeatures(self, weight, screen_size, os, battery):
        self.__mobile.weight = weight
        self.__mobile.screen_size = screen_size
        self.__mobile.os = os
        self.__mobile.battery = battery

builder = MyMobileBuilder()
builder.build_name('Samsung S10')
builder.build_memory('8GB')
builder.build_camera('16 megapixels')
mobile = builder.get_mobile()
mobile.show()
name: Samsung S10
weight: None
screen_size: None
ram: 8GB
os: None
camera_mp: 16 megapixels
battery: None
# Builder is less valuable in python, because you can specify default values

class Mobile:
    def __init__(self,
                 name,
                 weight='157gm',
                 screen_size='5inches',
                 ram='8GB',
                 os='Android',
                 camera_mp='16 megapixels',
                 battery='3400 mAh'):

        self.name = name
        self.weight = weight
        self.screen_size = screen_size
        self.ram = ram
        self.os = os
        self.camera_mp = camera_mp
        self.battery = battery

    def show(self):
        print("name:", self.name)
        print("weight:", self.weight)
        print("screen_size:", self.screen_size)
        print("ram:", self.ram)
        print("os:", self.os)
        print("camera_mp:", self.camera_mp)
        print("battery:", self.battery)

samsung_s10 = Mobile('Samsung S10')
samsung_s10.show()
name: Samsung S10
weight: 157gm
screen_size: 5inches
ram: 8GB
os: Android
camera_mp: 16 megapixels
battery: 3400 mAh
samsung_s8 = Mobile('Samsung S8', screen_size='4.4inches', ram='4GB')
samsung_s8.show()
name: Samsung S8
weight: 157gm
screen_size: 4.4inches
ram: 4GB
os: Android
camera_mp: 16 megapixels
battery: 3400 mAh

Object Pool Pattern

  • Used when the cost of initializing objects is high
  • Number of objects in use at a time is low
  • Rate of object instantiation is high
  • Pools used to cache and manage objects
  • Avoid creating new objects, when an existing one is available
  • Reuse objects rather than incur the cost of creating one
  • common example: thread pools
  • some processes are embarrassingly parallel
  • threads are expensive to create and free up
  • use a thread pool -> mitigates the overhead of pool creation
  • avoids needless re-instantiation and expensive acquisition of resources
class Connection:

    def __init__(self):
        self.__is_used = False

        # Imagine a very heavy-duty initialization process here
        # to set up the database connections and connect
        self.connect_to_database()

    def acquire(self):
        self.__is_used = True

    def release(self):
        self.__is_used = False

    def is_used(self):
        return self.__is_used

    def connect_to_database(self):
        pass
class ConnectionPool:

    __instance = None

    def __new__(cls, num_connections):
        if cls.__instance is None:
            print('No instance exists, creating a new one')

            cls.__instance = super(ConnectionPool, cls).__new__(cls)

            cls.__instance.__num_connections = num_connections
            cls.__instance.__connections = []

            for i in range(num_connections):
                cls.__instance.__connections.append(Connection())

        else:
            print('A previously created instance exists, returning that same one')


        return cls.__instance


    def acquire(self):
        for i in range(self.__num_connections):
            connection = self.__connections[i]

            if not connection.is_used():
                connection.acquire()
                return connection

        return None

    def release(self, connection):
        if connection.is_used():
            connection.release()
pool = ConnectionPool(2)
No instance exists, creating a new one
pool = ConnectionPool(2)
A previously created instance exists, returning that same one
conn_1 = pool.acquire()
conn_1
<__main__.Connection at 0x1935180dd30>
conn_2 = pool.acquire()
conn_2
<__main__.Connection at 0x1935180d820>
conn_3 = pool.acquire()
conn_3 is None
True
pool.release(conn_2)
conn_3 = pool.acquire()
conn_3
<__main__.Connection at 0x1935180d820>