Day 02
You can also download the Complete Day 02 Notes from Here
Strings in Python
String literals can be defined with either single or double quotes (your choice), and can be defined on multiple lines with a backslash like so:
A Python string is an iterable series of characters. You can loop through a string just like a list:
Most importantly, strings in Python are immutable. This means you cannot change strings like so:
Also, when you build strings with += in a loop, you're creating a new string every iteration:
Unicode vs ASCII
In Python 2 strings, are stored internally as 8 bit ASCII. But in Python 3, all strings are represented in Unicode.
Uh, what?
Before we talk about methods on strings in Python, let's learn a little bit about the history of character encodings. If you would like a longer description, feel free to read this excellent article.
When we as humans see text on a computer screen, we are viewing something quite different than what a computer processes. Remember that computers deal with bits and bytes, so we need a way to encode (or map) characters to something a computer can work with. In 1968, the American Standard Code for Information Interchange (or ASCII) was standardized as a character encoding. ASCII defined codes for characters ranging from 0 to 127.
Why this range? Remember that computers work in base 2 or binary, so each bit represents a power of two. This means that 7 bits can get us 2^7 = 128 different binary numbers; since each bit can equal0or1, with7bits we can represent all numbers from 0000000 up to 1111111. With ASCII, we can then map each of these numbers to a distinct character. Since there are only 26 letters in the alphabet (52 if you care about the distinction between upper and lower case), plus a handful of digits and punctuation characters, ASCII should more than cover our needs, right?
ASCII was a great start, but issues arose when non English characters like é or ö could not be processed and would just be converted to e and o. In the 1980s, computers were 8- bit machines which meant that bytes now held 8 bits. The highest binary number we could obtain on these machines was 11111111 or 2^0 + 2^1 + 2^2 + 2^3 + 2^4 + 2^5 + 2^6 + 2^7, or 255. Different machines now used the values of 128 to 255 for accented characters, but there was not a standard that emerged until the International Standards Organization (or ISO) emerged.
Even with an additional 128 characters, we started running into lots of issues once the web grew. Languages with completely different character sets like Russian, Chinese, Arabic, and many more had to be encoded in completely different character sets, causing a nightmare when trying to deliver a single text file with multiple character sets.
In the 1980s, a new encoding called Unicode was introduced. Unicode's mission was to encode all possible characters and still be backward compatible with ASCII. The most popular character encoding that is dominant on the web now is UTF-8, which uses 8- bit code units, but with a variable length to ensure that all sorts of characters can be encoded. In Python3, strings are Unicode by default.
String Methods
Python contains quite a few helpful string methods; here are a few. Try running these in a REPL to see what they do!
Let's start with a simple variable:
Upper
To convert every character to upper-case we can use the upper function.
Lower
To convert every character to lower-case we can use the lower function.
Capitalize
To convert the first character in a string to upper-case and everything else to lower-case we can use the capitalize function.
Title
To convert every first character in a string to upper-case and everything else to lower- case we can use the title function.
Find
To find a subset of characters in a string we can use the find method. This will return the index at which the first match occurs. If the character/characters is/are not found, find will return -1
isalpha
To see if all characters are alphabetic we can use the isalpha function.
isspace
To see if a character or all characters are empty spaces, we can use the isspace function
islower
To see if a character or all characters are lower-cased, we can use the islower function (there is also a function, which does the inverse called isupper)
istitle
To see if a string is a "title" (first character of each word is capitalized), we can use the istitle function.
Endswith
To see if a string ends with a certain set of characters we can use the endswith function.
partition
To partition a string based on a certain character, we can use the partition function.
String Interpolation with Formatted Strings
One of the most common string methods you'll use is theformatmethod. This is a powerful method that can do all kinds of string manipulation, but it's most commonly just used to pass varaibles into strings. In general this is preferred over string concatenation, which can quickly get cumbersome if you're mixing a lot of variables with strings. For example:
Here, the greeting variable looks fine, but all that string concatenation isn't easy on the eyes. It's very easy to forget about a + sign, or to forget to separate words with extra whitespace at the beginning and end of our strings.
This is one reason why format is nice. Here's a refactor:
When we call format on a string, we can pass variables into the string! The variables will be passed in order, wherever format finds a set of curly braces.
Starting in Python 3.6, however, we have f-strings, which are a cleaner way of doing string interpolation. Simply put f in front of the string, and then brackets with actual variable names.
Functions, Inbuilt Functions & Modules
What is a function?
A function is a repeatable process or procedure. A real world analogy of a function is the brew button on a coffee machine. The coffee machine has inputs (hot water, coffee grounds), and outputs (hot coffee). When you press the button to brew a pot of coffee, you are starting a process that should return an expected output to you. The same thing is true in programming. A function takes a set of variables as inputs and returns a value as an output.
We have already seen many functions in action. For example, in the list chapter, we learned about append and many others. These are built-in functions that operate on a list. But in addition to built in-functions, we can also write our own functions! In Python, a function has the following format:
Note
Notice that we MUST indent on the following line. If you do not indent your code, you'll get an IndentationError! To invoke a function, use the ():
Next, let's try to write a function called add_five_plus_five which outputs the sum of 5 + 5. Here's what that might look like:
Now let's run this function and our output is....nothing! Why is that? We are missing a very important keyword:
In order to output values from a function, we need to use the return keyword. Let's see how we can fix our function now.
Now let's run this function add_five_plus_five() and our output is....10! If we would like, we can also save this information to a variable and use it at a later point in time like this:
If we don't have a return statement in our function, it will always return None to us. This is true regardless of what else happens in the function. Take a look at this example:
In the real world, we'd never really write functions like these because they are very rigid; all they do is add 5 and 5. Ideally, we'd like to be able to provide some input to our functions, but in order to do that we need to introduce a concept called parameters or arguments.
Function Parameters
Function with Arguments
Here is an example of a function that takes two arguments:
In this case, our function takes two arguments (a cat_name and a dog_name), and returns a message about the pets.
Keyword Arguments
One nice thing about Python is that if you know the names of the arguments that you will pass to a function, you can pass them into the function in any order. All you need to do is provide the name (or keyword) for the argument, then the value you want to pass in. Check it out:
When you call a function by passing in akeyword=valuepair, you're said to be using keyword arguments. This can be especially useful if you have a function that accepts many parameters.
Default argument values
Sometimes you may want to set default values for parameters you pass into your function. In Python, the syntax looks the same as when you use keyword arguments, with one crucial difference: you use keyword arguments when you call a function, but you use default argument values when you define a function. Here's an example:
In this case, if we don't pass in any values when we call add, the first parameter will be 5 and the second parameter will be 15. But we can overwrite these defaults by simply passing numbers into the function when we call it:
Variable number of function arguments
Sometimes we might want to write a function that can be called with an unknown number of arguments. There are two ways we can do this. The first is by using *, in the function definition which allows us to pass in an unknown number of arguments:
Inside of the function, the named parameter after the * corresponds to a tuple of the arguments passed in.
This can be helpful if we want to iterate through all of the arguments or apply some other function on a tuple:
We can also use the * operator when invoking a function. In that case, the * will take an iterable like a list and split it up into separate parameters. Here is an example:
Unpacking an argument
The same way that we can include a * before a parameter, we can also do this for values passed to a function. If you're coming from JavaScript, this is going to look very similar to the spread operator. We unpack arguments when we need to convert a collection (tuple / list) to comma separated values. The idea here would be, we want to invoke a function, but all we have is a collection - let's see an example.
When there is a * as a parameter to a function, that parameter will be a tuple of values when the function is invoked. When you find a * that is not a parameter to a function, we are unpacking a value.
Variable number of keyword arguments
What if you want to pass in an unknown number of keyword arguments? In this case, we can use **, which allows us to access all of the keyword arguments inside of a function as a dictionary when we do not know how many keyword arguments will be passed.
Unpacking a dictionary into keyword arguments
The same way that we can include a ** before a parameter, we can also do this for values passed to a function. Previously, we saw that * will unpack a collection (list / tuple), so what about **? That's for dictionary unpacking! What the ** operator will do is turn a dictionary into keyword arguments so that if we have one single dictionary, we can pass it to a function and unpack it into keyword arguments! Since dictionaries do not guarantee any kind of order, it's useful that they become keyword arguments since a function that uses keyword arguments can accept those keyword arguments in any order!
When there is a * as a parameter to a function, that parameter will be a tuple of values when the function is invoked. When you find a * that is not a parameter to a function, we are unpacking a value.
Function Scope
Scope
In Python we have function scope, which prohibits us from accessing variables created inside of a function from outside of that function:
The global keyword
The global scope includes all variables defined outside of functions. But if we try to use a global variable in a method, we will see UnboundLocalError: local variable VARIABLE_NAME referenced before assignment. This happens because a method in Python either has local variables or global variables. If variable is defined anywhere in a method and that variable has the same name as a global variable, then the new local variable will be used in the function instead of the global. But if you actually want to assign a global variable from within a function, you need to use the global keyword. Using global variables in general is not best practice:
In Python you need to explicitly state that a variable should be global, using the global keyword.
Listing Locals and Globals
In Python we can display all of the local variables and global variables using the locals and globals functions
Python Nested Functions (sort of like closures)
In Python we do have support for closures, a feature where an inner function has access to variables in an outer function's scope, even after the outer function has finished executing.
However, closures in Python are "weak" and have some limitations. For example, if you want to change the value of a variable from an outer scope, you'll run in to problems:
Again, this is because the x inside of increment is a new variable, bound to the scope of increment. It's not a reference to x coming from the scope of counter.
We can get around the problem with the example above by setting attributes on the inner function, rather than trying to change variables from an outer scope:
Documenting our functions
Something that Python offers us is the ability to add what is called a docstring. Let's see what that looks like
We can call this function using
Docstrings are essential when writing methods and can be thought of like an enhanced comment. Docstrings are also very useful when writing tests, as you can see what the docstring is when running the test. You are highly encouraged to write docstrings for your functions, and inside classes as well.
Default argument types for Python
Unlike languages like Java and C++, Python is a dynamically typed language. This means that we do not need to explicitly define the data type of a variable when initializing it. This gives us a bit more flexibility around our code, but sometimes we want to clearly indicate that a certain data type is what should be passed as a parameter, or that a function returns a specific value. We can do that in Python! Let's see what that looks like:
We are specifying that both a and b are ints and the return value from the function is an int as well. We can also combine this with default parameter values!
Popular Inbuilt functions of Python
len()
Returns the number of items in an object.
print()
Prints the given object(s) to the standard output device.
type()
Returns the type of an object.
int(), float(), str()
Converts objects to an integer, floating point number, or string, respectively.
input()
Allows user input.
max()
Returns the largest item.
min()
Returns the smallest item.
sum()
Sums up the items.
range()
Generates a sequence of numbers.
sorted()
Returns a sorted list from the items in an iterable.
map()
Applies a function to all the items in an input list.
filter()
Constructs an iterator from elements of an iterable for which a function returns true.
abs()
Returns the absolute value of a number.
round()
Rounds a number to a specified number of digits.
eval()
Evaluates a given expression string and executes it as Python code.
globals() and *locals()
Return the current global and local dictionary, respectively.
help()
Invokes the built-in help system.
id()
Returns the identity of an object.
These examples cover a range of functionalities provided by Python's built-in functions, from basic data manipulation to more complex operations.
Popular Inbuilt Modules of Python
Let's explore some popular built-in modules in Python along with examples of how to use them:
math
Provides access to mathematical functions.
datetime
For manipulating dates and times.
os
Offers a way of using operating system dependent functionality.
sys
Provides access to some variables used or maintained by the Python interpreter.
json
For encoding and decoding JSON data.
random
Generates pseudo-random numbers.
re
Provides regular expression matching operations.
subprocess
Allows you to spawn new processes.
urllib
For opening and reading URLs.
collections
Implements specialized container datatypes.
itertools
Implements a number of iterator building blocks.
threading
For threading (running multiple threads).
multiprocessing
Offers both local and remote concurrency.
socket
For network connections.
sqlite3
For working with SQLite databases.
logging
Provides a flexible framework for emitting log messages.
argparse
For writing user-friendly command-line interfaces.
time
For time-related functions.
Lambda Functions
The closest we have to "anonymous" functions in Python is lambdas. Lambdas are useful if you want to write a function which can be described in a single line of code. Here are some examples:
Lambda functions start with the keyword lambda. Next comes a comma separated list of arguments, then a colon, then the expression you want the lambda to return. For simple one-line functions, lambdas can be a convenient shorthand for the traditional function definition. But these functions are anonymous; as you can see, they all share the same name.
Conditional and Iterable Statements
Conditional Statements
Conditional statements are used to execute certain pieces of code based on some condition. The most common conditional statements in Python are if, elif (else if), and else.
if Statement
if-else Statement
if-elif-else Statement
Iterable Statements
Iterable statements allow you to execute a block of code multiple times. The most common iterable statements in Python are for and while loops.
For Loop
you can also iterate over elements in a list:
While Loop
Nested Loop
You can also nest loops within each other, combining both for and while loops.
break Statement
The break statement is used to exit a loop prematurely, breaking out of the enclosing for or while loop.
In this example, the loop terminates when i becomes 5, and thus numbers from 5 to 9 are not printed.
continue Statement
The continue statement is used to skip the rest of the code inside a loop for the current iteration only. The loop does not terminate but continues with the next iteration.
Here, when i is 5, the continue statement is executed, which causes the loop to skip the print statement for 5 and proceed with the next iteration.
break ans continue in while Statement
In this while loop example, the loop skips printing the number 5 due to the continue statement, and it breaks out of the loop entirely when i becomes 8.
break and continue are fundamental to controlling the flow of loops in Python, allowing for more complex and efficient looping constructs.
Deep Dive into Data Structures (Lists, Dictionaries, Tuples, Sets)
Lists in Python
Lists in Python are simply collections of elements. They can be as long as you want, and the individual elements can have the same type or not:
To access an element in a list, we use bracket notation and pass in the index we're interested in. Lists in Python use a zero-based index:
Note that if you try to access an element with an invalid index, Python will give you an error.
We can also reassign values in lists using =:
In addition to getting and setting values in lists, there are a number of built-in methods you can use:
- append(x): Adds a single element x to the end of the list.
- extend(iterable): Extends the list by appending all the items from the iterable.
- insert(i, x): Inserts an item x at a given position i.
- remove(x): Removes the first item from the list whose value is equal to x.
- pop([i]): Removes the item at the given position in the list, and returns it. If no index is specified, pop() removes and returns the last item in the list.
- clear(): Removes all items from the list.
- index(x[, start[, end]]): Returns the index of the first item whose value is equal to x.
- count(x): Returns the number of times x appears in the list.
- sort(*, key=None, reverse=False): Sorts the items of the list in place (the arguments can be used for sort customization).
- reverse(): Reverses the elements of the list in place.
- copy(): Returns a shallow copy of the list.
Slicing Lists in Python
Slices return portions of a list or string. While this seems like a pretty minor concept, there's actually quite a bit you can do with slices that you might not expect.
List Iterations and Comprehension
Iterating over lists and strings
In Python we have a few ways of iterating over lists and strings. One of the most common types of loops is a for in loop; while loops are also common. Let's see what those look like.
for in
The most common way of iterating over a list is a for in loop. The syntax is for ELEMENT in LIST:. As with if statements, don't forget about the colon!
Sometimes you may want to have access to the element's index in the list as well as the element itself. In this case, you can pass the list into the enumerate function. You'll need to name two variables in the for loop: the first will refer to the current index, the second will refer to the current element:
while
You can also do a while loop with Python, but this is a bit less common when iterating:
If you ever want to move to the next step of the iteration, you can prematurely break out of the current iteration with the the continue keyword. Similarly, you can exit from a loop entirely using the break keyword.
range
In Python we can also create ranges, which represent a range of numbers, with the following syntax: . Note that the range is not inclusive. In other words, will include 1, 2, and 3, but not 4!
Ranges take up less memory than lists, so if you find yourself needing a bunch of numbers that increment by the same amount each time, try to use a range instead of a list.
List Comprehension
List comprehensions are one of the most powerful tools in Python. They allow you to build lists in a more concise way, often in a single line. List comprehensions are a wonderful alternative to loops!
One way to use a list comprehension is to transform a set of values from a range or another list into some new set of values. This is sometimes referred to as a mapping operation. Here are a few examples:
We can also put if statements inside of our list comprehensions to filter out certain transformed values!
For longer list comprehensions, we can also split it into multiple lines for readability:
Dictionary in Python
A dictionary in Python is an unordered collection of items. While other compound data types have only value as an element, a dictionary has a key-value pair.
Characteristics:
- Mutable: You can change their content without changing their identity.
- Dynamic: They can grow and shrink as needed.
- Nested: You can nest dictionaries inside dictionaries, lists, etc.
- Key-Value Pairs: Each item is a pair (key, value).
Basic Operations
Creating a Dictionary
Accessing Elements
Adding or Modifying Elements
Deleting Elements
Dictionary Comprehension
Dictionary comprehension is a concise and readable way to create dictionaries. It is similar to list comprehension but for dictionaries.
Syntax
For Examples,
Creating a simple dictionary comprehension
Using conditionals in dictionary comprehension
Using multiple iterables
Tuples in Python
Tuples are similar to lists in Python, but they are immutable, meaning they cannot be changed after they are created. Tuples are often used for data that should not be modified.
Characteristics:
- Immutable: Once a tuple is created, you cannot change its values.
- Ordered: The items have a defined order, and that order will not change.
- Indexed: Tuples can be indexed and sliced like lists.
Basic Operations of Tuples
Creating a Tuple
Accessing Elements in Tuples
Slicing Elements
Iterating through Elements
Concatination and Repetition
Sets in Python
Sets are unordered collections of unique elements. They are mutable and are often used for operations involving membership tests, removing duplicates from a sequence, and computing mathematical operations such as intersection, union, difference, and symmetric difference.
Characteristics:
- Unordered: The elements in a set do not have a defined order.
- Mutable: You can add or remove items from a set.
- Unique Elements: Each element in a set is unique; duplicates are not allowed.
Basic Operations of Sets
Creating a Set
Adding Element in Set
Removing Element from Set
Set Operations (Union, Intersection, Difference, Symmetric Difference)
Set Comprehension
Tuples and sets are fundamental data structures in Python, each with their specific use-cases and characteristics. Tuples are used for ordered and immutable collections of items, whereas sets are used for unordered collections of unique elements, particularly for set operations.