Codecademy Logo

Software Engineering in Python I

Python repr method

The Python __repr__() method is used to tell Python what the string representation of the class should be. It can only have one parameter, self, and it should return a string.

class Employee:
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
john = Employee('John')
print(john) # John

Python class methods

In Python, methods are functions that are defined as part of a class. It is common practice that the first argument of any method that is part of a class is the actual object calling the method. This argument is usually called self.

# Dog class
class Dog:
# Method of the class
def bark(self):
print("Ham-Ham")
# Create a new instance
charlie = Dog()
# Call the method
charlie.bark()
# This will output "Ham-Ham"

Instantiate Python Class

In Python, a class needs to be instantiated before use.

As an analogy, a class can be thought of as a blueprint (Car), and an instance is an actual implementation of the blueprint (Ferrari).

class Car:
"This is an empty class"
pass
# Class Instantiation
ferrari = Car()

Python Class Variables

In Python, class variables are defined outside of all methods and have the same value for every instance of the class.

Class variables are accessed with the instance.variable or class_name.variable syntaxes.

class my_class:
class_variable = "I am a Class Variable!"
x = my_class()
y = my_class()
print(x.class_variable) #I am a Class Variable!
print(y.class_variable) #I am a Class Variable!

Python init method

In Python, the .__init__() method is used to initialize a newly created object. It is called every time the class is instantiated.

class Animal:
def __init__(self, voice):
self.voice = voice
# When a class instance is created, the instance variable
# 'voice' is created and set to the input value.
cat = Animal('Meow')
print(cat.voice) # Output: Meow
dog = Animal('Woof')
print(dog.voice) # Output: Woof

Python type() function

The Python type() function returns the data type of the argument passed to it.

a = 1
print(type(a)) # <class 'int'>
a = 1.1
print(type(a)) # <class 'float'>
a = 'b'
print(type(a)) # <class 'str'>
a = None
print(type(a)) # <class 'NoneType'>

Python class

In Python, a class is a template for a data type. A class can be defined using the class keyword.

# Defining a class
class Animal:
def __init__(self, name, number_of_legs):
self.name = name
self.number_of_legs = number_of_legs

Python dir() function

In Python, the built-in dir() function, without any argument, returns a list of all the attributes in the current scope.

With an object as argument, dir() tries to return all valid object attributes.

class Employee:
def __init__(self, name):
self.name = name
def print_name(self):
print("Hi, I'm " + self.name)
print(dir())
# ['Employee', '__builtins__', '__doc__', '__file__', '__name__', '__package__', 'new_employee']
print(dir(Employee))
# ['__doc__', '__init__', '__module__', 'print_name']

__main__ in Python

In Python, __main__ is an identifier used to reference the current file context. When a module is read from standard input, a script, or from an interactive prompt, its __name__ is set equal to __main__.

Suppose we create an instance of a class called CoolClass. Printing the type() of the instance will result in:

<class '__main__.CoolClass'>

This means that the class CoolClass was defined in the current script file.

Functional Programming is Declarative Review

Functional programming is a programming paradigm that adheres to the declarative style of programming.

Declarative Programming Review Card

In the declarative style of programming, the programmer describes what must be done as opposed to how it must be done.

nums = [9, 6, 5, 2, 3]
nums.sort() # Sorting the list declaratively using the sorting algorithm provided by the Python langauge
def custom_sort(list):
# Custom sorting algorithm defined here
custom_sort(nums) # Sorting the list non-declarativley by using a custom built sorting algorithm

Functions Should Have no Side Effects

In functional programming, a function is expected to be “pure”, meaning it should have no side effects!

A side effect is when a function alters the state of an external variable.

A function can read an external variable, but it should not change it!

nums = [1, 3, 5, 9]
# This function has side effects because it alters the nums list!
def square1():
for i in range(len(nums)):
nums[i] = nums[i]**2
# This function does not have side effects because it does not alter the nums list!
def square2(lst):
new_list = []
for i in lst:
new_list.append(i)
return i
# Note: squre2 should loop using recursion. The for-loop is used instead for simplicity!

Using namedtuple

When storing data that contains multiple properties, using a namedtuple is more efficient than using a regular tuple. It allows you to store and reference the properties of a data entry by their name.

The accompanying code shows a data entry for a student which contains information about the student’s age, eye color, and gender.

from collections import namedtuple as namedtuple
# Record for student Peter stored in a regular tuple
peter = (16, blue, "male") # This is error-prone because you are forced to remember what each entry means.
student = namedtuple("student", ["age", "eye_color", "gender"])
peter = student(16, "blue", "male")
# This is more efficient and less error-prone as the student's data can be accssed like so: peter.age, peter, eye_color, peter.gender

Lazy Iteration

We use lazy iteration in functional programming to be more efficient with memory. With lazy iteration, the iterator is triggered only when the next value is needed.

In the example given, evens is intended to be a collection of the even numbers in nums.

The next even number in evens will be obtained by calling next(evens) and should only be done when the next even number is needed!

nums = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
evens = filter(lambda x: x % 2 == 0, nums)
print(evens) # This will not output a tuple of the even numbers in nums because the iterator has not yet been triggered!
print(next(evens)) # This will output 2

Using Higher-order Functions Together

The higher-order functions map(), filter() and reduce() can be used together to execute a task that would otherwise require many loops neatly.

from functools import reduce
nums = (2, 4, 6, 8, 10, 12, 14, 16)
# The following adds 1 to all numbers greater than 8 (in nums) and sums them all up
sum = reduce(lambda x, y: x + y, map(lambda x: x+1, filter(lambda x: x > 8, nums)))

Working With Large Data Sets

Functional programming is widely used to process data stored in CSV files or JSON files. Since the files could contain a large amount of data, lazy iteration plays an essential role. Data is imported when needed instead of occupying too much memory by loading it all at once.

Storing Data From CSV Files in a namedtuple

We can represent data records stored in CSV files using a namedtuple. In the code block shown, the map() function is used to read in a record of data and represent it as a namedtuple.

import csv
from collections import namedtuple
from functools import reduce
tree = namedtuple("tree", ["index", "girth", "height", "volume"])
with open('trees.csv', newline = '') as csvfile:
reader = csv.reader(csvfile, delimiter=',', quotechar='|')
fields = next(reader)
# This will return an iterator that will be triggerd when the next tree is needed!
trees = map(lambda x: tree(x[0], x[1], x[2], x[3]), reader)

Setting the Logging Level

Setting the logging level for the logger object will allow all messages at and above that level to be produced in the output stream. The logging level can be set by using setLevel(). If no level is manually set, the default level logging.WARNING is used.

import logging
logger.setLevel(logging.INFO)

Formatting Options for Logging

Formatting of logged messages can be changed by using the Formatter class. Information like timestamps, function names, and line numbers can be included in the logged message.

import logging
formatter = logging.Formatter('[%(asctime)s] %(message)s')

Logging to File and Console

Log messages can be directed to both files and the console by adding FileHandler and StreamHandler handler objects to the logger.

While logging to console is helpful if needing to review debugging log messages in real-time, logging to a file allows for the logged messages to exist well after the program execution has occurred.

import logging
import sys
file_handler = logging.FileHandler("program.log")
stream_handler = logging.StreamHandler(sys.stdout)

Configuring the Logger with basicConfig

For a simple, one-liner configuration of the logger, there is a basicConfig() method that will allow for adding handlers, setting formatting options for the logged messages, and setting the log level for the logger.

import logging
logging.basicConfig(filename='program.log', level=logging.DEBUG, format= '[%(asctime)s] %(levelname)s - %(message)s')

Using the Logging Module

Logging messages and exceptions is important for debugging applications and for logging important information that generates during execution. The proper method for logging these messages is the logging module.

import logging

Logging Levels

There are six logging levels associated with the logging module:

  • NOTSET, which has a numeric value of 0
  • DEBUG, which has a numeric value of 10
  • INFO, which has a numeric value of 20
  • WARNING, which has a numeric value of 30
  • ERROR, which has a numeric value of 40
  • CRITICAL, which has a numeric value of 50
import logging
logging.DEBUG
logging.INFO
logging.WARNING
logging.ERROR
logging.CRITICAL
logging.NOTSET

Learn more on Codecademy