Skip to content

Prototype Design Pattern

Video Lecture

Section Video Links
Prototype Overview Prototype Overview Prototype Overview Prototype Overview 
Prototype Use Case Prototype Use Case Prototype Use Case Prototype Use Case 
Python id() function python id function python id function 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

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
# pylint: disable=too-few-public-methods
"Prototype Concept Sample Code"

from abc import ABCMeta, abstractmethod

class IProtoType(metaclass=ABCMeta):
    "interface with clone method"
    @staticmethod
    @abstractmethod
    def clone():
        """The clone, deep or shallow.
        It is up to you how you want to implement
        the details in your concrete class"""

class MyClass(IProtoType):
    "A Concrete Class"

    def __init__(self, field):
        self.field = field  # any value of any type

    def clone(self):
        " This clone method uses a shallow copy technique "
        return type(self)(
            self.field  # a shallow copy is returned
            # self.field.copy() # this is also a shallow copy, but has
            # also shallow copied the first level of the field. So it
            # is essentially a shallow copy but 2 levels deep. To
            # recursively deep copy collections containing inner
            # collections,
            # eg lists of lists,
            # Use https://docs.python.org/3/library/copy.html instead.
            # See example below.
        )

    def __str__(self):
        return f"{id(self)}\tfield={self.field}\ttype={type(self.field)}"

# The Client
OBJECT1 = MyClass([1, 2, 3, 4])  # Create the object containing a list
print(f"OBJECT1 {OBJECT1}")

OBJECT2 = OBJECT1.clone()  # Clone

# Change the value of one of the list elements in OBJECT2,
# to see if it also modifies the list element in OBJECT1.
# If it changed OBJECT1s copy also, then the clone was done
# using a 1 level shallow copy process.
# Modify the clone method above to try a 2 level shallow copy instead
# and compare the output
OBJECT2.field[1] = 101

# Comparing OBJECT1 and OBJECT2
print(f"OBJECT2 {OBJECT2}")
print(f"OBJECT1 {OBJECT1}")

Output

When using the shallow copy approach. Changing the inner item of OBJECT2s list, also affected OBJECT1s list.

1
2
3
4
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.

1
2
3
4
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

Example 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

Prototype Use Case 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 Use Case Example Code"
from document import Document

# Creating a document containing a list of two lists
ORIGINAL_DOCUMENT = Document("Original", [[1, 2, 3, 4], [5, 6, 7, 8]])
print(ORIGINAL_DOCUMENT)
print()

DOCUMENT_COPY_1 = ORIGINAL_DOCUMENT.clone(1)  # shallow copy
DOCUMENT_COPY_1.name = "Copy 1"
# This also modified ORIGINAL_DOCUMENT because of the shallow copy
# when using mode 1
DOCUMENT_COPY_1.list[1][2] = 200
print(DOCUMENT_COPY_1)
print(ORIGINAL_DOCUMENT)
print()

DOCUMENT_COPY_2 = ORIGINAL_DOCUMENT.clone(2)  # 2 level shallow copy
DOCUMENT_COPY_2.name = "Copy 2"
# This does NOT modify ORIGINAL_DOCUMENT because it changes the
# list[1] reference that was deep copied when using mode 2
DOCUMENT_COPY_2.list[1] = [9, 10, 11, 12]
print(DOCUMENT_COPY_2)
print(ORIGINAL_DOCUMENT)
print()

DOCUMENT_COPY_3 = ORIGINAL_DOCUMENT.clone(2)  # 2 level shallow copy
DOCUMENT_COPY_3.name = "Copy 3"
# This does modify ORIGINAL_DOCUMENT because it changes the element of
# list[1][0] that was NOT deep copied recursively when using mode 2
DOCUMENT_COPY_3.list[1][0] = "1234"
print(DOCUMENT_COPY_3)
print(ORIGINAL_DOCUMENT)
print()

DOCUMENT_COPY_4 = ORIGINAL_DOCUMENT.clone(3)  # deep copy (recursive)
DOCUMENT_COPY_4.name = "Copy 4"
# This does NOT modify ORIGINAL_DOCUMENT because it
# deep copies all levels recursively when using mode 3
DOCUMENT_COPY_4.list[1][0] = "5678"
print(DOCUMENT_COPY_4)
print(ORIGINAL_DOCUMENT)
print()

./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
"A sample document to be used in the Prototype example"
import copy  # a python library useful for deep copying
from interface_prototype import IProtoType

class Document(IProtoType):
    "A Concrete Class"

    def __init__(self, name, l):
        self.name = name
        self.list = l

    def clone(self, mode):
        " This clone method uses different copy techniques "
        if mode == 1:
            # results in a 1 level shallow copy of the Document
            doc_list = self.list
        if mode == 2:
            # results in a 2 level shallow copy of the Document
            # since it also create new references for the 1st level list
            # elements aswell
            doc_list = self.list.copy()
        if mode == 3:
            # recursive deep copy. Slower but results in a new copy
            # where no sub elements are shared by reference
            doc_list = copy.deepcopy(self.list)

        return type(self)(
            self.name,  # a shallow copy is returned of the name property
            doc_list  # copy method decided by mode argument
        )

    def __str__(self):
        " Overriding the default __str__ method for our object."
        return f"{id(self)}\tname={self.name}\tlist={self.list}"

./prototype/interface_prototype.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# pylint: disable=too-few-public-methods
"Prototype Concept Sample Code"
from abc import ABCMeta, abstractmethod

class IProtoType(metaclass=ABCMeta):
    "interface with clone method"
    @staticmethod
    @abstractmethod
    def clone(mode):
        """The clone, deep or shallow.
        It is up to you how you  want to implement
        the details in your concrete class"""

Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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]]

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
print(id(0))
print(id(1))
print(id(2))

Outputs

1
2
3
2032436013328
2032436013360
2032436013392

Summary

...Refer to Book or Videos for extra content.