A decorator is syntactic sugar for a function that decorates another.
A decorator function takes in another function as a callback and returns a modified version of the callback function.
We can use decorators to add to or modify the behavior of regular functions.
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
Without the decorator syntax, we would have to define our function, then reassign our function to the return value from invoking the decorator function on our old function.
# decorator function from datetime import datetime def timer(func): def wrapper(): before_time = datetime.now() val = func() print(val) after_time = datetime.now() total = after_time - before_time return total return wrapper # function to decorate def my_function(): return "hello" # before decorating print(my_function()) # returns "hello" # decorated function my_function = timer(my_function) # after decorating print(my_function())
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?
from datetime import datetime def timer(func): def wrapper(name): before_time = datetime.now() val = func(name) 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}"
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
A module is code that is imported from a file or directory. A package is a collection of modules. A module/package can be:
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.
import
keywordThe Python standard library has a number of packages you can import without having to install them—they are included when you install Python (documentation).
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))
With aliasing
import random as rand # import everything, alias random as rand print(rand.randint(0, 10))
import
keywordYou can also import just specific functions from a package using the from
keyword.
from random import randint # import just the randint function print(randint(0, 10))
from random import randint, shuffle # import multiple functions at the same time print(randint(0, 10))
from random import randint as r_i, shuffle print(r_i(0, 10))
If I have two files at the same level, I can import one file using the filename (minus the .py
).
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
When I import the other_code.py
file, all of the code in that file will run, even if I'm just importing one function.
project_folder
| my_code.py
| other_code.py
| subfolder
| __init__.py
| file_one.py
To import code from inside a subfolder, use import folder_name.file_name
.
# inside my_code.py import subfolder.file_one # or from subfolder import file_one # or from subfolder.file_one import my_function
__init__.py
This file should go in any directory being imported. It will transform a plain old directory into a Python module/package.
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).
__init__.py
if we can import without it?Python 3.3+ creates an implicit namespace package if no __init__.py
file exists for a directory. We want to avoid this most of the time!
We need a __init__.py
if we want to run the directory as a module, if we want to run pytest
on it, etc.
This file can be completely empty (and often will be). It can also be the place where we initialize our applications!
Reminder: In Javascript, when we imported from other files, we used relative import statements.
project_folder
| top_level_file.js
└──subfolder
| file_one.js
| file_two.js
The import path changes depending on what file we are in.
// inside top_level_file.js import { someObject } from "./subfolder/file_two"
// inside file_one.js import { someObject } from "./file_two"
project_folder
| top_level_file.py
└──subfolder
| __init__.py
| file_one.py
| file_two.py
In Python, absolute import statements are preferred when we are importing code from other files.
"Absolute" means that all imports are relative only to one location - the top-level file being executed.
Absolute imports are preferred because they are more explicit and straightforward.
# inside top_level_file.py import subfolder.file_two
# inside file_one.py import subfolder.file_two
However…
…that means that if I try to run a file directly, instead of from the intended entrypoint of my application, the file won't work correctly.
# inside top_level_file.py import subfolder.file_one print("Hello from top_level_file.py")
# inside file_one.py import subfolder.file_two print("Hello from file_one.py")
# inside file_two.py print("Hello from file_two.py")
__init__.py
file in any folder that has python code if you are going to be importing from that folder. The __init__.py
file can be completely empty (and often will be)..venv
folder)Python tool | Node.js equivalent |
---|---|
pyenv | nvm |
pip | npm –global |
virtualenv | nvm + node_modules |
pipenv | npm + nvm |
Pipfile | package.json |
Pipfile.lock | package-lock.json |
pipenv
Create a virtual environment by running pipenv install
. If there is a Pipfile present, this will install the dependencies in the Pipfile, otherwise it will create a new Pipfile along with a virtual environment.
You can specify a particular version of Python to use in your virtual environment with --python
flag.
pipenv install --python 3.9.4
You can also pass in a path instead of a number .
pipenv install --python "/Users/username/.pyenv/versions/3.9.4/bin/python"
Many of the projects this week will specify a version of Python to use. If you try to use a version that you don't have installed, it will not work. Also, these projects expect you to be specifying the path instead of just a number.
If you see something like this
pipenv install --python "$PYENV_ROOT/versions/3.8.3/bin/python"
Run this instead:
pipenv install --python 3.9.4 # or whatever version you do have installed
If you aren't sure, you can check to see which version you have available with the command pyenv versions
.
pipenv
Install a dependency:
pipenv install package-name
Install a development-only dependency:
pipenv install --dev package-name
Uninstall a dependency:
pipenv uninstall package-name
pipenv
commandsActivate your virtual environment shell:
pipenv shell
Remove a virtual environment:
pipenv --rm
unittest
packageTo run tests with unittest:
python -m unittest
All tests must be in a folder called test
at the top level of the project. The test
folder must contain a __init__.py
unittest
testsInside a test file:
unittest
.unittest.TestCase
.import unittest from widget import Widget class TestWidget(unittest.TestCase): pass
unittest
testsTests are written as methods on the class.
test_
unittest.TestCase
class to make assertions
import unittest from widget import Widget class TestWidgetInitialize(unittest.TestCase): def test_initialize_widget_with_color(self): # arrange color = "blue" test_widget = Widget(color) # act result = test_widget.color # assert self.assertEqual(result, color)
pytest
packageCreate a virtual environment if you haven't yet, and install pytest.
pipenv install pytest --python 3.9.4
Run tests at the command line by running
pytest
Make a directory called test
at the top level of your project (be sure it contains a __init__.py
).
Test files must be in test
directory, and filenames must begin or end with test
.
pytest
testsDefine test functions directly—no need for classes. Function names must begin with test
to be treated as a unit test.
Use assert
keyword, followed by the conditional you are trying to test.
You can run unittest
tests with pytest
, but not vice versa.
from widget import Widget def test_initialize_widget_with_color(): # arrange color = "blue" test_widget = Widget(color) # act result = test_widget.color # assert assert result == color