Python Programming
Python Programming
FUNCTION ARGUMENTS
Unpacking operator (*), place in front of parameter name to allow for unlimited
positional args
e.g., def fun1 (*args)
here all args when function is called will be stored in a tuple, use loop to iterate
2nd Unpacking operator (**), place in front of parameter name to allow for unlimited
keyword arguments
e.g., def fun2 (**args)
here when function is called then keywords are stored as keys and arguments are
stored as values in a dictionary.
UNPACKING ITERABLES: a, *b, c = [3,12,56,7,8,9,0]
Here a=3,b=[12,56,7,8,9],c=0
Unpacking operator breaks an iterable into its constituent elements, separated by commas
NAMESPACES
A namespace is a dictionary in python which is used by
the language to store the value of objects, where the
key is name of the object.
1. Built-in namespaces: Exist in every program and can be used anywhere. Exists
while program is running, to get these we use print(dir(__builtins__))
3. Local namespaces: Only accessible in the specific block of code. Exists while
the block is running. To get these we use print(locals())
NOTE: local namespaces for a function also include arguments passed to it if it
is called at any point in the program.
A type of local namespace is Enclosed namespace that only exists when
working for nested functions and only exists when function is executing.
Namespace of an enclosing function is enclosing namespace.
SCOPES
Most of the concepts are similar to namespaces, but some key points to be noted:
Scope can be modified by using nonlocal <variable name>, used for
modifying names in the enclosing scope from the nested block, as without
this it wouldn’t be possible. This statement is to be written in the nested
function.
Similarly, global <variable name> can be used to modify global variables
from local scope. If such a variable does not exist, the statement will create
a variable of that name in the global scope.
E.g. list=[1,2,3,4,5]
Product=return(lambda x,y:x*y,list)
# This will return the products of all the elements in the list.
#first the red nos. (1,2) are multiplied to give result 2, then the result (2) is
multiplied with the blue number (3) to give 6, then the new result (6) is
multiplied with the pink number (4) and so on until the end of the list.
DECORATORS
Another function which ‘decorates’ or adds to the output of another function adds to
the output.
E.g.
Overriding Methods:
If you want the method of a child class inherited from a parent
class to behave differently for the child, just redefine the same
method for the child class.
Accessing behaviour of overridden method:
If you want to access the behaviour of overridden method of
the overridden method, we use super().method(), here super is
the superclass or parent class. Super() does this by giving us a
proxy object.
Multiple Inheritance:
When an object inherits from more than one parent class,
e.g.,
1. its parent class has its own parent class i.e., the object has
a super-superclass.
e.g.
class Animal():
…
…
class Cat(Animal):
…
…
class Angry_Cat(Cat):
…
…
class Dog(Animal):
def action(self):
print("{} wags tail. Awwww".format(self.name))
class Wolf(Animal):
def action(self):
print("{} bites. OUCH!".format(self.name))
class Hybrid(Dog, Wolf):
def action(self):
super().action()
Wolf.action(self)
here hybrid can do what both dog and wolf can but dog and
wolf cannot show each other’s properties.
NOTE: in action() method of Hybrid class, calling
super().action() will choose Dog as the superclass as it has
been written first in the bracket of class Hybrid() [and thus
given higher position] to use Wolf’s action method, we use
Wolf.action(self).
POLYMORPHISM
The ability to apply an identical operation onto different types of
objects. Useful when object type unknown during runtime.
Inheritance is a form of it.
DUNDER METHODS
To decide an object’s behaviour when acted on by an operator (‘+’ in
this instance), we use dunder methods.
ABSTRACTION
When a program starts to get big, classes might start to share
functionality or we may lose sight of the purpose of a class’s
inheritance structure. In order to alleviate issues like this, we can use
the concept of abstraction.
class Animal(ABC):
def __init__(self, name):
self.name = name
@abstractmethod
def make_noise(self):
pass
class Cat(Animal):
def make_noise(self):
print("{} says, Meow!".format(self.name))
class Dog(Animal):
def make_noise(self):
print("{} says, Woof!".format(self.name))
kitty = Cat("Maisy")
doggy = Dog("Amber")
kitty.make_noise() # "Maisy says, Meow!"
doggy.make_noise() # "Amber says, Woof!"
Above we have Cat and Dog classes that inherit from Animal.
The Animal class now inherits from an imported class ABC, which
stands for Abstract Base Class.
This is the first step to making Animal an abstract class that cannot be
instantiated. The second step is using the imported
decorator @abstractmethod on the empty method .make_noise().
The below line of code would throw an error.
an_animal = Animal("Scruffy")
# TypeError: Can't instantiate abstract class Animal with abstract method make_noise
The abstraction process defines what an Animal is but does not allow
the creation of one. The .__init__() method still requires a name,
since we feel all animals deserve a name.
The .make_noise() method exists since all animals make some form
of noise, but the method is not implemented since each animal
makes a different noise. Each subclass of Animal is now required to
define their own .make_noise() method or an error will occur.
ENCAPSULATION
The process of making methods and data hidden inside the object
they relate to. Languages accomplish this with access modifiers like:
Public
Protected
Private
def __repr__(self):
return self.name
a1 = Animal("Horse")
a2 = Animal("Penguin")
a3 = a1 + a2
print(a1) # Prints "Horse"
print(a2) # Prints "Penguin"
print(a3) # Prints "HorsePenguin"
UNIT TESTING:
A unit test validates a single behaviour and will make
sure all of the units of a program are functioning
correctly.
RAISING EXCEPTIONS
Using raise keyword we can force Python to throw an error and stop
program execution, we can also display a custom message to make it
easier for the error to be understood.
One way to use the raise keyword is by pairing it with a specific exception
class name. We can either call the class by itself or call a constructor and
provide a specific error message. So for example we could do:
raise NameError
# or
raise NameError('Custom Message')
When only the class name is provided (as in the first example), Python calls the
constructor method for us without any arguments (and thus no custom
message will come up).
For a more concrete example, let’s examine raising a TypeError for a function
that checks if an employee tries to open the cash register but does not have
the correct access:
def open_register(employee_status):
if employee_status == 'Authorized':
print('Successfully opened cash register')
else:
# Alternatives: raise TypeError() or TypeError('Message')
raise TypeError
When an employee_status is not 'Authorized', the function open_register() will
throw a TypeError and stop program execution.
Alternatively, when no built-in exception makes sense for the type of error our
program might experience, it might be better to use a generic exception with
a specific message. This is where we can use the base Exception class and
provide a single argument that serves as the error message. Let’s modify the
previous example, this time raising an Exception object with a message:
def open_register(employee_status):
if employee_status == 'Authorized':
print('Successfully opened cash register')
else:
raise Exception('Employee does not have access!')
TRY/EXCEPT
Used for exception handling i.e. executing program even after an
error occurs.
First code in try block is executed. If error (or exception) is raised,
execution stops and except block is executed. (except block
sometimes knows as handler)
Catching specific exceptions:
If we place the name of a specific exception class in front of
except keyword for e.g. except NameError: , we can catch
specific errors in our program, it is considered good practice
for error catchers to be specific, unless generalisation is
necessary. In this case, if any error other than NameError
occurs, the exception is unhandled and the program
terminates.
When we specify exception types, Python also allows us to capture
the exception object using the as keyword. The exception object
hosts information about the specific error that occurred. Examine our
previous function but now capturing the exception object
as errorObject:
try:
print(undefined_var)
except NameError as errorObject:
print('We hit a NameError')
print(errorObject)
Would output:
We hit a NameError
name 'undefined_var' is not defined
ASSERT STATEMENT
Used for auto testing of code, to check if condition is satisfied or not.
assert <condition>, 'Message if condition is not met'
Problems with this system are:
1. If AssertionError occurs, the program stops and any subsequent
tests are not conducted.
2. Tests cannot be grouped and we need to make an assertion
statement for each test.
UNITTEST FRAMEWORK