PE2 : Module 2 & 3
Spoiled Multi-Inheritance Code Ex.
here we've swapped both superclass names in the Bottom class def : class Top: def m_top(self): print("top") class Middle(Top): def m_middle(self): print("middle") class Bottom(Top, Middle): def m_bottom(self): print("bottom") object = Bottom() object.m_bottom() object.m_middle() object.m_top() ---> output ---> TypeError: Cannot create a consistent method resolution order (MRO) for bases Top, Middle - new order we try to force (Top, Middle) is incompatible w/ the inheritance path ^
Palindrome Lab
text = input("Enter text: ") # remove all spaces... text = text.replace(' ','') # ... and check if the word is equal to reversed itself if len(text) > 1 and text.upper() == text[::-1].upper(): print("It's a palindrome") else: print("It's not a palindrome")
Anagram Lab
word1 = input("please input a word") word2 = input("please input another word") word1 = word1.replace(" ", "").lower() word2 = word2.replace(" ", "").lower() if sorted(word1) == sorted(word2): print("Anagrams") else: print("Not anagrams")
Exceptions are classes Ex.
try: i = int("Hello!") except Exception as e: print(e) print(e.__str__()) ---> output ---> invalid literal for int() with base 10: 'Hello!' invalid literal for int() with base 10: 'Hello!' > ex. presents a very simple way of utilizing the received obj. : printing it out (the output is produced by the obj.'s __str__() method) w/ it containing a brief msg describing the reason > same msg will be printed if there's no fitting "except" block in the code, & Python is forced to handle it alone
try / except ex.2
try: print("1") **x = 1 / # print("2") except: print("Oh dear, something went wrong...") print("3") -----> output -----> **when the # in the code is 1 1 \ 2 \ 3 ---------------------------------- **when the # in the code is 0 1 \Oh dear, something went wrong...\3
Exception Names : IndexError
"IndexError" in this msg is the exception name : Ex. Traceback (most recent call last): File "lst.py", line 2, in x = list[0] IndexError: list index out of range
Exception Names : ValueError
"ValueError" in this msg is the exception name : Ex. ValueError: math domain error
Exception Names : ZeroDivisionError
"ZeroDivisionError" in this msg is the exception name : Ex. Traceback (most recent call last): File "div.py", line 2, in value /= 0 ZeroDivisionError: division by zero
Methods : Self Parameter Ex. 2
"self" is also used to invoke other obj./class methods from inside the class : class Classy: def other(self): print("other") def method(self): print("method") self.other() obj = Classy() obj.method() ---> output ---> method other
Methods : Self Parameter Ex. 1
"self" is used to obtain access to obj.'s instance & class var.'s : class Classy: varia = 2 def method(self): print(self.varia, self.var) obj = Classy() obj.var = 3 obj.method() ---> output ---> 2 \ 3
LED
# ### ### # # ### ### ### ### ### ### # # # # # # # # # # # # # # # ### ### ### ### ### # ### ### # # # # # # # # # # # # # # # # ### ### # ### ### # ### ### ### def zeros(w = 7, h = 7): m = [] m.append(w//2*"0 " + "0") for i in range(h-2): m.append("0"+" "*(w-2)+"0") m.append(w//2*"0 " + "0") # for s in m: # print(s) return m zero = ''' ####### # # # # # # ####### ''' one = ''' # # # # #''' two = '''### # ### # ###''' three = '''### # ### # ###''' four = '''# # # # ### # #''' five = '''### # ### # ###''' six = '''### # ### # # ###''' seven ='''### # # # ''' eight = '''### # # ### # # ###''' nine = '''### # # ### # ###''' da_digit = input("Enter a positive number...") print(da_digit) for ix in range(len(da_digit)): if da_digit[ix] == "0" : m = zeros() for s in m: print(s) elif da_digit[ix] == "1" : print(one) print() elif da_digit[ix] == "2" : print(two) print() elif da_digit[ix] == "3" : print(three) print() elif da_digit[ix] == "4" : print(four) print() elif da_digit[ix] == "5" : print(five) print() elif da_digit[ix] == "6" : print(six) print() elif da_digit[ix] == "7" : print(seven) print() elif da_digit[ix] == "8" : print(eight) print() elif da_digit[ix] == "9" : print(nine) print() else : print("Your # was not valid! Try again!")
Inheritance : Testing Properties w/ Class Var.'s Ex.
#Testing prop.'s: class var.'s# class Super: supVar = 1 class Sub(Super): subVar = 2 obj = Sub() print(obj.subVar) ---> 2 print(obj.supVar) ---> 1 - "Super" class defines a class var. - "supVar" - "Sub" class defines a class var. - "subVar" - BOTH var.'s are visible inside the obj. of class "Sub"
Polymorphism : Building a hierarchy of classes Ex.
(*direc* is actually direction) import time class Vehicle: def change_direc(left, on): pass def turn(left): change_direc(left, True) time.sleep(0.25) change_direc(left, False) class TrackedVehicle(Vehicle): def control_track(left, stop): pass def change_direc(left,on): control_track(left, on) class WheeledVehicle(Vehicle): def turn_front ... ... _wheels(left, on): pass def change_direc(left,on): turn_front_wheels(left, on)
Exceptions : the (unnamed) raise instruction
(unnamed) raise instruction : will immediately re-raise the same exception as currently handled - using this, you can distribute the exception handling among different parts of the code - instruction may be used inside the except branch only > in any other context --> error Ex. def bad_fun(n): try: return n / 0 except: print("I did it again!") raise try: bad_fun(0) except ArithmeticError: print("I see!") print("THE END.") -----> outputs -----> I did it again! \ I see! \ THE END. ZeroDivisionError is raised twice : - 1st inside the try part of the code (this is caused by actual zero division) - 2nd inside the except part by the raise instruction *******
Inheritance : Testing Properties w/ Instance Var.'s Ex.
- "Sub" class constructor creates an instance var. - "subVar" - "Super" constructor creates an instance var. - "supVar" - BOTH var.'s, again, are accessible from w/in the obj. of class "Sub" # Testing properties: instance variables # class Super: def __init__(self): self.supVar = 11 class Sub(Super): def __init__(self): super().__init__() self.subVar = 12 obj = Sub() print(obj.subVar) ---> 12 print(obj.supVar) ---> 11 > existence of "supVar" var. is conditioned by the "Super" class constructor invoc. - omitting it would result in absence of var. in the created obj.
Strings: The find() method [three-param. mutation]
- 3rd arg. points to the 1st index which WON'T be taken into consideration during the search (upper limit of the search) Ex. print('kappa'.find('a', 1, 4)) --> output --> 1 print('kappa'.find('a', 2, 4)) --> output --> -1 (^ 'a' can't be found in given search boundaries here)
Your first object
- A newly defined class becomes a tool that is able to create new objects. (has to be used explicitly, on demand) - The act of creating an object of the selected class is also called an.......... Instantiation = the object becomes an instance of the class To create exactly 1 object of the TheSimplestClass : > assign a var. to store the newly created object of that class > create an object at the same time Ex. my_first_object = TheSimplestClass() >> here ^ class name tries to pretend that it's a function >> the newly created object is equipped w/ all that the class brings >> as this class is completely empty, the object is empty too
More details about comparing strings/use of strings
- Comparing strings in a strict way (as Python does) can be unsatisfactory w/ advanced searches (e.g. during extensive database queries) - Responding to this demand, many fuzzy string comparison algorithms have been created to find strings which aren't = in the Python sense, but are similar > One such concept is the Hamming distance >> used to determine the similarity of 2 strings https://en.wikipedia.org/wiki/Hamming_distance > Another solution of the same kind, based on a different assumption, is the Levenshtein distance https://en.wikipedia.org/wiki/Levenshtein_distance - Another way of comparing str.'s is finding their acoustic similarity (determining if 2 strings sound similar (like "raise" and "race")) > Such a similarity has to be est. for every language (or even dialect) separately >> algorithm used to perform such a comparison for the English language is called Soundex & was invented in 1918 : https://en.wikipedia.org/wiki/Soundex - Due to limited native float & integer data precision, it's sometimes reasonable to store & process huge numeric values as strings. > the technique Python uses when you force it to operate on an int # consisting of a very large number of digits
How do computers understand single characters?
- Computers store single characters as unique numbers & vice versa - there's more characters than you might expect - many are invisible to humans, but essential to comp.'s - some of them are called : > whitespaces > control characters
Methods exclusively for processing characters
- Python strings have a significant # of methods intended exclusively for processing characters NOT intended for work w/ with any other collections ***complete list is presented here: https://docs.python.org/3.4/library/stdtypes.html#string-methods
UTF-8 (Unicode Transformation Format-8)
- Unicode Transformation Format-8 (smarter forms of encoding Unicode texts vs. UCS-4) - uses as many bits for each of the code points as it really NEEDS to represent them instead Ex.'s : - all Latin characters (& all standard ASCII characters) occupy 8 bits - non-Latin characters occupy 16 bits - CJK (China-Japan-Korea) ideographs occupy 24 bits - no need to use the BOM, bc of method features used by the to store code points > BUT some of the tools look for it when reading the file, & many editors set it up during the save - Python 3 fully supports Unicode and UTF-8 - Python3 is completely I18Ned > can use Unicode/UTF-8 encoded characters to name var.' s & other entities > can use them during all input & output
UCS-4
- Universal Character Set - 4 - the most general standard describing the techniques used to implement Unicode in actual computers & computer storage systems (how to code & store the characters in the memory & files) - it's a rather wasteful standard > increases a text's size by 4x compared to standard ASCII - uses 32 bits (four bytes) to store each character - the code is just the Unicode code points' unique # - file containing UCS-4 encoded text may start with a BOM (byte order mark), an unprintable combination of bits announcing the nature of the file's contents ( Some utilities may require it )
Stacks: Object Approach Program
- Use a list as the stack's storage - Want the class to have one property as the stack's storage - We have to "install" a list inside each object of the class (each OBJECT has to have its OWN list - the list mustn't be shared among different STACKS) - We want the list to be hidden from the class users' sight Python has no means of allowing you to simply declare such a property - need to add a specific statement or instruction - the properties need to be added to the class manually - to guarantee that the activity^ has taken place every time the new stack is created, you must equip the class w/ a (dually) specific func. > it has to be named in a strict way > it is invoked implicitly, when the new object is created Such a function is a "constructor"
Presenting an Object/Class as a string
- When Python needs any class/object to be presented as a string it tries to invoke a mthd named __str__() from obj & use the returned str - simply putting an obj. as an arg. in the print() func. returns a default "ugly" str) - can change default by defining your own method : def __str__ , returning ur str.
Code Point
- a # which makes a character Ex. 32 is a code point which makes a space in ASCII encoding - classic form of ASCII code uses 8 bits for each sign - 8 bits mean 256 different characters > first 128 are used for the standard Latin alphabet (both upper-case & lower-case characters) > can only make use of the remaining 128 >> this may be sufficient for 1 language, or for a small group of similar languages - have to know the target code page to determine the meaning of a specific code point (code points derived from code page concept are ambiguous)
Reviewing 1 kind of class property
- a class can be equipped w/ 2 different kinds of data to form a class's properties - one of them we've seen exists only when it's explicitly created & added to an obj.- this can be done during : - obj. initialization - performed by the constructor - in any moment of the object's life - any existing ones can be removed @ any time
Inheritance : issubclass()
- a func. which is able to identify a relationship btwn two classes, checking if a particular class is a subclass of any other class Format: issubclass( ... ...ClassOne, ClassTwo) - func. returns True if ClassOne is a subclass of ClassTwo, & False otherwise > each class is considered to be a subclass of itself !!!
Overloading
- ability to use the same operator against completely different kinds of data (like numbers vs. strings) (as an operator is "overloaded" w/ different duties)
Exceptions : the assert instruction
- assert is a keyword The assert expression instruction : - evaluates the expression... > if evaluates to True, non-0 numerical val., non-empty string, or any other value different than None >> it won't do anything else - otherwise, it automatically raises an exception named AssertionError (here we say that assertion has failed) Ex. import math x = float(input("Enter a number: ")) assert x >= 0.0 x = math.sqrt(x) print(x) - program runs flawlessly if you enter a valid numerical value >= 0 - otherwise, it stops & emits the following message : [ Traceback (most recent call last): File ".main.py", line 4, in assert x >= 0.0 AssertionError]
ASCII (American Standard Code for Information Interchange)
- can create virtually any number of character-number assignments - BUT^... every type of computer using a different character encoding would not be very convenient : - Thus, we introduce a universal & widely accepted standard implemented by (almost) all computers & OSs all over the world : - ASCII (American Standard Code for Information Interchange) > most widely used in nearly all modern devices (like computers, printers, mobile phones, tablets, etc.) > code provides space for 256 different characters, but we are interested only in the first 128 > letters arranged in same order as in Latin alphabet *(table as 2 screenshots is in graphics folder)* Ex1. Look at the code of the most common character - the space. This is 32. Ex2. code of the lower-case letter a = 97 code of the upper-case A = 65 difference between the code of a and A = 32 (space!)
in operator w/ strings
- checks if its left argument (a string) can be found anywhere within the right argument (another string) - result of the check is simply True or False Ex. alphabet = "abcdefghijklmnopqrstuvwxyz" print("f" in alphabet) --> output --> True print("F" in alphabet) --> output --> False print("ghi" in alphabet) --> output --> True print("Xyz" in alphabet) --> output --> False
not in operator w/ strings
- checks if its left argument (a string) is not found anywhere within the right argument (another string) - result of the check is simply True or False Ex. alphabet = "abcdefghijklmnopqrstuvwxyz" print("f" not in alphabet) --> output --> False print("F" not in alphabet) --> output --> True print("ghi" not in alphabet) --> output --> False print("Xyz" not in alphabet) --> output --> True
Consequences of previous kind of class property data's approach
- different objects of the same class may possess different sets of prop.'s - must be a way to safely check if a specific object owns the property you want to utilize (unless you want to provoke an exception - it's always worth considering) - each object carries its own set of properties - they don't interfere w/ each other in any way
whitespaces (characters)
- ex. of one that's completely invisible to the naked eye is a special code [or pair of codes, which are used to mark the ends of lines inside text files (different OSs may treat this issue differently)] - you don't see this sign (or signs), but can observe the effect of application where the lines are broken
Operations on strings: min() function
- finds minimum (in terms of ASCII table value) element of the sequence passed as an arg. : > the sequence cannot be empty, or ValueError > string AND lists can use this, but must follow rule^ > more than just strings can be presented Ex1. print(min("aAbByYzZ")) --> output --> A Ex2. t = 'The Knights Who Say "Ni!"' print('[' + min(t) + ']') --> output --> [ ] (square [ ] representing a space so it's easier to see) Ex3. t = [0, 1, 2] print(min(t)) --> output --> 0
Operations on strings: max() function
- finds the maximum (in terms of ASCII table value) element of the sequence passed as an arg. : > the sequence cannot be empty, or ValueError > string AND lists can use this, but must follow rule^ > more than just strings can be presented Ex1. print(max("aAbByYzZ")) --> output --> z Ex2. t = 'The Knights Who Say "Ni!"' print('[' + max(t) + ']') --> output --> [y] Ex3. t = [0, 1, 2] print(max(t)) --> output --> 2
Avoiding issues w/ attribute's existence : Checking class var.'s availability Ex.'s
- hasattr() func. can : > operate on classes as well > returns True if the specified class contains a given attribute - else, False > use it to find out if a class var. is available, like this : Ex1. class ExampleClass: attr = 1 print(hasattr(ExampleClass, 'attr')) print(hasattr(ExampleClass, 'prop')) ------> output ------> True \ False Ex2. class ExampleClass: a = 1 def __init__(self): self.b = 2 example_object = ExampleClass() print(hasattr(example_object, 'b')) print(hasattr(example_object, 'a')) print(hasattr(ExampleClass, 'b')) print(hasattr(ExampleClass, 'a')) ------> output ------> True \ True \ False \ True
Disadvantage / Solution for try-except blocks
- if there is a possibility that more than one exception may skip into an except: branch, you may have trouble figuring out what actually happened Ex. try: x = int(input("Enter a number: ")) y = 1 / x except: print("Oh dear, something went wrong...") print("THE END.") - when getting the msg "Oh dear, something went wrong..." in the console, it says nothing about the reason....but there are 2 possible causes of the exception: - non-integer data entered by user - integer val = to 0 assigned to x var. Technically, there's 2 ways to solve it : - build 2 consecutive try-except blocks, 1 for each possible exception reason (easy, but will cause unfavorable code growth) - use a more advanced variant of the instruction
Operations on Strings: chr() function
- if you know the code point (number) and want to get the corresponding character, you can use a function named chr() (as in character) > takes a code point and returns its character (so other direction from ord()) > invalid argument (a negative or invalid code point) causes ValueError or TypeError exceptions Ex. print(chr(97)) --> output --> a print(chr(945)) --> output --> α NOTE : chr( ord(x) ) == character x ord( chr(x) ) == codepoint of x
Class Variable Ex. Explanation
- initializing a var. inside the class but outside any of its mthds makes the var. a class var. - there's an assigned var. "counter" in the first list of the class definition that is setting the counter to 0 - accessing the var. is like accessing any instance attribute (can see it in the constructor body) - constructor increments the var. by 1, meaning var. counts all created obj.'s 2 vital conclusions come from the ex. : > class var.'s aren't shown in an obj.'s __dict__ (this is natural, as class var.'s aren't parts of an obj. - BUT you can always try to look into the var. of the same name at the CLASS level) > a class var. always presents the same val. in all class instances (obj.'s)
Diamond Problem
- name reflects the shape of the inheritance diagram - 2nd ex. of the spectrum of issues that can possibly arise from multi. inheritance - diamonds may bring some problems into your life - both the real ones & those offered by Python - There is the top-most superclass named A - There are 2 subclasses derived from A: B & C - There is the bottom-most subclass "D" derived from B & C (or C & B - these mean different things) > Some prog. lang.'s forbid multi. inheritanced & as a consequence, won't let you build a diamond (like Java & C#) > Python allows multiple inheritance & doesn't mind if you write & run code like the one in the editor. But, MRO - it's always in charge!
More about introspection & reflection
- needn't know a complete class / obj. def. to manipulate the obj., as the obj. &/or its class contain the metadata that makes the obj.'s features recognizable during program execution You can find out everything about classes in Python - reflection & introspection enable a programmer to do anything with every obj., no matter where it comes from
Strings: The join() method
- performs a join - it expects one argument as a list; it must be assured that all the list's elements are strings - all the list's elements will be joined into one string - BUT the string from which the method has been invoked is used as a sep. , put among the strings - the newly created string is returned as a result Ex. print(",".join(["omicron", "pi", "rho"])) --> output --> omicron,pi,rho - the join() method is invoked from w/in a string containing a comma - (the string can be arbitrarily long, or be empty) - the join's arg. is a list containing 3 strings - the method returns a new string
Instance Var. full code Variant w/ Privatizing Ex.
- privatizing the first, second, & third property names (w/ addition of "__" in front of each) in the instance var. ex. : __first / __second / __third yields ---> output ---> {'_ExampleClass__first': 1} {'_ExampleClass__first': 2, '_ExampleClass__second': 3} {'_ExampleClass__first': 4, '__third': 5} - when Python sees that you want to add an instance var. to an obj. inside any of the obj.'s methods, it mangles the operation in the following way : > puts a class name before your name > puts an additional "_" in front of it^ So, __first ---> _ExampleClass__first - name is now fully accessible from outside the class, can run this code : print(example_object_1._ExampleClass__first) --> valid - no errors/exceptions > making a property private is limited > if you add a private instance var. outside the class code, mangling won't work
control characters
- purpose is to *control* input/output devices -
Exceptions : the raise instruction
- raise is a keyword - raise exc raises the specified exception named "exc" as if it was raised in a normal (natural) way The instruction enables you to : - simulate raising actual exceptions (e.g., to test your handling strategy) - partially handle an exception & make another part of the code responsible for completing the handling (separation of concerns) Ex. def bad_fun(n): raise ZeroDivisionError try: bad_fun(0) except ArithmeticError: print("What happened? Error?") print("THE END.") -----> outputs -----> What happened? Error? \ THE END. ---> in this way ^ you can test your exception handling routine without forcing the code to do stupid things
Strings: index() method
- searches the seq. from the beginning, in order to find the 1st element of the value specified in its arg. > it's a METHOD, NOT a function > elem. searched MUST occur in seq. or ValueError > returns the index of the first occurrence of the arg. >> lowest possible result is 0 >> highest is length of arg. decremented by 1 Ex. print("aAbByYzZaA".index("b")) --> output --> 2 print("aAbByYzZaA".index("Z")) --> output --> 7 print("aAbByYzZaA".index("A")) --> output --> 1
Multiline Strings
- simply using ' ' (1/1 apostrophes) here won't let you use a string occupying more than one line of text : Ex. multiline = 'Line #1 Line #2' print(len(multiline)) --> output --> erroneous - for strings spanning multilines, begin & terminate them with 3 apostrophes (or quotes), not one : Ex. (have spaced out the apostrophes here for view) multiline = ' ' 'Line #1 Line #2' ' ' print(len(multiline)) --> output --> 15 -> (the triple 'apostrophes' can also be triple "quotes") All the characters in the multiline string are counted up as well as one that is invisible- a whitespace! It's located btwn the 2 text lines & is denoted as: \n > a special (control) character used to force a line feed (hence its name: LF) - can't see it, but it counts
Strings: The split() method
- splits the string & builds a list of all detected substrings - method assumes the substrings are delimited by whitespaces - the spaces don't take part in the operation, and aren't copied into the resulting list - if the string is empty, the resulting list is empty too - the reverse operation can be performed by the join() method Ex. print("phi chi \npsi".split()) --> output --> ['phi', 'chi', 'psi']
Operations on strings: the list() function
- takes its arg. (a string) & creates a new list containing all the string's characters, 1 per list elem. > it's strictly a string function > it's able to create a new list from many other entities (e.g., from tuples & dictionaries) Ex. print(list("abcabc")) --> output --> ['a', 'b', 'c', 'a', 'b', 'c']
Unicode
- temporary concept that solved the problem of making programs written w/ ability for use all around the world was Code Pages, BUT the solution for the long term was .... Unicode ! - it assigns unique (unambiguous) characters (letters, hyphens, ideograms, etc.) to > a million code points : > first 128 Unicode code points are identical to ASCII > first 256 Unicode code points are identical to the ISO/IEC 8859-1 code page (code page designed for western European lang.'s) - names all available characters & assigns them to planes (a group of characters of similar origin, application, or nature)
Operations on Strings: ord() function
- to know a specific character's ASCII/UNICODE code point value, you can use a function named : ord( ) (as in ordinal) > needs a 1-character string as its argument (letter) > breaching this requirement causes a TypeError exception which returns a # denoting arg.'s length Ex. char_1 = 'a' char_2 = ' ' # space print(ord(char_1)) print(ord(char_2)) --> output --> 97 \ 32 ---------------------------------------------------------- Ex. char_1 = 'α' # Greek alpha char_2 = 'ę' # letter in the Polish alphabet print(ord(char_1)) print(ord(char_2)) --> output --> 945 \ 281
Handling Exceptions w/ the try keyword
- try keyword begins a block of the code which may or may not be performing correctly - Python tries to perform the risky action; if it fails, an exception is raised & Python starts to look for a solution - except keyword starts a piece of code which will be executed if anything inside the try block goes wrong - if an exception is raised inside a previous try block, it will fail here > so code located after the except keyword should provide an adequate reaction to the raised exception Python tries to perform all instructions placed between the try: & except: statements : - if nothing wrong w/ the execution & all instructions are performed successfully, the execution jumps to the point after last line of the except: block & the block's execution is considered complete - if anything goes wrong inside the try: & except: block, execution immediately jumps out of the block & into the first instruction located after the except: keywd. - this means that some of the instructions from the block may be silently omitted
Advanced Try-Except Variant Ex.3
- we've removed the unnamed branch & user enters 0 here: try: x = int(input("Enter a number: ")) y = 1 / x print(y) except ValueError: print("You must enter an integer value.") print("THE END.") - exception raised won't be handled by ValueError - it has nothing to do with it, thus this msg will be output : Traceback (most recent call last): File "exc.py", line 3, in y = 1 / x ZeroDivisionError: division by zero
code page
--------------temporary solution for I18N--------------- - a standard for using the upper 128 code points to store specific national characters (setting the higher half of the code points differently for different languages) Ex. there are different code pages for : Western Europe & Eastern Europe Cyrillic & Greek alphabets Arabic & Hebrew languages etc. ... *means that the one & same code point can make different charac.'s when used in different code pages* Ex. the code point 200 makes... > Č (a letter used by some Slavic languages) when utilized by the ISO/IEC 8859-2 code page > Ш (a Cyrillic letter) when used by the ISO/IEC 8859-5 code page ------------------------------------------------------------
Ex. issubclass()
2 nested loops that check all possible ordered pairs of classes, and print the results of the check to see if the pair matches the subclass-superclass relationship : Ex. class Vehicle: pass class LandVehicle(Vehicle): pass class TrackedVehicle(LandVehicle): pass for cls1 in [Vehicle, LandVehicle, TrackedVehicle]: for cls2 in [Vehicle, LandVehicle, TrackedVehicle]: print(issubclass(cls1, cls2), end="\t") print()
Strings: The rstrip() method***
2 variants of the rstrip() method - do nearly the same as lstrips, but affect the opposite side of the string Ex. print("[" + " upsilon ".rstrip() + "]") --> output --> [ upsilon] print("cisco.com".rstrip(".com")) --> output --> cis
How Python looks for obj.components
> "Sub" class inherits goods from 2 superclasses : Left & Right (these names are given meaningfully) > No doubt that class var. "var_right" comes from the "Right" class, & "var_left" comes from the "Left" class > The output proves that both unclear cases have a solution for where "var" / invoc. of fun() mthd. comes from - inside the "Left" class > Thus, we can deduce that Python looks for obj. components in this order: - inside the object itself - in its superclasses, from bottom to top - if there is > 1 class on a particular inheritance path , Python scans them left to-> right
Comparing Strings : == / != / < / >
> 2 strings ARE equal when they : - consist of the same characters in the same order Ex. 'alpha' == 'alpha' --> output --> True > 2 strings ARE NOT equal when they: - don't consist of same charact.'s in the same order - final relation btwn strings is determined by comparing 1st different character in both strings ( keep ASCII/UNICODE code points in mind always ) Ex. 'alpha' != 'Alpha' --> output --> True > 2 strings of different lengths : - longer string is considered greater when : > shorter one is identical to longer one's beginning Ex. 'alpha' < 'alphabet' --> output --> True > String comparison is always case-sensitive (upper-case letters are taken as < than lower-case) Ex. 'beta' > 'Beta' ---> output ---> True [string >= number : always raises an exception]
Create your own exception Ex2. Second Class Explained
> A more specific problem (like an excess of cheese) can require a more specific except. - it's possible to derive the new class from alrdy def.'d PizzaError class > TooMuchCheeseError exception needs more info than the regular PizzaError exception, so we add it to the constructor - the name cheese is then stored for further processing
Orig. Ex. Built Diamond-like Explained
> Both Middle classes define a mthd. of the same name: m_middle() > The Object.m_middle() will actually be invoked when the line is executed > The invoc. will activate the "m_middle()" mthd., which comes from the Middle_Left class (bc the class is listed before Middle_Right on the Bottom class's inheritance list)
Working Multi-Inheritance Code Ex.
> Changing 1 line of the Ex. "class Bottom(Middle):" --> "class Bottom(Middle, Top):" - this turns a very simple code w/ a clear single-inheritance path into a mysterious, but valid, multiple-inheritance riddle - order the 2 superclasses are listed in btwn. the ( ) is compliant w/ the code's structure: the Middle class precedes the Top class ( just like in the real inheritance path ) - sample is correct & works as expected, but this notation doesn't bring any new functionality or additional meaning
Exceptions are classes
> Exceptions are classes > when one is raised, an obj. of the class is instantiated, & goes thru all levels of prog. execution lookin for an except branch that's prepared to deal w/ it >> that obj. carries info which can help to precisely identify all aspects of the pending situation > Python offers a special variant of the exception clause- it's extended & contains an additional phrase starting w/ "as" keywd + an identifier > identifier is designed to catch the exception obj., so its nature can be analyzed & proper conclusions drawn > the identifier's scope covers ITS except branch, & doesn't go any further
Create your own exception Ex3. Explained
> We've coupled together 2 previously defined exceptions > One is raised in the make_pizza func. when any of these 2 erroneous situations is discovered: - a wrong pizza request - a request for too much cheese [ removing the except TooMuchCheeseError branch will cause all appearing exceptions to be classified as PizzaError ] [ removing the except PizzaError branch will cause the TooMuchCheeseError exceptions to be unhandled & program termination ] > Previous solution has 1 important weakness - the new exceptions can't be used as-is, w/out a full list of required arg.'s - SO, we'll set default val.'s for all constructor param.'s > Now, if circumstances permit, we can use the class names alone
type()
> a func. which is able to find a class which has been used to instantiate any obj. (among other things)
Single inheritance vs. multiple inheritance
> a single inheritance class is always simpler, safer, & easier to understand & maintain > multiple inheritance is always risky: - mistakes can be made easily when identifying these parts of the superclasses which will influence the new class - may make overriding extremely tricky - using the super() func. becomes ambiguous - violates the single responsibility principle [as it makes a new class of 2 (or more) classes that know nothing about each other] (https://en.wikipedia.org/wiki/Single_responsibility_principle) - multi. inheritance is thus not very recommended : if you really need the many different abilities offered by different classes, composition is a better opt.
__module__
> a string which stores the name of the mod. which contains the definition of the class > any mod. named "__main__" is actually not a mod., but the file being currently run
Exception Tree Ex. Explanation
> a tree is a perfect ex. of a recursive data structure > a recursion seems like the best tool to traverse thru it print_exception_tree() function takes 2 arg's : - point inside the tree from which we start traversing it - a nesting level (used to build a simplified drawing of the tree's branches) Starting from tree's roots : > Root of Python's exception classes is the BaseException class (a superclass of all other exceptions) > For each encountered class, perform the same set of operations : - print its name, taken from the __name__ property - iterate thru the list of subclasses delivered by the __subclasses__() method - recursively invoke the print_exception_tree() func., incrementing the nesting level respectively
__bases__
> a tuple containing classes (not class names) which are direct superclasses for the class > the order is the same as that used inside the class definition > only classes have this attribute - objects don't > a class w/out explicit superclasses points to obj. (a predefined Python class) as its direct ancestor
__name__
> built-in property > a string containing the name of the class > name attribute exists only inside the class > if you want to find the class which was used to instantiate an obj.: - can use type() around the obj. to utilize "name" prop. in that particular way^
Stacks: Procedural Approach - Taking a value off of the stack
> define a func. (pop) - doesn't take any parameters - func. reads the value from the top of the stack and removes it - func. returns the value taken from the stack - doesn't check if there is any element in the stack Ex. stack = [] def pop(): val = stack[-1] del stack[-1] return val
Building a hierarchy of classes
> dividing a problem among classes & deciding which should be at the top & bottom of the hierarchy > remembering a consequence of the general rules presented earlier may be key to understanding how some codes work / how the effect may be used to build a flexible set of classes > do_it()method is defined 2x, but invoked just once : inside One - 1st invoc. from obj. "one" works as expected - 2nd invoc. (keeping in mind how Python finds class components) launches do_it() in the form existing inside the "Two" class, though the invocation takes place w/in the "One" class Note: (like in the ex.) when the subclass is able to modify its superclass behavior is called polymorphism
Strings: The lstrip() method***
> parameterless : returns newly created str formed from original one by removing all leading whitespaces - the brackets aren't a part of the result - they only show the result's boundaries > one-parameter : returns a newly created string formed from the original one by removing all characters enlisted in its argument (a string), not just whitespaces Ex. print("[" + " tau ".lstrip() + "]") --> output --> [tau ] Ex. print("www.cisco.com".lstrip("w.")) --> output --> cisco.com Ex. print("pythoninstitute.org".lstrip(".org")) --> output --> pythoninstitute.org [ Leading characters, leading whitespaces ]
Introspection
> the ability of a program to examine an obj.'s type / properties @ runtime
Reflection
> the ability of a program to manipulate an obj.'s values, properties, &/or functions @ runtime
Exceptions : Try-Except + Else
> the objective nature of Python's exceptions makes them a very flexible tool, able to fit to specific needs Syntactic / Semantic aspects of how Python treats the try-except block : - 1st ft. is an additional, possible branch placed inside (directly behind) the try-except block (always after the last "except" branch) - [it's the part of the code w/ else in the ex.] - this type of code is executed only when no exception has been raised in the try: part exactly 1 branch can be executed after try: - either the one beginning w/ except (can be more than 1 branch of this type) - the one starting with else
Composition
> the process of composing an object using other different objects - the obj.'s used in the composition deliver a set of desired traits (prop.'s &/or mthd.'s) ; they act like blocks used to build a more complicated structure > it projects a class as a container able to store & use other obj.'s (derived from other classes) where each of the obj.'s implements a part of a desired class's behavior ( as opposed to inheritance, which extends a class's capabilities by : adding new components & modifying existing ones )
Polymorphism
> the situation in which the subclass is able to modify its superclass behavior it can extend class flexibility > helps developer keep the code clean & consistent > the word is from Greek : (polys = "many, much" & morphe = "form, shape") [one & same class can take various forms depending on the redefinitions done by any of its subclasses] > No class is given once & for all - each class's behavior may be modified any time by any of its subclasses
Exceptions : Finally Block
> try-except block can be extended w/ the finally keyword (must be the last branch of code designed to handle exceptions) > these 2 variants (else & finally) aren't dependent in any way - can coexist or occur independently > this block is always, invariably executed (it finalizes the try-except block execution) - no matter what happened earlier, even when raising an exception, whether this has been handled or not
Strings: The replace() method
> two-parameter : returns a copy of the original string in which all occurrences of the first arg. have been replaced by the 2nd arg. - if 2nd arg. is an empty string, replacing is actually removing the 1st arg.'s string - if both arg's are empty strings , the whole original string is returned - if 1st arg. is an empty string the 2nd argument is repeated once in front of each character of the entire original string Ex. print("www.netacad.com".replace("netacad.com", "pythoninstitute.org")) --> output --> www.pythoninstitute.org print("This is it!".replace("is", "are")) --> output --> Thare are it! print("Apple juice".replace("juice", "")) --> output --> Apple - 3-parameter : uses the 3rd arg. (a #) to limit the number of replacements. Ex. print("This is it!".replace("is", "are", 1)) --> output --> Thare is it! print("This is it!".replace("is", "are", 2)) --> output --> Thare are it!
Polymorph. : Build a hierarchy of classes
> we defined a superclass "Vehicle", which uses the turn() method for a general scheme of turning > the turning itself is done by a mthd "change_direc()" - (its empty- we're going to put details in the subclass) - (such a method is often called an abstract method : it only demonstrates some possibility which will be instantiated later) > defined a subclass "TrackedVehicle" (derived from the Vehicle class) -instantiated change_direc() method by using the specific (concrete) mthd "control_track()" ["WheeledVehicle" does the same trick, but uses the turn_front_wheels() method to force the vehicle to turn] > most important adv. of this form of code is it enables you to implement a brand new turning algorithm just by modifying the "turn()" mthd, which can be done in just 1 place, as all the vehicles will obey it
Stacks: Object Approach Program Ex. (more about constructors)
A "constructor" function : - general purpose is to construct a new object - it should know everything about the object's structure - it must perform all the needed initializations - constructor's name is always __init__ - it has to have >= 1 param. (it's used to represent the newly created object) - you can use the parameter to : manipulate the object & enrich it with the needed properties - the obligatory parameter is usually named self - (only a convention, but you should follow it) - it simplifies the process of reading and understanding your code Ex. # Def. the Stack class # class Stack: # Def. the constructor function # def __init__(self): print("Hi!") # Instantiating the object # stack_object = Stack() ---> output ---> Hi! - no trace of invoking the constructor inside the code - has been invoked implicitly & automatically
Methods
A method is a function embedded inside a class - it's obliged to have at least one parameter (1st (or only) one is "self") ("self" identifies the obj. for which the mthd is invoked) (NO parameterless mthd.'s) - may be invoked w/out an arg., but not w/out param.'s - if invoking a mthd Python will alrdy set "self" arg. for u Ex. class Classy: def method(self): print("method") obj = Classy() obj.method() ---> output ---> method ^ we've created the obj. by treating the class name like a func., returning a newly instantiated obj. of the class
Stack
A structure developed to store data in a very specific way - an alternate name (only in I.T.) is... LIFO = Last In, First Out - a stack is an object with 2 elementary operations: > push : when a new element is put on the top > pop : when an existing element is taken away from the top Stacks are used very often in many classical algorithms - the use of them is critical in the implementation of many widely used tools
Class Hierarchies : Vehicles Ex.
A whole "vehicle" class is too broad We have to define more : - "vehicles" class (superclass) for all - specialized classes (subclasses) > hierarchy grows from top to bottom (like tree roots, NOT branches) - most general & widest class is always at the top (the superclass) - its descendants are located below (the subclasses) Based on the environment (vehicles), & say that there's at least 4 subclass's : - land vehicles - water vehicles - air vehicles - space vehicles For ex., let's focus on land vehicles Land vehicles may be divided more : - wheeled vehicles - tracked vehicles - hovercrafts Visually, the direction of arrows on a diagram of all this would always point to the superclass (& the top-level class doesn't have its own superclass)
Strings: The islower() method
Accepts lower-case letters only & returns True / False - a fussy variant of isalpha() - any string element that isn't a lower-case letter (i.e. #, space, etc.) causes method to return False - an empty string also causes method to return False Ex. print("Moooo".islower()) --> output --> False print("moooo".islower()) --> output --> True
Strings: The isupper() method
Accepts upper-case letters only & returns True / False - a fussy variant of isalpha() - any string element that isn't an upper-case letter (i.e. #, space, etc.) causes method to return False - an empty string also causes method to return False Ex. print('Mu40'.isalpha()) --> output --> False print('Mu40'.isalpha()) --> output --> False print("Moooo".isalpha()) --> output --> True
Stacks : Defining a New Subclass (Sum Variable) *** review / break his up!!!
Add a new subclass for handling stacks that evaluates the sum of all the elements currently stored on the stack (don't wanna modify the previously defined stack, just want a new stack with new capabilities) Steps for Defining a New Subclass : - should pointing to the class which will be used as the superclass - name of (superclass) is written before the colon directly after the new class name > we want the push method not only to push the value onto the stack but also add the value to the sum var. > we want the pop function not only to pop the value off the stack but also subtract the value from sum var. - must add private var. named "sum" storing the total of all the stack's val.'s (+ new prop. to class via constru. func.) Python forces you to explicitly invoke a superclass's constructor : "Stack.__init__(self)" Omitting it would be harmful - the object will be deprived of __stack_list Such a stack will not function properly - this is the only time you can invoke any of the available constructors explicitly (can be done inside the subclass's constructor)
Method Name Mangling/ Hiding Ex.
All info about property name mangling applies to method names, too - a method whose name starts w/ "__" is (partially) hidden : class Classy: def visible(self): print("visible") def __hidden(self): print("hidden") obj = Classy() obj.visible() try: obj.__hidden() except: print("failed") obj._Classy__hidden() ---> output ---> visible \ failed \ hidden
Stacks : Adding / Accessing a property to an object
Any change you make inside the constructor that modifies the state of the "self" parameter will be reflected in the newly created object : - this means you can add any property to the object > that property will remain there until the object finishes its life -OR- > the property is explicitly removed Adding 1 property to the new object - a list for a stack : Ex. class Stack: def __init__(self): self.stack_list = [] stack_object = Stack() print(len(stack_object.stack_list)) ---> 0 Just like when invoking .methods, we've used the dotted notation : - general convention for accessing an object's properties - name the object, put a dot (.) after it, & specify the desired property's name NO parentheses! NOT invoking a mthd - you want to access a PROPERTY !! Setting a property's value for the very first time (like in the constructor) is creating it - from then, the obj. has got the property & is ready to use its value Ex. also tries to access the stack_list property from outside the class right after the obj has been created (we wanna check current len of stack) However, we don't want a value to be returned - we prefer stack_list to be hidden from the outside world
Inheritance
Any object bound to a specific level of a class hierarchy INHERIT all the traits ( + requirements & qualities) defined inside any of the superclasses > the object's home class may define new traits ( + requirements & qualities) which will be INHERITED by any of its subclasses > this is one of the fundamental concepts of object programming
Find a Word! Lab
Are the characters comprising the first string hidden inside the second string? : word = input("Enter the word you wish to find: ").upper() strn = input("Enter the string you wish to search through: ") .upper() filter_string = [""] def find_a_word(word, strn): for ch in word: if strn.find(ch) < 0: break else: filter_string.append(ch) find_a_word(word, strn) x = str("".join(filter_string)) if x==word: print("Yes") else: print("No")
Defining a constructor w/ a default arg. value Ex.
As __init__ is a mthd., & a mthd. is a func., you can do the same tricks with constructors/methods as done w/ ordinary func.'s : class Classy: def __init__(self, value = None): self.var = value obj_1 = Classy("object") obj_2 = Classy() print(obj_1.var) print(obj_2.var) ---> output ---> object \ None
Create your own exception Ex2. First Class Explained
Build your own exception structure Ex. : > if you work on a large simulation system which models the activities of a pizza restaurant, you may want to form a separate hierarchy of exceptions > first, you can define a general exception as a new base class for any other specialized exception > we want to collect more specific info here than a regular Exception, so constructor takes 2 arg.'s : - one specifies a pizza as a subj. of the process - another contains a fairly precise descrip. of the prob. 1st param. is saved inside our own property & 2nd param. is passed to the superclass constructor
Handling 2 or more exceptions
Can check for more than 1 type of excep. simultaneously by putting all the engaged exception names into a comma-separated list w/ ( )'s Format Ex. try: : except (exc1, exc2): :
*** name & review / break his up!!!
Change the functionality of 2 mthd.'s that are in the subclass (not names) [more precisely --> interface (the way in which the objects are handled) of the class remains the same when changing implementat. @same time] Implementation of push func. should : - to add the value to the __sum var. - to push the value onto the stack > the 2nd activity has already been implemented inside the superclass - we have to use it, as there's no other way to access the __stackList variable Ex. how push mthd. looks in subclass : def push(self, val): self.__sum += val Stack.push(self, val) - Note the way we've invoked the implementation of the push method available in the superclass - must specify the superclass's name (must do this in order to clearly indicate class containing the method, avoid duplicate func. name confusion) - must specify the target object & pass it as the 1st argument (it's not implicitly added to the invoc. in this context) --> push method has been overridden - same name as in the superclass now represents a different functionality
Strings: The title() method
Changes every word's 1st letter to upper-case, turning all other ones to lower-case Ex. print("I know that I know nothing. Part 1.".title()) --> output --> I Know That I Know Nothing. Part 1.
Strings: The startswith() method
Checks if a given str starts w/ the specified substring - a mirror reflection of endswith() Ex. print("omega".startswith("meg")) --> output --> False print("omega".startswith("om")) --> output --> True
Strings: The endswith() method
Checks if the given string ends with the specified arg. & returns a result of True or False, depending : - the substring MUST adhere to the string's LAST character - it can't just be located somewhere near the end of the string Ex. (Idea of what the function does) : if "epsilon".endswith("on"): print("yes") else: print("no") --> output --> yes Ex. t = "zeta" print(t.endswith("a")) --> output --> True print(t.endswith("A")) --> output --> False print(t.endswith("et")) --> output --> False print(t.endswith("eta")) --> output --> True
Strings: The isalpha() method
Checks if the string contains only alphabetical characters (letters), & returns True / False - any string element that isn't a letter (i.e. #, space, etc.) causes method to return False - an empty string also causes method to return False Ex. print("Moooo".isalpha()) --> output --> True print('Mu40'.isalpha()) --> output --> False
Strings: The isdigit() method
Checks if the string contains only digits & returns True / False - any string element that isn't a digit (i.e. letter, space, etc.) causes method to return False - an empty string also causes method to return False Ex. print('2018'.isdigit()) --> output --> True print("Year2019".isdigit()) --> output --> False
Strings: The isalnum() method
Checks if the string contains only digits or alphabetical characters (letters), & returns True / False - any string element that isn't a digit or a letter causes method to return False - an empty string also causes method to return False Ex. print('lambda30'.isalnum()) --> output --> True print('lambda'.isalnum()) --> output --> True print('30'.isalnum()) --> output --> True print('@'.isalnum()) --> output --> False print('lambda_30'.isalnum()) --> output --> False print(''.isalnum()) --> output --> False t = 'Six lambdas' print(t.isalnum()) --> output --> False [cause of this result is a space - (neither digit / letter)] t = 'ΑβΓδ' print(t.isalnum()) --> output --> True t = '20E1' print(t.isalnum()) --> output --> True
Class Hierarchies : Vehicles vs. Dog Ex.
Class definition (that we are concerned w/) : - like the def. of a category - as a result of precisely defined similarities Ex. All existing vehicles & (those not yet in existence) are related by a single, important feature: "the ability to move" You may argue that a dog moves, too Is a dog a vehicle? No! It's not! > must improve the definition (enrich it w/ criteria that distinguishes vehicles from other beings & creates a stronger connection) > vehicles are : - artificially created entities - used for transportation - moved by forces of nature - directed (driven) by humans Based on THIS definition, a dog is not a vehicle!
Instance Variable full code explanation
Class named ExampleClass has : - a constructor : it unconditionally creates an instance var. "first" > sets it w/ the val. passed thru the 1st arg. (from class user's perspective) or the 2nd arg. (from constructor's perspective) [the default value of the parameter - any trick you can do w/ a regular func. param. can be applied to mthds also] - class also has a method which creates another instance var. "second" -3 obj.'s of the ExampleClass, but have been created in ex., but they all differ : > example_object_1 only has the property named first > example_object_2 has 2 properties: first & second > example_object_3 has been enriched w/ a property "third" just on the fly, outside the class's code (which is possible & fully permissible)
Class vs Object Var. Distinguishing Ex. Explanation
Class var.'s exist even w/out class inst. (obj.) creation here, we can see the difference btwn these 2 __dict__ var.'s - 1 from the class & 1 from the obj. - We define 1 class "ExampleClass" - The class defines 1 class var. "varia" - The class constructor sets the var. w/ the parameter's value - Naming the var. is the most important aspect of the ex., bc : > Changing it to self.varia = val would create inst. var. of the same name as the class's > Changing it to varia = val would operate on a method's local var. (testing both cases makes it easier to remember the difference) - First line of the off-class code prints value of "ExampleClass.varia" attribute > we use the val. before the very 1st obj. of the class is instantiated (the class' "__dict__" now contains much more data than its obj.'s counterpart - most of them are useless now - carefully notice the one that shows the current "varia" value) - obj.'s "__dict__" is empty - obj. has no instance var.'s
Comparing Strings : strings vs numbers
Comparing str.'s against #s is generally a bad idea - only comparisons you can perform with impunity are == / != operators - they always yield False / True , respectively - using any of the remaining comparison operators will raise a TypeError exception Ex. '10' == 10 ---> output ---> False '10' != 10 ---> output ---> True '10' == 1 ---> output ---> False '10' != 1 ---> output ---> True '10' > 10 ---> output ---> TypeError
Strings vs. Numbers : number-string
Convert a number (an integer or a float) into a string : - it's a routine way to process input/output data - #-string conversion is simple, as it is always possible - str() function Ex. itg = 13 flt = 1.3 si = str(itg) sf = str(flt) print(si + ' ' + sf) ---> 13 1.3
Strings: the capitalize() method
Creates a new string filled w/ characters taken from the source string, but it tries to modify them : - if the first character inside the string is a letter, it will be converted to upper-case (1st character is an elem. w/ an index = to 0 NOT just the first visible character) ^(Ex.print(' Alpha'.capitalize()) --> output --> " alpha")^ - all remaining letters from the string will be converted to lower-case Don't forget that: - the original string (from which the method is invoked) is NOT CHANGED in any way (string's immutability must be obeyed w/out reservation) - the MODIFIED (capitalized in this case) string is what is actually returned as a result - if unused (not assigned a var. or passed to a func./method) it will disappear w/out a trace - methods don't have to be invoked from w/in var.'s - can be invoked directly from w/in string literals Ex. print('aBcD'.capitalize()) --> output --> Abcd
Stacks: Procedural Approach Disadvantages
Disadvantages of completed procedural approach stack program : - The essential variable (the stack list) is highly vulnerable > anyone can modify it in an uncontrollable way, destroying the stack (not necessarily w/ malicious intent, but maybe bc of carelessness) (maybe by confusing var. names) Ex. accidentally writing this : stack[0] = 0 > functioning of the stack will be completely disorganized - At some point if you need more than one stack, you'll have to : > create another list for the stack's storage > create other push & pop func.'s - At some point if you need more conveniences than push/pop func.'s, it will be difficult to manage w/ dozens of separately implemented stacks
Important Rules of Try-Except Blocks
Don't forget that: - except branches are searched in same order that they appear in code - you mustn't use more than 1 except branch with a certain exception name - # of different except branches is arbitrary - only condition is that if you use try, you must put at least 1 except - named or not - after it - except keyword mustn't be used w/out a preceding try - if any of the except branches is executed, no other branches will be - if none of the specified except branches matches the raised exception, the exception remains unhandled - if an unnamed except branch exists (one without an exception name), it has to be specified as the last
Inside Classes & Objects Ex. ****
Each Python class & obj. is pre-equipped w/ a set of useful attrib.'s which can examine its capabilities : - "__dict__" is such a property - look @ the context in which (obj./class) mthd.'s & attrib.'s exist class Classy: varia = 1 def __init__(self): self.var = 2 def method(self): pass def __hidden(self): pass obj = Classy() print(obj.__dict__) print(Classy.__dict__)
Exceptions
Each time your code tries to do something.... wrong/foolish/irresponsible/crazy/unenforceable : - Python does 2 things : > stops your program > creates a special kind of data called an "exception - can say that Python always raises an exception^ (or that an exception has been raised) when it has no idea what to do with your code - the raised exception expects to be noticed it & fixed > if nothing happens to take care of it, the program will be forcibly terminated (you''ll see an error msg sent to console by Python) > if the exception is taken care of & handled properly, the suspended program can be resumed & its execution can continue Python gives you the tools to observe exceptions, identify them & handle them efficiently - this is possible bc all potential exceptions have their unambiguous names, so you can categorize them & react appropriately Standard Python Lib on them : https://docs.python.org/3.6/library/exceptions.html.
Comparing Strings : String containing digits
Even if a string contains digits only, it's still not a # - interpreted as-is, like any other regular string - numerical aspect is not taken into consideration Ex. '10' == '010' ---> output ---> False '10' > '010' ---> output ---> True '10' > '8' ---> output ---> False '20' < '8' ---> output ---> True '20' < '80' ---> output ---> True
Overall try-except format :
Ex. : # The code that always runs smoothly. : try: : # Risky code. : except Except_1: # Crisis management takes place here. except Except_2: # We save the world here. except: # All other issues fall here. : # Back to normal. : - You cannot add more than 1 anonymous (unnamed) except branch after the named ones - You shouldn't put more concrete exceptions before more general ones inside same except branch seq.
Instance Variable full code Ex.
Ex. class ExampleClass: def __init__(self, val = 1): self.first = val def set_second(self, val): self.second = val example_object_1 = ExampleClass() example_object_2 = ExampleClass(2) example_object_2.set_second(3) example_object_3 = ExampleClass(4) example_object_3.third = 5 print(example_object_1.__dict__) print(example_object_2.__dict__) print(example_object_3.__dict__) ---> output ---> {'first': 1} {'second': 3, 'first': 2} {'third': 5, 'first': 4}
Avoiding issues w/ attribute's existence Ex.1 :
Ex. class ExampleClass: def __init__(self, val): if val % 2 != 0: self.a = 1 else: self.b = 1 example_object = ExampleClass(1) print(example_object.a) ------------------------------------- try: print(example_object.b) except AttributeError: pass -> (placing this ^ in code after --- line) Try-except instruction : > not very sophisticated option - just sweeps the issue under the carpet) ------> output ------> 1 if hasattr(example_object, 'b'): print(example_object.b) -> (placing this ^ in code after --- line) Using function is named hasattr : - it's able to safely check if any obj. / class contains a specified property - expects 2 arg.'s to be passed to it : > the class or object being checked > the name of the property whose existence must be reported (must be a str containing attribute name, not the name alone) [ The function returns True or False ] ------> output ------> 1
Ex. of Subclass Adding in the code *** review
Ex. class Stack: def __init__(self): self.__stack_list = [] def push(self, val): self.__stack_list.append(val) def pop(self): val = self.__stack_list[-1] del self.__stack_list[-1] return val class AddingStack(Stack): def __init__(self): Stack.__init__(self) self.__sum = 0
Multiple Inheritance Ex.
Ex. class SuperA: var_a = 10 def fun_a(self): return 11 class SuperB: var_b = 20 def fun_b(self): return 21 class Sub(SuperA, SuperB): pass obj = Sub() print(obj.var_a, obj.fun_a()) print(obj.var_b, obj.fun_b()) -> output -> 10 \ 11 \ 20 \ 21 "Sub" class has 2 super's: "SuperA" & "SuperB" , meaning the "Sub" class inherits all the goods offered by SuperA & SuperB
try / except ex.1
Ex. first_number = int(input("Enter the first number: ")) second_number = int(input("Enter the second number: ")) try: print(first_number / second_number) except: print("This operation cannot be done.") print("THE END.") -----> output -----> Enter the first number: 4 Enter the second number: 2 2.0 \ THE END. ----------------------------------- Enter the first number: 4 Enter the second number: 0 This operation cannot be done. THE END.
Ex. Complete code of the class : (add 5 subsequent val.'s onto stack, print sum & take them all off of it) *** review / break his up!!! (cards 131-139)
Ex. class Stack: def __init__(self): self.__stack_list = [] def push(self, val): self.__stack_list.append(val) def pop(self): val = self.__stack_list[-1] del self.__stack_list[-1] return val class AddingStack(Stack): def __init__(self): Stack.__init__(self) self.__sum = 0 def get_sum(self): return self.__sum def push(self, val): self.__sum += val Stack.push(self, val) def pop(self): val = Stack.pop(self) self.__sum -= val return val stack_object = AddingStack() for i in range(5): stack_object.push(i) print(stack_object.get_sum()) for i in range(5): print(stack_object.pop()) -> output -> 10 \ 4 \ 3 \ 2 \ 1 \ 0
Stacks : Adding / Accessing a property to an object Ex. variation w/ encapsulation
Ex. (the *__* is the only new change) class Stack: def __init__(self): self.*__*stack_list = [] stack_object = Stack() print(len(stack_object.__stack_list)) --> output --> AttributeError exception - the change invalidates the program - any class component whose name starts w/ 2 "_ _", it becomes private - this means that it can be accessed only from w/in the class (can't be seen from the outside world) ^ this is how Python implements the concept of encapsulation !
Stack from scratch : Obj. Approach w/Multiple Stacks Ex.2 *** review / break his up!!!
Ex. 3 obj. of class Stack, juggled up : class Stack: def __init__(self): self.__stack_list = [] def push(self, val): self.__stack_list.append(val) def pop(self): val = self.__stack_list[-1] del self.__stack_list[-1] return val little_stack = Stack() another_stack = Stack() fun_stack = Stack() little_stack.push(1) another_stack.push( ... ... little_stack.pop()+1) fun_stack.push(another_stack.pop()-2) print(funny_stack.pop()) ----> output ----> 0
Stacks : Coin Ex.
Ex. Imagine a STACK of COINS : > You aren't able to put a coin anywhere else but on the top of the stack > You can't get a coin off the stack from any place other than the top of the stack > You have to remove all the coins from the higher levels to get a coin that lies at the very bottom > The coin that came last onto the stack will leave first
Ex. of moving up thru hierarchy tree
Ex. of up moving up from a leaf thru tree branches to the root of the tree : ZeroDivisionError (leaf) : is a special case of a more general exception class named ArithmeticError ArithmeticError : is a special case of a more general exception class named Exception Exception is a special case of a more general class, BaseException (root) We can describe it like this : (note the direction of ↑ - always point to more general entity) : BaseException ↑Exception ↑ArithmeticError ↑ZeroDivisionError
Ex. of Operations on Strings
Ex. str1 = 'a' \ str2 = 'b' print(str1 + str2) \ print(str2 + str1) print(5 * 'a') \ print('b' * 4) --> output --> ab \ ba \ aaaaa \ bbbb - The + used against two or more strings produces a new string containing all the characters from its arguments (ORDER MATTERS - this overloaded +, in contrast to its numerical version, is NOT commutative) - The * operator needs a string & a # as arguments to produce a new string created by the nth replication of the argument's string (in this case, the order doesn't matter - you can put the # before the string, or vice versa - result will be the same)
*** name / review
Ex. the new pop func. def pop(self): val = Stack.pop(self) self.__sum -= val return val - defined the __sum var. , but haven't provided mthd. to get its val. (seems to be hidden, so we need to reveal it (& protect it from modific.'s) - so, define a new method : get_sum > only task will be to return __sum val. Ex. def get_sum(self): return self.__sum
Strings: The isspace() method
Identifies whitespaces only - it disregards any other character (the result is False then) Ex. print(' \n '.isspace()) --> output --> True print(" ".isspace()) --> output --> True print("mooo mooo mooo".isspace()) --------> --> output --> False
Adv. Try-Except Variant Ex.4 : replacing exceptions w/ more general ones
Ex. try: y = 1 / 0 except ZeroDivisionError: print("Oooppsss...") print("THE END.") -----> output -----> Oooppsss... THE END. Ex2. **same as above BUT** : "except ZeroDivisionError:" is "except ArithmeticError:" -----> output -----> Oooppsss... THE END. ^here, we replaced ZeroDivisionError w/ ArithmeticError (general class including Zerodiverror.) - bc of this, code's output is the same - also means that replacing exception name w/ either Exception or Base Exception won't change the output - each exception raised falls into the 1st matching branch - matching branch doesn't have to specify the same exception exactly - it's enough that the exception is more general (more abstract) than the raised one
Exceptions w/ Functions Rules
Exception raised can cross func. & mod. boundaries, and travel through the invocation chain looking for a matching except clause able to handle it - if there's no such clause > the exception remains unhandled > code is terminated (standard way Python solves the problem) > a diagnostic message is emitted
Methods : w/ multiple param.'s + self
For method to accept param.'s other than self : > place them after self in the method's definition > deliver them during invocation w/out specifying self (as before) Ex. class Classy: def method(self, par): print("method:", par) obj = Classy() obj.method(1) obj.method(2) obj.method(3) ---> output ---> method: 1 method: 2 method: 3
Slices w/ Strings
How slices work in the string world : Ex. alpha = "abdefg" print(alpha[1:3]) --> output --> bd print(alpha[3:]) --> output --> efg print(alpha[:3]) --> output --> abd print(alpha[3:-2]) --> output --> e print(alpha[-3:4]) --> output --> e print(alpha[::2]) --> output --> adf print(alpha[1::2]) --> output --> beg --> these ex.'s seem like they are starting from where indicated, going thru the whole string until it's exhausted, and then doing so based how many places are indicated last (with each character it's left off on becoming the new 0 in counting the next character to output) ????? 3rd spot is a step
IBAN and other similar program tasks
IBAN (International Bank Account Number) : European banks' algorithm of validating account #s against simple typos that can occur during rewriting IBAN-compliant account # consists of : - 2-letter country code (ex. FR for France, GB for Great Britain, so on) - 2 check digits used for validity checks - > fast & simple but not fully reliable tests > shows whether a # is invalid (distorted by typo) - Actual account # > up to 30 alphanumeric characters > the length of that part^ depends on the country Validation Requires : - (step 1) Check that the total IBAN length is correct as per the country (this program won't do that) - (step 2) Move the 4 initial characters to end of string > (i.e., the country code & the check digits) - (step 3) Replace each letter in string with 2 digits, thereby expanding the string : > where A = 10, B = 11 ... Z = 35 - (step 4) Interpret the string as a decimal integer & compute the remainder of that # on division by 97 > if the remainder is 1, check digit test passed > IBAN might be valid
Methods : Class w/ a constructor
If a method is named "__init__", it's a constructor (not a regular method) If a class has a constructor, it's invoked automatically & implicitly when obj. of the class is instantiated The constructor : > it must have the self parameter (it's set automatically, as usual) > it may (or need not) have more param.'s than just self : if it does, how the class name is used to create obj. must reflect __init__ def. > it can be used to set up obj. (properly initialize its internal state, create inst. var.'s, instantiate other obj.'s if existence needed, etc.) > it can't return a value, as it's designed to return a newly created obj. & that's it > it can't be invoked directly either from the obj. or from inside the class (but can invoke a constructor from obj.'s subclasses)
Exceptions raised inside functions
If an exception is raised inside a function , it can be handled : - inside the function - outside the function - ZeroDivisionError exception (being a concrete case of the ArithmeticError exception class) is raised inside bad_fun() & it doesn't leave the function - the function itself takes care of it Ex. def bad_fun(n): try: return 1 / n except ArithmeticError: print("Arithmetic Problem!") return None bad_fun(0) print("THE END.") -----> outputs -----> Arithmetic problem! THE END
Advanced Try-Except Variant Ex.2
If we remove the ZeroDivisionError branch from the advanced variant ex. & the user enters 0 as an input : - there are no dedicated branches for division by 0 - the raised exception falls into the general (unnamed) branch - thus, in this case, program says : "Oh dear, something went wrong... THE END."
Create your own exception Concept Ex.
Imagine you've created a brand new arithmetic, ruled by your own laws & theorems. It's clear too that division has been redefined & must behave differently way than routine dividing : > this new divis. should raise its own exception (MyZeroDivisionError, derived, but different, from built-in ZeroDivisionError & "pass"ed w/ no component) > an exception of this class can be - treated like a plain ZeroDivisionError, or considered separately > do_the_division() func. raises either a MyZeroDivisionError or ZeroDivisionError exception, depending on the arg.'s value func. is invoked 4x total - first 2 invoc.'s are handled using only 1 except branch (the more general one) & the last 2 ones w/ 2 different branches, able to distinguish the exceptions (order of branches makes a fundamental difference!!!)
OOP approach vs. procedural style
In the procedural approach : - it's possible to distinguish the completely separate worlds of data & code - world of data is populated w/ var.'s of different kinds - world of code is inhabited by code grouped into mod.'s & func.'s Func.'s are able to use data & abuse data (use it in an unauthorized manner, like if sine function gets a bank account balance as a parameter) Some special kinds of data (methods) can use func.'s - these are functions which are invoked from w/in the data, not beside them object approach suggests a completely different way of thinking - the data & the code are enclosed together in the same world, divided into classes
3.3 Summarized
Instance var. = a property whose existence depends on creation of obj. > every obj. can have a different set of instance variables > they can be freely added & removed from obj.'s during their lifetime. > all obj. ins. var.'s are stored in a dedicated dictionary "__dict__" contained in every obj. separately 2. An inst. var. can be private when name starts w/ "__" (but such a property is still accessible from outside the class using a mangled name "_ClassName__Private PropertyName") 3. A class var. is a property which exists in exactly 1 copy & doesn't need any created obj. to be accessible > such var.'s aren't shown as __dict__ content > all a class's class var.'s are stored in a dedicated dict. "__dict__" contained in every class separately 4. A func. "hasattr()" can be used to determine if any obj./class contains a specified property Ex class Sample: gamma = 0 # Class var. def __init__(self): self.alpha = 1 # Inst. var. self.__delta = 3 # Priv. inst var. obj = Sample() obj.beta = 2 # Another inst. var. (exists only in "obj" inst.) print(obj.__dict__) ---> output --> {'alpha': 1, '_Sample__delta': 3, 'beta': 2}
Strings: The find() method [two-param. version]
It looks for a substring and returns the index of ALL occurrence of this substring (from any position) : - 2nd arg. specifies index at which the search will be started (it doesn't have to fit inside the string) Ex1. print('kappa'.find('a', 2)) --> output --> 4 (among the 2 "a" letters, only the 2nd will be found) Ex2. the_text = """A variation of the lorem ipsum text has been used in typesetting since the 1960s or earlier, when it was popularized by ads for Letraset transfer sheets.""" fnd = the_text.find('the') while fnd != -1: print(fnd) fnd = the_text.find('the', fnd + 1) ------> output ------> 15 \ 71
Strings: The find() method [one-param. version]
It looks for a substring and returns the index of first occurrence of this substring : - it's similar to index() - it's safer - doesn't generate error for arg. containing a non-existent substring (it returns -1 then) - it works with strings only - don't try to apply it to any other sequence - don't use find() if you only want to check if a single character occurs w/in a string - the in operator will be significantly faster Ex. print("Eta".find("ta")) --> output --> 1 print("Eta".find("mma")) --> output --> -1 t = 'theta' print(t.find('eta')) --> output --> 2 print(t.find('et')) --> output --> 2 print(t.find('the')) --> output --> 0 print(t.find('ha')) --> output --> -1
Exceptions outside functions
It's also possible to let the exception propagate outside the function : Ex. def bad_fun(n): return 1 / n try: bad_fun(0) except ArithmeticError: print("What happened? An exception was raised!") print("THE END.") -----> outputs -----> What happened? An exception was raised! THE END. - problem has to be solved by the invoker (or by invoker's invoker, etc.)
I18N (Internationalization)
Latin alphabet isn't sufficient for everyone - - different languages use completely different (sometimes complex) alphabets - it's necessary to come up with something more flexible & capacious than ASCII to make all the software in the world workable to internationalization - "internationalization" is commonly shortened to I18N - first, there's an "I" at the front of the word - then, 18 different letters & finally, N at the end - I18N is a standard software presently - programs should be written w/ ability for use all around the world : among different cultures & languages & alphabets
Strings: The lower() method
Makes a copy of a source string, replaces all upper-case letters with their lower-case counterparts - source string remains untouched - doesn't take any parameters - if string doesn't contain any upper-case characters, the method returns the original string Ex. print("SiGmA=60".lower()) --> output --> sigma=60 (as you can see, having different types of characters works fine here)
Strings: The upper() method
Makes a copy of the source string, replaces all lower-case letters with their upper-case counterparts Ex. print("I know that I know nothing. Part 2.".upper()) -----------> output ----------> I KNOW THAT I KNOW NOTHING. PART 2.
Strings: The strip() method
Makes a new str lacking all leading & trailing whitespa. - combines the effects caused by rstrip() and lstrip() Ex. print("[" + " aleph ".strip() + "]") -> output -> [aleph]
Strings: The swapcase() method
Makes a new string by swapping the case of all letters w/in the source string: lower-case characters become upper-case, & vice versa - all other characters remain untouched Ex. print("I know that I know nothing.".swapcase()) --> output --> i KNOW THAT i KNOW NOTHING.
Mangling a Class Var.'s Name
Mangling a class var.'s name : [ ex. changing any instance of "counter" --> "__counter" & substituting this for orig. print func.'s : print(example_object_1.__dict__, example_object_1._ExampleClass__counter) print(example_object_2.__dict__, example_object_2._ExampleClass__counter) print(example_object_3.__dict__, example_object_3._ExampleClass__counter) ] Has the same effects as those you're already familiar w/ & here, outputs --> {'_ExampleClass__first': 1} 3 {'_ExampleClass__first': 2} 3 {'_ExampleClass__first': 4} 3
Errors, failures, & other plagues
Murphy's law : Anything that could go wrong, will. If our code's execution can go wrong, too, it will. Ex. import math x = float(input("Enter x: ")) y = math.sqrt(x) print("The square root of", x, "equals to", y) 2 possible ways it can "go wrong" : - a user is able to enter a completely arbitrary string of characters, so there's no guarantee that the string can be converted into a float value ! - the sqrt() function fails if it gets a negative arg. -------> output -------> Enter x: Abracadabra Traceback (most recent call last): File "sqrt.py", line 3, in <module> x = float(input("Enter x: ")) ValueError: could not convert string to float: 'Abracadabra' [ --or-- ] Enter x: -1 Traceback (most recent call last): File "sqrt.py", line 4, in <module> y = math.sqrt(x) ValueError: math domain error
Function which behaves almost exactly like the original split() method !!! I did it all myself !!! WOOHOO!!!!!!
My Program ::: def mysplit(strng): emptee = ["" , " "] for ch in strng : if strng not in emptee : separat = strng.replace(' ', "','") fixupa = ("['" + separat + "']") fixupb = fixupa.replace("'',", "") spli = fixupb.replace(",''", "") return spli neh = "[ ]" return neh print(mysplit("To be or not to be, that is the question")) print(mysplit("To be or not to be,that is the question")) print(mysplit(" ")) print(mysplit(" abc ")) print(mysplit(""))
Syntax of Subclass Adding *** review / break his up!!!
Note the syntax: - you specify the superclass's name (this is the class whose constructor you want to run) - you put a dot (.)after it - you specify the name of the constructor - you have to point to the object (the class's instance) which has to be initialized by the constructor - this is why you have to specify the arg. & use the self va. here - invoking any method (including constructors) from outside the class never requires you to put the self argu. at the arg.'s list - invoking a method from w/in the class demands explicit usage of self arg. & it has to be put 1st on the list > recommended practice is to invoke the superclass's constructor before any other initializations you want to perform inside the subclass
Stack from scratch : Obj. Approach w/Multiple Stacks Ex. *** review / break his up!!!
Now that you have such a class..... > you can now have more than 1 stack behaving in the same way > each stack will have its own copy of private data, but will utilize the same set of methods Ex. class Stack: def __init__(self): self.__stack_list = [] def push(self, val): self.__stack_list.append(val) def pop(self): val = self.__stack_list[-1] del self.__stack_list[-1] return val stack_object_1 = Stack() stack_object_2 = Stack() stack_object_1.push(3) stack_object_2.push( ... ... stack_object_1.pop()) print(stack_object_2.pop()) ---> output ---> 3 \ 3 - There are 2 stacks created from the same base class - (they each work independently) - You can make more of if desired
Stacks: Objective App. - Solutions to Procedural App. Disadvantages
OO Solutions to Procedural Disadv. : - Encapsulation : The ability to hide (protect) selected values against unauthorized access > the encapsulated values cannot accessed or modified if you want to use them exclusively - When you have a class implementing all the needed stack behaviors, you can produce as many stacks as you want (needn't copy or replicate any part of the code) - The ability to enrich the stack w/ new func.'s comes from inheritance > you can create a new class (a subclass) which inherits all the existing traits from the superclass, & adds some new ones
object-oriented approach
OOP approach : - firstly, procedural style of programming works very well for specific types of projects (generally, not very complex ones and not large ones, but there are lots of exceptions to that rule) - object approach is young & particularly useful when applied to big and complex projects carried out by large teams consisting of many developers - Python is a universal tool for both
object approach
Object approach suggests a completely different way of thinking - the data & the code are enclosed together in the same world, divided into classes Every class is like a recipe which can be used when you want to create a useful object (this is where the name of the approach comes from) - can produce as many objects as you need to solve your problem - may be modified if they are inadequate for specific purposes - new classes may be created > inherit properties & methods from originals & usually add new ones, creating new, more specific tools Every object has a set of traits (they're called properties / attributes) & is able to perform a set of activities (which are called methods)
What does an object have?
Object programming assumes that every existing object may be equipped w/ 3 grps of attributes : > an object has a name that uniquely identifies it w/in its home namespace (tho there's sometimes anonym. obj.s) > an object has a set of individual properties which make it original, unique or outstanding (tho it's possible that some objects may have no properties at all) > an object has a set of abilities to perform specific activities, able to change the object itself, or some of the other objects
OP Classes & Objects (Official definitions/relation)
Object programming is the art of defining and expanding classes A class = a set of objects (a model of a very specific part of reality, reflecting properties / activities found in the real world) > classes form a hierarchy - each superclass is more general (more abstract) than any of its subclasses - each subclass is more specialized (more specific) than its superclass [we've presumed that a class may only have 1 superclass --> isn't always true] An object = a being belonging to a class > it's an incarnation of the requirements, traits, & qualities assigned to a specific class - an object belonging to a specific class belongs to all the superclasses at the same time - also means that any object belonging to a superclass may not belong to any of its subclasses Ex. Your dog (or your cat) is an object included in the domesticated mammals class - explicitly means that it's included in the animals class also
OOP
Objects are incarnations of ideas expressed in classes :: cheesecake on your plate is an incarnation of the idea expressed in a recipe printed in an old cookbook - objects interact with each other, exchanging data or activating their methods - no clear border btwn data and code: they live as one in objects A properly constructed class (& its objects) are able to : - protect the sensible data - hide it from unauthorized modifications These concepts aren't abstract : - all are taken from real-life experiences, & are extremely useful in computer programming They don't create artificial life : - they reflect real facts, relationships, & circumstances
Comparing Strings : Sorting - sort() function
Ordering is performed in situ by the sort() method : - affects the list itself - no new list is created Ex. second_greek = ['omega', 'alpha', 'pi', 'gamma'] print(second_greek) ---> ['omega', 'alpha', 'pi', 'gamma'] second_greek.sort() print(second_greek) ---> ['alpha', 'gamma', 'omega', 'pi']
Sudoku Lab
PE2 : 2.5.1.11
Class Variable
Property which exists in just 1 copy & is stored outside any object (even if there are no objects in the class) [recall - no instance variable exists if there is no obj. in the class]
Exceptions : hierarchy tree
Python 3 defines: - 63 built-in exceptions > all of them form a tree-shaped hierarchy > although the tree is a bit weird w/ it's root located on top > some are more general (they include other exceptions) > others are completely concrete (they represent themselves only) > we can say that the closer to the root an exception is located, the more general (abstract) it is > in turn, the exceptions located at the branches' ends are concrete (we can call them leaves)
Stack from scratch : The object approach *** review / break THIS up!!!
Python assumes that func.'s like those func.'s (methods) implementing the push and pop operations (a class activity) should be immersed inside the class body - just like a constructor We want to invoke these functions to push & pop val.'s, so they should both: - be accessible to every class's user (public) - thus you can't begin its name with 2 (or more) "_ _" as in private - name may have only 0 or 1 trailing "_" Ex. class Stack: def __init__(self): self.__stack_list = [] def push(self, val): self.__stack_list.append(val) def pop(self): val = self.__stack_list[-1] del self.__stack_list[-1] return val stack_object = Stack() \ stack_object.push(3) stack_object.push(2) stack_object.push(1) \ print(stack_object.pop()) [typed 3x] - the func.'s look familiar, but have more parameters than their procedural counterparts - here, both func.'s have the parameter "self" @ the 1st position of the parameters list - all methods must have "self" param. (it plays the same role as the 1st constructor parameter) - it allows the mthd to access entities (properties & activities/methods) carried out by the actual object (you can't omit it) - every time Python invokes a method, it implicitly sends the current object as the first arg. > this means a method is obligated to have at least 1 param.- used by Python itself - you have no influence on it > if the mthd needs no param. at all, this one must be specified anyway > if it's designed to process just 1 param. , you have to specify 2, & the 1st one's role is still the same The way methods are invoked from w/in the __stack_list var. : - 1st stage delivers the object as a whole → self - next, you need to get to the __stack_list list → self.__stack_list - with __stack_list ready to be used, can perform the third & last step → self.__stack_list.append(val) --> Class declaration is complete All its components have been listed The class is ready for use
all info summary from w3 schools!
Python is an OOP language > almost everything in Python is an obj. , w/ its properties & methods > a class is like an obj. constructor, or a 'blueprint' for creating obj.'s Creating a class : Ex. keyword "class" + (Name) ---> class + (MyClass) ^add a property named "x" : Ex. class myClass: x = 5 Create an obj. from class & print the value of "x" : Ex. p1 = MyClass() print(p1.x) These ex.'s ^ are classes & obj.'s in their simplest form (not useful in real-life app.'s) All classes have "__init__()" - it's always executed when the class is *initiated* - it's called automatically every time the class is being used to create a new obj., assign values to obj. prop.'s, or other oper.'s necessary If a class contains a constructor (a method named __init__) it can't return any value & can't be invoked directly The self parameter : - is used to represent the current instance of the class - must be the 1st param. of any func. in the class - is used to access var.'s that belong to the class - is used to refer to the obj & instance attributes & methods of the class - need not be named "self" Create a class "Person", use the __init__() func. to assign values for name & age: Ex. class Person: def __init__(self, name, age): self.name = name self.age = age \ p1 = Person("John", 36) \ print(p1.name) print(p1.age) Objects can also contain methods : - methods in obj.'s are func.'s belonging to the obj Insert a function that prints a greeting, and execute it (method) on the p1 object : Ex. class Person: def __init__(self, name, age): self.name = name self.age = age def myfunc(self *-> abc*): \ print("Hello my name is " + self*-> abc*.name) \ p1 = Person("John", 36) p1.myfunc() [ *-> abc* (representing that the param. doesn't HAVE to be named "self") ] Modifying Obj. Properties : set age of p1 --> p1.age = 36 Delete Obj./Obj. Properties: - can use del keyword delete the p1 object --> del p1 del. age property from p1 --> del p1.age Pass Statement : - class def.'s can't be empty - if you have one^ w/ no content...to avoid error... : Ex. class Person: pass Python Inheritance : allows us to define a class that "inherits" all methods & prop.'s from another class ; a way of building a new class by using an already defined repertoire of traits Parent class : - the class being "inherited from", (a.k.a base class) - any class can be one Create a (parent) class named Person, with... firstname & lastname properties & a printname method : Ex. class Person: def __init__(obj, firstn, lastn): obj.firstname = firstn obj.lastname = lastn def printname(obj): print(obj.firstname, .. .. obj.lastname) x = Person("John", "Doe") x.printname() --> John Doe Child class : - the class that "inherits from" another class, (a.k.a derived class) Create a class named "Student", which will inherit the properties & methods from the "Person" class : Ex. class Student(Person): pass __init__() func. w/ child class : [ * __init__() func. auto-called every time class is being used to create a new obj. ] - adding this will stop / override child class from inheriting parent's __init__() Create code adding __init__() to the child class (Student) & a call to parent's __init__(): Ex. class Student(Person): def __init__(self, firstn, lastn): Person.__init__(self, firstn, .. .. lastn) Use the "Student" class to create an obj., & then execute printname mthd. : Ex. x = Student("Amy", "Olsen") x.printname() super() function : - makes child class auto-inherit all the methods & prop.'s from it's parent w/out having to use the name of the parent elem. Ex. class Student(Person): def __init__(self, firstn,lastn): super().__init__(firstn,lastn) Add a prop. "graduationyr" to the Student class & "year" param. to __init__() w/ the year 2019 as a var. passed into Student class upon creating student obj.'s: Ex. class Student(Person): def __init__(self, firstn,lastn): super().__init__(firstn,lastn) self.graduationyr = year x = Student("Amy","Ro",2019) Add a method called welcome to the Student class: Ex. class Student(Person): def __init__(self, firstn, lastn.. ... ,year): super().__init__(firstn, lastn) self.graduationyear = year def welcome(self): print... ("Welcome",self.firstname, self.lastname, "to the class of", self.graduationyear) *NOTE : If you add a mthd. in the child class w/ the same name as a function in the parent class, the inheritance of the parent method will be overridden* Iterator : - an object that contains a countable # of values - implements the protocol, to create an obj/class as iterator consisting of mthd.'s : __iter__() & __next__() - iterable obj.'s include : lists, tuples, dictionar.'s, sets (^ iterable containers that u can get an iterator from) Tuple Ex. mytuple = ("apple", ... ..."banana", "cherry") myit = iter(mytuple) print(next(myit)) -> apple print(next(myit)) -> banana print(next(myit)) -> cherry String Ex. mystr = "banana" myit = iter(mystr) print(next(myit)) -> b print(next(myit)) -> a print(next(myit)) -> n print(next(myit)) -> a print(next(myit)) -> n print(next(myit)) -> a for-loop: iterating thru tuple Ex. mytuple = ("apple", "banana" ,"cherry") for x in mytuple: print(x) ---> apple ---> banana ---> cherry for-loop: iterating thru str Ex. mystr = "banana" for x in mystr: print(x) -> b -> a -> n -> a -> n -> a (for loop creates an iterator obj. & executes the next() method for each loop)
Checking an attribute's existence Ex.
Python's attitude to obj. instantiation raises an issue, as opposed to other languages - you may not expect that : all obj.'s of the same class ------> ---> have the same sets of properties - obj. created by constructor can have only 1 of 2 possible attributes: a or b : Ex. class ExampleClass: def __init__(self, val): if val % 2 != 0: self.a = 1 else: self.b = 1 example_object = ExampleClass(1) print(example_object.a) print(example_object.b) ------> output ------> 1 \ Traceback (most recent call last) : File ".main.py", line 11, in <module> print(example_object.b) AttributeError: 'ExampleClass' object has no attribute 'b' [Accessing a non-existing obj. (class) attribute causes AttributeError excep.]
Python Strings
Python's strings are immutable sequences : - any string can be empty. Its length is 0 then - don't forget that a backslash (\) used as an escape character isn't included in the string's total length !!! Ex. 1 word = 'by' print(len(word)) --> output --> 2 Ex. 2 empty = ' ' print(len(empty)) --> output --> 0 Ex. 3 i_am = 'I\'m' print(len(i_am)) --> output --> 3
Python Strings vs. Lists --> What you CAN'T do w/ Strings
Python's strings are immutable, meaning the similarity of strings & lists is limited- not everything you can do with a list may be done w/ a string : - del instruction can't be used to remove anything from a string, ONLY to remove the string as a whole Ex. alphabet = "abcdefghijklmnopqrstuvwxyz" del alphabet[0] (but "del alphabet" would do what you want it to) - append() method can't be used (can't expand strings in any way) Ex. alphabet = "abcdefghijklmnopqrstuvwxyz" alphabet.append("A") - insert() method can't be used (can't expand strings in any way) Ex. alphabet = "abcdefghijklmnopqrstuvwxyz" alphabet.insert(0, "A")
Strings vs. Numbers : string-number
Reverse conversion (string-number) - possible only when string represents a valid # - if the condition is not met, expect ValueError - int() function - to get an integer - float() to get a floating-point value - using an integer as floats() arg. works, but using a float in int() doesn't Ex. si = '13' sf = '1.3' itg = int(si) flt = float(sf) print(itg + flt) ---> 14.3
Hint for Object Attribute Groups
Small hint that doesn't always work : (can help you identify any of the 3^) When you describe an object, you probably define the object's : > NAME, if you use a NOUN > PROPERTY, if you use an ADJ. > ACTIVITY, if you use a VERB Ex1. A pink Cadillac went quickly. > Obj. Name = Cadillac > Home Class = Wheeled Vehicles > Property = Color (Pink) > Activity = Go (Quickly) Ex2. Max is a large cat who sleeps all day. > Obj. Name = Max > Home Class = Cat > Property = Size (Large) > Activity = Sleep (All Day)
Comparing Strings : Sorting - sorted() function
Sorted() function : - func. takes 1 arg. (a list) & returns a new list, filled w/ the sorted arg.'s elem.'s > the original list remains untouched ( Note: this description is a bit simplified ) Ex. first_greek = ['omega', 'alpha', 'pi', 'gamma'] first_greek_2 = sorted(first_greek) print(first_greek) ---> ['omega', 'alpha', 'pi', 'gamma'] print(first_greek_2) ---> ['alpha', 'gamma', 'omega', 'pi']
Comparing Strings : Sorting
Sorting is a very sophisticated case of comparing Sort lists containing strings : - such an operat. is very common in the real world > any time you see a list of names, goods, titles, or cities, you expect them to be sorted 2 possible ways : - sorted() func. - sort() func.
StopIteration statement
StopIteration statement : > The previous ex. would continue forever if you had enough next() statements, or if used in a for loop > To prevent indefinite iter. in __next__() method, we can add a terminating condition to raise an error if iteration is done a certain # of times Ex2. (stop after 20 iter.'s) : class MyNumbers: def __iter__(self): self.a = 1 return self def __next__(self): if self.a <= 20: x = self.a self.a += 1 return x else: raise StopIteration myclass = MyNumbers() myiter = iter(myclass) \ for x in myiter: print(x) --> output --> 1 - 20 (printed individually)
Strings as Sequences: Indexing
Strings as Sequences : - Python strings are sequences - strings AREN'T lists, BUT can treat them like lists in many particular cases ----------------------------------------------------------- Indexing to access any of a string's characters : Ex.* the_string = 'silly walks' for ix in range(len(the_string)): print(the_string[ix], end=' ') print() --> output --> s i l l y w a l k s > trying to pass a string's boundaries cause exception > negative indices behave as expected as well ! Ex2.** for ix in range(len(the_string)): print(the_string[ix], end=' ') print() --> output --> s s s s s s s s s s s Ex3. *** for ix in range(len(the_string)): print(the_string[-ix], end=' ') print() --> output --> s s k l a w y l l i ----------------------------------------------------------- Iterating through the strings : Ex. the_string = 'silly walks' for character in the_string: print(character, end=' ') print() --> output --> s i l l y w a l k s
Operations on Strings
Strings have their own set of operations although they're rather limited compared to #'s : Strings can be :: > concatenated (joined) : (performed by the + operator - it's NOT an addition) > replicated : (performed by the * operator it's NOT a multiplication) >> NOTE : shortcut variants of the above operators are also applicable for strings (+= and *=) ^ (these are examples of overloading) ^
Stacks: Full Procedural Approach Program (push-pop)
The completed program : - pushes 3 #'s onto the stack - pulls the 3 #'s off - prints their values on the screen Ex. stack = [] def push(val): stack.append(val) def pop(): val = stack[-1] del stack[-1] return val push(3) push(2) push(1) print(pop()) print(pop()) print(pop()) ----> output ----> 1 \ 2 \ 3
Strings: the count() method
The count() method counts all occurrences of the elem. inside the seq. > absence of elem. doesn't cause any problems! Ex. print("abcabc".count("b")) --> output --> 2 print('abcabc'.count("d")) --> output --> 0
Creating your own exception
The exceptions hierarchy is not closed or finished - it's always extendable if you want/need to create your own world populated w/ your own exceptions - may be useful when creating a complex mod. which detects errors & raises exceptions, & you want exceptions to be easily distinguishable from any others Python brings > This is done by defining your own, new exceptions as subclasses derived from predefined ones - to create an exception which will be utilized as a specialized case of any built-in exception, derive it from just this one - to build your own heir., w/out it being closely connected to Python's exception tree, derive it from any of the top exception classes (like Exception)
Virtual
The method that is redefined in any of the superclasses, thus changing the behavior of the superclass
Strings: The rfind() method
The one-, two-, three-parameter methods all do nearly the same things as their counterparts (find()'s - searching to find something, where that search starts, where that search will end) BUT begins their searches from the END of the string, not the beginning (hence the prefix r, for right) Ex. print("tau tau tau".rfind("ta")) --> output --> 8 print("tau tau tau".rfind("ta", 9)) --> output --> -1 print("tau tau tau".rfind("ta", 3, 9)) --> output --> 4
Strings: the center() method
The one-parameter variant of the center() method makes a copy of the original string, trying to center it inside a field of a specified width : - the centering is actually done by adding some spaces before and after the string - if the target field's length is too small to fit the string, the original string is returned (we've used brackets to clearly show where the centered string begins & terminates) : Ex. print('[' + 'alpha'.center(10) + ']') -> output -> [ alpha ] print('[' + 'Beta'.center(2) + ']') -> output -> [Beta] print('[' + 'Beta'.center(4) + ']') -> output -> [Beta] print('[' + 'Beta'.center(6) + ']') -> output -> [ Beta ] The two-parameter variant of center() makes use of the charac. from the 2nd argument, instead of a space Ex. print('[' + 'gamma'.center(20, '*') + ']') ---> output ---> [*******gamma********]
Comparing Strings
These operators can compare str..'s in addition to #s : == != / > >= / < <= - Python isn't in at all aware of subtle linguistic issues - just compares code point val.'s , charact. by charact. - thus results of ^ oper.'s may sometimes be surprising
Defining Classes & Objects summarized
To define a Python class, use the : "class" **keyword** Ex. class This_Is_A_Class: pass To create an object of the previously defined class, use the class... : ...as if it's a function Ex. this_is_an_object = This_Is_A_Class()
BaseException
Tree Location: BaseException Description: the most general (abstract) of all Python exceptions - all other exceptions are included in this one; it can be said that the following two except branches are equivalent: except: and except BaseException:.
ArithmeticError
Tree Location: BaseException ← Exception ← ArithmeticError Description: an abstract exception including all exceptions caused by arithmetic operations like zero division or an argument's invalid domain
OverflowError
Tree Location: BaseException ← Exception ← ArithmeticError ← OverflowError Description: a concrete exception raised when an operation produces a number too big to be successfully stored Ex. # The code prints subsequent values of exp(k), k = 1, 2, 4, 8, 16, ...# from math import exp ex = 1 try: while True: print(exp(ex)) ex *= 2 except OverflowError: print('The number is too big.')
AssertionError
Tree Location: BaseException ← Exception ← AssertionError Description: a concrete exception raised by the assert instruction when its argument evaluates to False, None, 0, or an empty string Ex. from math import tan, radians angle = int(input('Enter integral angle in degrees: ')) # We must be sure that angle != 90 + k * 180 assert angle % 180 != 90 print(tan(radians(angle)))
LookupError
Tree Location: BaseException ← Exception ← LookupError Description: an abstract exception including all exceptions caused by errors resulting from invalid references to different collections (lists, dictionaries, tuples, etc.)
IndexError
Tree Location: BaseException ← Exception ← LookupError ← IndexError Description: a concrete exception raised when you try to access a non-existent sequence's element (e.g., a list's element) Ex. # The code shows an extravagant way of leaving the loop : # the_list = [1, 2, 3, 4, 5] ix = 0 do_it = True while do_it: try: print(the_list[ix]) ix += 1 except IndexError: do_it = False print('Done')
KeyError
Tree Location: BaseException ← Exception ← LookupError ← KeyError Description: a concrete exception raised when you try to access a collection's non-existent element (e.g., a dictionary's element) Ex. # How to abuse the dictionary & how to deal with it? # dictionary = { 'a': 'b', 'b': 'c', 'c': 'd' } ch = 'a' try: while True: ch = dictionary[ch] print(ch) except KeyError: print('No such key:', ch)
MemoryError
Tree Location: BaseException ← Exception ← MemoryError Description: a concrete exception raised when an operation cannot be completed due to a lack of free memory. Ex. # This code causes the MemoryError exception. Warning: executing this code may affect your OS. Don't run it in production environments! string = 'x' # try: while True: string = string + string print(len(string)) except MemoryError: print('This is not funny!')
ImportError
Tree Location: BaseException ← Exception ← StandardError ← ImportError Description: a concrete exception raised when an import operation fails Ex. # 1 of these imports will fail - which?# try: import math import time import abracadabra except: print('1 of your imports has failed.')
KeyboardInterrupt
Tree Location: BaseException ← KeyboardInterrupt Description: a concrete exception raised when the user uses a keyboard shortcut designed to terminate a program's execution (Ctrl-C in most OSs); if handling this exception doesn't lead to program termination, the program continues its execution. Note: this exception is not derived from the Exception class. Run the program in IDLE. Ex. # This code cannot be terminated # by pressing Ctrl-C. from time import sleep seconds = 0 while True: try: print(seconds) seconds += 1 sleep(1) except KeyboardInterrupt: print("Don't do that!")
Stacks: Procedural Approach - Putting & Storing a Value onto a Stack
We need to decide how to store the val.'s which will arrive onto the stack : - use the simplest of methods - employ a list for this job Assuming that the : - size of the stack is not limited at all - last element of the list stores the top element > create an empty stack (a list) > define a func. (push) - func. puts a value onto the stack - it takes one parameter (^that val.) - returns nothing - BUT appends the parameter value to the the end of the stack Ex. stack = [] def push(val): stack.append(val)
overriding
When more than 1 of the superclasses defines an entity of a particular name : > Python looks for an entity from bottom to top - it is fully satisfied w/ 1st entity of the desired name - this feature can be used intentionally to modify default (or previously def.'d) class behaviors when any of its classes needs to act in a different way to its ancestor - In the ex., Level1 & Level2 classes def. a method "fun()" & a property, "var" - Level3 class obj. won't be able to access the 2 copies of each entity - the entity defined later overrides the same entity defined earlier (inheritance-wise)
3-level inheritance line Ex. Explained
When you try to access any object's entity, Python will try to find it (in this order): - inside the obj. itself - in all classes involved in the obj.'s inheritance line from bottom to top - if both fail - exception (AttributeError) is raised 1st condition may need additional attention (as obj.' s deriving from a particular class may have different sets of attrib.'s, & some attrib.'s may be added to the obj. long after its creation)
Ex. Question on Var.'s
Which of the Python class properties are instance var.'s & which are class var.'s? Which are private? class Python: population = 1 victims = 0 def __init__(self): self.length_ft = 3 self.__venomous = False - pop. & victims are class - var.'s / len. & __venomous are inst. var.'s (latter is priv.)
Ex. Checking if an obj. contains a prop.
Write an expression which checks if the version_2 object contains an instance property "constrictor" ---> hasattr(version_2, 'constrictor')
Your first class
You can create as many classes as you need, and the one's you define has nothing to do with the object (existence of a class doesn't mean that any of the compatible objects will automatically be created) - A recipe itself isn't able to create a meal - you have to create it yourself, & Python allows you to do this w/ code : Ex. class TheSimplestClass: pass > def. begins with keyword "class" > a name identifier follows "class" > add colon (:), as classes, like func.'s, form their own nested block > content inside the block define all the class's properties & activities > "pass" keyword fills class w/ nothing (doesn't contain any methods / prop's)
How can the assert instruction be used?
You may want to put it into your code where: - you want to be absolutely safe from evidently wrong data - where you aren't absolutely sure that the data has been carefully examined before (like inside a function used by someone else) - raising an AssertionError exception secures your code from producing invalid results & clearly shows the nature of the failure - assertions don't supersede exceptions or validate the data - they are their supplements exceptions & data validation : careful driving :: assertion : airbag
Composition Ex. Explained
[ - the previous approach led us to a hierarchy of classes in which the top-most class was aware of the general rules used in turning the vehicle, but didn't know how to control the right components (wheels/tracks) - the subclasses applied this ability by introducing specialized mechanisms ] > The class is still aware of how to turn the vehicle, but the actual turn is done by a specialized obj. stored in a property "controller" > The controller is able to control the vehicle by manipulating the relevant vehicle's parts >2 classes "Tracks"&"Wheels" - know how to control the vehicle's direction > "Vehicle" class can use any of the available controllers (the already defined 2, or any others in the future) - the controller itself is passed to the class during initialization So, the vehicle's ability to turn is composed using an external obj., NOT implemented inside the Vehicle class! We now have a universal vehicle that can have either tracks or wheels
examples more info
__iter__() method : > acts similar to __init__() [func. that all classes have for allowing for some initializing when the obj. is being created] > you can do operations (initializing etc.), but must always return the iterator obj. itself __next__() method : > also allows for doing operations, & must return the next item in the sequence Create an iterator that returns #'s, starting w/ 1, & each sequence will increase by 1 (returning 1, 2, 3, 4, 5) : Ex1. class MyNumbers: def __iter__(self): self.a = 1 return self def __next__(self): x = self.a self.a += 1 return x myclass = MyNumbers() myiter = iter(myclass) print(next(myiter)) [5 x] --> 1 \ 2 \ 3 \ 4 \ 5
Inheritance : isinstance()
a func. that can detect crucial to know if an obj. is of a certain class or not, (having / not having certain characteristics) : [Being an instance of a class means that the object (the cake) has been prepared using a recipe contained in either the class or one of its superclasses] Format Ex. : isinstance( ... ... objectName, ClassName) - returns True if the obj. is an instance of the class if a subclass contains (at least) the same equipment as any of its superclasses, obj.'s of the subclass can do the same as obj.'s derived from the superclass : it's an instance of its home class & any of its superclasses
Python Strings are capable!
a string's immutability doesn't completely limits your ability to operate with them : - you just have to remember it & implement your code in a slightly different way - creating a new copy of a string each time you modify its contents does slightly worsen effectiveness of the code, BUT it's not a problem at all Ex. alphabet = "bcdefghijklmnopqrstuvwxy" alphabet = "a" + alphabet alphabet = alphabet + "z" print(alphabet) --> output --> abcdefghijklmnopqrstuvwxyz
Method Resolution Order (MRO)
a way / a strategy in which a particular programming language scans through the upper part of a class's hierarchy in order to find the mthd. it currently needs - Python's MRO can't be bent or violated - not just bc that's the way Python works, but also bc it's a rule you have to obey
Strings Methods Summarized
capitalize() - changes all string letters to capitals center() - centers the string inside the field of a known length count() - counts the occurrences of a given character join() - joins all items of a tuple/list into one string lower() - converts all the string's letters into lower-case letters lstrip() - removes the white characters from the beginning of the string replace() - replaces a given substring with another rfind() - finds a substring starting from the end of the string rstrip() - removes the trailing white spaces from the end of the string split() - splits the string into a substring using a given delimiter strip() - removes the leading and trailing white spaces swapcase() - swaps the letters' cases (lower to upper and vice versa) title() - makes the first letter in each word upper-case upper() - converts all the string's letter into upper-case letters ------------------------------------------------------------ String content can be determined using the following methods (all of them return Boolean values): ------------------------------------------------------------ endswith() - does the string end with a given substring? isalnum() - does the string consist only of letters and digits? isalpha() - does the string consist only of letters? islower() - does the string consists only of lower-case letters? isspace() - does the string consists only of white spaces? isupper() - does the string consists only of upper-case letters? startswith() - does the string begin with a given substring?
Diamond Problem Format Ex.
class A: pass class B(A): pass class C(A): pass class D(B, C): pass d = D()
Methods : Class w/ a constructor Ex. 3
class Classy: def __init__(self, value): self.var = value obj_1 = Classy("object") print(obj_1.var) ---> output ---> object
__module__ Ex.
class Classy: pass print(Classy.__module__) obj = Classy() print(obj.__module__) output--> __main__ \ __main__
type() & __name__ together Ex.
class Classy: pass print(Classy.__name__) obj = Classy() print(type(obj).__name__) -> output -> Classy \ Classy NOTE: print(obj.__name__) ^this^ would cause an error
Class Variable Ex.
class ExampleClass: counter = 0 def __init__(self, val = 1): self.__first = val ExampleClass.counter += 1 example_object_1 = ExampleClass() example_object_2 = ExampleClass(2) print(example_object_1.__dict__, example_object_1.counter) print(example_object_2.__dict__, example_object_2.counter) print(example_object_3.__dict__, example_object_3.counter) ------> output ------> {'_ExampleClass__first': 1} 3 {'_ExampleClass__first': 2} 3 {'_ExampleClass__first': 4} 3
Class vs Object Variable Distinguishing Ex.
class ExampleClass: varia = 1 def __init__(self, val): ExampleClass.varia = val print(ExampleClass.__dict__) example_object = ExampleClass(2) print(ExampleClass.__dict__) print(example_object.__dict__) ------> output ------> {'__module__': '__main__', 'varia': 1, '__init__': <function ExampleClass.__init__ at 0x7fed146c80e0>, '__dict__': <attribute '__dict__' of 'ExampleClass' objects>, '__weakref__': <attribute '__weakref__' of 'ExampleClass' objects>, '__doc__': None} {^ x2 ^ , but 'varia': 1 --> 'varia': 2} \ { }
Python looks for obj. components Ex.
class Left: var = "L" var_left = "LL" def fun(self): return "Left" class Right: var = "R" var_right = "RR" def fun(self): return "Right" class Sub(Left, Right): pass obj = Sub() print(obj.var, obj.var_left, obj.var_right, obj.fun()) --> output --> L LL RR Left a small amendment in the code - replacing : class Sub(Left, Right): w/ class Sub(Right, Left): yields ---> R LL RR Right (changing the "var" & "fun()")
Inheritance: Overriding Ex.
class Level1: var = 100 def fun(self): return 101 class Level2(Level1): var = 200 def fun(self): return 201 class Level3(Level2): pass obj = Level3() print(obj.var, obj.fun()) --> output --> 200 \ 201
3-level inheritance line Ex.
class Level1: variable_1 = 100 def __init__(self): self.var_1 = 101 def fun_1(self): return 102 class Level2(Level1): variable_2 = 200 def __init__(self): super().__init__() self.var_2 = 201 def fun_2(self): return 202 class Level3(Level2): variable_3 = 300 def __init__(self): super().__init__() self.var_3 = 301 def fun_3(self): return 302 obj = Level3() --------- \n print(obj.variable_1, obj .var_1, obj.fun_1()) \n print(obj.variable_2, obj.varterm-197_2, obj.fun_2()) \n print(obj.variable_3, obj.var_3, obj.fun_3())
Create your own exception Ex.
class MyZeroDivisionError(ZeroDivisionError): pass def do_the_division(mine): if mine: raise MyZeroDivisionError("some worse news") else: raise ZeroDivisionError("some bad news") for mode in [False, True]: try: do_the_division(mode) except ZeroDivisionError: print('Division by zero') for mode in [False, True]: try: do_the_division(mode) except MyZeroDivisionError: print('My division by zero') except ZeroDivisionError: print('Original division by zero') ---> output ---> Division by zero Division by zero Original division by zero My division by zero
Building a hierarchy of classes Ex.
class One: def do_it(self): print("do_it from One") def doanything(self): self.do_it() class Two(One): def do_it(self): print("do_it from Two") one = One() two = Two() one.doanything() two.doanything() --> output --> do_it from One do_it from Two
Create your own exception Ex2.
class PizzaError(Exception): def __init__(self, pizza, message): Exception.__init__(self, message) self.pizza = pizza class TooMuchCheeseError(PizzaError): def __init__(self, pizza, cheese, message): PizzaError._init__(self, pizza, message) self.cheese = cheese ---> output --->
Exception Tree Ex.
def print_exception_tree... ....(thisclass, nest = 0): if nest > 1: print(" |" * (nest - 1), end="") if nest > 0: print(" +---", end="") print(thisclass.__name__) for subclass in thisclass.__subclasses__(): print_exception_ tree(subclass, nest + 1) print_exception_tree(BaseException) -> yields output -> really long list of types of Errors in different hierarchies > this program dumps all predefined exception classes in the form of a tree-like printout Output --> > the printout branches & forks aren't sorted at all > there are some subtle inaccuracies w/ how some branches are presented
Create your own exception Ex3.
class PizzaError(Exception): def __init__(self, pizza='uknown', message=''): Exception.__init__(self, message) self.pizza = pizza class TooMuchCheeseError(PizzaError): def __init__(self, pizza='uknown', cheese='>100', message=''): PizzaError.__init__(self, pizza, message) self.cheese = cheese def make_pizza(pizza, cheese): if pizza not in ['margherita', 'capricciosa', 'calzone']: raise PizzaError if cheese > 100: raise TooMuchCheeseError print("Pizza ready!") for (pz, ch) in [('calzone', 0), ('margherita', 110), ('mafia', 20)]: try: make_pizza(pz, ch) except TooMuchCheeseError as tmce: print(tmce, ':', tmce.cheese) except PizzaError as pe: print(pe, ':', pe.pizza)
Queue Lab
class QueueError(Queue): class Queue: def __init__(self): self.__que = [] def put(self, elem): self.__que[0].append(elem) def get(self): if len(self.__que) == 0 raise QueueError else: elem = self.__que[-1] del self.__que[-1] return elem que = Queue() que.put(1) que.put("dog") que.put(False) try: for i in range(4): print(que.get()) except: print("Queue error")
Ex. is operator
class SampleClass: def __init__(self, val): self.val = val object_1 = SampleClass(0) object_2 = SampleClass(2) object_3 = object_1 object_3.val += 1 print(object_1 is object_2) print(object_2 is object_3) print(object_3 is object_1) print(object_1.val, ... ... object_2.val, object_3.val) string_1 = "Mary had a little " string_2 = "Mary had a little lamb" string_1 += "lamb" print(string_1 == string_2, string_1 is string_2) ----> False \ False \ True \ 1 2 1 \ True \ False (results prove that object_1 and object_3 are actually same obj.'s, while string_1 & string_2 aren't, despite contents being same)
Counting Stack Lab
class Stack: def __init__(self): self.__stk = [] def push(self, val): self.__stk.append(val) def pop(self): val = self.__stk[-1] del self.__stk[-1] return val class CountingStack(Stack): def __init__(self): Stack.__init__(self) self.__counter = 0 def get_counter(self): return self.__counter def pop(self): self.__counter += 1 return Stack.pop(self) stk = CountingStack() for i in range(100): stk.push(i) stk.pop() print(stk.get_counter())
Subclasses Inheriting Methods Ex.
class Super: def __init__(self, name): self.name = name def __str__(self): return "My name is " + self.name + "." class Sub(Super): def __init__(self, name): Super.__init__(self, name) obj = Sub("Andy") print(obj) ---> output ---> My name is Andy. [ As there's no __str__() mthd. w/in the Sub class, the printed string is to be produced w/in the Super class, meaning the __str__() method has been inherited by the Sub class ]
lab : w/ timer counting seconds +1/-1
class Timer: def __init__(self, hr=0, min=0, sec=0): self.__hr = hr self.__min = min self.__sec = sec def __str__(self): # # print(str() # def next_second(self): self.__sec += 1 if self.__sec > 59: self.__sec = 0 self.__min += 1 if self.__min > 59: self.__min = 0 self.__hour += 1 if self.__hours > 23: self.__hours = 0 def prev_second(self): self.sec -= 1 if self.__sec < 0: self.__sec = 59 self.__min -= 1 if self.__min < 0: self.__min = 59 self.__hour -= 1 if self.__hours < 0: self.__hours = 23 timer = Timer(23, 59, 59) print(timer) timer.next_second() print(timer) timer.prev_second() print(timer)
Original Ex. Built more Diamond-like
class Top: def m_top(self): print("top") class Middle_Left(Top): def m_middle(self): print("middle_left") class Middle_Right(Top): def m_middle(self): print("middle_right") class Bottom(Middle_Left, Middle_Right): def m_bottom(self): print("bottom") object = Bottom() object.m_bottom() object.m_middle() object.m_top() ---> ouput ---> bottom middle_left top
Exceptions : Finally Block Ex.
def reciprocal(n): try: n = 1 / n except ZeroDivisionError: print("Division failed") n = None else: print("Everything went fine") finally: print("It's time to say goodbye") return n print(reciprocal(2)) print(reciprocal(0))
Exceptions : Try-Except + Else Ex.
def reciprocal(n): try: n = 1 / n except ZeroDivisionError: print("Division failed") return None else: print("Everything went fine") return n print(reciprocal(2)) print(reciprocal(0) ---> ouput ---> Everything went fine \ 0.5 Division failed \ None
This Ex. doesn't yield any glaring issues :
how MRO works in 2 peculiar cases of problems which may occur when you try to use multi. inheritance too recklessly : class Top: def m_top(self): print("top") class Middle(Top): def m_middle(self): print("middle") class Bottom(Middle): def m_bottom(self): print("bottom") object = Bottom() object.m_bottom() object.m_middle() object.m_top() ---> output ---> bottom \ middle \ top
prog : func. that's able to input int values & check if they're w/in a specified range
if user enters a string that is: not an int value : > func should emit the msg : "Error: wrong input" & ask the user to input the value again if the user enters a # which falls outside the specified range : > func should emit the msg : "Error: the value is not w/in permitted range (min..max)" & ask the user to input the value again if the input value is valid, return it as a result def read_int(prompt, min, max): ok = False while not ok: try: value = int(input(prompt)) ok = True except ValueError: print("Error: wrong input") if ok: ok = value >= min and value <= max if not ok: print("Error: the value is not within permitted range (" + str(min) + ".." + str(max) + ")") return value; v = read_int("Enter a number from -10 to 10: ", -10, 10) print("The number is:", v)
Triangle Hypotenuse lab w/ Classes
import math class Point: def __init__(self, x=0.0, y=0.0) self.__x = x self.__y = y class Triangle(Point) class Triangle: def __init__(self, vertice1, vertice2, vertice3): self.__vertice1 = self.__vertice2 self.__vertice3 def perimeter(self): # # Write code here # triangle = Triangle(Point(0, 0), Point(1, 0), Point(0, 1)) print(triangle.perimeter())
Composition Ex.
import time class Tracks: def change_direction(self, left, on): print("tracks: ", left, on) class Wheels: def change_direction(self, left, on): print("wheels: ", left, on) class Vehicle: def __init__(self, controller): self.controller = controller def turn(self, left): self.controller.change_direction(left, True) time.sleep(0.25) self.controller.change_direction(left, False) wheeled = Vehicle(Wheels()) tracked = Vehicle(Tracks()) wheeled.turn(True) tracked.turn(False) ---> output ---> wheels: True True wheels: True False tracks: False True tracks: False False
Investigating Classes : Introsp/Reflect Ex.
incIntsI() func. gets an obj. of any class, scans its contents in order to find all int. attrib.'s w/ names starting w/ i, & increments them by 1 : Ex. class MyClass: pass obj = MyClass() obj.a = 1 \ obj.b = 2 \ obj.i = 3 obj.ireal = 3.5 obj.integer = 4 obj.z = 5 def incIntsI(obj): for name in obj.__... ... dict__.keys(): if name.startswith('i'): val = getattr(obj, name) if isinstance(val, int): setattr(obj, name,... ... val + 1) print(obj.__dict__) incIntsI(obj) print(obj.__dict__) ---> output ---> {'a': 1, 'integer': 4, 'b': 2, 'i': 3, 'z': 5, 'ireal': 3.5} {'a': 1, 'integer': 5, 'b': 2, 'i': 4, 'z': 5, 'ireal': 3.5}
Detailed anatomy of exce. Ex. Explained
looking @ exception's obj. : The BaseException class introduces a prop. "args", which is a tuple designed to gather all arg.'s passed to the class constructor : - it's empty if construct is invoked w/out any args - it contains just 1 elem. when the constructor gets 1 arg. (we don't count the self arg. here), and so on func. is used to print the contents of the args prop. in 3 different cases, where the exception of the Exception class is raised in 3 different ways ( obj. itself is also printed, along w/ result of __str__() invoc. ) > 1st case looks normal - (the obj. of this class has been created normally) 2nd and 3rd cases are just the constructor invocations In 2nd raise statement, the constructor is invoked with 1 arg., & in the 3rd, w/ 2 arg.'s
Ex. Question on negating prop. of an obj.
negate the __venomous property of the version_2 object, ignoring the fact that the property is private : " vers_2 = Python() " ---> vers_2._Python__venomous = not vers_2._Python__venomous
Multiple Inheritance
occurs when a class has more than 1 superclass [syntactically, presented as a comma-separated list of superclasses put in ( ) after the new class name]
Inheritance: the is operator
operator checks whether 2 var.'s refer to the same obj. Format Ex. object_one is object_two > var.'s don't store the obj.'s themselves, but only the handles pointing to the internal Python memory - assigning a value of an obj. var. to another var. doesn't copy the obj. (only handle)
Handling Exceptions
rather than checking all circumstances first which seems like it makes more sense, the reality is this method is actually makes programming more illegible - Python prefers this ---> recipe for success as follows: - try to do something - check whether everything went well
Subclasses Inheriting Methods Ex.2
same as previous ex., w/ same output - except instead of referring to superclass w/in subclass as : Super.__init__(self, name) [explicitly naming the superclass] we make use of : super().__init__(name) [accesses superclass w/out needing to know its name] > creates a context in which you don't have to / mustn't, pass the self arg. to the mthd. being invoked - this is why it's possible to activate the superclass constructor using only 1 arg. > can use this to invoke, & get access to any of the resources available inside, the superclass (constructor)
Detailed anatomy of exceptions Ex.
simple func. to print the args property elegantly : def print_args(args): lng = len(args) if lng == 0: print("") elif lng == 1: print(args[0]) else: print(str(args)) try: raise Exception except Exception as e: print(e, e.__str__(), sep=' : ' ,end=' : ') print_args(e.args) try: raise Exception("my exception") except Exception as e: print(e, e.__str__(), sep=' : ', end=' : ') print_args(e.args) try: raise Exception("my", "exception") except Exception as e: print(e, e.__str__(), sep=' : ', end=' : ') print_args(e.args) ---> output ---> : : my exception : my \ exception : my exception ('my', 'exception') : ('my', \ 'exception') : ('my', \ 'exception')
Advanced Variant of try-except block Ex.
try: x = int(input("Enter a number: ")) y = 1 / x print(y) except ZeroDivisionError: print("You cannot divide by zero, sorry.") except ValueError: print("You must enter an integer value.") except: print("Oh dear, something went wrong...") print("THE END.") The code, when run, produces one of the following four variants of output : - if you enter a valid, non-0 int. value (for ex. 5) it says: 0.2 \ THE END. - if you enter 0, it says: You cannot divide by 0 \ THE END. - if you enter any non-int str, it says : You must enter an int val. \ THE END. - if you press Ctrl-C (locally on your machine) while program is waiting for user's input (causes an exception named KeyboardInterrupt), the program says: Oh dear, something went wrong... \ THE END.
instance variables
variables (properties) - word "instance" suggests that they are closely connected to the objects (which are class instances) , not to the classes themselves Ex. print(example_object_1.__dict__) print(example_object_2.__dict__) - Each obj. is gifted w/ a small set of predefined properties & methods upon creation > one is a var. "__dict__" (it's a dictionary) >> it contains the names & values of all properties (var.'s) the obj. is currently carrying - Modifying an instance variable of any object has no impact on all the remaining objects. Instance variables are perfectly isolated from each other
Ex. isinstance()
we check all obj.-class pairs to find out if the obj.'s are instances of the classes : class Vehicle: pass class LandVehicle(Vehicle): pass class TrackedVehicle(LandVehicle): pass my_vehicle = Vehicle() my_land_vehicle = LandVehicle() my_tracked_vehicle = TrackedVehicle() for obj in [my_vehicle, my_land_vehicle, my_tracked_vehicle]: for cls in [Vehicle, LandVehicle, TrackedVehicle]: print(isinstance(obj, cls), end="\t") print()
Single Inheritance
when a subclass has exactly 1 superclass - (most common & recommended situation)