Skip to main content Link Search Menu Expand Document (external link)

Lecture 06

2022-10-12 | Week 3 | by Ashwin Ranade

Ashwin here; this continues from last lecture, and covers the slides 19-41 on python_intro_v1. The next lecture notes will be the extra video content, a continuation of Lecture 6.

Table of Contents

Variables and Types (continued)

Variables and Scope

  • in Python, once you define a variable, it stays in scope until function ends (unlike C++)
def foo():
  if True:
    x = 10
  print(x)
foo()
----------------
10

Range and Loops

  • range(x,y,z) returns a sequence of integers between [x,y), advacing by z each iteration (z is default 1).
    • range(5,9,2) => [5,7]
  • break terminates a loop

Classes

Instantiating a class Car and using an object of the Car class:

c1 = Car(16) #instantiation
c1.drive(10) #calling a method of the Car class

Class definition:

class Car:
  MILES_PER_GAL = 30
  def __init__(self, gallons):
    self.gas_gallons = gallons
    self.odometer = 0

  def drive(self, miles):
    gals_needed = miles / Car.MILES_PER_GAL
    if self.gas_gallons > gals_needed:
      self.gas_gallons -= gals_needed
      self.__update_odometer(miles)

  def __update_odometer(self, miles):
    self.odometer += miles

  def get_odometer(self):
    return self.odometer
  • first param of each method must be self (this in C++)
    • normal functions have no self param
  • constructor: __init__
  • we can’t declare a class’s member variables without assigning them (unlike C++)

question: Why does changing get_odemeter() to:

def get_odometer(self):
  return odemeter

result in an error?

answer: We refer to object member variables through self.odometer inside Python classes. odemeter now refers to a (non-existent) local variable!

Note: MILES_PER_GAL is a class variable, since it’s associated with the overall class; self.odomoeter and self.gas_gallons are object member variables, since they’re specific to the object.

more classes

  • calling a method from another method: same as class variables, use the self. prefix
  • private method: prefix with 2 underscores (__update_odometer); all other methods are public by default

Definition of a Python class is a sequence of statements, just like a regular Python program! For example, this code is legal:

class Car:
  print('I like traffic lights')
  def drive(self,miles):
    print(f'I drove {miles} miles!')
  print ("As long as they're green")

Objects

Python allocates objects using object references.

Syntax: use the . dot operator to call methods through object references

c = Circle(10)
print(c.area())
  • Every Python object is allocated on the heap (unlike C++, where we can choose between stack and heap)
  • ALL variables are object references!
  • even integer variables created through things like i = 42 are object references that point to an integer value sitting in heap memory
    • in practice, you can view things like i = 42 \n i = 10 as reassigning i’s value when i is a primitive
    • under the hood, i first points to 42; then, a new object 10 is allocated on the head, and i changes its pointer to the new object

Every Python object is allocated on the heap, and then pointed to be an object reference.

image

Object Construction

c = Circle(10)

  • Before our constructor runs, Python allocates RAM for a new Circle object
    • self param of the object points at the newly reserved block of RAM
    • member variables are dynamically created and then assigned inside the block of RAM
  • finally, the variable (object reference, in this case c) is assigned so that it points to the Circle object

Question: Is every Python object of a given class guaranteed to have the same set of member variables? Answer: No. Example:

def __init__(self, rad):
    self.rad = rad
    if rad > 100:
      self.really_big_circle = True

Method Calling

print(c.area())

  • when we call a method, Python passes the object reference to the self param
  • in this case, the object reference is c, from c = Circle(10)

Copying of Objects:

  • Assignment of object references means both object references will point to the same object! Hence, a change in one object, will affect both!
  • If you want to make a copy of an object and everything it points to, you must use the copy command
c = Circle(1)
c2 = c
c.set_radius(10) #c and c2 radius is BOTH changed to 10

Making a copy:

import copy
c = Circle(1)
c2 = copy.deepcopy(c)

Garbage Collection + Destructors:

  • Python has automatic GC
  • as long as some variables refer to an object, the object is kept around

Destructors: rarely used; if an object is garbage collected (not guaranteed), the destructor will automatically be called

  • Python automatically frees allocated objects, unlike in C++, so we don’t have to do de-allocate objects in Python destructors!

Destructor example

 def __del__(self):
    if self.book.finished_reading():
      print("You graduated!")

Destructors could be used to free a system resource that’s not managed by GC. However, since we have no guarantee that the destructor will ever run, we should define our own tear-down method and explicitly call it!

Example of an explicit tear-down method:

  def delete_temp_file(self):
    self.tmpfile.close()         # closes file
    os.remove(self.tmpfile.name) # deletes file

Inheritance

class Student(Person) -> derived class Student is inherited from base class Person

  • you may re-define any method in derived class (methods are virtual by default)
  • derived class can call any base class method with super() prefix
  • make sure the derived constructor calls the base class constructor! super().__init__(name)

Example:

class Person:
  def __init__(self, name):
    self.name = name

  def talk(self):
    print('Hi!\n')

  def get_name(self):
    return self.name

class Student(Person):
  def __init__(self, name):
    super().__init__(name)
    self.units = 0

  def talk(self):
    print(f"Heya, I'm {super().get_name()}.")
    print("Let's party! Oh... and ")
    super().talk();

Duck Typing

class PersonInDuckSuit:
  ... 			  # code omitted for clarity
  def quack(self):
    print('Hi! Err... oops, I mean quack quack.')

class Duck:
  ... 			  # code omitted for clarity
  def quack(self):
    print('Quack quack quack!')

class Vehicle:
  ... 			  # code omitted for clarity
  def drive(self):
    print('Vrooooom!')

If an object doesn’t have a quack() method, Python generates an error!

Hi! Err... oops, I mean quack quack.
Quack quack quack!
AttributeError: 'Vehicle' object has no attribute 'quack'

This is called “duck typing” – in Python, if an object has the requested method, a call to it will just work. “If it quacks like a duck, then it must be a duck.”

Practical uses of Duck Typing:

  • Enabling classes to provide a description of themselves in a generic way.
  • Enabling objects support comparison against other objects in a generic way.
  • Enabling container objects to be iterated over in a generic way.

example: __str__

  • In Python, you may add a method named __str__ to any class to print out a human-readable description
  • When you use print() on such an object, Python will automatically call the object’s __str__ function!
class Duck:
  ...
  def __str__(self):
    return "I'm a duck with " + \
         str(self.feathers) + " feathers."

class Vehicle:
  ...
  def __str__(self):
    return "I'm a vehicle with " + \
         str(self.hp) + " horsepower."

daffy = Duck(1000)
print(daffy)
outback = Vehicle(210)
print(outback)
----
I'm a duck with 1000 feathers.
I'm a vehicle with 210 horsepower.

Object Equality in Python

  • The == operator tests for equality of value.
  • The “is” operator tests if two object references refer to the same exact object in memory.
  • “is not” tests if two object references refer to different objects in memory.
# Different types of equality in Python
fav = 'pizza'
a = f'I <3 {fav}!'
b = f'I <3 {fav}!'
c = a

if a == b:
  print('Both objects have same value!')
if c is a:
  print('c and a refer to the same obj')
if a is not b:
  print('a and b refer to diff. objs')
------------------------------
Both objects have same value!
c and a refer to the same obj
a and b refer to diff. objs
  • In this case, a and b refer to two different objects in memory.

Every distinct object has a unique ID number or “identity” in Python – you can ask for this with the id() function.

print(id(a))
print(id(b))
print(id(c))
-----------------
140426129935136
140426129649376
140426129935136

These 2 printed IDs are different:

# Each object has a unique ID in Python
booger = 10
print(id(booger))
booger = booger + 1 #same deal with booger += 1
print(id(booger))

This is because booger += 1 creates a new booger object in Python.

Note: this behavior is slightly different for lists, see Campuswire post here

None

Setting a variable to None is like setting a C++ pointer to nullptr.

#This is the Pythonic (preferred) way of seeing if an object reference does not refer to None!
if obj is not None:
  <code here>
#This is the Pythonic way of seeing if an object reference refers to None!
if what is None:
 <code here>

Some common mistakes/pitfalls:

  • False is DIFFERENT than None in Python
  • if not q != if q is None
    • if not q checks if q is considered false (empty list, None, False)
    • if q is None checks specifically if q is None; for example, q is None is False if we have q=False
    • further reading: https://stackoverflow.com/questions/7152441/python-if-not-val-vs-if-val-is-none
q = None

if q is False:
  print('Is None the same as False?')

if not q:
  print('Does not work with None?')

if q is None:
  print('Ahhh q is None!')

if q == None:
  print('Ahh q == None!')
-------------------------
Does not work with None?
Ahh q is None!
Ahh q == None!

Note: q == None may not work if a custom comparison operator (overwriting ==) is defined.

  • there’s an example on this page: https://stackoverflow.com/questions/3257919/what-is-the-difference-between-is-none-and-none