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 byz
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
- normal functions have no
- 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 wheni
is a primitive - under the hood,
i
first points to 42; then, a new object10
is allocated on the head, andi
changes its pointer to the new object
- in practice, you can view things like
Every Python object is allocated on the heap, and then pointed to be an object reference.
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 theCircle
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
, fromc = 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 thanNone
in Pythonif not q
!=if q is None
if not q
checks ifq
is considered false (empty list, None, False)if q is None
checks specifically if q isNone
; for example,q is None
is False if we haveq=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