tags: Week 17 W17D4

Python Imports, Decorators, and Classes

Week 17 Day 4

Review from yesterday…

  • tuples
  • ranges & enumerate
  • dictionaries
  • sets
  • built ins
    • sorted
    • any/all
    • filter/map
    • enumerate
    • zip
  • list comprehensions
  • dictionary comprehensions

Today's Topics

  • Importing
  • Decorators
  • Classes
    • Basic Class Syntax
    • Inheritance
    • Polymorphism

The Python Import System

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.

The import keyword

The 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))

Import Python code from another file

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).

Import Practices (30 min)

Python Import Short Practice - 15 min

Decorators

What's a decorator?

A decorator is a function that takes in another function as a callback and modifies or extends the behavior of the callback function.

Decorators

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

Decorators

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.

Passing arguments through a 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

Decorator Practices (35 min)

Decorators Quiz - 5 min (in your howework)
Hello World Decorator - 3 min
Order Decorator - 3 min
Timer Decorator - 10 min
Chain Decorator - 10 min

Classes

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

Instances of classes

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")

Wait, what is 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")

Instance variables and methods

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

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"

Class methods

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

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 and setters

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.

Getters

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)

Setters

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)

Basic Class Syntax Practice (35 min)

Bad Calculator - 10 min
Getters and Setters - 10 min
Regular Polygon - 15 min
Tree Traversal - Challenge - 15 min

Inheritance

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

Polymorphism

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())

Class Inheritance Practice (30 min)

Quadrilateral with Inheritance - 10 min
Triangle with Inheritance - 10 min

Polymorphism Practices (40 min)

Book Polymorphism - 10 min
Magic Methods - 10 min
Linked List Iterator - 20 min

Long Practice (2 hrs)

Linked List Project - 2 hrs

1
{"metaMigratedAt":"2023-06-17T00:45:38.182Z","metaMigratedFrom":"Content","title":"Python Imports, Decorators, and Classes","breaks":true,"description":"Importing","contributors":"[{\"id\":\"dafd1858-850b-4d12-9d53-a1ac5e891cf8\",\"add\":1394,\"del\":1931},{\"id\":\"a6f34c0b-3567-4ed5-ba81-c2299c2d9369\",\"add\":23632,\"del\":11376}]"}
   changed a year ago 417 views
   owned this note