<jupyter_start><jupyter_text>Python Design Patterns II- The adapter pattern - The facade pattern - Lazy initialization and the flyweight pattern - The command pattern - The compositon pattern __By <NAME>__  Patterns: - Developed by the "Gang of Four" in their book     - <NAME>, <NAME>, <NAME>, <NAME> - They define a set of object oriented programming patterns that can help provide standards and best practice for object oriented programming  ![](https://imgur.com/muxOZxM.png)  Three Kinds of Patterns: 1. __Creational__     - Provide ways to instantiate single objects or groups of related objects 2. __Structural__    - Provide a manner to define relationships between classes 3. __Behavioral__    - Define manners of communication between classes and objects  The Adapter Pattern- __Structural Pattern__- Adapters are used to allow two pre-existing objects to work together, even if their interfaces are not compatible  __What is it good for?__ - Converting arguments to a different format - Rearranging the order of arguments - Calling a differently named method - Supplying default arguments __Essentially__ they are just the "connection" that links two different objects (like dongles for our computers)   Structure: - Similar to simplified decorator pattern- Map between two different interaces     - Whereas decorators provide the same interface they replace   ![Adapter Patterns- SOURCE](https://i.imgur.com/qkyXVp8.png) - ```Interface1``` is expecting to call a method called ```make_action(some, arguments)```- We already have ```Interface2``` class that does the things we want     - However, it provides a method called ```different_actin(other, arguments)``` instead!- The ```Adapter``` class implements the ```make_action``` interface and maps the arguments to the existing interface.   What's the Advantage to Adapters: - Code that maps from one interface to another is all in one place     - If we didn't do this we would have to convert whenever we wanted to use that code!!<jupyter_code>class AgeCalculator:
    def __init__(self, birthday):
        # this class formats the year month, day into int's from strings
        self.year, self.month, self.day = (
            int(x) for x in birthday.split('-'))
        
    def calculate_age(self,date):
        year, month, day = (
                int(x) for x in date.split('-'))
        age = year - self.year
        if (month,day) < (self.month,self.day):
            age -= 1
        return age<jupyter_output><empty_output><jupyter_text>What's Wrong Here? - Class takes in a string for a birthday    - Many pythonistas 🐍 might expect to input a datetime object instead     - Datetime objects are far more flexible and easier to build on top of later!! __Solutions__: 1. We could __rewrite the class__ to accept datetime ☀️    - not always an option! ☹️2. We could just __convert strings to datetime on the fly__ with ```datetime.date.strftime('%Y-%m-%d')``` ⛈    - But honestly, do you want to do that over and over again?     - Leaves you exposed to mistypes ('%M') is minutes whereas ('%m') is months!3. We could __write an adapter__ that allows a normal date to be entered into a normal ```AgeCalculator``` class 🌤<jupyter_code>import datetime
class DateAgeAdapter:
    def _str_date(self, date):
        return date.strftime("%Y-%m-%d")
    
    def __init__(self, birthday):
        birthday = self._str_date(birthday)
        self.calculator = AgeCalculator(birthday)
        
    def get_age(self, date): # method name can be different, and arguments can change
        date = self._str_date(date)
        return self.calculator.calculate_age(date)<jupyter_output><empty_output><jupyter_text>Now we can enter datetime objects instead of strings but use our existing codebase:<jupyter_code>d = DateAgeAdapter(datetime.date(1975,1,1))
print("Adapter Implementation: {}".format(d.get_age(datetime.date.today())))

d1 = AgeCalculator("1975-01-01")
print("String Implementation: {}".format(d1.calculate_age("2019-03-17")))<jupyter_output>Adapter Implementation: 44
String Implementation: 44<jupyter_text>Downsides to Class Adapaters: - Over time we forget why we need this interface unless it's really clear - Unless there is significant downsides to restructuring the original code we don't want to keep adding more adapters to keep  old classes useable    - More classes = more overhead__Alternatives to Classes__:- We can often monkey patch at runtime - WE can often times use a simple function as an adapter   The Facade Pattern: - __Structural Pattern__- Designed to provide a simple interface to a complex system of components.    - If a system is really complex, we don't usually want to interact with it directly     - We often have a set of "typical" use cases - __Facade patterns allow us to create a set of simplified operations to simplify your interaction with complex objects without compromising the ability to interact directly__![](https://imgur.com/Y6bA3K3.png)  Facade vs Adapter - Facade trying to abstract a simpler inferface from a complex one- Adapter is trying to map one existing interface to another  Now, an Example:Let's create a facade for an email application. - There are two libraries for receiving email messages,```smtplib```/```imaplib```, and they are quite complex!- Let's write a facade that allows us to send an email and list the emails in our inbox on either an ```IMAP``` or ```SMTP``` connection.__Structure:__- Makes assumptions about the connection     - Host for SMTP and IMAP are same    - Username and password for each is same     - They both use standard ports This will cover most email needs, but nothing is preventing a programming from cracking open the hood and accessing the core functionality<jupyter_code>import smtplib
import smtplib

class EmailFacade:
    def __init__(self, host, username, password):
        self.host = host
        self.username = username
        self.password = password
    
    def send_email(self, to_email, subject, message):
        if not "@" in self.username: # is username wino6687 or <EMAIL>
            from_email = "{0}@{1}".format(
                    self.username, self.host)
        else:
            from_email = self.username
        message = ("From: {0}\r\n"
                  "To: {1}\r\n"
                  "Subject: {2}\r\n\r\n{3}").format(
                            from_email,
                            to_email,
                            subject,
                            message)
        
        smtp = smtplib.SMTP(self.host)
        smtp.login(self.username, self.password)
        smtp.sendmail(from_email, [to_email],message)    
def get_inbox(self):
    mailbox = imap.IMAP4(self.host)
    mailbox.login(bytes(self.username, 'utf8'),
                 bytes(self.password, 'utf8'))
    mailbox.select()
    x,data = mailbox.search(None, 'ALL')
    messages = []
    for num in data[0].split():
        x,message = mailbox.fetch(num, '(RFC822)')
        messages.append(message[0][1])
    return messages<jupyter_output><empty_output><jupyter_text>- Now we can send and receive email relatively simply    - Easily abstracting away the complexity we don't need - This is a very pythonic thing to do     - The python language as whole likes to offer abstractions to make your life esier (for loop structure, list comprehensions, generators)   The Flyweight Pattern- __Structural Pattern__- Memory optimization pattern (useful when  of objects > 100)- Tempting to ignore memory  and let Python's wonderful garbage collector to do all of our work for us 🚛    - But __as things grow, paying attention to memory can be critical!__ Especially if you are working on a local machine or limited cloud resources    __Concept__:- Ensures that objects that share a state can use the same memory for that shared state. - Often implemented after a program has demonstrated memory problems     - Don't have to start with this in mind     - __"Premature optimization is the most effective way to create a program that is too complicated to maintain"__ ![flyweight](https://imgur.com/NRGX2uu.png) - Each ```Flyweight``` has no specific state; any time it needs to perform an operation on ```SpecificState```, that state needs to be passed into the ```Flyweight``` by the calling code- The factory that returns a flyweight is a seperate object    - it's purpose is to return a flyweight for a given key identifying that flyweight.     - If the flyweight exists, we return it; if not, we create a new one __Example: Let's Build an Inventory System for Cars__- Each individual car has a unique serial number and color    - But most of the details about that car are the same for all of the cars of that model - Normally we would have to store a list of all of the attributes a given car does or doesn't have     - If you have a lot of car models, then this will waste memory fast __Goal:__ If we use the flyweight pattern we can make shared objects for the list of features for each model, and reference the model with the serial number and color to find an individual vehicle! __Strategy:__- We can use the ```__new__``` constructur to implement the flyweight pattern     - However, this will only return one instance of the class, and we want to have different instances depending on the keys - We could try using a dictionary to store the items and look up based on key    - BUT, when we sold out of one model of a car, it would still exist in the dictionary and thus still take up memory     - Instead, let's use python's garbage collector to solve this for us     Using the ```weakref``` module: - provides a ```WeakValueDictionary``` object- If we end up with an object reference location that has no values stored at that time, the garbage collector will come along and clean it  1. Build Factory- We use a factory to build and construct flyweight instances    - We need a factory for each type of flyweight instance- Whenever we construct a new flyweight with a given name, we look up that name in the weak referenced dict    - If it exists $\rightarrow$ return the model     - If not $\rightarrow$ create new model- Regardless, we will call the ```__init__``` method of the flyweight every time<jupyter_code>import weakref
class CarModel:
    _models = weakref.WeakValueDictionary()
    
    def __new__(cls, model_name, *args, **kwargs):
        model = cls._models.get(model_name)
        if not model: 
            model = super().__new__(cls)
            cls._models[model_name] = model
            
        return model
    
    def __init__(self, model_name, air = False, tilt = False,
                cruise_control=False, power_locks=False,
                alloy_wheels=False, usb_charger=False):
        if not hasattr(self,"initted"): # ensure we only initialize object once
            self.model_name = model_name
            self.air = air
            self.tilt = tilt
            self.cruise_control = cruise_control
            self.power_locks = power_locks
            self.alloy_wheels = alloy_wheels
            self.usb_charger = usb_charger
            self.initted=True
            
    def check_serial(self, serial_number):
        print("Sorry, we can't check that"
             "The serial number {0} on the {1}"
             "at this time".format(
             serial_number, self.model_name))<jupyter_output><empty_output><jupyter_text>__Define class that stores additional info and references the flyweight:__<jupyter_code>class Car: 
    def __init__(self, model, color, serial):
        self.model = model
        self.color = color 
        self.serial = serial
        
    def check_serial(self):
        return self.model.check_serial(self.serial)
dx = CarModel("FIT DX")
lx = CarModel("FIT LX", air=True, cruise_control=True,
              power_locks=True, tilt=True)
car1 = Car(dx, "blue", "122345")
car2 = Car(dx, "black", "12346")
car3 = Car(lx, "red", "12347")
print(id(lx))
del lx
del car3

import gc
gc.collect()
lx = CarModel("FIT LX", air=True, cruise_control=True,
             power_locks=True, tilt=True)
print(id(lx))

lx = CarModel("FIT LX")
print(id(lx))

print(lx.air)<jupyter_output>4533178832
4533178832
True<jupyter_text>The Command Pattern: - __Behavioural Pattern__    - Express a request, including the call to be made and all of its required paraemtersl, in a command object     - Can be used any time- Adds a level of abstraction between actions that must be done, and the object that invokes those actions, normally at a later time.![](https://imgur.com/uP6s72c.png) ![](https://imgur.com/uP6s72c.png)- We have client code that creates a ```Command``` object that can be executed at a later date. - This object knows about a receiver object that manages its own internal state when the command is executed on it - The ```Command``` object implements a specific interface, typically it has an ```execute``` or ```do_action``` method, and also keeps track of any arguments required to perform the action - Lastly, one or more Invoker objects execute the command at the correct time   Common Use for Command Pattern: Graphical Windows- Buttons that exist in a simple GUI are essentially just functionility that is built and ready but waiting to be executed     - Executing something when it is selected is called __invoking__- Actions that occur when you select a menu item, a keyboard shortcut, or a toolbar icon are all examples of invoker objects!__Example:__ Simple command pattern to provide ```Save``` and ```Exit``` actions<jupyter_code>import sys 

class Window:
    def exit(self):
        sys.exit(0)

class Document:
    def __init__(self,filename):
        self.filename = filename 
        self.contents = "This file cannot be modified"
    def save(self):
        with open(self.filename, 'w') as file:
            file.write(self.contents)<jupyter_output><empty_output><jupyter_text>__Note__: - These objects are trivially simple. - In a real environment the Window would need to handle things like mouse clicks and movement. The document would need to handle things like insertion and deletion.<jupyter_code># define invoker classes
class ToolbarButton:
    def __init__(self,name, iconname):
        self.name = name 
        self.iconname = iconname
    
    def click(self):
        self.command.execute()
    
class MenuItem:
    def __init__(self, menu_name, menuitem_name):
        self.menu = menu_name 
        self.item = menuitem_name
        
    def click(self):
        self.command.execute()

class KeyboardShortcut:
    def __init__(self, key, modifier):
        self.key = key 
        self.modifier = modifier 
        
    def keypress(self):
        self.command.execute()
# hook up the commands 
class SaveCommand:
    def __init__(self, document):
        self.document = document 
    
    def execute(self):
        self.document.save()
        
class ExitCommand:
    def __init__(self, window):
        self.window = window
    
    def execute(self):
        self.window.exit()
window = Window()
document = Document("a_document.txt")
save = SaveCommand(document)
exit = ExitCommand(window)

# create save button that can be used later 
save_button = ToolbarButton('save', 'save.png')
save_button.command = save
save_keystroke = KeyboardShortcut("s", "ctrl")
save_keystroke.command = save
exit_menu = MenuItem("File", "Exit")
exit_menu.command = exit<jupyter_output><empty_output><jupyter_text>The Composite Pattern: - Allows complex tree-like structures to be built from simple components     - Components are called composite objects         - behave like a container and kinda like a variable depending on whether they have child components- __Generally__, each component object is either a leaf node that can't contain other objects or it is a composite node ![](https://imgur.com/lw5mZOz.png) ![](https://imgur.com/HiwM1Cp.png) __Why is the Composite Pattern Useful?__- The purpose of a composite is to compose objects into tree structres to represent part-whole hierarchies- Allows clients to treat individual objects and composositions uniformly__Structure:__- Collection of nodes - Node is an abstract base class, and its derivatives are either leaves or collections of other nodes- When an operation is performed on the parent, that operation is recursively passed down the hierarchy - For each composite object, we keep a dictionary of children (usually a list is enough)     - Using a dictionary to allow us to lookup children by name    __Example: File System__- We want to have objects for Folders and Files- We need to be able to access both (as both have unique methods) - We want to be able to essentially mix the objects in a tree, which means we need a parent class to manage a tree of objects (component)<jupyter_code>class Component:
    def __init__(self, name):
        self.name = name
    def move(self, new_path):
        new_folder = get_path(new_path)
        del self.parent.children[self.name]
        new_folder.children[self.name] = self
        self.parent = new_folder 
    def delete(self):
        del self.parent.children[self.name]
        
class Folder(Component):
    def __init__(self, name):
        super().__init__(name)
        self.children = {}
    def add_child(self, child):
        child.parent = self
        self.children[child.name] = child
    def copy(self, new_path):
        pass
    
class File(Component):
    def __init__(self, name, contents):
        super().__init__(name)
        self.contents = contents
    def copy(self, new_path):
        pass
root = Folder('')
def get_path(path):
    names = path.split('/')[1:]
    node = root
    for name in names:
        node = node.children[name]
    return node
folder1 = Folder('folder1')
folder2 = Folder('folder2')
root.add_child(folder1)
root.add_child(folder2)
folder11 = Folder('folder11')
folder1.add_child(folder11)
file111 = File('file111', 'contents')
folder11.add_child(file111)
file21 = File('file21', 'other contents')
folder2.add_child(file21)
folder2.children
folder2.move('/folder1/folder11')
folder11.children<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.07954797252382008, 'fraction_numerical': 0.008586306226456902, 'mean_word_length': 3.4053196681307956, 'pattern_counts': {'":': 0, '<': 42, '<?xml version=': 0, '>': 42, 'https://': 8, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '122862', 'license': 'MIT', 'n_tokens_mistral': 5020, 'n_tokens_neox': 4791, 'n_words': 2093, 'path': 'week-10/ch11_designpatterns2.ipynb', 'repo_name': 'earthlab/oop-group', 'size': '18098'}