Week 17
W17D4
To import code from a module, we use the import
keyword. The import keyword will locate and initialize a module, and give you access to the specific names you have imported in the file.
There are no exports in Python! Anything we define in a module/file is automatically available for import.
import
keywordThe Python standard library has a number of packages you can import without having to install them. Let's use the random
package as an example (this would work the same with any package).
import random # import everything from random
print(random.randint(0, 10))
Use the as
keyword to alias package/object names.
import random as rand # import everything, alias random as rand
print(rand.randint(0, 10))
You can also import specific objects from a package using the from
keyword.
from random import randint, shuffle # import multiple functions at the same time
print(randint(0, 10))
If two files are at the same level, import using the filename (without the .py
extension).
project_folder
| my_code.py
| other_code.py
# inside my_code.py
import other_code
# import just a specific item
from other_code import my_function
If we need to specify a path to a file, we use dot notation:
# inside my_code.py
from folder.subfolder.filename import something
__init__.py
This file should go in any directory being imported. It will transform a plain old directory into a Python module/package. It can be completely empty, and often will be!
Upon import from a module/package, its__init__.py
file is implicitly executed, and all objects it defines are bound to the module's namespace (documentation).
Python Import Short Practice - 15 min
A decorator is a function that takes in another function as a callback and modifies or extends the behavior of the callback function.
Let's create a decorator that could be used for timing function calls.
from datetime import datetime
# our decorator, which takes in a callback function
def timer(func):
# define the wrapper function that we're going to return
def wrapper():
# get current time before function call
before_time = datetime.now()
# invoke the callback
val = func()
# log the return value of the function
print(val)
# get current time after function call
after_time = datetime.now()
# calculate total time
total = after_time - before_time
# return the total time
return total
# decorator returns the wrapper function object
return wrapper
Using the @decorator_name
syntax, we can shorten this:
def my_function():
return "hello"
my_function = timer(my_function)
To this:
@timer
def my_function():
return "hello"
Decorating a function definition (with the @decorator
syntax) does the same thing as reassigning the function name to the return value of the decorator.
What if I want to wrap functions that take arguments… but I want to be flexible about what kind of arguments the function takes?
from datetime import datetime
def timer(func):
def wrapper(*args, **kwargs):
before_time = datetime.now()
val = func(*args, **kwargs)
print(val)
after_time = datetime.now()
total = after_time - before_time
return total
return wrapper
@timer
def my_function_args(name):
return f"hello {name}"
@timer
def my_sum(sum1, sum2):
return sum1 + sum2
Decorators Quiz - 5 min (in your howework)
Hello World Decorator - 3 min
Order Decorator - 3 min
Timer Decorator - 10 min
Chain Decorator - 10 min
To create a class we use the class
keyword, and by convention, we capitalize the names of classes.
# python example
class Icon:
# more code to come
Python's constructor method is called __init__()
.
# python example
class Icon:
def __init__(self, color, shape):
self.color = color
self.shape = shape
We create instances of a class by invoking the class as though it is a function (this invokes the class's __init__()
method).
# in python
my_new_icon = Icon("blue", "circle")
self
?self
refers to the instance that a method was called on.
Whenever you invoke an instance method on a class instance, it is as though you are invoking the class's own method, and passing in the instance as an argument.
some_icon = Icon("blue", "square")
# both below do the same thing
some_icon.my_method("other argument")
Icon.my_method(some_icon, "other argument")
You can set attributes on the instance with dot notation (self.some_attribute = value
).
You can add instance methods to the class by defining functions and passing in self
.
class Icon:
def __init__(self, color, shape):
self.color = color
self.shape = shape
def my_method(self, word):
print(f"hello {word}")
return
Class variables are not attached to self
. They are available for access on the class itself and across instances.
If we update a class variable on an instance, a shadow instance variable is created that hides the class variable of the same name.
class Widget:
price = "$5"
def __init__(self, color):
# instance variables
self.color = color
my_widget = Widget("blue")
second_widget = Widget("chartreuse")
print(my_widget.price) # "$5"
print(Widget.price) # "$5"
my_widget.price = "$100"
print(second_widget.price) # "$5"
print(Widget.price) # "$5"
Widget.price = "$50"
print(second_widget.price) # "$50"
print(my_widget.price) # "$100"
We can use the @classmethod
decorator to write class methods.
The first argument will refer to the class itself (conventionally called cls
), rather than an individual instance.
# inside class
@classmethod
def widget_factory(cls, colors):
widgets = [cls(color) for color in colors]
print([widget.greet_widget() for widget in widgets])
return widgets
print(Widget.widget_factory(["red",
"yellow", "beige"]))
Static methods don't take implicit arguments—they can't access the class or any instance of it.
@staticmethod
def something_about_widgets():
return "widgets are neat"
Getters & setters allow us to have methods that behave like properties.
They provide a convenient interface for implementing more complicated logic necessary for getting/setting a class property.
They can also be useful for protecting "private" values on your class.
A getter allows you to define a method that behaves like a readable property. The @property
decorator over a method creates a getter.
While the getter is a function, it is invoked as if it were a property.
class Icon():
def __init__(self, color, shape):
self.color = color
self.shape = shape
# getter for ~secret~ password
@property
def my_password(self):
return "somebody's secret password"
my_icon = Icon("blue", "square")
print(my_icon.color)
# call the getter method as if we were just
# reading a property
print(my_icon.my_password)
A setter allows you to define a method that updates the getter "property". The decorator used to create a setter is @<getter_method_name>.setter
.
You can have a standalone getter, but you must have a getter in order to have a setter. The setter method runs when you change the getter "property."
class Icon():
def __init__(self, color, shape, pswd):
self.color = color
self.shape = shape
# set initial ~secret~ password
# this calls the setter method!
self.my_password = pswd
# getter for ~secret~ password
@property
def my_password(self):
return self._password
# setter for ~secret~ password
@my_password.setter
def my_password(self, new_val):
print("hashing password....")
self._password = str(new_val) + "12345" * 3
my_icon = Icon("blue", "square", "beepboop")
print(my_icon.my_password)
# call the setter method as if we were
# setting my_password as a regular property
my_icon.my_password = "new thing"
print(my_icon.my_password)
Bad Calculator - 10 min
Getters and Setters - 10 min
Regular Polygon - 15 min
Tree Traversal - Challenge - 15 min
To inherit from another class, we pass a reference to that class as an argument in the class definition.
We can use the super()
function to get a reference to the parent class, then invoke the desired function.
class Icon:
def __init__(self, color, shape):
self.color = color
self.shape = shape
class Gadget(Icon):
def __init__(self, color, shape, noise):
super().__init__(color, shape)
self.noise = noise
thingie = Icon("purple", "spiral", "whrrrrrr")
print(thingie) # <__main__.Gadget object at 0x105103d60>
print(thingie.color, thingie.shape, thingie.noise) # purple spiral whrrrrrr
In OOP, polymorphism allows us to have methods/attributes that behave differently for different classes.
Polymorphism is tied to inheritance. When a child class inherits from a parent class, that child class can override methods from the parent class.
With polymorphism, we can have our Gadget
class redefine a method from our parent Icon
class:
class Icon:
def __init__(self, color, shape):
self.color = color
self.shape = shape
def info(self):
return f"Icon that is a {self.color} {self.shape}"
class Gadget(Icon):
def __init__(self, color, shape, noise):
super().__init__(color, shape)
self.noise = noise
def info(self):
return f"Gadget that is a {self.color} {self.shape} and makes a {self.noise} noise"
icon = Icon("blue", "square")
print(icon.info())
thingie = Gadget("purple", "spiral", "whrrrrrr")
print(thingie.info())
Quadrilateral with Inheritance - 10 min
Triangle with Inheritance - 10 min
Book Polymorphism - 10 min
Magic Methods - 10 min
Linked List Iterator - 20 min
Linked List Project - 2 hrs