primes = []
i = 2
while len(primes) < 25:
    for p in primes:
        if i % p == 0:
            break
    else:
        primes.append(i)
    i += 1

print(primes)

"""
Explanation: Week 3 - Programming Paradigms
Learning Objectives

List popular programming paradigms
Demonstrate object oriented programming
Compare procedural programming and object oriented programming
Apply object oriented programming to solve sample problems

Computer programs and the elements they contain can be built in a variety of different ways. Several different styles, or paradigms, exist with differing popularity and usefulness for different tasks.
Some programming languages are designed to support a particular paradigm, while other languages support several different paradigms.
Three of the most commonly used paradigms are:

Procedural
Object oriented
Functional

Python supports each of these paradigms. 
Procedural
You may not have realized it but the procedural programming paradigm is probably the approach you are currently taking with your programs.
Programs and functions are simply a series of steps to be performed. 
For example:
End of explanation
"""


def square(val):
    print(val)
    return val ** 2

squared_numbers = [square(i) for i in range(5)]
print('Squared from list:')
print(squared_numbers)

squared_numbers = (square(i) for i in range(5))
print('Squared from iterable:')
print(squared_numbers)

def squared_numbers(num):
    for i in range(num):
        yield i ** 2
    print('This is only printed after all the numbers output have been consumed')

print(squared_numbers(5))
for i in squared_numbers(5):
    print(i)

import functools

def plus(val, n):
    return val + n


f = functools.partial(plus, 5)
f(5)

def decorator(inner):
    def inner_decorator():
        print('before')
        inner()
        print('after')
    return inner_decorator

def decorated():
    print('decorated')

f = decorator(decorated)
f()

@decorator
def decorated():
    print('decorated')
    
decorated()

import time

@functools.lru_cache()
def slow_compute(n):
    time.sleep(1)
    print(n)

start = time.time()
slow_compute(1)
print(time.time() - start)

start = time.time()
slow_compute(1)
print(time.time() - start)

start = time.time()
slow_compute(2)
print(time.time() - start)

"""
Explanation: Functional
Functional programming is based on the evaluation of mathematical functions. This is a more restricted form of function than you may have used previously - mutable data and changing state is avoided. This makes understanding how a program will behave more straightforward.
Python does support functional programming although it is not as widely used as procedural and object oriented programming. Some languages better known for supporting functional programming include Lisp, Clojure, Erlang, and Haskell.
Functions - Mathematical vs subroutines
In the general sense, functions can be thought of as simply wrappers around blocks of code. In this sense they can also be thought of as subroutines. Importantly they can be written to fetch data and change the program state independently of the function arguments.
In functional programming the output of a function should depend solely on the function arguments.
There is an extensive howto in the python documentation.
This presentation from PyCon US 2013 is also worth watching.
This presentation from PyGotham 2014 covers decorators specifically.
End of explanation
"""


DISCUSSED IN CLASS

Classes: 
data
style
source
axes
grid 
legend
points
lines
    - draw
    - position
    - color
    - size / thickness
    - symbol
    - slope / intercept
    - text representation
figure
plot
title
labels
text

"""
Explanation: Object oriented
Object oriented programming is a paradigm that combines data with code into objects. The code can interact with and modify the data in an object. A program will be separated out into a number of different objects that interact with each other.
Object oriented programming is a widely used paradigm and a variety of different languages support it including Python, C++, Java, PHP, Ruby, and many others.
Each of these languages use slightly different syntax but the underlying design choices will be the same in each language. 
Objects are things, their names often recognise this and are nouns. These might be physical things like a chair, or concepts like a number. 
While procedural programs make use of global information, object oriented design forgoes this global knowledge in favor of local knowledge. Objects contain information and can do things. The information they contain are in attributes. The things they can do are in their methods (similar to functions, but attached to the object).
Finally, to achieve the objective of the program objects must interact.
We will look at the python syntax for creating objects later, first let's explore how objects might work in various scenarios. 
Designing Object Oriented Programs
These are the simple building blocks for classes and objects. Just as with the other programming constructs available in python, although the language is relatively simple if used effectively they are very powerful.
Learn Python the Hard Way has a very good description of how to design a program using the object oriented programming paradigm. The linked exercise particularly is worth reading.
The best place to start is describing the problem. What are you trying to do? What are the items involved?
Example 1: A Laboratory Inventory
I would like to keep track of all the items in the laboratory so I can easily find them the next time I need them. Both equipment and consumables would be tracked. We have multiple rooms, and items can be on shelves, in refrigerators, in freezers, etc. Items can also be in boxes containing other items in all these places.
The words in bold would all be good ideas to turn into classes. Now we know some of the classes we will need we can start to think about what each of these classes should do, what the methods will be. Let's consider the consumables class:
For consumables we will need to manage their use so there should be an initial quantity and a quantity remaining that is updated every time we use some. We want to make sure that temperature sensitive consumables are always stored at the correct temperature, and that flammables are stored in a flammables cabinet etc.
The consumable class will need a number of attributes:

Initial quantity
Current quantity
Storage temperature
Flammability

The consumable class will need methods to:

Update the quantity remaining
Check for improper storage?

The consumable class might interact with the shelf, refrigerator, freezer, and/or box classes.
Reading back through our description of consumables there is reference to a flammables cabinet that was not mentioned in our initial description of the problem. This is an iterative design process so we should go back and add a flammables cabinet class. 
Exercise: A Chart
We have used matplotlib several times now to generate charts. If we were to create a charting library ourselves what are the objects we would use?
I would like to plot some data on a chart. The data, as a series of points and lines, would be placed on a set of x-y axes that are numbered and labeled to accurately describe the data. There should be a grid so that values can be easily read from the chart.
What are the classes you would use to create this plot?
Pick one class and describe the methods it would have, and the other classes it might interact with.
End of explanation
"""


DISCUSSED IN CLASS

classes:
appetizers
main courses
desserts
recipe
     __Attributes/Methods__
            - preparation time
            - cooking time
            - serving portion
            - servings
            - name
            - nutritional information
            - ingredients
            - methods
            - scaling
            - equipment
            - special occasion 
            - instructions
     __Interactions__
            - Cookbook
            - Ingredients
            - Instructions
            - Nutrition
            - Equipment
            - Scaling
            - Portions
servings
ingredients
 - quantity (mass/volume)
    - units
 - nutritional info
 - specific dietary info
 - cost
 - storage temperature
 - sustitute options
 - name
 - translations
 - alternative names
 - special handling instructions
 - sources
     * can interact with the following classes:
     * recipe
     * Instructions
     * Equipment
     * nutrition 
     * scaling
     * portions 
     * cookbook
     * units
solid ingredients
liquid ingredients
equipment
instructions
nutritional info
style
units conversion
scaling 
portions
cookbook

(recipe class can have an attribute, list of ingredients which are instances of another class themselves)
(classes interact with each other often in an app like this)
(instructions can be a class, and an instance of it can be saved as an attribute in Recipe class)

"""
Explanation: Exercise 2: A Cookbook
A system to manage different recipes, with their ingredients, equipment needed and instructions. Recipes should be scalable to different numbers of servings with the amount of ingredients adjusted appropriately and viewable in metric and imperial units. Nutritional information should be tracked.
What are the classes you would use to create this system?
Pick one class and describe the methods it would have, and the other classes it might interact with.
End of explanation
"""


class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    mammal = True
    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age


person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

print(person1, person2)


"""
Explanation: Building Skills in Object Oriented Design is a good resource to learn more about this process.
Syntax
Now let's look at the syntax we use to work with objects in python.
There is a tutorial in the python documentation.
Before we use an object in our program we must first define it. Just as we define a function with the def keyword, we use class to define a class. What is a class? Think of it as the template, or blueprint from which our objects will be made.
Remember that in addition to code, objects can also contain data that can change so we may have many different instances of an object. Although each may contain different data they are all formed from the same class definition.
As an example:
End of explanation
"""


class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    mammal = True
    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age
    
    
    def __str__(self):
        return '{0} who is {1} years old.'.format(self.name, self.age)


person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

print(person1, person2)

"""
Explanation: There is a lot happening above.
class Person(object): The class keyword begins the definition of our class. Here, we are naming the class Person. Next, (object) means that this class will inherit from the object class. This is not strictly necessary but is generally good practice. Inheritance will be discussed in greater depth next week. Finally, just as for a function definition we finish with a colon.
"""Documentation""" Next, a docstring provides important notes on usage.
mammal = True This is a class attribute. This is useful for defining data that our objects will need that is the same for all instances.
def init(self, name, age): This is a method definition. The def keyword is used just as for functions. The first parameter here is self which refers to the object this method will be part of. The double underscores around the method name signify that this is a special method. In this case the __init__ method is called when the object is first instantiated.
self.name = name A common reason to define an __init__ method is to set instance attributes. In this class, name and age are set to the values supplied.
That is all there is to this class definition. Next, we create two instances of this class. The values supplied will be passed to the __init__ method.
Printing these objects don't provide a useful description of what they are. We can improve on this with another special method.
End of explanation
"""


class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    friends = []
    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age
    
    
    def __str__(self):
        return '{0} who is {1} years old'.format(self.name, self.age)


person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

person1.friends.append('Charlie')
person2.friends.append('Danielle')

print(person1.friends, person2.friends)

"""
Explanation: There are many more special methods.
Before we go on a note of caution is needed for class attributes. If only using strings and numbers the behaviour will likely be much as you expect. However, if using a list, dictionary, or other similar type you may get a surprise.
End of explanation
"""


class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age
        self.friends = []
    
    
    def __str__(self):
        return '{0} who is {1} years old'.format(self.name, self.age)


person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

person1.friends.append('Charlie')
person2.friends.append('Danielle')

print(person1.friends, person2.friends)

"""
Explanation: Both of our objects point to the same instance of the list type so adding a new friend to either object shows up in both.
The solution to this is creating our friends attribute only at instantiation of the object. This can be done by creating it in the __init__ method.
End of explanation
"""


print('This works:', person1.friends)
print('This does not work:', friends)

"""
Explanation: Objects have their own namespace, although we have created variables called name, age, and friends they can only be accessed in the context of the object.
End of explanation
"""


class Person(object):
    """A class definition for a person. The following attributes are supported:
    
    Attributes:
    name: A string representing the person's name.
    age: An integer representing the person's age."""

    
    
    def __init__(self, name, age):
        """Return a Person object with name and age set to the values supplied"""
        self.name = name
        self.age = age
        self.friends = []
    
    
    def __str__(self):
        """Return a string representation of the object"""
        return '{0} who is {1} years old'.format(self.name, self.age)
    
    
    def add_friend(self, friend):
        """Add a friend"""
        self.friends.append(friend)

        
person1 = Person('Alice', 25)
person2 = Person('Bob', 30)

person1.add_friend('Charlie')
person2.add_friend('Danielle')

print(person1.friends, person2.friends)

"""
Explanation: We are not limited to special methods when creating classes. Standard functions, or in this context methods, are an integral part of object oriented programming. Their definition is identical to special methods and functions outside of classes.
End of explanation
"""


class Test(object):
    
    
    def do_something(self):
        pass

"""
Explanation: Private vs Public
Some programming languages support hiding methods and attributes in an object. This can be useful to simplify the public interface someone using the class will see while still breaking up components into manageable blocks 'under-the-hood'. We will discuss designing the public interface in detail in week 5.
Python does not support private variables beyond convention. Names prefixed with a underscore are assumed to be private. This means they may be changed without warning between different versions of the package. For public attributes/methods this is highly discouraged.
Glossary
Class: Our definition, or template, for an object.
Object: An instance of a class.
Method: A function that belongs to an object
Attribute: A characteristic of an object, these can be data attributes and methods.
Exercises
Please send to me prior to class next week.

Pick one of the examples above (the laboratory inventory manager, the chart or the cookbook) and write out the classes it requires with their methods. You don't need to complete the code in the methods, using the pass keyword can act as a placeholder, e.g.
End of explanation
"""


