Prototype Design Pattern
Video Lecture
Section | Video Links |
---|---|
Prototype Overview | |
Prototype Use Case | |
Python id() function |
Overview
The Prototype design pattern is good for when creating new objects requires more resources than you want to use or have available. You can save resources by just creating a copy of any existing object that is already in memory.
E.g., A file you've downloaded from a server may be large, but since it is already in memory, you could just clone it, and work on the new copy independently of the original.
In the Prototype patterns interface, you create a static clone method that should be implemented by all classes that use the interface. How the clone method is implemented in the concrete class is up to you. You will need to decide whether a shallow or deep copy is required.
- A shallow copy, copies and creates new references one level deep,
- A deep copy, copies and creates new references for all levels.
In Python, you have mutable objects such as Lists, Dictionaries, Sets and any custom Objects you may have created. A shallow copy, will create new copies of the objects with new references in memory, but the underlying data, e.g., the actual elements in a list, will point to the same memory location as the original list/object being copied. You will now have two lists, but the elements within the lists will point to the same memory location. So, changing any elements of a copied list will also affect the original list. Be sure to test your implementation that the copy method you use works as expected. Shallow copies are much faster to process than deep copies and deep copies are not always necessary if you are not going to benefit from using it.
Terminology
- Prototype Interface: The interface that describes the
clone()
method. - Prototype: The Object/Product that implements the Prototype interface.
- Client: The client application that uses and creates the ProtoType.
Prototype UML Diagram
Source Code
Experiment with the concept code.
By default, it will shallow copy the object you've asked to be cloned. The object can be any type from number to string to dictionary to anything custom that you've created.
In my example, I have created a list of numbers. At first impressions, when this list is copied, it will appear that the list was fully cloned. But the inner items of the list were not. They will point to the same memory location as the original list; however, the memory identifier of the new list is new and different from the original.
In the MyClass.clone()
method, there is a line self.field.copy()
that is commented out. Uncomment out this line, and comment out the line before it to now be # self.field
. Re-execute the file, and now the list items will be copied as well. This however is still not a full deep copy. If the list items were actually other lists, dictionaries or other collections, then only the 1st level of that copy would have been cloned to new memory identifiers. I call this a 2-level copy.
For a full recursive copy, use the copy.deepcopy()
method that is part of an extra dedicated copy
import included with Python. I demonstrate this in the example use case further down.
Remember that full deep copies can potentially be much slower for very complicated object hierarchies.
./prototype/prototype_concept.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
|
Output
When using the shallow copy approach. Changing the inner item of OBJECT2s list, also affected OBJECT1s list.
python ./prototype/prototype_concept.py
OBJECT1 1808814538656 field=[1, 2, 3, 4] type=<class 'list'>
OBJECT2 1808814538464 field=[1, 101, 3, 4] type=<class 'list'>
OBJECT1 1808814538656 field=[1, 101, 3, 4] type=<class 'list'>
When using the 2-level shallow, or deep copy approach. Changing the inner item of OBJECT2s list, does not affect OBJECT1s list. Read notes below for caveat.
python .\prototype\prototype_concept.py
OBJECT1 1808814538656 field=[1, 2, 3, 4] type=<class 'list'>
OBJECT2 1808814538464 field=[1, 101, 3, 4] type=<class 'list'>
OBJECT1 1808814538656 field=[1, 2, 3, 4] type=<class 'list'>
Notes
The 2-level shallow copy was used in the above sample code. This only copies collections (list, dictionary, set) one level deep.
E.g., It won't deep copy collections containing inner collections, such as lists of lists, or dictionaries of lists, sets and tuples of any combination, etc.
For full recursive deep copying, use the library at https://docs.python.org/3/library/copy.html
SBCODE Editor
Prototype Use Case
In this example, an object called document is cloned using shallow, 2 level shallow, and full recursive deep methods.
The object contains a list of two lists. Four copies are created, and each time some part of the list is changed on the clone, and depending on the method used, it can affect the original object.
When cloning an object, it is good to understand the deep versus shallow concept of copying.
Example UML Diagram
Source Code
./prototype/client.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
./prototype/document.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
./prototype/interface_prototype.py
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Output
python ./prototype/client.py
2520526585808 name=Original list=[[1, 2, 3, 4], [5, 6, 7, 8]]
2520526585712 name=Copy 1 list=[[1, 2, 3, 4], [5, 6, 200, 8]]
2520526585808 name=Original list=[[1, 2, 3, 4], [5, 6, 200, 8]]
2520526585664 name=Copy 2 list=[[1, 2, 3, 4], [9, 10, 11, 12]]
2520526585808 name=Original list=[[1, 2, 3, 4], [5, 6, 200, 8]]
2520526585520 name=Copy 3 list=[[1, 2, 3, 4], ['1234', 6, 200, 8]]
2520526585808 name=Original list=[[1, 2, 3, 4], ['1234', 6, 200, 8]]
2520526585088 name=Copy 4 list=[[1, 2, 3, 4], ['5678', 6, 200, 8]]
2520526585808 name=Original list=[[1, 2, 3, 4], ['1234', 6, 200, 8]]
SBCODE Editor
New Coding Concepts
Python id() Function
The Python id()
function returns the memory address of an object.
All objects in Python will have a memory address.
You can test if an object is unique in Python by comparing its ID.
In the examples above, I can tell how deep the copies of the dictionaries and lists were, because the IDs of the inner items will be different. I.e., they point to different memory identifiers.
Note that every time you start a Python process, the IDs assigned at runtime will likely be different.
Also note that integers in Python also have their own IDs.
1 2 3 |
|
Outputs
2032436013328
2032436013360
2032436013392
Summary
- Just like the other creational patterns, a Prototype is used to create an object at runtime.
- A Prototype is created from an object that is already instantiated. Imagine using the existing object as the class template to create a new object, rather than calling a specific class.
- The ability to create a Prototype means that you don't need to create many classes for specific combinations of objects. You can create one object, that has a specific configuration, and then clone this version many times, rather than creating a new object from a predefined class definition.
- New Prototypes can be created at runtime, without knowing what kind of attributes the prototype may eventually have. E.g., You have a sophisticated object that was randomly created from many factors, and you want to clone it rather than re applying all the same functions over and over again until the new object matches the original.
- A prototype is also useful for when you want to create a copy of an object, but creating that copy may be very resource intensive. E.g., you can either create a new houseboat from the builder example, or clone an existing houseboat from one already in memory.
- When designing your
clone()
method, you should consider which elements will be shallow copied, how deep, and whether full recursive deep copy is necessary. - For recursive deep copying, use the library at https://docs.python.org/3/library/copy.html