# Python has a sort of philosophy to it.
import this
=
assignment operator. The assignment operator only binds to names, it never copies an object by value (more on this below).x = 4
x
type(x)
id(x) # Identity of the object
type | Description | Example |
---|---|---|
int |
integer types | 4 |
float |
64-bit floating point numbers | 4.567 |
bool |
boonlean logical values | True |
None |
null object (serves as a valuable place holder) | None |
All scalar data types are immutable.
int
¶x = 1
type(x)
int(3.4) # Constructor
float
¶x = 1.3
type(x)
float(1) # Constructor
float
+ int
= float
3.0 + 3
# addition
4 + 4
# Subtraction
50 - 25
# Multiplication
5 * 5
Float Division Operator (/
) vs. Integer Division Operator (//
)
1000/50 # float division
1000//50 # integer division
# Exponentiation
5**4
# Remainders ('modulo')
10%6
NoneType
¶y = None
type(y)
None == 6
6 is None
bool
¶b = True
type(b)
b == False
# Any non-zero value is truthy
print(bool(1))
print(bool(55))
print(bool(-5500))
# Zero is false
bool(0)
# NoneTypes are false
bool(None)
# Empty containers are false
print(bool([]))
print(bool({}))
print(bool(""))
Python knows how to behave given the methods assigned to the object when we create an instance. The methods dictate how different data types deal with similar operations (such as addition, multiplication, comparative evaluations, ect.)
Note that special attributes in python are delimited by double underscores (or "dunder")
x = 4
int(4)
dir(x)
x.__add__(4) # addition
x.__mod__(3) # modulo
x.__mul__(6) # multiplication, etc.
x.__eq__(5) # 4 == 5
x.__float__()
Type | Description | Example | Mutable |
---|---|---|---|
list |
heterogeneous sequences of objects | [1,2,3] |
✓ |
str |
sequences of characters | "A word" |
✘ |
dicts |
associative array of key/value mappings | {"a": 1} |
keys ✘ values ✓ |
sets |
unordered collection of distinct objects | {1,2,3} |
✓ |
tuples |
heterogeneous sequence | (1,2) |
✘ |
We can access the information contained within python collection types using a 0-based index.
list
¶Construction:
[]
list()
x = [1, 2.2, "str", True, None]
x
list constructor (needs another iterable object for this to work... more on this later)
list("This")
Can grab and replace elements using a 0-based index
x[0]
x[4] = "a"
x
We can reverse the index using negative values
0, 1 , 2 , 3 , 4
[1, 2.2, "str", True, None]
0, -4 , -3 , -2 , -1
x[-1]
Adding to lists.
x + 1
x = [4,3,5,6]
x + [1]
print(x)
x = x + [1]
x.append("r")
x
Multiplying lists repeats them.
[1,2,3] * 6
# Generate a string using quotes
s = "this is a string"
print(s)
print(type(s))
# or using the string constructor
ss = str(3456)
ss
print(type(ss))
# layer quotation (when need be)
s = "this is a 'string'" # good
print(s)
s = 'this is a "string"' # good
print(s)
String elements can be accessed using a 0-based index, much like objects in a list
my_str = "Georgetown"
print(my_str[0])
print(my_str[5])
Appending ('adding') two strings pastes their values together
"Cat" + "Cat" + "Dog"
Multiplying strings repeats the value.
"class"*5
tuple
¶Construction:
()
tuple()
(where the input is an iterable object, like a list)tt = ("apple",2.4,6)
print(type(tt))
print(tt)
# constructor
tuple([4,5,6])
tt2 = 1,2,3,4,5,6
print(tt2)
Indexing a tuple (0-based index)
tt[0]
adding tuples combines/appends them
(1,2,3) + (99,-100)
multiplying tuples repeats them
(1,2,3) * 3
Tuple Unpacking: allows one to deconstruct the tuple object into named references. This allows for flexibility regarding which objects we want when performing sequential operations, like iterating.
Note that when holding nested collections, copies are shallow (see below)
print(tt)
a, b, c = tt
print(a)
print(b)
print(c)
tt3 = ((1,2,3),(4,5),(1,3,4))
print(tt3)
a2, b2, c2 = tt3
print(a2)
print(b2)
print(c2)
use _
for placeholder assignments
a,_,c,_,d = (1,2,3,4,5)
print(a)
print(c)
print(d)
set
¶Construction:
{}
brackets (but with no key value pairs -- see below)set()
constructormy_set = {1,2,3,3,3,4,4,4,5,1}
print(type(my_set))
my_set
set("caaaaaaaaaaaaaaaaaat")
Add elements to a set using the .add()
method or .update()
.
my_set.add(6)
my_set
my_set.update({8})
my_set
Adding values that are already members doesn't change anything. So sets are efficient ways to keep track of unique values in a series.
my_set.add(1)
my_set
set operations:
s1 = {"a","b","c"}
s2 = {"z","b","c","g","x"}
s3 = {"z","g","x"}
# Join set 1 and set 2
print(s1.union(s2))
print(s1.union(s2) == s2.union(s1)) # commutative
# in set 1 AND set 2
s1.intersection(s2)
# Set 1 not in set 2
s1.difference(s2)
# in the set 1 and set 2 but not both
s1.symmetric_difference(s2)
# check if one set is a subset of another sets
s3.issubset(s2)
# check if one set is a super set of another set
s2.issuperset(s3)
# test if two sets have no members in common
s3.isdisjoint(s1)
dict
¶Construction:
{<key>:<value>}
dict()
my_dict = {'a': 4, 'b': 7, 'c': 9.2}
print(type(my_dict))
print(my_dict)
Dictionary constructor
my_dict = dict(a = 4.23, b = 10, c = 6.6)
my_dict
Accessing the dictionary's 'keys'
my_dict.keys()
Accessing the dictionary's 'values'
my_dict.values()
Access key value pairs as tuples (useful for iteration)
my_dict.items()
We can construct dictionaries from scratch by stringing together tuple pairs and converting to a dictionary type using the dict()
constructor.
# recall that a list can hold any type of object, including tuples
xx = [("a",4),("b",10),("c",8)]
print(xx)
dict_xx = dict(xx)
print(dict_xx)
We can index a dictionary using the key.
print(dict_xx['a'])
print(my_dict['c'])
We get a key error when an index doesn't exist
dict_xx['d']
Or we can use the .get()
method to the same end -- but without the error if the key does not exist.
print(dict_xx.get('a'))
print(dict_xx.get('d'))
Adding additional values to a dictionary.
dict_xx + {'d':4}
The addition method doesn't work (there is none, e.g. dict.__add__()
). Rather we need to .update()
the dictionary.
dict_xx.update({'d':4})
dict_xx
Note that the keys must be immutable (e.g. str
, int
, float
, bool
, tuple
) but the values can be mutable (e.g. lists
, dicts
, sets
+ all immutable types). (we'll go into the specifics of this below)
print({'apple': "a"})
print({2: "a"})
print({2.5: "a"})
print({True: "a"})
print({None: "a"})
print({(4,5): "a"})
print({[1,2,3]: "a"})
print({{1,2,3}: "a"})
We'll delve more into manipulating container objects like strings in the next lecture.
lists
dict
set
immutable → "object cannot be changes after it is created"
int
float
bool
str
tuple
mutable objects are useful when you need add or edit values. Immutable objects are useful when you need values to remain consistent.
For further reading, see the following Medium post
# Mutability with lists
gg = [1,2,3,4]
gg[1] = 9
gg
# Immutability with tuples
tt = (1,2,3,4)
tt[1] = 9
# A mixture of worlds with dictionaries
dd = {"a":[1,2,3,4],"b":[33,44]}
print(dd.keys()) # can't change the keys (immutable)
# But we can add to the dict
dd.update({'c':9})
print(dd)
# and change values
dd['a'][2]= "SSSS"
print(dd)
my_set = {3,4,5,5,5,6}
print(my_set)`
my_set.pop() # pop out the first value
print(my_set)
my_str = "This is a string"
my_str[0] = "X"
As noted, each object obtains a unique object id
when instantiated. When we alter an immutable object, the object gets a new id in memory. However, this is not always the case when dealing with mutable objects. We can actually assign multiple references to the same objects. This can results in desirable behavior. To get around this, we can make copies of an object.
# Instantiate an object with the integer value 4.
x = 4
id(x)
# Add 1 integer to the value increasing it by one
x += 1
id(x) # A new object id is assigned to memory!
# These assignments correspond with the values not the object.
print(id(4))
print(id(5))
Note that when dealing with mutable objects, a very different story emerges.
# Create a list object called my_list
my_list = ["a", 2, 3.3]
# Make another object that is a copy of my_list
your_list = my_list
Note that both the contents (the values contained within) are the same
print(my_list)
print(your_list)
And the object ids are the same
print(id(my_list))
print(id(your_list))
Note we can test for two forms of equivalence when comparing objects.
print(my_list is your_list) # `is` tests for equality of identity
print(my_list == your_list) # `==` tests for equality of value.
Now let's update one of the list objects.
your_list.append(10)
your_list
When we look at the other list object, however, we note something surprising: it changed as well!
my_list # ????
To understand what's going on, we need to think closely about what's coming on underneath the hood when we assign mutable objects to new references. See the following interactive code diagram.
To get around the following issue, we need to make a copy of a mutable data object.
There are three ways we can make a copy.
list()
.copy()
my_list[:]
a = my_list
a is my_list
a = list(my_list)
b = my_list.copy()
c = my_list[:]
# Values are equivalent
print(a == my_list)
print(b == my_list)
print(c == my_list)
# But their identities are not
print(a is my_list)
print(b is my_list)
print(c is my_list)
Recall that a list
can hold a heterogeneous types of objects, including other lists. We call this a "nested list" (or a nested data structure).
Nested data containing other mutable data types can generate similar types of problems.
nested_list = [[1,2,3],[4,7,88],[69,21,9.1]]
nested_list
print(nested_list[1])
print(nested_list[1][1])
# Copy the list
new_nested_list = list(nested_list)
# Equivalent values
print(new_nested_list == nested_list)
# Not identified as the same object... so the copying did work!
print(new_nested_list is nested_list)
Let's now edit the nested list by appending new values. When we do this, we see the values for the one list changed, but not the other. That's what we want.
# Let's augment this list...
nested_list.append([1,2,3,4,5])
print(nested_list)
print(new_nested_list)
Let's now edit a specific value within one of the nested lists.
# Let's augment one of the lists within the lists...
print(nested_list[1])
nested_list[1][1] = "AAA"
print(nested_list)
Oh no! The value was also altered in the other list as well!
print(new_nested_list)
To understand what is going on, let's again refer to the interactive diagram and try to reproduce the circumstances.
The take-away: Copies are shallow → this means that copying a list will still maintain the references to the nested lists.
Deep copies allow use to ensure that a copy is made recursively for all mutable data types in the nested data structure.
import copy # from the standard library
my_list = [[1,2],[3,4]]
my_list2 = copy.deepcopy(my_list)
my_list2[1][1]=55
print(my_list)
print(my_list2)
Python comes with an extensive standard library and built-in functions.
Some examples of these modules (to name a few...)
math
for mathematical computations¶import math
math.log(100)
re
for string computations¶import re
my_string = "this is a dog"
re.sub("this","That",my_string)
random
for random number generation¶import random
random.randint(1, 10)
datetime
for dealing with dates¶import datetime
date1 = datetime.date(year=2009,month=1,day=13)
date2 = datetime.date(year=2010,month=1,day=13)
date2 - date1
Excerpt from Real Python post
Modular programming refers to the process of breaking a large, unwieldy programming task into separate, smaller, more manageable subtasks or modules. Individual modules can then be cobbled together like building blocks to create a larger application.
There are several advantages to modularizing code in a large application:
Simplicity: Rather than focusing on the entire problem at hand, a module typically focuses on one relatively small portion of the problem. If you’re working on a single module, you’ll have a smaller problem domain to wrap your head around. This makes development easier and less error-prone.
Maintainability: Modules are typically designed so that they enforce logical boundaries between different problem domains. If modules are written in a way that minimizes interdependency, there is decreased likelihood that modifications to a single module will have an impact on other parts of the program. (You may even be able to make changes to a module without having any knowledge of the application outside that module.) This makes it more viable for a team of many programmers to work collaboratively on a large application.
Reusability: Functionality defined in a single module can be easily reused (through an appropriately defined interface) by other parts of the application. This eliminates the need to recreate duplicate code.
Scoping: Modules typically define a separate namespace, which helps avoid collisions between identifiers in different areas of a program. (One of the tenets in the Zen of Python is Namespaces are one honking great idea—let’s do more of those!)
Functions, modules and packages are all constructs in Python that promote code modularization.
import sys
import numpy as np
from sklearn import metrics
PiPy
¶!pip install numpy
Anaconda
¶!conda install numpy