0% found this document useful (0 votes)
4 views28 pages

Advanced Programming Notes Univeristy Level

The document covers advanced Python concepts including *args and **kwargs for handling variable numbers of arguments in functions, higher-order functions, and object-oriented programming principles such as encapsulation and inheritance. It also introduces the Unified Modeling Language (UML) and libraries like NumPy and Pandas. Key topics include iterable unpacking, decorators, and closures in Python.

Uploaded by

bigbiguz04
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views28 pages

Advanced Programming Notes Univeristy Level

The document covers advanced Python concepts including *args and **kwargs for handling variable numbers of arguments in functions, higher-order functions, and object-oriented programming principles such as encapsulation and inheritance. It also introduces the Unified Modeling Language (UML) and libraries like NumPy and Pandas. Key topics include iterable unpacking, decorators, and closures in Python.

Uploaded by

bigbiguz04
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 28

*ARGS and **KWARGS ............................................................................................

2
Iterable Unpacking ............................................................................................... 2
Extended Unpacking - * operator ........................................................................... 2
Extended Unpacking - ** operator ......................................................................... 3
*args ................................................................................................................... 3
**kwargs ............................................................................................................. 4
HIGHER-ORDER FUNCTIONS................................................................................... 6
Decorators .......................................................................................................... 9
Decorators for Classes ....................................................................................... 11
OBJECT-ORIENTED PROGRAMMING ...................................................................... 13
Encapsulation ................................................................................................... 14
Inheritance ........................................................................................................ 15
Overriding ...................................................................................................... 16
Overloading .................................................................................................... 17
Overwriting ..................................................................................................... 17
Data abstraction ................................................................................................ 17
Polymorphism ................................................................................................... 19
UNIFIED MODELLING LANGUAGE (UML)................................................................. 20
NUMPY................................................................................................................. 24
PANDAS ............................................................................................................... 25

1
*ARGS and **KWARGS

Iterable Unpacking

Any iterable in python, e.g. a list or a tuple, is said to have a pack of values. We can
unpack the values inside an iterable into individual variables.
my_list = [1, '2', {1,2}, 2.3]
a, b, c, d = my_list
a: 1
b: 2
c: {1, 2}
d: 2.3

N.B. with unordered objects like sets, the unpacking does not necessarily happen in the order
shown in the assignment. E.g., a, b, c = {‘a’, ‘b’, ‘c’} might result in ‘a’ getting assigned to b.
When we unpack the values of an iterable into individual variables we must know in advance the
number of elements contained by (length of) the iterable.
a, b, c, d, e = [1, '2', {1,2}, 2.3]
ValueError: not enough values to unpack (expected 5, got 4)

Extended Unpacking - * operator

Moving a step further, we always don't wish to unpack a single index value of the iterable
in a single variable. What we want to do now resembles the slicing of a list.
a, b, *r = [1, 2, 3, 4, 5, 6, 7, 8, 9 ,10]
a: 1
b: 2
r: [3, 4, 5, 6, 7, 8, 9, 10]

Here, the first index value of iterable list gets assigned to a, the second is assigned to b and the
rest of the values of the iterable are assigned to r as a list.
v = ['python', 'json', 'rdf', 'xml', 'java']
*b, = v #to unpack must have multiple receivers => comma “tricks” interpreter
b: ['python’, ‘json’, ‘rdf’, ‘xml’, ‘java’]

Here, we write *b, = d as the star assignment must be in a list or tuple.


def sumall(l: list): def concat(arr):
if not l: if(len(arr) == 1):
return 0 first, = arr
elif len(l) == 1: return first
return int(l[0]) elif(len(arr) > 1):
elif len(l) > 0: first, *rest = arr
first, *rest = l return first + concat(rest)
return int(first) + sumall(rest)
*rest allows to do as list[1:]

2
Extended Unpacking - ** operator

When working with dictionaries * operator is only able to unpack the keys of the dictionary.
d = {
1: 'Andrea',
2: 'John',
5: 'Mary'}

*b, = d
b: [1, 2, 5]

To successfully unpack dictionaries, we use the ** operator.


Consider an example where we need to combine 3 dictionaries into a single dictionary.
v1 = {1: 'a'}
v2 = {2: 'b'}
v3 = {3: 'c'}
A more pythonic solution is to use the ** A simple solution would be to create a new
operator. empty dictionary and use the .update() to
combine the previous three dictionaries.
v = {**v1, **v2, **v3} new_v = dict()
v: {1: 'a', 2: 'b', 3: 'c'} new_v.update(v1)
new_v.update(v2)
Note again that ** and * are different. new_v.update(v3)
v = {*v1, *v2, *v3} Step 1: {1: 'a'}
v: {1, 2, 3} Step 2: {1: 'a', 2: 'b'}
Step 3: {1: 'a', 2: 'b', 3: 'c'}

*args

You might have come across this many times inside the function parameters. It is used to
exhaust positional arguments that are passed to any function. For example, doing so we can
use the function somma with an undefined number of arguments:
def somma(*args):
print('Function somma:', args, type(args))
out = 0
for arg in args:
out += arg
return out

x = somma(3420, 5358920, 352358, 3525)


print(‘x:’, x)
Function somma: (3420, 5358920, 352358, 3525) <class 'tuple'>
x: 5718223

def sumall(a, b, *c):


print(c, type(c))
return = somma(a, b, *c)
#not working: return somma(a, b, c) as c is tuple and somma requires only int

Other solutions:

3
Relies on recursion and unpacking. This is based on recursion and dìvide et
def sumall_recursive(a, b, *c): impera (divide and conquer) paradigm.
if not c or len(c) == 0: def sum_merge(*num):
return a+b if not num or len(num) == 0:
else: return 0
first, *rest = c elif len(num) == 1:
return sumall_recursive(a+b, first, return num[0]
*rest) else:
p = int(len(num)/2)

part_1 = num[0:p]
part_2 = num[p:]

return sum_merge(*part_1) +
sum_merge(*part_2)

The official documentation of python declares print as -


print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

What would be the output of the code of the snippet below?

def func(a,b,*c,d=0):
print(f'Positional argument a is set to {a}')
print(f'Positional argument b is set to {b}')
print(f'Starred argument c is set to {c}')
print(f'Keyword-based argument d is set to {d}')
func(10, 20, 123, 582, 52, 4, d=543)
func(10, 20, 30, 40, 50, 60, 70, 80)
Positional argument a is set to 10 Positional argument a is set to 10
Positional argument b is set to 20 Positional argument b is set to 20
Starred argument c is set to (123, 582, 52, 4) Starred argument c is set to (30, 40, 50, 60, 70, 80)
Keyword-based argument d is set to 543 Keyword-based argument d is set to 0

We need to pass only keyword-only arguments once extended unpacking has been used.

**kwargs
If you do not know how many keyword arguments will be passed into your function,
then you add two stars, i.e. ** before the parameter name in the function definition. This way the
function will receive a dictionary of arguments and can access the items accordingly.
def func(a,b=1,*args, e, **kwargs):
print(f'Positional argument a is set to {a}')
print(f'Positional argument b is set to {b}')
print(f'Starred argument args is set to {args}')
print(f'Keyword-based argument e is set to {e}')
print(f'Double starred argument kwargs is set to a dictionary object containing
{kwargs}')

print('The kwargs is empty.') if len(kwargs) == 0 else print('We do have kwargs!')

4
func(10, 20, 89, e=89)
func(a=10, b=20, e=3)
func(10, 20, 30, 40, 50, e=60, f=70, g=80)
func(10, 20, 30, 40, 50, 60, f=70, g=80)
Positional argument a is set to 10 Positional argument a is set to 10
Positional argument b is set to 20 Positional argument b is set to 20
Starred argument args is set to (89,) Starred argument args is set to ()
Keyword-based argument e is set to 89 Keyword-based argument e is set to 3
Double starred argument kwargs is set to Double starred argument kwargs is set to
a dictionary object containing {} a dictionary object containing {}
The kwargs is empty. The kwargs is empty.
Positional argument a is set to 10 TypeError: func() missing 1 required
Positional argument b is set to 20 keyword-only argument: 'e'
Starred argument args is set to (30, 40,
50)
Keyword-based argument e is set to 60
Double starred argument kwargs is set to
a dictionary object containing {'f': 70,
'g': 80}
We do have kwargs!
Remember: only keyword args can follow keyword args
def func(a, b=1, *args, e, g=2, **kwargs):
Breaking the pieces of the puzzle
• a - positional argument, mandatory, can be a named argument;
• b - positional argument, not mandatory, can be a named argument;
• *args - catches all following positional arguments. Remember, no additional
positional arguments without names allowed after this;
• e - keyword-only argument, mandatory, should be named argument;
• g - keyword-only argument, not mandatory as a default value is associated with the
argument into the function definition;
• **kwargs - catches all following named arguments. No arguments follow this.
Possible uses:
def create_dictionary(**kwargs):
d = dict()
d.update(kwargs)
return d

def add_to_dict(d, **kwargs):


d.update(kwargs)

def create_dictionary(given_name, family_name, occupation, seniority_level, id):


return {
'given_name': given_name,
'family_name': family_name,
'occupation': occupation,
'seniority_level': seniority_level,
'id': id
}

def create_general_dictionary(**kwargs):
return kwargs

5
HIGHER-ORDER FUNCTIONS

A function is called a Higher Order Function if it contains other functions as a


parameter or returns a function as an output i.e., the functions that operate with another
function are known as Higher order Functions.
Properties of higher-order functions:
• A function is an instance of the Object type.
• You can store the function in a variable.
• You can pass the function as a parameter to another function.
• You can return the function from a function.
• You can store them in data structures such as hash tables, lists, …

Examples:
def to_uppercase(text):
return text.upper()
def to_lowercase(text):
return text.lower()

var_x = to_uppercase
<function to_uppercase at 0x7f06905f68c0>
print(var_x)
CIAO ITALIA
res = var_x('ciao italia')
print(res)

l = [to_uppercase, to_lowercase]

for f in l:
ret = f('PrOgraMMing') PROGRAMMING
print(ret) programming

def apply_fun(f, text):


return f(text)

s = 'I love coding in Python!'


message = apply_fun(to_uppercase, s)
print(message) I LOVE CODING IN PYTHON!

apply_fun(to_lowercase('AAA'), s) TypeError: 'str' object is not callable

def apply_all(L, f=int):


"""Assumes L is a list, f a function
Mutates L by replacing each element, e,
of L by f(e)"""
for i in range(len(L)):
L[i] = f(L[i])
return L

my_list = [7.4, -2, 8.33]


apply_all(my_list, f=str)
print(my_list) ['7.4', '-2', '8.33']

6
Defining functions inside other functions
def plus_one(number): #wrapping (or nesting) function !!
def add_(number): #wrapped (or nested) !!
return number + 1
result = add_(number)
return result

plus_one(4) 5

Functions returning other functions


def hello_function():
def say_hi():
def say_hi_now():
def i_order_you_to_say_hi():
return "Hi"
return i_order_you_to_say_hi
return say_hi_now
<function hello_function.<locals>.say_hi at
return say_hi
0x7af499ffb640> with type <class 'function'>

hello = hello_function() <function hello_function at 0x7af499ff9cf0>


print(f'{hello} with type {type(hello)}')
<function hello_function.<locals>.say_hi at
f = hello_function 0x7af499ffab90>
print(f)
f = f() <function
print(f) hello_function.<locals>.say_hi.<locals>.say_hi_n
f = f() ow at 0x7af499ffb0a0>
print(f)
<function
f = f()
hello_function.<locals>.say_hi.<locals>.say_hi_n
print(f)
ow.<locals>.i_order_you_to_say_hi at
f = f()
0x7af499ffaf80>
print(f)
print('----') Hi
----
f = hello_function()()()() Hi
print(f)
<function
hello_function.<locals>.say_hi.<locals>.say_hi_n
hello = hello_function()
ow at 0x7adbe6893370> with type <class
hello = hello()
'function'>
print(f'{hello} with type {type(hello)}')

Nested Functions have access to the Enclosing Function's Variable Scope


Python allows a nested function to access the outer scope of the enclosing function. This is a
critical concept in decorators -- this pattern is known as a Closure.
Closure in Python is an inner function object, a function that behaves like an object, that
remembers and has access to variables in the local scope in which it was created even after the
outer function has finished executing.
def divider(y, msg): When you write return divide, you're returning
def divide(x): the divide function as an object. This allows

7
print(msg) you to call it later with a specific argument (in
return x / y # y is defined in the outer scope of the this case, 1).
inner function divide When you immediately call (1) after divider(2,
value = 0 'Hello there!'), you're invoking the divide
print(value)
function that was returned. This is where the
return divide
actual division happens and a number is
returned.
w=2 So, in this context:
print(divider(2, 'Hello there!')(1))
divider(2, 'Hello there!') returns the divide
0
function. When you call that returned
Hello there! function with (1), it executes the code inside
0.5 divide, prints the message, and returns the
result of 1 / 2.
Same as
div = divider(2, 'Hello there!')
print(div(1))

Description:
This program demonstrates the concept of closures in Python using a nested function structure.
It defines a function `divider` which takes two arguments: `y` and `msg`.
Inside `divider`, another function `divide` is defined, which takes `x` as an argument.
`divide` accesses `y` and `msg` which are defined in the outer scope of `divide` within the
`divider` function.
The `divider` function returns the `divide` function.
Explanation of Closure:
A closure is a function object that remembers values in enclosing scopes even if they are not
present in memory.
In this program, when `divider` is called with arguments (2, 'Hello there!'), it returns the `divide`
function.
The `divide` function remembers the value of `y` (2) and `msg` ('Hello there!') even after the
execution of the `divider` function.
This is because `divide` is a closure. This is evident when `divider(2, 'Hello there!')(1)` is executed.
The `divider` function is called, returning the `divide` function. This `divide` function is then
called with the argument 1.
Inside `divide`, the value of `y` is still accessible and it is used to divide `x` (1) by `y` (2) to
produce the result.
In essence, the `divide` function "closes over" the variables `y` and `msg` from its enclosing
scope. This allows us to create functions with specific data attached to them, without relying on
global variables or explicit parameter passing for every invocation of the inner function.
def division(x): Closure occurs when a nested function retains
def second_division(): access to its enclosing scope’s variables even
t=6 after that outer function has finished
return x/w #w is defined out of the outer function executing. In this case, second_division retains
u=5
access to x and w.
return second_division()/w

w=2

8
print(division(8)) Variable Scope: the w variable is defined in
the global scope but is accessible within both
2.0 division and second_division.
The variable x is defined in the local scope of
division, and second_division can access it even
after division has been called.

Decorators

Decorators are the most common use of higher-order functions in Python. Has
function in input, has a wrapped function and returns it.

• A decorator allows programmers to modify the behavior of a function or class without


modifying their definition
• Decorators allow us to wrap another function to extend the behavior of wrapped function,
without permanently modifying it
• In Decorators, functions are taken as the argument into another function and then called
inside the wrapper function.

Let's go ahead and create a simple decorator that will convert a sentence to uppercase. We do
this by defining a wrapper inside an enclosed function. As you can see it is very similar to the
function inside another function that we created earlier.
def uppercase_decorator(function: callable) -> callable:

def wrapper(arg: str) -> str :


func: str = function(arg)
make_uppercase: str = func.upper()
return make_uppercase

return wrapper
Our decorator function takes a function as an argument, and we shall, therefore, define a
function and pass it to our decorator. We learned earlier that we could assign a function to a
variable. We'll use that trick to call our decorator function. However, Python provides a much
easier way for us to apply decorators. We simply use the @decorator_function syntax before the
function we'd like to decorate.
def say_hi(name: str): @uppercase_decorator
return f'Hello {name}' def say_hi(name: str) -> str:
return f'hello {name}'.lower()

say_hi('Mary') # -> 'hello Mary'


decorate = uppercase_decorator(say_hi)
decorate('John')
‘HELLO JOHN’ ‘HELLO MARY’

Applying Multiple Decorators to a Single Function


We can use multiple decorators to a single function. However, the decorators will be applied
in the order that we've called them.

9
def split_string(function):
def wrapper(*strings):
func = function(*strings)
splitted_string = func.split() # 'This is my string'.split(): 'This is my string'.split() -> ['This', 'is',
'my', 'string']
return splitted_string
return wrapper

@split_string
@uppercase_decorator
def say_hi(name: str) -> str:
return f'hello {name}'

say_hi('Mary')

['HELLO', 'MARY']

Defining General Purpose Decorators

To define a general-purpose decorator that can be applied to any function we use *args and
**kwargs. *args and **kwargs collect all positional, keyword arguments, and store them in the
args and kwargs variables. args and kwargs allow us to pass as many arguments as we would like
during function calls.

Passing Arguments to the Decorator

Now let's see how we'd pass arguments to the decorator itself. To achieve this, we define a
decorator maker that accepts arguments then define a decorator inside it. We then define a
wrapper function inside the decorator as we did earlier.
def decorator_maker_with_arguments(decorator_arg1, decorator_arg2, decorator_arg3):
def decorator(func):
def wrapper(function_arg1, function_arg2, function_arg3) :
#This is the wrapper function
print("The wrapper can access all the variables\n"
"\t- from the decorator maker: {0} {1} {2}\n"
"\t- from the function call: {3} {4} {5}\n"
"and pass them to the decorated function"
.format(decorator_arg1, decorator_arg2,decorator_arg3,
function_arg1, function_arg2,function_arg3))
return func(function_arg1 + decorator_arg1, function_arg2,decorator_arg2 + function_arg3)

return wrapper

return decorator

@decorator_maker_with_arguments("Pandas", "Numpy","Scikit-learn")
def decorated_function_with_arguments(function_arg1, function_arg2,function_arg3):
# We experiment with alternative solutions for formatting strings
print("This is the decorated function and it only knows about its arguments: {0}"
" {1}" " {2}".format(function_arg1, function_arg2,function_arg3))
print(f"This is the decorated function and it only knows about its arguments: {function_arg1} {function_arg2}
{function_arg3}")
print("This is the decorated function and it only knows about its arguments: " + function_arg1 + " " +
function_arg2 + " " +function_arg3)
print("This is the decorated function and it only knows about its arguments:", function_arg1, function_arg2,
function_arg3)

decorated_function_with_arguments("Data", "Science", "Tools")

10
The wrapper can access all the variables
- from the decorator maker: Pandas Numpy Scikit-learn
- from the function call: Data Science Tools and pass them to the decorated function
This is the decorated function and it only knows about its arguments: DataPandas Science NumpyTools
This is the decorated function and it only knows about its arguments: DataPandas Science NumpyTools
This is the decorated function and it only knows about its arguments: DataPandas Science NumpyTools
This is the decorated function and it only knows about its arguments: DataPandas Science NumpyTools

Decorators for Classes

The @classmethod and @staticmethod decorators are used to define methods inside a class
namespace that are not connected to a particular instance of that class
• A class method is a method that is bound to the class and not the object of the class.
It has access to the state of the class as it takes a class parameter (class as an implicit
first argument, just like an instance method receives the instance) that points to the
class and not the object instance.
It can modify a class state that would apply across all the instances of the class. For
example, it can modify a class variable that will be applicable to all instances.

• A static method is bound to a class rather than the objects for that class. It can be
called without an object for that class. This means that static methods cannot modify
the state of an object as they are not bound to it.

• Class vs Static:
o A class method takes class as the first parameter while a static method needs
no specific parameters
o A class method can access or modify the class state while a static method
cannot
o We generally use class methods to create factory methods. Factory methods
return class objects (like a constructor) for different use cases. In this case it is
possible to create inheritable constructors. We generally use static methods to
create utility functions.

11
The @property decorator is used to customize getters and setters for class attributes. It is used
to give "special" functionality to certain methods to make them act as getters, setters, or deleters
when we define properties in a class.
Properties can be considered the "Pythonic" way of working with attributes. The syntax used to
define properties is very concise and readable. You can access instance (private) attributes exactly
as if they were public attributes while using the "magic" of intermediaries (getters and setters) to
validate new values and to avoid accessing or modifying the data directly. By using @property,
you can "reuse" the name of a property to avoid creating new names for the getters, setters, and
deleters.

12
OBJECT-ORIENTED PROGRAMMING

OOP is a programming language model in which programs are organised around data, or
objects, rather than functions and logic. An object can be defined as a data field that has unique
attributes and behaviour. Examples of an object can range from physical entities, such as a
human being that is described by properties like name and address, down to small computer
programs, such as widgets.
The first step in OOP is to identify all the objects a programmer wants to manipulate and
how they relate to each other, an exercise often known as data modelling. Once an object is
known, it is generalised as a class of objects that defines the kind of data it contains and any
logical sequences that can manipulate it. Each distinct logic sequence is known as a method and
objects can communicate with well-defined interfaces called messages.
This approach to programming is well-suited for programs that are large, complex and actively
updated or maintained.

Procedural Programming vs OOP

Procedural programming creates a step-by-step program that guides the application through a
sequence of instructions. Each instruction is executed in order. PP also focuses on the idea that
all algorithms are executed with functions and data that the programmer has access to and can
change. OOP is much more like the way the real-world works - a message must be sent
requesting the data. Just like people must ask one another for information and we cannot see
inside each other’s heads.

Objects and Classes

Objects are the basic run-time entities in an object-oriented system. Objects interact by
sending messages to one another. Objects have two components:
1. Attributes (i.e., data)
2. Methods (i.e., behaviors)
An object, practically speaking, is a segment of memory (RAM) that references both data
(instance variables) of various types and associated functions that can operate on the data.

A Class is a special data type which defines how to build a certain kind of object. The
class also stores some data items that are shared by all the instances of this class Instances are
objects that are created following the definition given inside of the class. Functions belonging to
classes are called methods. Said another way, a method is a function that is associated with a
class, and an instance variable is a variable that belongs to a class.

A class is a prototypical definition of entities of a certain type, and it is defined by the


programmer at coding time. Objects are those entities which are created at runtime during the
execution of the code.
Example – a general definition of a Gene is the class, while objects are Gene AY342 with
sequence CATTGAC and Gene G54B with sequence TTACTAGA.

13
OOP in Python

Python is naturally “object oriented”. Objects are the data that we have been associating with
variables. Everything is an object. What the methods are, how they work, and what the data are
(e.g., a list of numbers, dictionary of strings, etc.) are defined by a class. Example: the array class
in Python defines methods like “append”, “pop”, “sort”, etc.

Class definition
Each class definition requires three things:
1. Methods (functions)
2. Attributes (instance variables referring to data)
3. The constructor __init__ – a special method called automatically whenever a new
object of the class is created. It usually does some initialization work. It can take any
number of arguments but the first is a reference to the current instance of the class.
By convention, it is named self. You do not give a value for this parameter when
you call the method, Python will provide it. Although you must specify self explicitly
when defining the method, you don’t include it when calling the method. Python
passes it for you automatically.

class nameOfTheClass (eventualSuperclass):


def __init__(self, *args, **kwargs):
#block of code with attributes
def method(self, *args, **kwargs):
#block of code with attributes

After the instantion, a method can be invoked using the dot notation – e.g., obj.method(args).
When you are done with an object, you don’t have to delete or free it explicitly. Python
has automatic garbage collection - it will automatically detect when all the references to a piece of
memory have gone out of scope and free that memory. It generally works well with few memory
leaks. There’s also no “destructor” method for classes.

The four major principles of object orientation are: Encapsulation, Data Abstraction,
Inheritance and Polymorphism.

Encapsulation

The terms encapsulation and abstraction (also data hiding) are often used as synonyms.
They are nearly synonymous, i.e. abstraction is achieved though encapsulation. Data hiding and
encapsulation are the same concept, so it's correct to use them as synonyms. Encapsulation is the
mechanism for restricting access to some of an object's components, this means, that the
internal representation of an object can't be seen from outside of the object's definition.
Access to this data is typically only achieved through special methods: Getters and
Setters. By using solely get() and set() methods, we can make sure that the internal data cannot be
accidentally set into an inconsistent or invalid state. C++, Java, and C# rely on the public,
private, and protected keywords to implement variable scoping and encapsulation.

14
Public Protected Private
name _name __attributename
Accessed from both inside Like public but they should Can’t be seen and accessed
and outside of the class. In not be directly accessed from from outside of the class.
Python, all class members are outside. Protected members Subclasses can’t access it.
public by default. are accessible within the class
and in subclasses. This is a
convention in Python, as the
language does not enforce
access restrictions strictly.
Protected members should be
treated as non-public and used
responsibly.
Also note that all these access modifiers are not strict like other languages such as C++,
Java, C#, etc. since they can still be accessed if they are called by their original or mangled
names. For example (from Geeksforgeeks), we can still access private members of a class outside
the class. We cannot directly call obj.__name, because they throw errors. In the list of callable
fields and methods, __name is saved as _Geek__name, . This conversion is called name
mangling, where the python interpreter automatically converts any member preceded with two
underscores to _<class name>__<member name>. Hence, we can still call all the supposedly
private data members of a class using the above convention.
>>> obj.public_attribute
Shows value of the public attribute
>>> obj.protected_attribute
Shows value of the protected attribute. NO ERRORS BUT BAD PRACTICE
>>> obj.private_attribute
SCOPING ERROR

Inheritance

It refers to defining a new (sub)class with little or no modification to an existing class.


The new class is called derived (or child) class and the one from which it inherits is called the
base (or parent) class. Derived class inherits features from the base class, adding new features to
it. This results in re-usability of code.
class BaseClassName:
def __init__(self, *args):
pass
class DerivedClassName (BaseClassName):
def __init__(self, *base_class_args, *args):
BaseClassName.__init__(self, name) # or super().__init__(self, name) refers to its superclass init
self.arg1 = arg1 # initialise all derived class arguments

The derived class can have its own initialiser. In python you may or may not call the base class
initialiser, but it is good practice to do so. It doesn't have to be, no, but it really should. If the
base class has any methods or properties which aren't overridden in the derived class then the

15
initialiser from the base class should be called, otherwise those methods and properties may not
work. You can define an initialiser for the derived class, but that initialiser really should call
BaseClassName.__init__ to make sure the base class is initialized properly.

Overriding

Overriding is OOP feature that allows a subclass or child class to provide a specific
implementation of a method that is already provided by one of its superclasses or parent
classes. The implementation in the subclass overrides (replaces) the implementation in the
superclass by providing a method that has same name, same parameters or signature, and same
return type as the method in the parent class. The version of a method that is executed will be
determined by the object that is used to invoke it (or by the closest class it is an instance of
where it is defined).
If a method is overridden in a class, the original method can still be accessed. The
original method defined in a superclass can be accessed from the derived class by calling the
method directly with the class name, e.g. Robot.say_hi(y) , where Robot is the superclass name and
y is an object instance of a subclass.

Multiple inheritance
Multiple inheritance is a feature in which a class can inherit attributes and methods from more
than one parent class.
class Robot: y=
def __init__(self, name): PhysicianRobot("James
self.name = name ", "Cardiovascular
def say_hi(self): medicine")
print("Hi, I am " + self.name)
y.say_hi()
class Physician: y.print_specialization()
def __init__(self, specialization):
self.specialization = specialization
def print_specialization(self):
print("My specialization is " + self.specialization)
>>> Everything will be
class PhysicianRobot(Robot, Physician): okay!
def __init__(self, name, specialization): James takes care of
you!
Robot.__init__(self, name)
My specialization is
Physician.__init__(self, specialization) Cardiovascular
def say_hi(self): medicine
print("Everything will be okay! ")
print(self.name + " takes care of you!")

The critics point out that multiple inheritance comes along with a high level of
complexity and ambiguity in situations such as the diamond problem.
The "diamond problem" (sometimes referred to as the "deadly diamond of death") is the
generally used term for an ambiguity that arises when two classes B and C inherit from a

16
superclass A, and another class D inherits from both B and C. If there is a method "m" in A that
B or C (or even both of them) )has overridden, and furthermore, if does not override this
method, then the question is which version of the method does D inherit? You can’t know.

Overloading

Overloading is the ability to define a function with the same name multiple times. The
definitions are different concerning the number of parameters and types of parameters. It's the
ability of one function to perform different tasks, depending on the number of parameters or the
types of them. We cannot overload functions like this in Python, but it is not necessary either.
OOP languages like Java or C++ implement overloading.

Overwriting

Overwriting is the process of replacing old information with new information If we overwrite a
function, the original function will be gone. The function will be redefined. This process has
nothing to do with object orientation or inheritance.
def f(x):
return x + 42
print(f(3)) >>> 45

def f(x):
>>> 46
return x + 43
print(f(3))

Data abstraction

Abstract classes:
• Are classes which contain one or more (but not necessarily all) abstract methods.
An abstract method is a method that has declaration but no implementation.
Abstract classes having only abstract methods are called interfaces.

• Unlike concrete classes, they cannot be instantiated and they need subclasses to
provide implementations for those abstract methods which are defined in abstract
classes.

• can be considered as blueprints for other classes, allows you to create a set of
methods that must be created within any child classes built from your abstract class.

abc module
The abc module provides the infrastructure for defining Abstract Base Classes (ABCs) in
Python.
@abc.abstractmethod is a decorator indicating abstract methods. Using this decorator

17
requires that the class’s metaclass is ABCMeta or is derived from it and that class cannot be
instantiated unless all its abstract methods and properties are overridden. abstractmethod() may be
used to declare abstract methods for properties and descriptors.
from abc import ABC, abstractmethod A = Animal()

class Animal(ABC): >>> Traceback (most recent call last):


@abstractmethod >>> TypeError: Can’t instantiate abstract class
def doAction(self): Animal with abstract methods doAction
pass

class Human(Animal):
def doAction(self):
print("I can walk and run")
class Snake(Animal):
def doAction(self):
print("I can crawl")

Abstract properties

Abstract properties are read-only by default

18
Writable abstract properties

Polymorphism

Polymorphism is used when you have methods with the same name across classes or
subclasses. This allows functions to use objects of any of these polymorphic classes without
needing to be aware of distinctions across the classes. Polymorphism can be carried out through
inheritance with subclasses making use of base class methods or overriding them.
The “Data abstraction” code is an example of polymorphism – see how “doAction()”
method is redefined in each subclass.
human = Human() I can walk and run
snake = Snake() I can crawl
dog = Dog() I can bark
lion = Lion() I can roar

animals = [human, snake, dog, lion] Python is unaware of the actual type of each
for animal in animals: animal
animal.doAction()

Python’s duck typing is a kind of polymorphism

19
“When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that
bird a duck.” [James Whitcomb Riley]
A special case of dynamic typing - uses techniques characteristic of polymorphism,
including late binding and dynamic dispatch. The use of duck typing is concerned with
establishing the suitability of an object for a specific purpose. When using normal typing this
suitability is determined by the type of an object alone, but with duck typing the presence of
methods and properties are used to determine suitability rather than the actual type of the object
in question.

UNIFIED MODELLING LANGUAGE (UML)

20
UML (Unified Modelling Language): a graphical language based on diagrams which helps
to develop an understanding of the relationships between the software that is being designed and
its external environment. It is independent from the development process and the programming
language and natively supports object-oriented design.

Class-Responsibility-Collaboration
(CRC) cards are a brainstorming
tool used in the design of object-
oriented software.
A class is represented as a rectangle
with internal slots for:
• Class name
(UpperCamelCase) -
mandatory
• Attributes (lowerCamelCase) - optional
• Operators (lowerCamelCase) - optional
Visibility types for attributes and operations
+ Public: a public element is visible to all elements that can access the contents of
the namespace that owns it.
- Private: element is only visible inside the namespace that owns it.
# Protected: element is visible to elements that have a generalization relationship to
the namespace that owns it.
⁓ Package: element is only visible by elements within a package and its sub
packages

21
Generalization
The process of a child or subclass taking on the
functionality of a parent or superclass, also known as
inheritance. It's symbolised with a straight connected line
with a closed arrowhead pointing towards the superclass

Realization
A semantic relation used for representing a provider that
exposes an interface and a client that realises the interface.
The canonical example is the relation between an interface
and a class implementing such an interface.

Association
The relationship between two classes. Both classes are
aware of each other and their relationship with the other.
This association is represented by a straight line between
two classes
Dependency
Is a directed relationship which is used to show that some
element or a set of elements requires, needs or depends on
other model elements for specification or implementation.
Because of this, dependency is called a supplier - client
relationship, where supplier provides something to the
client, and thus the client is in some sense incomplete
while semantically or structurally dependent on the
supplier element(s). Modification of the supplier may
impact the client elements
Aggregation
A binary association between a property and one or more
composite objects which group together a set of instances.
Aggregation has the following characteristics:
-- it is binary association it is asymmetric
- only one end of association can be an aggregation it is
transitive
- aggregation links should form a directed, acyclic graph,
so that no composite instance could be indirect part of
itself

22
Composition
Composite aggregation (composition) is a "strong" form
of aggregation with the following characteristics: ---- it is
binary association it is a whole/part relationship a part
could be included in at most one composite (whole) at a
time if a composite (whole) is deleted, all of its composite
parts are "normally" deleted with it

23
NUMPY

The name is an acronym for "Numeric Python"


or "Numerical Python”. NumPy enriches the
programming language Python with powerful data
structures, implementing multi-dimensional arrays and
matrices. These data structures guarantee efficient
calculations with matrices and arrays (array-oriented
programming).

The advantages of Core Python:


• high-level number objects: integers, floating point
• containers: lists with cheap insertion and append methods, dictionaries with fast
lookup
Advantages of using NumPy with Python:
• efficiently implemented multi-dimensional arrays
• designed for scientific computation
• High-performing computing (more efficient than a for loop)
e.g. C = np.array(cvalues)
C = C * 9 / 5 + 32
• Computational efficiency (vectorized, in parallel)

N-dimensional arrays

− Zero-dimensional arrays: scalars x = np.array(42)


− One-dimensional arrays: lists y = np.array([1, 1, 2, 3, 5, 8, 13, 21])
− Two-dimensional arrays: matrices
w = np.array(
[
[3.4, 8.7, 9.9],
[1.1, -7.8, -0.7],
[4.1, 12.3, 4.8]
])
− Multi-dimensional arrays
o 3D arrays
z = np.array(
[
[
[111, 112],
[121, 122]
],
[
[211, 212],
[221, 222]
],
[
[311, 312],
[321, 322]

24
]
])

Useful functions (np.):


array(args) ndim(arr)
creates a np array returns the dimension
arange([start=0,] stop [,step=1] [, dtype=None)
The values are generated within the half-open interval '[start, stop)’. If the parameter 'step' is
given, the 'start' parameter cannot be optional, i.e. it has to be given as well. The type of the
output array can be specified with the parameter 'dtype'. If not given, the type will be
automatically inferred from the other input arguments.
linspace(start, stop, num=50, endpoint=True, retstep=False)
The values consist of 'num' equally spaced samples in the closed interval [start, stop] or the
half-open interval [start, stop). If a closed or a half-open interval will be returned, depends on
whether 'endpoint' is True or False. 'stop' will the end value of the sequence, unless 'endpoint' is
set to False. In the latter case, the resulting sequence will consist of all but the last of 'num + 1'
evenly spaced samples. The number of samples to be generated can be set with 'num', which
defaults to 50.
shape(arr) arr.reshape(int_tuple)
The shape is a tuple of lengths of the changes the shape of an array.
corresponding array dimension.
newarr = oldarr.copy() newarr = oldarr.view()
The copy owns the data and any changes The view does not own the data and any
made to the copy will not affect original array, changes made to the view will affect the
and any changes made to the original array will original array, and any changes made to the
not affect the copy. original array will affect the view.
Other useful:
nditer(), ndenumerate(), concatenate(), stack(), hstack(), vstack(), dtsack(), array_split(), vsplit(),
dsplit(), where(), searchsorted(), sort()
Ufuncs:
frompyfunc(), add(), subtract(), multiply(), divide(), power(), mod(), divmod(), absolute(), log2(),
log10(), log(), sum(), prod(), diff()
To filter an array: newarr = oldarr condition e.g. filter_arr = arr > 42

PANDAS

25
Pandas is a library providing high-performance data manipulation and analysis tools using
its powerful data structures.
A DataFrame is a two-dimensional array of values with both a row and a column index. A
Series is a one-dimensional array of values with an index.

df = pd.DataFrame(data = [
[ 'NJ', 'Towaco', 'Square'],
[ 'CA', 'San Francisco', 'Oval'],
[ 'TX', 'Austin', 'Triangle'],
[ 'MD', 'Baltimore', 'Square'],
[ 'OH', 'Columbus', 'Hexagon'],
[ 'IL', 'Chicago', 'Circle']
],
columns = [ 'State', 'City', 'Shape'])

Useful methods and properties

• df.head(n_rows): returns a new DataFrame composed of the first n_rows rows. The
parameter n_rows is optional, and it is set to 5 by default
• df.tail(n_rows): returns a new DataFrame composed of the last n_rows rows. The
parameter n_rows is optional, and it is set to 5 by default
• df.shape: returns the shape of the DataFrame that provides the number of elements
for both the dimensions of the DataFrame
• df.index: returns the labels of the DataFrame indexes
• df.to_numpy(): coverts the DataFrame to a NumPy array
• df.describe(): shows a quick statistic summary of your data.
• df.to_dict(orient='dict', index=True) returns a dictionary. Possible values for orient:
‘dict’, ‘list’, ‘series’, ‘split’, ‘tight’, ‘records’, ‘index’
• df.to_html(header=True, index=True, classes=None, escape=True, table_id=None):
returns an html table
• pd.read_csv(filename, delimiter=’,’) and df.to_csv(path, delimiter=’,’)

26
• df.apply(function) or df.map(function)
• Slicing:
o series = df['State'] by label (columns)
o sliced_df = df[1:4] getting a slice (rows)
o multiaxis_slice = df.loc[1:3, ['State', 'City']] slice by label (rows and cols),
includes the fourth row
o multiaxis_slice_iloc = df.iloc[1:3, 0:2] slice by position (rows and cols)
• Arithmetical and statistical methods:
o df.mean() Mean column per column
o df.max() Max value in each column
o df.min() Min value in each column
o df.sum() Sum of the values in each column
o df.count() Count non-NA cells for each column or row
o df.diff() First discrete difference of element

27
28

You might also like