<style>
.present {
text-align: left;
}
img[alt=set_operations] {
width: 60%;
}
</style>
---
###### 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).
```python=
import random # import everything from random
print(random.randint(0, 10))
```
Use the `as` keyword to alias package/object names.
```python=
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.
```python=
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
```
```python=
# 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:
```python=
# 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](https://docs.python.org/3/reference/import.html#regular-packages)).
---
### 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.
```python=
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:
```python=
def my_function():
return "hello"
my_function = timer(my_function)
```
To this:
```python=
@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?
```python=
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=
# python example
class Icon:
# more code to come
```
Python's constructor method is called `__init__()`.
```python=
# 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).
```python=
# 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.
```python=
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`.
```python=
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.
```python=
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.
```python
# 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.
```python=
@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.
```python=
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."
```python=
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.
```python=
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:
```python=
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
---
{"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}]"}