Python comes with an interactive interpreter. When you type python in your shell or command prompt, the python interpreter becomes active with a >>>
prompt and waits for your commands.
$ python
Python 3.5.1 |Anaconda 2.5.0 (x86_64)| (default, Dec 7 2015, 11:24:55)
[GCC 4.2.1 (Apple Inc. build 5577)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
In the class we are going to use Jupyter/Ipython notebook. It provides an the Python interpreter in a web page.
Try any simple mathematical expressions and Python will evaluate them and print the response back to you.
1 + 2
2 * 7
# 2 raised to the power of 1000
2 ** 1000
The print
function can be used to print something explicitly.
print("Hello Python!")
%%file hello.py
print("hello world")
The %%file
in the first line tell Jypyter notebook to save the contents after the first line as a file instead of executing it as python code.
Now we can run that python file as a script.
!python hello.py
Again the !
is a marker to tell Jyputer notebook that we are executing an external command and not Python code.
Unlike in the interpreter, we need to explicitly use the print function to print any value.
Problem: Write a program to print the value of 123 * 456
.
Create a variable is as simple as assigning a value to it.
x = 4
print(x*x)
If we assign a new value to an exiting variable, its value will get replaced.
x = 5
print(x)
In Python, variables don't have any types. They are place holders that can hold any type of value.
x = "python"
print(x)
Python has integers.
1 + 2
Python has floating point numbers.
1.2 + 2.3
Python has strings. Strings can be enclosed either in single quotes or double quotes.
"hello world"
x = "hello"
x + " world"
print(x, "python")
Multi-line strings are written using three double quotes or three single quotes.
x = """This is a multi line string.
one
two
thee
"""
print(x)
Strings in Python support the usual escape codes.
print("a\nb\nc")
print("a\tb\tc")
Python has lists.
x = ["a", "b", "c"]
x
The len
function can be used to find the length of a list.
len(x)
The []
syntax can be used to get the element at an index in the list.
x[0]
x[1]
x[2]
It is perfectly okay for a list to have elements of different datatypes.
x = [1, 2, 'hello', [3, 4]]
Python has a type called tuple
to represent fixed length records.
x = (1, 2)
a, b = x
a
b
x = (1, 2)
print(x[0], x[1])
Python has boolean values. The built-in variables True and False represents boolean truth and false.
True
False
Three are more datatypes like dictionary, set etc. We'll see them later.
Python has many built-in functions. We've already seen print
and len
functions.
print("hello", "python")
len(["a", "b", "c"])
len("hello world")
Python is usally doesn't allow operations on incompatible datatypes. For example, trying to add a number to a string will result in an error.
1 + '2'
Unlike, some other programming languages, Python doesn't try to implicitly convert the datatypes.
Python provides built-in functions int
to convert a string into an integer and function str
to convert value of any datatype to string.
int("2")
1 + int("2")
str(1234)
len(str(1234))
len(str(2**100))
2**1000
len(str(2**1000))
Lets see how to write our own functions.
def square(x):
return x*x
print(square(4))
The important thing to notice here is that the body of the square function is indented.
def square(x):
return x*x
print(square(4))
Python uses indentation, the leading spaces, to identify the body of the function. Indentation is very important in Python and it is used for all kinds of compound statements like if
, for
etc.
Function body can have multiple lines as well.
def square(x):
y = x*x
return y
print(square(4))
By default the variables defined in a function are considered local the function.
y = 0
def square(x):
y = x*x
return y
print(square(4))
print(y)
The y
inside square function is different from the global y
, even though they have the same name.
Problem: Write a function cube
to compute cube of a number.
>>> cube(2)
8
>>> cube(3)
27
Problem: Write a function count_digits that takes a number of argument and returns the number of digits it has.
>>> count_digits(12345)
5
>>> count_digits(2**100)
31
print
vs. return
¶Consider the following two functions:
def square1(x):
return x*x
def square2(x):
print(x*x)
print(square1(4))
square2(4)
One of them is returning the value and the other is printing the value. Which one of these functions is better and why?
The function square1
is better because it is returning the value and that value can be used in other computations, that is not possible with square2
.
1 + square1(2)
square1(square1(2))
print("square of 2 is", square1(2))
Just like integers, string etc., functions are also just another type of values. They can also be assigned to a variable, passed to a function as argument and even can be returned from another function.
def square(x):
return x*x
print(square(2))
print(square)
f = square
f(4)
Lets see a use case.
def square(x):
return x*x
def sum_of_squares(x, y):
return square(x) + square(y)
print(sum_of_squares(3, 4))
def cube(x):
return x*x*x
def sum_of_cubes(x, y):
return cube(x) + cube(y)
print(sum_of_cubes(3, 4))
The sum_of_squares and sum_of_cubes functions are doing almost the same thing. Can we generalize that into a single function?
def sumof(f, x, y):
return f(x) + f(y)
print(sumof(square, 3, 4))
print(sumof(cube, 3, 4))
sumof(abs, -3, 4)
sumof(len, "hello", "python")
There are even some built-in functions which accept functions as arguments, like in the above example.
max(['one', 'two', 'three', 'four', 'five'])
The above example computed the maximum of a list of words based on the dictionary order.
What if we want to find the longest word? In other words, find the maximum based on length?
max(['one', 'two', 'three', 'four', 'five'], key=len)
def mylen(x):
print("len", x)
return len(x)
max(['one', 'two', 'three', 'four', 'five'], key=mylen)
Methods are special type of functions that operate on an object.
x = "Hello"
x.upper()
The upper
method, available on strings, converts the string to upper case. Similarly the lower
method converts a string to lower case.
x.lower()
Couple of more examples of methods on strings:
"mathematics".count("mat")
"mathematics".replace("mat", "rat")
Problem: Write a function icount to count the number of occurances of a substring in a string, ignoring the case.
>>> icount("mathematics", "mat")
2
>>> icount("Mathematics", "mat")
2
>>> icount("Mathematics", "MAT")
2
The split
method splits the string into multiple parts.
sentence = "Anything that can go wrong, will go wrong."
sentence.split()
The split method split the string at every whitespace. Optionally a different delimiter can be specified.
sentence.split(",")
Problem: Write a function count_words to count the number of words in a sentence.
>>> count_words("one two three")
3
Problem: Write a function longest_word to find the longest word in a sentence.
>>> longest_word("one two three four five")
'three'
We've seen how to split strings, lets see how to join them.
"-".join(["a", "b", "c"])
Modules are the reusable libraries containing useful functions and variables. In Python, modules are imported using the import
statement.
The following example, imports the time modules and calls a function asctime
from that module.
import time
print(time.asctime())
That is like the date command in unix, isn't it? Lets try to make it into a reusable script.
%%file date.py
import time
print(time.asctime())
!python date.py
To find help about a module, try help("modulename") in the python interperter, or help(module) after importing it:
>>> import time
>>> help(time)
...
or pydoc modulename
in the unix terminal.
$ pydoc time
...
print(time.time()) # time in seconds since epoch
Lets try some more modules.
import os
os.listdir(".")
The above example lists all files in the current directory.
Problem: Write a function count_files
to count the number of files in a directory.
>>> count_files("/tmp")
11
random
module¶import random
random.choice(['a', 'b', 'c', 'd'])
random.choice(['a', 'b', 'c', 'd'])
The choice function takes a list of elements and returns one of them at random.
Problem: Write a function random_word that takes a sentence as argument and returns one random word from it.
>>> random_word("one two three")
'two'
>>> random_word("one two three")
'one'
Command-line arguments are usually the prefered way to pass inputs to a program.
In Python, the command-line arguments passed to a program are available in a special variable argv
in the sys
module.
%%file args.py
import sys
print(sys.argv)
!python args.py hello world
!python args.py
The sys.argv
variable is a list containing the program name followed by the list of arguments passed to it.
Note that the elements are sys.argv
will always be strings.
!python args.py 1 2 3 4 5
Lets write a program to print the first command-line argument.
%%file echo.py
import sys
print(sys.argv[1])
!python echo.py hello
!python echo.py hello world
Problem: Write a program square.py
that takes a number as command-line argument and prints its square.
$ python square.py 4
16
Problem: Write a program add.py
that takes two numbers as command-line arguments and prints their sum.
$ python add.py 3 4
7
The following are some examples of conditional expressions in Python. Conditional expressions always evaluate to a boolean value.
age = 30
age == 30
age > 25
name = "Alice" # assignment
name == "Alice" # comparison
name != "Alice"
Conditional expressions can be combined using and
and or
.
name == "Alice" and age > 25
age < 20 or age > 40
# we can also write the above expression as:
20 < age < 40
The not
clause can be used to negate a boolean expression.
not name == "Alice"
Often it is useful to wrap the boolean as a function.
def is_senior_citizen(age):
return age >= 60
is_senior_citizen(70)
The in
operator can be used to check if an element is part of a string or a list.
"hell" in "hello"
"yell" in "hello"
"yell" not in "hello"
"a" in ["a", "b", "c"]
vowels = "aeiou"
def is_vowel(c):
return c in vowels
is_vowel('a')
is_vowel('x')
There are some useful methods on strings to check if a string starts with a prefix or ends with a suffix.
name = "python"
name.startswith("py")
name.endswith("on")
def is_python_file(filename):
return filename.endswith(".py")
is_python_file("hello.py")
is_python_file("hello.c")
The if
statement is what we use for conditional execution.
n = 35
if n % 2 == 0:
print("even")
else:
print("odd")
A typical if
statement looks like this:
if condition:
if-block
else:
else-block
If the condition is True, the if-block is executed and if the condition is False, the else-block is executed.
It is important to note that both if-block and else-block are indented to indicate the grouping.
If we use the if condition in a function, there will be two levels of indentation. One for the function and another for if/else.
def checkeven(n):
if n % 2 == 0:
print(n, "is even")
else:
print(n, "is odd")
checkeven(35)
checkeven(48)
The else
part is optional. It is perfectly alright to have an if
statement without the else
.
filename = "a.c"
if not filename.endswith(".py"):
print("please provide a python file")
Checking multiple conditions can be done using if
followed by multiple elif
statements.
def checknumber(n):
if n < 10:
print(n, "is a one digit number")
elif n < 100:
print(n, "is a two digit number")
else:
print(n, "is a big number")
checknumber(5)
checknumber(55)
checknumber(555)
Problem: Write a function minimum
to compute minimum of two numbers (without using the built-in min
function).
>>> minimum(3, 7)
3
>>> minimum(13, 7)
7
Problem: Write a function minimum3
to compute minimum of three numbers. Can you do it by using the minimum function implemented above?
>>> minimum3(2, 3, 4)
2
>>> minimum3(12, 3, 4)
3
>>> minimum3(12, 13, 4)
4
We have already looked at lists briefly.
x = ['a', 'b', 'c', 'd']
x[1]
len(x)
The for
loop is used to iterate over a list of elements. Just like if
, the body of the for
loop is indented.
x = ['a', 'b', 'c', 'd']
for c in x:
print(c)
for c in x:
print(c, c.upper())
for c in x:
print(c, end=" ")
The built-in range
function can be used to iterate over a sequence of numbers.
for i in range(5):
print(i)
range(5) # numbers up to 5 (from 0)
list(range(5))
list(range(0, 5))
list(range(0, 5, 2)) # from 0 to 5 in steps of 2
list(range(2, 10, 3)) # from 2 to 10 in steps of 3
Python has a built-in function sum
to compute sum of a list of numbers.
sum([1, 2, 3, 4])
sum(range(5))
# sum of all intergers below one million
sum(range(1000000))
Lets try to implement our own sum function.
def my_sum(numbers):
result = 0
for n in numbers:
result += n
return result
print(my_sum([1, 2, 3, 4]))
print(my_sum(range(10)))
print(my_sum(range(1000000)))
Problem: Write a function product to compute the product of given list of numbers.
>>> product([1, 2, 3, 4])
24
Problem: Write a function factorial
to compute factorial of a number. Can you use the above implementation of product function in computing this?
>>> factorial(4)
24
Problem: Write a program listfiles.py
that takes path to a directory as command-line argument and prints all the files (and directories) in it.
$ python listfiles.py .
Readme.md
1-python.ipynb
...
Hint: See os.listdir
Elements of a list can be replaced and new elements can appended at the end.
x = ['a', 'b', 'c', 'd']
x[1] = 'bb'
x
x.append('e')
x
Lets write a program to computes squares of a list of numbers.
def squares(numbers):
result = []
for n in numbers:
result.append(n*n)
return result
print(squares([1, 2, 3, 4]))
# sum of squares of numbers below one million
sum(squares(range(1000000)))
Problem: Write a function evens that takes a list of numbers as argument and returns a list containing only the even numbers from it.
>>> evens([1, 2, 3, 4, 5, 6])
[2, 4, 6]
List Comprehensions provide a concise way of transforming one list into another. Quite often a complex task can be modelled in a single line of code.
x = [1, 2, 3, 4]
[a*a for a in x]
[a*a for a in x if a%2 == 0]
[a for a in x if a%2 == 0]
How to compute sum of squares of all even numbers below one million?
sum([x*x for x in range(1000000) if x%2 == 0])
Problem: Write a function list_pyfiles that takes a directory as argument and returns a list of all python files in that directory.
>>> list_pyfiles(".")
["square.py", "hello.py", "args.py"]
Lets look at various iteration patterns commonly used in Python. We've already seen the first two of them.
This is the most commonly used iteration pattern.
x = ['a', 'b', 'c', 'd']
for c in x:
print(c, c.upper())
The range
function can be used to iterate over a sequence of numbers.
for i in range(5):
print(i)
for i in range(1, 5):
print(i)
The zip
function can be used to iterate over two lists at the same time.
names = ["a", "b", "c", "d"]
scores = [10, 20, 30, 40]
for name, score in zip(names, scores):
print(name, score)
zip(names, scores)
list(zip(names, scores))
It is not too hard to implement zip function yourself. Why not give it a try?
Some times we need both the index and the element when iterating over a list. The built-in function enumerate
can be used in that case.
names = ["a", "b", "c", "d"]
for i, name in enumerate(names):
print(i, name)
chapters = ["Getting Started", "Lists", "Working with Files"]
for i, title in enumerate(chapters):
print("chapter", i+1, ":", title)
We've already seen how to index a list.
x = ['a', 'b', 'c', 'd']
x[0]
x[1]
x[2]
x[3]
How to find the last element of a list?
x[len(x)-1]
Python provides a short-hand for this.
x[-1]
The indices -1, -2, -3 etc. index the list from the right side, with index -1 being that last element.
def get_last_word(sentence):
return sentence.split()[-1]
get_last_word("one two three")
Problem: Write a function getext
to get extension of given filename.
>>> getext("a.py")
'py'
>>> getext("a.tar.gz")
'gz'
Assume that the filename will always have an extension.
Python has very elegant way to create a new list by slicing a list.
x = ["a", "b", "c", "d", "e", "f", "g", "h"]
x[0:2] # first two elements of a list
x[:2] # upto second element
x[2:] # from second element onwards
x[2:6]
x[1:6:2] # take every second element starting from index 1 to index 6
x[:] # a copy of the entire list
x[::-1] # reverse the list
Lets try to improve the echo program that we wrote earlier to print all the command-line arguments instead of just the first one.
%%file echo2.py
import sys
args = sys.argv[1:]
print(" ".join(args))
!python echo2.py hello world
Problem: Write a program sum.py
that takes multiple numbers as command-line arguments and prints their sum.
$ python sum.py 1 2 3 4 5
15
names = ["alice", "dave", "bob", "charlie"]
names.sort() # sorts the list in-place
names
names = ["alice", "dave", "bob", "charlie"]
sorted(names) # returns a new sorted list
names
How to sort the names by length?
sorted(names, key=len)
sorted(names, key=len, reverse=True)
Problem: Write a function isorted to sort given list of strings, ignoring the case.
>>> names = ["Alice", "bob", "Dave", "charlie"]
>>> isorted(names)
["Alice", "bob", "charlie", "Dave"]
Lets try another example.
Given a list of student records with name and marks, how to sort the records by the marks?
marks = [
("A", 10),
("B", 65),
("C", 48),
("D", 58)
]
def get_marks(record):
return record[1]
sorted(marks, key=get_marks)
The get_marks
function is written just for using with the sorted function. Wouldn't it be nice if we can write it directly there?
Python has a feature called lambda expressions for doing that.
sorted(marks, key=lambda record: record[1])
In Python, strings are very much like lists.
x = "hello"
len(x)
x[0]
for c in x:
print(c)
max(x)
x[:4]
name = "Python"
message = "Hello, {}!".format(name)
print(message)
"Chapter {}: {}".format(1, "Getting Started")
"Chapter {0}: {1}".format(1, "Getting Started")
"Chapter {index}: {title}".format(index=1, title="Getting Started")
Lets a real example.
def make_link(url):
return '<a href="{url}">{url}</a>'.format(url=url)
make_link("http://www.google.com/")
It is even possible to specify width for parameter.
"{:3} - {}".format(10, "A")
For floating point numbers, we can specify precission as well.
0.1 + 0.2
"{:.2f} +{:.2f} = {:.2f}".format(0.1, 0.2, 0.1+0.2)
There is an old-way of string formatting, that is still supported.
"hello %s" % "Python"
"chapter %d: %s" % (1, "getting started")
%%file three.txt
1
2
3
f = open("three.txt")
The easiest way to read the contents of a file is:
f.read()
open("three.txt").read()
The readlines
methods returns all the lines of the file as a list.
open("three.txt").readlines()
f = open("three.txt")
f.readline()
f.readline()
f.readline()
f.readline()
Empty string indicates end of file.
Problem: Write a program cat.py
that takes a filename as command-line argument and prints all contents of the file.
$ python cat.py three.txt
1
2
3
Lets try to implement the unix command wc
in python. The wc
command computers number of lines, words and characters in a file.
We'll use the following file as input to test our program.
%%file numbers.txt
1 one
2 two
3 three
4 four
5 five
The %%file
doesn't add a new line at the end of the file. Fit it by editing the file and adding a new line at the end.
!wc numbers.txt
%%file wc.py
# Program to compute line count, word count and char count of a file.
import sys
def linecount(f):
return len(open(f).readlines())
def wordcount(f):
return len(open(f).read().split())
def charcount(f):
return len(open(f).read())
def main():
f = sys.argv[1]
print(linecount(f), wordcount(f), charcount(f), f)
main()
!python wc.py numbers.txt
Problem: Write a program head.py that takes a filename as command-line argument and print first 5 lines of it.
$ python head.py one-to-ten.txt
1
2
3
4
5
Problem: Write a program sumfile.py
that takes a filename as command-line argument and prints sum of all numbers in that file. It is assumed that the file has one number in every line.
$ python sumfile.py one-to-ten.txt
55
Problem: Write a program grep.py
that takes a pattern and a filename as command-line arguments and prints all tthe lines in the file containing the given pattern.
$ python grep.py def wc.py
def linecount(f):
def wordcount(f):
def charcount(f):
def main():
File can be open in write mode by specifying "w"
as the second argument to open
.
f = open("a.txt", "w")
f.write("one\n")
f.write("two\n")
f.close()
It is important to close the file after writing. Only when the file is closed, all the content written to the file gets flushed to the disk.
open("a.txt").read()
To add more contents to an existing file, we need to open
the file in append mode.
f = open("a.txt", "a")
f.write("three\n")
f.close()
open("a.txt").read()
with
statement¶The with
statement is handly when writing to files as it automatically closes the file at the end of with block.
with open("b.txt", "w") as f:
f.write("one\n")
f.write("two\n")
# f is automatically closed here
open("b.txt").read()
Problem: Write a file copyfile.py to copy contents of one file into another. The program should accept two filenames as command-line arguments and copy ther first one into the second.
$ python copyfile.py a.txt a2.txt
Warning: Don't call that file copy.py as that interferes with system module copy.
Python, version 3 esp., treats text and binary differently.
type("helloworld")
The encode method encodes a string as bytes using specified encoding.
x = "helloworld".encode('ascii')
x
type(x)
Literal bytes are written like strings, but with a b
prefix.
b'hello'
Lets look at couple of unicode characters to understand how that makes a difference.
name = "అఆఇఈ" # or "\u0c05\u0c06\u0c07\u0c08"
len(name)
name_bytes = name.encode("utf-8")
name_bytes
len(name)
When writing text to a file, we can also specify the encoding as well. If you don't know anything about encoding, just use utf-8
. That is the most sane encoding.
text = "hello \u0c05\u0c06\u0c07\u0c08"
with open("a.txt", "w", encoding='utf-8') as f:
f.write(text)
!wc -c a.txt
len(text)
len(text.encode('utf-8'))
Notice the file size is same as the number of bytes.
Dictionaries are key-value pairs with fast lookup.
d = {'x': 1, 'y': 2}
d['x']
d['y']
d['x'] = 11
d['z'] = 3
print(d)
The keys in the dictionary are not ordeded.
The keys
, values
and items
methods provide the keys, values and key-value pairs of the dictionary respectively.
d.keys()
d.values()
d.items()
for k in d.keys():
print(k)
Directly iterating over the dictionary also iterates over the keys.
for k in d:
print(k)
for v in d.values():
print(v)
for k, v in d.items():
print(k, v)
The in
operator works for dictionaries as well.
'x' in d
'xx' in d
Other commonly used methods on dictionaries are get
and setdefault
.
d.get('x', 0)
d.get('xx', 0)
The get
method takes a key and a default-value as argument and returns the value for that key if it exists, default-value otherwise. The dictionary will not be modified at all.
The setdefault
method works like get
but also updates the dictionary when the key is not available in the dictionary.
d.setdefault('x', 0)
d.setdefault('xx', 0)
d
%%file words.txt
five
five four
five four three
five four three two
five four three two one
%%file wordfreq.py
"""Program to compute frequency of all words in a file.
USAGE: python wordfreq.py file.txt
"""
import sys
def read_words(filename):
return open(filename).read().split()
def wordfreq(words):
freq = {}
for w in words:
freq[w] = freq.get(w, 0) + 1
return freq
def print_freq(freq):
# TODO: improve this
print(freq)
def main():
filename = sys.argv[1]
words = read_words(filename)
freq = wordfreq(words)
print_freq(freq)
if __name__ == "__main__":
main()
!python wordfreq.py words.txt
Problem: Improve the above program to print one word per line, like the following:
four 4
one 1
five 5
three 3
two 2
Problem: Improve the above program further to print the words sorted by frequency, with most frequent word on the top.
five 5
four 4
three 3
two 2
one 1
Problem: Write a program extcount.py
to count the number of files per extension in the given directory. The program should take path to a directory as command-line argument and print count and extension for each available extension.
$ python extcount.py foo
14 py
2 txt
1 csv
%%file mymodule.py
print("begin mymodule")
x = 1
def add(a, b):
return a+b
print(add(3, 4))
print("end mymodule")
!python3 mymodule.py
%%file a.py
import mymodule
print(mymodule.x)
print(mymodule.add(10, 20))
!python3 a.py
The __name__
magic variable
%%file mymodule2.py
x = 1
def add(a, b):
return a+b
print(add(3, 4))
print(__name__)
!python mymodule2.py
When the file is run as a script, the __name__
is set to "__main__"
.
!python -c "import mymodule2"
When the file is imported as a module, the __name__
is set to the module name.
%%file mymodule3.py
x = 1
def add(a, b):
return a+b
if __name__ == "__main__":
# If this file is executed as script, then run the following code
# Ignore this when the file is imported as a module
print(add(3, 4))
!python mymodule3.py
!python -c "import mymodule3"
help("mymodule3")
%%file mymodule4.py
"""This is module 4.
Long description of the module.
"""
x = 1
def add(a, b):
"""Adds two numbers.
Example:
>>> add(2, 3)
5
"""
return a+b
if __name__ == "__main__":
print(add(3, 4))
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
p = Point()
print(p.x, p.y)
p.z = 3 # new attributes can be added to an object, any time.
p.z
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def getx(self):
return self.x
def display(self):
print(self.x, self.y)
def add(self, p):
"""Adds this point to another point and returns the new point.
"""
x = self.x + p.x
y = self.y + p.y
return Point(x, y)
p1 = Point()
p1.x = 1
p1.y = 2
print(p1.getx())
p2 = Point()
p2.x = 3
p2.y = 4
p1.display()
p2.display()
p3 = p1.add(p2)
p3.display()
Point
Point.getx
Point.getx(p1)
Point.getx(p2)
The method calling syntax is shorthand for the above.
p1.getx() # Point.getx(p1)
"mathematics".count("mat") # str.count("mathematics", "mat")
str.count("mathematics", "mat")
Problem: Write a method double
in the above point class. It should return a new point with both x and y doubled.
>>> p = Point(1, 2)
>>> q = p.double()
>>> q.display()
2 4