# Numbers¶

Here we focus on the 'data type' of numbers, of which there are two main ones we are concerning ourselves with: int and float

## What is a Data Type?¶

A data type is (binary) data that is structured and has rules of its access and use applied to it by the programming language you are using (Python). A bunch of binary data in memory might be interpreted by Python as a string for characters, or a decimal number, or an integer. It might also represent the data for a function definition! Only when you follow Python's rules for data types can you give this blob of binary bits structured existence and use.

For example, the following values are stored in the variables a and b look the same to a human, but are fundamental different to Python.

• a refers to a numerical literal 22. It is interperted as a valid number that can be used for arithmetic expressions.
• b is a string of characters '2' and '2'. Mathematically they are meaningless.

In the cell below, I attempt to add a + b and it fails. This is expected! The string characters is not a mathmetically capable data type!

In [1]:
a = 22

In [2]:
b = "22"

In [3]:
a + b

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-bd58363a63fc> in <module>()
----> 1 a + b

TypeError: unsupported operand type(s) for +: 'int' and 'str'

## int and float values¶

Integer (int) and decimal (float) numbers are compatible data types in Python. While they are not at all stored the same underneath, they are mathematically compatible. What happens if you perform math using an int and a float (mixed data types)? In this case, it usually defaults to the data type that gives the value the most information (which is a float.) A fraction part of a number is more informative data than just a whole number.

In the example below, we add a to the literal float of 0.6767. The resulting value from this addition is a new float value of 22.6767. This result cannot be stored as an integer, obviously.

In [4]:
a + 0.6767

Out[4]:
22.6767

### type-ing¶

If you want to know exactly what data type a value is, we can use the type() function. Give it anything (an identifier, a literal) and type() explains to you what data type that value is.

In [5]:
type(a)

Out[5]:
int
In [6]:
type(b)

Out[6]:
str
In [7]:
type(0.6767)

Out[7]:
float
In [8]:
print(type(a))

<class 'int'>


## Arithmetic Operations¶

Below are examples of the basic arithmetic operations. Note the data type returned from the evaluations.

In [11]:
val = 3.0 + 4.0
print(val)

7.0

In [12]:
3 + 4

Out[12]:
7
In [13]:
3 * 4

Out[13]:
12
In [14]:
10.0/3.0

Out[14]:
3.3333333333333335
In [15]:
10//3

Out[15]:
3

### Converting between numeric types¶

The evaluation of result, below, from the use of the "integer division" operator, /, is a whole number: 3.0 despite being whole it's value represented as a float data type instead of an integer data type. Some functions, like range() only operate on integers. What if I wanted to use result in my call to range()? Well, we can call the function int(). It's a special function that is representative of the int data type. This function makes new integers! We use it to our advantage here by making a new int from the float value of 3.0. In the end, result now stores 3, not 3.0.

In [57]:
result = 10.0//3.0

In [58]:
result = int(result)

In [59]:
result

Out[59]:
3
In [60]:
for n in range(result):
print(n)

0
1
2


## Comparison Operators¶

You have a full breadth of numerical comparison operators (equality, inequality, greater than, less than, etc), logical operators (and, or, not) that can be nicely intermixed with your regular arithmetic operators to make some complex expressions.

In [26]:
c = 75667

In [27]:
a > c

Out[27]:
False
In [28]:
a == c

Out[28]:
False
In [29]:
a != c

Out[29]:
True
In [30]:
a >= c

Out[30]:
False
In [31]:
c >= a

Out[31]:
True
In [35]:
c <= (a ** 10)

Out[35]:
True
In [36]:
(a != 6) and (c > 3)

Out[36]:
True
In [37]:
(a != 6)

Out[37]:
True
In [38]:
(c > 3)

Out[38]:
True
In [41]:
(17 / 4) * 4 == 17

Out[41]:
True
In [42]:
17.0 == 17

Out[42]:
True

# Importing Modules¶

## First, 'objects' and What They Are¶

Data in other languages are 'dumb', 'passive'. They exist as just a bundle of bits 'typed' as the data they are supposed to be (int, float, like in the C language); however any actions on the data are performed externally on that data and it might violate the rules of the data type (C can do crazy things).

In Python (and other languages), data types are extended in concept and are based on "objects" (hence, "object-oriented" languages). The data is now 'smarter', more active. Everything used in Python is an object of one type or another. Every literal you type is evaluted into its proper data type (str, int, float, list, etc). Data is now not just the value(s) it represents but it also comes packaged with actions, the functions that act on that data.

In short, objects are data types that are packaged values and functions that operate on those values, in one nicely encapsulated entity. Every int, for example, has the value and functions within that int that operate on that one int value. Every int created has their own set of these functions meant for int manipulation. The same goes for any data type. This enforces the correct actions that can be done the data.

For example, a lesser known but still valid operation to perform on an int is to check its 'bit length', or the number of binary bits used to represent a base-10 integer.

We set a to the int value of 3. This is base-10 that we all know and love. To ask that int how many binary bits (one's and zero's) are used to represent this number, we call the bit_length() function on the int object. It is a valid action only for ints! It returns 2, which means that the binary string ($11$, or $2^1 + 2^0$, represents base-10 integer 3). OK, that's so important as the fact we used a function bundled with what we thought was just plain old integer data!

In [69]:
a = 3
a.bit_length()

Out[69]:
2

Even mundane operations like a + 2 do not externally act on the int represented by a but actually calls a special function within the int object called __add__ (pronounced "dunder add"). See the two versions, below. No matter what, you are "asking" an object to perform work. If it doesn't support that action it will fail!

In [71]:
a + 2

Out[71]:
5
In [72]:
a.__add__(2)

Out[72]:
5

## Namespaces¶

How do objects store all these bits of information about itself? The int object needs to store its value and the functions it supports that operate on that value. These attributes are stored in a namespace. Every object in Python has a namespace and it's a 'bucket' that holds all the identifier attributes stored within an object.

The following is the namespace of our int object, a. We use the function dir():

In [73]:
print(dir(a))

['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']


All the "dunder names", or attributes with "" can be ignored for now. We will touch on them near the end of the course and only a select few. The functions you might use on an int would be near the end of that list.

Each data type has its own namespace with its own attributes valid of that data type

You use the . (dot) operator to access function or value attributes inside an object's namespace.

## Modules¶

Modules are an excellent example of objects. You can import another Python code file into yours to use the code within it for something useful. The math module is one such module that comes included with Python upon installation. We import math to "bring in" the math module for use.

In [43]:
import math


The identifier math is created and is now a variable that refers to a module object. The namespace of this of this module shows you all the items it has made available for use. The math module comes with many math operations (pow, sqrt, log) and even some float constants like (pi, or e).

As mentioned above, the . (dot) operator is used to access these attributes of any object, the module object included.

In [44]:
math

Out[44]:
<module 'math' from '/usr/local/Cellar/python3/3.6.4_2/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload/math.cpython-36m-darwin.so'>
In [45]:
dir(math)

Out[45]:
['__doc__',
'__file__',
'__name__',
'__package__',
'__spec__',
'acos',
'acosh',
'asin',
'asinh',
'atan',
'atan2',
'atanh',
'ceil',
'copysign',
'cos',
'cosh',
'degrees',
'e',
'erf',
'erfc',
'exp',
'expm1',
'fabs',
'factorial',
'floor',
'fmod',
'frexp',
'fsum',
'gamma',
'gcd',
'hypot',
'inf',
'isclose',
'isfinite',
'isinf',
'isnan',
'ldexp',
'lgamma',
'log',
'log10',
'log1p',
'log2',
'modf',
'nan',
'pi',
'pow',
'sin',
'sinh',
'sqrt',
'tan',
'tanh',
'tau',
'trunc']

We use the dot operator to access sqrt:

In [74]:
math.sqrt

Out[74]:
<function math.sqrt>

Opps, it's a function and I didn't properly run it! Instead, I asked Python to 'describe' that identifier and it explained to me that it's a function. OK, let's properly run it:

In [75]:
math.sqrt(100)

Out[75]:
10.0

Let access the constant for $\pi$!

In [78]:
math.pi + 10

Out[78]:
13.141592653589793

If you attempt to access an attribute that doesn't exist in an object's namespace, its as if you access a variable name that wasn't defined yet, but referred to as an AttributeError in this context:

In [77]:
math.foobar

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-77-64d315a5c94c> in <module>()
----> 1 math.foobar

AttributeError: module 'math' has no attribute 'foobar'

### Selective import¶

Namespaces should be kept clean. Import the module you need, wholesale or selectively import what you need into your "main" or top-level namespace. The following imports just sqrt() into my main namespace. I don't need to access it using math.sqrt anymore!

In [79]:
from math import sqrt

In [80]:
sqrt(100)

Out[80]:
10.0

What's my "main namespace" and what does it look like? Namespaces are hierarchial trees of objects. You start with a main namespace in which other objects with their own namespaces are stored. You can call dir() without any parameters to see what this main namespace looks like:

In [53]:
dir()

Out[53]:
['In',
'Out',
'_',
'_12',
'_13',
'_14',
'_15',
'_16',
'_17',
'_20',
'_23',
'_24',
'_27',
'_28',
'_29',
'_30',
'_31',
'_32',
'_33',
'_34',
'_35',
'_36',
'_37',
'_38',
'_39',
'_4',
'_40',
'_41',
'_42',
'_44',
'_45',
'_46',
'_47',
'_48',
'_5',
'_51',
'_52',
'_6',
'_7',
'_9',
'__',
'___',
'__builtin__',
'__builtins__',
'__doc__',
'__name__',
'__package__',
'__spec__',
'_dh',
'_i',
'_i1',
'_i10',
'_i11',
'_i12',
'_i13',
'_i14',
'_i15',
'_i16',
'_i17',
'_i18',
'_i19',
'_i2',
'_i20',
'_i21',
'_i22',
'_i23',
'_i24',
'_i25',
'_i26',
'_i27',
'_i28',
'_i29',
'_i3',
'_i30',
'_i31',
'_i32',
'_i33',
'_i34',
'_i35',
'_i36',
'_i37',
'_i38',
'_i39',
'_i4',
'_i40',
'_i41',
'_i42',
'_i43',
'_i44',
'_i45',
'_i46',
'_i47',
'_i48',
'_i49',
'_i5',
'_i50',
'_i51',
'_i52',
'_i53',
'_i6',
'_i7',
'_i8',
'_i9',
'_ih',
'_ii',
'_iii',
'_oh',
'a',
'b',
'c',
'exit',
'get_ipython',
'math',
'quit',
'result',
'sqrt',
'val']