A variable refers to a certain value with specific type. For example, we may want to store a number, a fraction, or a name, date, maybe a list of numbers. All those need to be reachable using some name, some reference, which we create when we create a variable. After we create a variable with a value, we can peek at what's inside using "print" method.
In [75]:
my_name = 'Adam'
print my_name
In [76]:
my_age = 92
your_age = 23
age_difference = my_age - your_age
print age_difference
In [77]:
a = 1
In [78]:
a, b, c = 1, 2, 3
print a, b, c
In [79]:
a = b = c = d = "The same string"
print a, b, c, d
You could ask: does Python use call-by-value, or call-by-reference? Neither of those, actually. Variables in Python are "names", that ALWAYS bind to some object, because mostly everything in Python is an object, a complex type. So assigning a variable means, binding this "name" to an object.
Actually, each time you create a number, you are not using a classic approach, like for example in C++:
int my_integer = 1;
When we look at an integer in Python, it's actually an object of type 'int'. To check the type of an object, use the "type" method.
In [80]:
type(my_age)
Out[80]:
To be completely precise, let's look at creating two variables that store some names. To see where in memory does the object go, we can use method "id". To see the hex representation of this memory, as you will usually see, we can use the method "id".
In [81]:
some_person = "Andrew"
person_age = 22
print some_person, type(some_person), hex(id(some_person))
print person_age, type(person_age), hex(id(person_age))
Now, let's change this name to something else.
In [82]:
some_person = "Jamie"
person_age = 24
print some_person, type(some_person), hex(id(some_person))
print person_age, type(person_age), hex(id(person_age))
The important bit is that, even though we use the same variable "person_age", the memory address changed. The object holding integer '22' is still living somewhere on the process heap, but is no longer bound to any name, and probably will be deleted by the "Garbage Collector". The binding that exists now, if from name "person_age" to the int object "24".
The same can be said about variable 'some_person'.
The reason we need to talk about this, is that when you use variables in Python, you have to understand that such a "binding" can be shared! When you modify one, the other shared bindings will be modified as well! This is true for "mutable" objects. There are also "immutable" objects, that behave in a standard, standalone, not-changeable way.
Immutable types: int, float, decimal, complex, bool, string, tuple, range, frozenset, bytes
Mutable types: list, dict, set, bytearray, user-defined classes
In [83]:
shared_list = [11,22]
my_list = shared_list
your_list = shared_list
print shared_list, my_list, your_list
Now, when we modify the binding of 'shared_list' variable, both of our variables will change also!
In [84]:
shared_list.append(33)
print shared_list, my_list, your_list
This can be very confusing later on, if you do not grasp this right now. Feel free to play around :)
What is a data type? It is a way of telling our computer, that we want to store a specific kind of information in a particular variable. This allows us to access tools and mechanisms that are allowed for that type.
We already mentioned that actually every time we create a variable, we create a complex type variable, or an object.
This is called creating an object, or instantiating an object. Each object comes from a specific template, or how we call it in Object Oriented Programming, from a class.
So when you assign a variable, you instantiate an object from a class.
In Python, every data type is a class!
Also, we will use some built-in tools for inspection - type() and isinstance() functions. The function type() will just say from which class does this object come from. THe function isinstance() will take an object reference, and then a class name, and will tell you if this is an instance of this class.
Let's review data types used in Python (most of them).
In [85]:
a = 111
print a, type(a)
In [86]:
b = 111111111111111111111111111111111
print b, type(b)
In [87]:
c = 11.33333
d = 11111.33
print c, type(c)
print d, type(d)
In [88]:
c = 2 + 3j
print c, type(c)
In [4]:
# Addition
print(1+1)
# Multiplication
print(2*2)
# Division
print(4/2)
# Remainder of division
print(5%2)
# Power
print(2**4)
In [89]:
a = "Something"
b = 'Something else'
print type(a), type(b)
Even though strings are not numbers, you can do a lot of operations on them using the usual operators.
In [90]:
name = 'Adam'
print name + name
print name * 3
Actually, strings are 'lists' of characters. We will explore lists in just a moment, but I want you to become familiar with a new notation. It is based on the order of sequence. When I say, "Give me the second character of this string", I can write is as such:
In [91]:
print 'Second character is: ' + name[1]
Since we are counting from 0, the second character has index = 1.
Now, say I want characters from second, to fourth.
In [92]:
print 'From second to fourth: ' + name[1:4]
In [93]:
print 'The last character (or first counting from the end) is: ' + name[-1]
In [94]:
print 'All characters, but skip every second: ' + name[0:4:2]
These operations are called 'slicing'.
We can also find substrings in other substrings. THe result is the index, at which this substring occurs.
In [6]:
some_string = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAxxAAAAAAAAAAAAAAAAAAAA"
substring = "xx"
location = some_string.find(substring)
print("Lets see what we found:")
print(some_string[location:location+len(substring)])
We can also replace substrings in a bigger string. Very convenient. But more complex replacements or searches are done using regular expressions, which we will cover later
In [7]:
some_string = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAxxAAAAAAAAAAAAAAAAAAAA"
substring = "xx"
print(some_string.replace( substring , "___REPLACED___"))
In [5]:
a = True
b = False
print("Is a equal to b ?")
print(a==b)
print("Logical AND")
print(a and b)
print("Logical OR")
print(a or b)
print("Logical value of True")
print( bool(a) )
print("Logical value of an empty list")
print( bool([]) )
print("Logical value of an empty string")
print( bool("") )
print("Logical value of integer 0")
print( bool(0) )
Prepare to use this data type A LOT. Lists can store any objects, and have as many elements, as you like. The most important thing about lists, is that their elements are ordered. You can create a list by making an empty list, converting something else to a list, or defining elements of a list right there, when you declare it.
In [95]:
empty_list = []
list_from_something_else = list('I feel like Im going to explode')
list_elements_defined_when_list_is_created = [1, 2, 3, 4]
print empty_list
print list_from_something_else
print list_elements_defined_when_list_is_created
In [96]:
l = ["a", "b", "c", "d", "e"]
print l[0]
print l[-1]
print l[1:3]
In [97]:
l = []
l.append(1)
print l
l[0] = 222
print l
In [98]:
l.remove(1)
print l
In [ ]:
l = [1,2,3,3,4,5,3,2,3,2]
# Make a new list from a part of that list
new = l[4:7]
print new
But lists are not only used to hold some sequences! You can iterate over a list. This means no more, no less, then doing something for each of the elements in a given range, or for all of them. We will cover the so-called 'for' loop in next lessons, but I guess you can easily imagine what this minimal example would do.
In [ ]:
# Do something for all of elements.
for element in [1, 2, 3]:
print element + 20
In [ ]:
# Do something for numbers coming from a range of numbers.
for number in range(0,3):
print number + 20
In [ ]:
# Do something for all of elements, but written in a short way.
some_list = ['a', 'b', 'c']
print [element*2 for element in some_list]
Even though the short notation is a more advanced topic, it is very elegant and 'pythonic'. This way of writing down the process of iteration is called 'list comprehensions'.
In [ ]:
some_tuple = (1,3,4)
print some_tuple
print type(some_tuple)
print len(some_tuple)
print some_tuple[0]
print some_tuple[-1]
print some_tuple[1:2]
other_tuple = 1, 2, 3
print other_tuple
print type(other_tuple)
In [ ]:
# This will cause an error! You can not modify a tuple.
some_tuple[1] = 22
This data structure is very useful. In essence, it stores pairs of values, first of which is always a "key", a unique identifier, and the "value", which is the connected object. A dictionary performs a mapping between keys and values. Because the key is always unique (has to be, we will find out in a minute), there is always exactly one key with specific content. A dictionary is also very efficient - finding a value in a dictionary takes only one operation, whereas searching through a list one by one could require going through the whole list. This means that for any situation, where you need to store lot's of values, that will be often used, it is much better to store them in a dictionary. Also, I recommend to read on Wikipedia on "hash maps".
In [ ]:
empty_dictionary = {}
print empty_dictionary
print type(empty_dictionary)
In [ ]:
dictionary_from_direct_definition = {"key1": 1, "key2": 33}
print dictionary_from_direct_definition
In [ ]:
# Let's create a dictionary from a list of tuples
dictionary_from_a_collection = dict([("a", 1), ("b", 2)])
print dictionary_from_a_collection
In [ ]:
# Let's create a dictionary from two lists
some_list_with_strings = ["a", "b", "c"]
some_list_with_numbers = [1,2,3]
dictionary_from_two_lists = dict(zip(some_list_with_strings, some_list_with_numbers))
print dictionary_from_two_lists
print type(dictionary_from_two_lists)
In [ ]:
# Let's create a dictionary from a dictionary comprehension
dict_from_comprehension = {key:value for key, value in zip(some_list_with_strings, some_list_with_numbers)}
print dict_from_comprehension
In [ ]:
d = {}
d["a"] = 1
d["bs"] = 22
d["ddddd"] = 31
print d
In [ ]:
d.update({"b": 2, "c": 3})
print d
In [ ]:
del d["b"]
print d
In [ ]:
d.pop("c")
print d
In [ ]:
# How many keys?
print d.keys()
print len(d)
print len(d.keys())
In [ ]:
# How many values?
print d.values()
print len(d.values())
In [ ]:
for key, value in d.items():
print key, value
In [ ]:
l = ["r", "p", "s", "t"]
d = {a: a for a in l}
# Find "t" in list.
for letter in l:
if letter == "t":
print "Found it!"
else:
print "Not yet!"
# Find "t" in dictionary keys.
print "In dictionary - found it! " + d["t"]
In [ ]:
some_sequence = [1,1,1,1,2,2,2,3,3,3]
some_set = set(some_sequence)
print some_set
some_string = "What's going ooooon?"
another_set = set(some_string)
print another_set
In [ ]:
some_dictionary = {"a": 2, "b": 2}
print some_dictionary
yet_another_set = set(some_dictionary)
print yet_another_set
print set(some_dictionary.values())