Jared Foy Ye know all things

Introduction to Python More Notes

Published December 10th, 2018 4:52 pm

Heres some more notes in regard to Python learning.

Creating and Calling Functions

Functions are comprised of all the other control structures we have reviewed previously. However, functions make it easier to 'encapsulate' suites of code which can be given parameters by the arguments that are passed into the function.

def functionName(args):
suite

Arguments are optional in functions and multiple arguments must be comma seperated. Functions have a return value. Functions have a default return value of the aforementioned None placeholder. If one would like to have the return value returned, the suite must include the syntax 'return value'. Multiple values can be returned in the form of a tuple. The return value can also be ignored by the 'caller' in which case the value is simply disposed of. The caller is the identifier of the function 'def caller(argument):'

The use of the 'def' statement initializes a function object and bonds it with the following object reference that is stated as the function variable identifier.

Functions are objects so they can be stored in collection data types and they can be used as arguments in other functions.

def get_int(msg): while True: try: i = int(input(msg)) return i except ValueError as err: print(err)

The function above has one argument "msg". The user is asked to enter a string because of the input() function. It is passed to the int() function which attempts to convert the input function argument into an integer. If this is not possible, in that the int() function was unable to convert the said input, the exception ValueError will save itself to the err variable. Inside the suite for the exception is a print() function which will output the string saved to the err variable. If the value entered satisfies the try block it is returned to the caller 'get_int()'. If it is an unsatisfiable return value the loop will then start from the top and try again.

In the statement "age = get_int('enter your age: ')", the function get_int() is being called and saved to the variable 'age'. I'm not sure I quite understand what is going on here but aparantly because the get_int function wasn't given a default value, we have to supply it with the string 'enter your age: '. It is said that Python supports 'a very sophisticated and flexible syntax for function parameters'. I'm not exactly sure how this is born out, but I'll take it on its face. Before you go and start making all your favorite new functions, beware that Python has a robust collection of built-in functions. What you are trying to do has probably already been done.

Python Modules

A module is simply a '.py' file that contains python interpretable syntax. You can import the functionality of any said python module. For example 'import sys' would import the functionality of the sys.py module. When envoking this just leave off the .py extension of the file.

Once you've got the module imported you can access all of the functions of the module in your code. I'm assuming that this is all done in your own Python code, subsequently using functions that the imported module is capable of. It seems to me that the interpreter is what is serving all of these functions to be usable in your own code. But I'm not sure.

In this example 'print(sys.argv)' we are invoking the argv variable provided by the sys.py file. The argv variable is apparently a list data type that has the first item in the list to be the name under which the program was invoked. The second and subsequent items are the program's command-line arguments. The following:

import sys
print(sys.argv)
...constitutes the entire 'echoargs.py' program. If the program is invoked with the command line echoargs.py -v it will print ['echoargs.py', '-v'] on the console. Which is a list comprised of the said items.

After importing a module using 'import moduleName' we can then use its functions by stating 'moduleName.functionName(args)'. This uses the 'dot' also known as the 'access atribute' operator. There are a whole bunch of modules in the standard python library that are available to be used. These modules all use lowercase names so you may want to differentiate your functions by using a different case formation such as 'camelCase' or 'Title-Case.'

As an example lets take a look at the random module.
import random
x = random.randint(1, 6)
y = random.choice(['apple','banana','cherry','durian'])

Because we imported 'random.py' we can now use the functions which are available in the file. The second line of code chooses a random integer (either 1 or 6) because those were the only arguments passed to it. The third line of code randomly selects an item from the list. I'm assuming these are providing psuedo-random results. Usually, all the import statements are placed at the top of the code you are writing, right after the 'shebang' line (which we'll talk more about later.) For consistency's sake we should import standard modules first, then third party modules, and then your own modules.

An example:

I suppose the use of sys.argv is to allow us to read in commands from the console. These are commands that the user of the program is giving to the program. I think this is different from that of the input() function because that is just reading in a string which is comprised of the key inputs of the user. sys.argv seems to be actually giving commands to the program itself.

For instance in this exercise we will read out big digits that are made up of asterisks that look like the digit being passed into the program. First we have to make these ASCII art thingies out of asterisks. Each line is seven characters wide.

Something to note here is that this list is spanning multiples lines, which is ok in Python. This can be done with parenthesized expressions, lists, sets, or dictionary literals (not sure exactly what those are), function call arguments, or a multiline statement in which every character that comes at the end of the line is escaped with a '\'.

For the sake of using less space we can also put these in a list without line breaks. For instance:

Two = [" *** ", "* *", "* * ", " * ", " * ", "* ", "*****"]

Here is the rest of the program:

Digits = [Zero,Two,Nine] try: digits = sys.argv[1] # selecting the function argv from the sys module row = 0 # defining row with a value of 0 while row < 7: # starting a while loop that will loop until row is no shorter than 7 line = '' # declaring line variable as an empty string column = 0 # declaring column as an int of 0 while column < len(digits): # creating a while loop which will run utnil column is no shorter than the length of digits number = int(digits[column]) digit = Digits[number] line += digit[row] + ' ' column += 1 print(line) row += 1 except IndexError: print('Usage: bigdigits.py ') except ValueError as err: print(err, 'in', digits)

FYI, I wasn't able to get this program to work, probably because I don't understand how he sys.argv function is working, and how to input from the console correctly. Such is life, moving on.

Creating test data with a Python module

import random def get_int(msg, minimum, default) # creating a function while True: #while loop wich is testing for the Boolean True try: #try block which tests for exceptions line = input(msg) #sets line to input from user if not line and default is not None: #an if statement which uses the identity operator 'not', the logical operator 'and', as well as the 'is' identity operator return default i = int(line) #testing the user input for ability to turn into integer if i < minimum: #If user input is less than the minimum print('must be greater than or equal to', minimum) else: return i except ValueError as err: print(err) The funciton we just made needs three arguments: a message string, a minimum value, and a default value.

Chapter 2: Identifiers and Keywords
Integral Types
Floating-Point Types
Strings
Here we will go farther in depth about all the concepts we covered in chp 1. Firstly we start with the rules that Python employs to govern the names given to object references. Also included is a list of keywords. Then data types are looked over. We can use data types from libraries. The only difference being when these library data types are used, we need to use the import and dot access attribute to invoke them.

He we go: Identifiers and Keywords

When creating a data type we can do one of two things: Assign it to a variable or insert it into a collection. When we use an object reference, we can call the name we give it an 'identifier' or simply a 'name'. A valid Python identifier is a nonempty sequence of characters of any length that consists of a 'start character' and zero or more 'continuation characters'. There are certain rules and convention which these must obey.

Rule 1:
The start character can be anything that Unicode considers to be a letter, these include the smaller subset of ASCII letters. We may also use '_' the underscore and letters from other non-english languages. (that is a pretty hegemonic denonym!) Continuation characters can be almost anything except whitespace. We can use digits and also this thing called the 'Catalan character' which is 'ยท' (looks like a concatenator from php) Remember, identifiers are case sensitive! A precise defintion of all acceptable start and continuation characters is available in documentation.

Rule 2:
No identifiers that you create can share the name of one of Python's built-in keywords. These include:
and,continue,except,global,lambda,pass,while,as,def,False,if,None,raise,with,assert,del,finally,import,nonlocal,return,yield,break,elif,for,in,not,True,class,else,from,is,or,try ...to name a few.
Also, don't use NotImplemented or Elipsis, neither int,float,list,str,tuple

We can find out if we are in danger of using one of these taken identifiers by using the built-in function dir(). If we don't add arguments then it returns a list of Python's built-in attributes. The __builtins__ attribute is a module that holds all of Python's built-in attributes (sounds straight forward enough.) If we call dir(__builtins__) we will be given a list containing these functions. If you just printed that list like I did then you will see 130 strings. The ones that start with a capital letter are Python's exceptions. The rest are functions and datatypes.

Also, don't use double underscores to start and end your identifier. Python uses these to define special methods and variables that use these names. Special methods are used this way, and we can reimplement them by making our own versions. Just don't make new ones for yourself using this convention, you'll just confuse yourself, and everyone else. Sometimes you can use a double underscore leading name (without the similar tail underscores), and sometimes its good to use a single underscore. Those are discussed later.

A single underscore alone can be used as an identifier. Inside an interactive interpreter or the Shell, the underscore holds the results of the last expression that was evaluated. Usually, a _ does not exist unless it is used in our code. As a matter of custom, some programmers will use the underscore in a 'for ... in' loop when they simply don't give a heck about the items being looped over. Be warned, that sometimes people will use the _ as a function call for translating text.

Python is super legit in the fact that you can utilize characters from basically any language in the world. When you use an invalid identifier you will raise a SyntaxError. Note, digits can only be used as continuation characters, not starting characters. Also note, you cant use a ' as denoting an abreviation of a word in a identifier.

Intergral types

There are two built in integral types: 'int' and 'bool'. The first is integer, and the second is boolean. They are both immutable, but augmented assignment operators make this almost of no practical import. When used in Boolean expressions both '0' and 'False' are evaluated to be False. Any other integer and True is always evaluated as True. When used in a numerical expression, 'True' evaluates to 1 and 'False' evaluates as 0. This allows for strange expressions to be written in Python. For example, we can augment i using
>>>i += True #this evaluates to i += 1
The size of an integer is limited only by the machine's memory. Accordingly, we may use integers which are hudreds of digits long. It may be slower however, because apparently there is something about native machine processing that matters (but I don't know what this is.)
I'm going to write these down, even though most are straight forward:

x + y #addition
x - y #subtraction
x * y #multiplication
x / y #division, returns a floating point decimal
x // y #division, returns a truncated integer value
x % y #produces the modulus (remainder) of dividing x by y
x ** y #raises x to the power of y; see pow() function

-x #negates x; changes x's sign if nonzero, no effect if x is 0
+x #does absolutely nothing, sometimes used to clarify code
abs(x) #returns the absolute value of x
divmod(x,y) #returns the quotient and remainder of dividing x by y as a tuple of two 'int's
pow(x,y) #raises x to the power of y; see ** operator
pow(x,y,z) #a faster way than doing (x**y) % z
round(x,n) #rounds x to the number of places given by n

The following are integer conversion functions:

bin(i) This returns the binary representation of int i as a string. For instance bin(1980) would spit out '0b1110111100'. (I'm not sure how binary actually works, return to this later)
hex(i) This returns the hexadecimal value of said integer as a string.
int(x) Converts x to an integer. It will truncate a float. May raise TypeError or ValueError


int(s, base) Converts the str value of s to an integer. Raises ValueError upon failure. 'base' is optional but should be between 2 and 36. (I have no idea what this even means)
oct(i) Returns the octal representation of i as a string. (Again, no idea.)
Integer literals are written using base 10 (decimal) by default. But this can be changed to suit need to preference.
Note: binary numbers are written with a leading '0b', octals with '0o', and hexadecimals with '0x'. Uppercase letters may also be used.
A note on the round() function: Using the second value as a positive integer gives likely results, however when you invert the value of the rounding integer and can get interesting and useful results. Using a negative rounding integer will push the rounding functions into the other side of the decimal, rounding to the nearest value (whether up or down) depending on the number of places the negative value corresponds to. This isn't well written in the book, btw.

You can use all the binary numeric operators such as '+,-,/,//,%,**' as augmented assignment operators such as '%='.

One may create an object by stating a identifier and a literal, such as 'x = 12'. Some objects such as those like decimal.Decimal can be created only by using the data type since they don't have a literal representation. There are three possible use cases for this:

Firstly, when a data type is called with no arguments. This may be seen when we create 'x = int()'. This creates an integer with the value of 0. All of the built-in data types in Python can be created with no argument.

Secondly, we can create a literal with one argument. If an argument of the same type is given then a new object will be created that is a 'shallow copy' of the original object. If an argument of a different type is given then Python attempts a conversion. If the argument falls within the conversion ability of the function then that value is used. However, if this is not possible it raises a ValueError exception. A TypeError is given if the function is unable to deal with such a conversion outright. The built-in str and float types provide integer conversions. Later, we can see that custom data types can also be given this ability.

Thirdly, if we use multiple arguments in a function. This can't be done with all types and the rules vary for its use with each one. We can see the int() function taking two arguments: >>>int('A4', 16) This creates an integer with the value of 164. I'm supposing this is because 'A4' is a hex code (maybe) 16 is the base.

Bitwise operators:

Binary bitwise operators have augmented assignment versions. The int.bit_length() method returns the number of bits required to represent the integer it is called on. For example int.bit_length(2145) returns 12. We can also call this in the OOP paradigm by doing (1245).bit_length()

Let me just state that I'm not exactly sure what a 'bitwise' operator is, but it looks similar to that of some PC that I have learned.

| -This is the OR operator ^ -This is the exclusive OR operator & -This is the AND operator << -This will shift bits 'i << j' shifts i left by j bits >> -The inverse of the aforementioned operator ~i -This inverts i's bits

Booleans:

As we have mentioned before, there are two boolean values. True and False. In similar fashion to that of all other Python data types, one may call this as a function. This is done by using bool(). When you leave out arguments all together it returns False. When you give it an argument it returns a copy of the argument. It will also attempt to convert the argument into a bool.

For instance:
>>>t = True
>>>f = False
>>>t and f
False >>>t and True True

As previously mentioned, Python provides three logical operators: and, or, not. I still don't get this next part, but 'and', as well as 'or' are called short-circuit logic operators and will return the 'operand' that determined the result of the boolean. This is something inherent to how logic works, but I am still trying to figure out how it is deciding. The 'not' operator will return 'True' or 'False'

Floating-Point Types:

Python provides three types of floating-point values: The built-in 'float' The 'complex' type The 'decimal.Decimal' type

These are immutable types. The 'float' holds 'double precision' floating point numbers with a range depending on the compiler that Python was built with. This means that they have limited precision and can't be reliably compared for equality. (I'm not exactly sure what this means). Numbers that are of the type 'float' are written with a decimal point, or using exponential notation.
Examples: 0.0, 3., -2.4, -2e9, 8.9e-4

Computers natively represent floating-point numbers using base two. Because of this some numbers can be represented exactly such as '0.5', but others can only be represented aproximately such as '0.1' or '0.2'. (Make not to learn what the heck this means) Also, the representation is created using a fixed number of bits, consequently there is a limit to the number of digits that can be held (interesting) This creates issues of inexactness with are not specific to Python, but to all programming languages. Using Python 3 you probably won't be able to reproduce the strangeness of these numbers, however. This is because it uses an algorithm by David Gay. This algorithm outputs the fewest digits without losing accuracy. However, even though the output is changed in Python, it is still saving an aproximation, albeit a good one.

If one needs supreme accuracy, perhaps try using the decimal.Decimal method, however it will be slower than the floating point type. We may also perform mixed mode arithmetic by using floats and integers interchangably. However, this is not possible with the decimal.Decimal types which can only be mixed with integers.

Floating point numbers can make use of all of the floating point operators and functions as previously discussed. You can call the float data type as a function. If you give it no argument it will return '0.0'. If you give it an argument that is already a float it will simply return a copy of it. With things that aren't floats, it will attempt to convert it to one. Be careful, you can create NaN 'not a number' or infinity when doing calculations involving floats. This can be inconsistent depending on the math library used.
The following is a simple program which can compare floats for equality to the limit of the machines accuracy:

def equal_float(a,b): return abs(a - b) <= sys.float_info.epsilon

Of course, in order to use this one must import sys first. We use the sys.float_info object which has a bunch of attributes. '.epsilon' is effectively the smallest difference that the machine can distinguish between to floating-point numbers. Traditionally this minute value is called Epsilon. Usually, Python will provide reliable accuracy for up to 17 significant digits. sys.float_info will print to the console a bunch of stats about the ability of the local machine to process floating-point integers. We can convert floating-point integers to integers by using int(). Doing this will just hack off the decimal value. We can also round() which will take in to account the decimal value. We can also use the math library and do math.floor() or math.ceil() which will round down or up, respectively. If a float has a fractional value of 0 it will give us a true bool value when using float.is_integer(). If it is a nonzero value it will return false. A float's fractional representation can be got using the float.as_integer_ratio() method (which is pretty cool, btw). We can also do all kinds of other sorts of type changing using other methods.
There are a whole bunch of possibilities with the math module. Note that it is very dependent on the underlying math library that Python was compiled against. This can make certain things behave differently.

Complex numbers:

The complex type holds a pair of floats. One float represents the real part and the other represents the imaginary part of a complex number. Literal complex numbers are written with the real and imaginary parts joined by a + or - sign. The imaginary part is followed by a j.
Let's do some examples:

z = -89.5+2.125j z.real, z.imag (-89.5, 2.125) # This is a tuple returned by the intepreter.

This seems like a really interesting data type that I would like to learn more about. Some operations can't be performed on it, such as //, %, divmod(), as well as the pow() when three arguments would otherwise be used.
An interesting note: Usually mathematicians use i to represent an imaginary number but Python uses the tradition of engineers in its use of j.
The complex type has a method called conjugate() which changes the sign of the imaginary number portion.
It is important to note that the math module does not work with complex numbers. It was designed this way to keep people from getting complex numbers without knowing it. If you really care to deal with complex numbers you can get the cmath module which provides methods for complex numbers. There are also some nifty complex number specific functions.

Decimal Numbers:

Most of the time you don't need the accuracy that the decimal object provides. The float module should be more than sufficient for most operations. However, in the case that you need it, you can certainly use the decimal object. The decimal module provides immutable Decimal numbers that are as accurate as we want. You may not notice the latency that is involved in Decimal calculations.
In order to create a decimal object we need to import it, of course.

import decimal a = decimal.Decimal(9876) b = decimal.Decimal('54321.012345678987654321') print(a + b)

As one can clearly see, the decimal.Decimal method is used to create a decimal number type. We can pass arguments that are either an integer or a string, but not floats because they are held inexactly whereas decimals are represented exactly. If we are inputing a string it can also be in exponential notation. Because these things are exact we can use them to compare for exact sameness when neccessary. If we want we can convert a float to a decimal by using the decimal.Decimal.from_float() method. The product is the number which gets closest to the aproximation that the float represents. Math and cmath methods are not appopriate for the decimal data type. But some of the same functionality is provided in the decimal object. The author says something about 'syntactic sugar', that sounds yummy.

Numbers of type decimal.Decimal work within the scope of a 'context'. This context is a 'collection of settings that affect how decimal.Decimals behave'. This context specifies the precision that should be used (default being 28 decimal places) the rounding technique and other things.

One thing to note is that when print() is called on something it may format the thing produced for the consol differently than it would otherwise. Using print() prints the string form of the answer. If we don't use print on a decimal object we get what is known as the 'representational form'. This goes to show that all Python objects have two output forms. The print() function gives us the human readable string form. Representational form is formatted to be read by the Python interpreter, but many times we will also be able make out the representational form with our human eyes. More info on the decimal module can be found in the docs.

Strings:

As noted before the str data type is immutable and holds a sequence of Unicode characters. We can use it as a function as in str(). Using it in such a way will create string object literals. If we don't give it an argument it will return an empty string. If we give it a nonstring argument (like an integer) it will convert it into a string. If we give it a string it will return a copy of the string. We can use it also to convert things that are not strings.

This is interesting, a str() function can include two additional arguments beside the thing to be string'd. The first being the encoding to use and the other being how to handle encoding errors. This is also legit: Python allows for 'tripple quoted strings' which give us the ability to use single and double quotes inside a string with impunity. We can also escape with a \ in order to span lines. Using strings we can also use escape keys to format specific things. We can use single quotes without escaping if we are inside a string which is begun using double quotes. If we use a triple quoted string then we don't need to escape newlines. If we use a standard quote format, we need to use '\n'. But what do you do when you want to use a literal \ in your string? Use 'raw strings'. Raw strings are quoted or triple quoted strings in which the first quote is preceeded by the letter r. When this is used all characters are taken to be literal therefore it is not necessary to escape. If we want to make a string that takes up more than one line, we can do this the ugly way by adding a + \ between string segments. Or we can simply put () around our normally quoted string. Because things in parentheses have their newlines ignored by the interpreter, it makes for nicer formatting. In fact, it is a poor practice to use an escaped newline to do this, don't do it.

.py files default to UTF-8 Unicode encoding so we can use all sorts of nifty characters. We can also use escaped codes as well to do the same thing. If we want to know the 'code point', the Unicode number that every character is assigned, we can use the ord() function. We can also convert any integer which is representative of the code point by using the built-in chr() function.