🐍 Python
Decorators
A decorator is any function that accepts a function and returns a function. Decorators are one of the main ways that Python implements functional programming principles.
Functions are first-class objects and can be passed as parameters.
import logging
def hello_wrapper(name, func):
func(f'Hello {name}')
hello_wrapper("world", func=print) # Hello world
hello_wrapper("logs", func=logging.warning) # WARNING:root:Hello logs
with open('hello.txt', 'w') as f:
hello_wrapper('everyone!', func=f.write)
import random
def anagram(t):
l = [c for c in t]
random.shuffle(l)
print("".join(l))
hello_wrapper('Japushku', anagram) # eHoulhluaskpJ
hello_wrapper("world", func=print()) # Error
def outer():
print('Hi from the outer function')
def inner():
print('Hello from the inner function')
inner()
We can use the __name__
attribute to access a passed function's name.
def hello(func):
print(f'Hello {func.__name__}')
hello(outer) # Hello outer
def hello(func):
print(f'Hello {func.__name__}')
return func
hello(outer)()
'''
Hi from the outer function
Hello from the inner function
'''
new_outer = hello(outer)
new_outer is outer # True
def wrapper(func):
print(f'Before {func.__name__}')
func()
print(f'After {func.__name__}')
wrapper(outer)
'''
Before outer
Hi from the outer function
Hello from the inner function
After outer
'''
wrapper
is called the decorator and outer
has been decorated.
def wrapper(func):
def _wrapper():
print(f'Before {func.__name__}')
func()
print(f'After {func.__name__}')
return _wrapper
outer = wrapper(outer)
@
:
@wrapper
def outer():
print('Hi from the outer function')
def inner():
print('Hello from the inner function')
inner()
_wrapper
here does not accept any positional arguments, so wrapping functions that take arguments will produce a TypeError
@wrapper
def say_hello(name):
print(f'Hello {name}!') # error
*args, **kwargs
into the definition of the inner function, as well as the invocation of the function passed in.
def wrapper(func):
def _wrapper(*args, **kwargs):
print(f'Before {func.__name__}')
func(*args, **kwargs)
print(f'After {func.__name__}')
return _wrapper
def wrapper(func):
def _wrapper(*args, **kwargs):
print(f'Before {func.__name__}')
value = func(*args, **kwargs)
print(f'After {func.__name__}')
return value
return _wrapper
__name__
attribute reveals that it is still named _wrapper
say_hello.__name__ # '_wrapper'
functools.wraps
is a decorator factory to reassign attributes to the wrapped function. This is considered superior to the functools.update_wrapper
function which is also available.
def wrapper(func):
@functools.wraps(func)
def _wrapper(*args, **kwargs):
print(f'Before {func.__name__}')
value = func(*args, **kwargs)
print(f'After {func.__name__}')
return value
return _wrapper
Classes
Properties
In the Python documentation, attributes accessed with accessor functions are called managed attributes, which makes the term equivalent to properties in C#.
Three methods can be defined using the @property
decorator
def __init__(self, price):
self._price = price
@property
def price(self):
return self._price
@price.setter
def price(self, new_price):
if new_price > 0:
self._price = new_price
else:
raise ValueError
@price.deleter
def price(self):
del self._price
Class methods
The @classmethod
decorator prevents the interpreter from passing in the instantiated object using self
, rather the class itself is passed in as the cls
argument. This means that the methods decorated as such must take not self
as the first argument but cls
@classmethod
def classmethod(cls):
pass
The @staticmethod
decorator prevents the interpreter from passing any additional arguments whatsoever. The resulting method has no access to the object itself nor the class and functions like a procedurally defined function.
Formatting
flake8, black, and yapf are CLI tools used to automatically format Python code.
Virtual environments
pipenv
pipenv --python 3.6
venv
Create a virtual environment named project
python -m venv project
virtualenv
Create a virtual environment named project
using a different version of Python
virtualenv -p /usr/bin/python2 project
Testing
Pytest is a popular testing framework preferred to unittest by many Python developers because it follows Pythonic conventions more closely.
In contrast to unittest's custom methods, pytest relies on the builtin assert
statement.
from phonebook import PhoneBook
import pytest
@pytest.fixture
def phonebook():
phonebook = PhoneBook()
yield phonebook
phonebook.clear()
def test_lookup_by_name(phonebook):
phonebook.add("Bob","1234")
assert "1234" == phonebook.lookup("Bob")
def test_phonebook_contains_all_names(phonebook):
phonebook.add("Bob", "1234")
assert "Bob" in phonebook.names()
def test_missing_name_raises_error(phonebook):
with pytest.raises(KeyError):
phonebook.lookup("Bob")
python -m pytest
import unittest
from phonebook import PhoneBook
class PhoneBookTest(unittest.TestCase):
def test_lookup_by_name(self):
self.phonebook.add("Bob", "12345")
number = self.phonebook.lookup("Bob")
self.assertEqual("12345", number)
def test_missing_name(self):
with self.assertRaises(KeyError):
self.phonebook.lookup("missing")
def test_empty_phonebook_is_consistent(self):
self.assertTrue(self.phonebook.is_consistent())
def setUp(self) -> None:
self.phonebook = PhoneBook()
def tearDown(self) -> None:
self.phonebook.clear()
python -m unittest
import os
class PhoneBook:
def __init__(self, cache_directory = os.getcwd()):
self.numbers = {}
self.filename = os.path.join(cache_directory, "phonebook.txt")
self.cache = open(self.filename, "w")
def add(self, name, number):
self.numbers[name] = number
def lookup(self, name):
return self.numbers[name]
def is_consistent(self):
return True
def names(self):
return set(self.numbers.keys())
def clear(self):
self.cache.close()
os.remove(self.filename )
Doctest
A doctest is a docstring containing what looks like interactive Python sessions. Python Docs
"""
Return the factorial of n, an exact integer >= 0.
>>> [factorial(n) for n in range(6)]
[1, 1, 2, 6, 24, 120]
>>> factorial(30)
265252859812191058636308480000000
>>> factorial(-1)
Traceback (most recent call last):
...
ValueError: n must be >= 0
Factorials of floats are OK, but the float must be an exact integer:
>>> factorial(30.1)
Traceback (most recent call last):
...
ValueError: n must be exact integer
>>> factorial(30.0)
265252859812191058636308480000000
It must also not be ridiculously large:
>>> factorial(1e100)
Traceback (most recent call last):
...
OverflowError: n too large
"""
if __name__ == '__main__':
import doctest
doctest.testmod()
pytest
PyTest relies on the built-in assert
statement.
Fixtures
The @pytest.fixture
decorator facilitiates the creation of test fixtures.
The fixture function's name is used as argument to the test case, and the value returned can be used by the logic within.
(src)
Any clean-up logic can be invoked in this fixture as well by replacing return
with yield
.
Pytest also provides its own tmpdir
test fixture for temporary directories. (src)
from phonebook import PhoneBook
import pytest
def test_lookup_by_name(phonebook):
phonebook=PhoneBook()
phonebook.add("Bob","1234")
assert "1234" == phonebook.lookup("Bob")
def test_phonebook_contains_all_names(phonebook):
phonebook = PhoneBook()
phonebook.add("Bob", "1234")
assert "Bob" in phonebook.names()
def test_missing_name_raises_error(phonebook):
phonebook = PhoneBook()
with pytest.raises(KeyError):
phonebook.lookup("Bob")
from phonebook import PhoneBook
import pytest
@pytest.fixture
def phonebook():
phonebook = PhoneBook()
yield phonebook
phonebook.clear()
def test_lookup_by_name(phonebook):
# phonebook = PhoneBook()
phonebook.add("Bob","1234")
assert "1234" == phonebook.lookup("Bob")
def test_phonebook_contains_all_names(phonebook):
# phonebook = PhoneBook()
phonebook.add("Bob", "1234")
assert "Bob" in phonebook.names()
def test_missing_name_raises_error(phonebook):
# phonebook = PhoneBook()
with pytest.raises(KeyError):
phonebook.lookup("Bob")
from phonebook import PhoneBook
import pytest
@pytest.fixture
def phonebook(tmpdir):
phonebook = PhoneBook(tmpdir)
return phonebook
def test_lookup_by_name(phonebook):
# phonebook = PhoneBook()
phonebook.add("Bob","1234")
assert "1234" == phonebook.lookup("Bob")
def test_phonebook_contains_all_names(phonebook):
# phonebook = PhoneBook()
phonebook.add("Bob", "1234")
assert "Bob" in phonebook.names()
def test_missing_name_raises_error(phonebook):
# phonebook = PhoneBook()
with pytest.raises(KeyError):
phonebook.lookup("Bob")
unittest
unittest is a testing framework built into Python's Standard Library that was based on JUnit. unittest came out in 2001, when JUnit was being ported and adapted to many languages. Collectively, these frameworks were referred to as the xUnit family. unittest's method names do not follow Python conventions because it predates the PEP-8 naming standard.
unittest allows you to create test classes that inherit from TestCase
.
Assertions
Assertions are implemented in individual methods of the TestCase subclass through unittest methods like assertEqual
and assertRaises
, etc.
Notably, TestCase subclasses must not have an __init__()
constructor method defined.
def test_lookup_by_name(self):
phonebook = PhoneBook()
phonebook.add("Bob", "12345")
number = phonebook.lookup("Bob")
self.assertEqual("12345", number)
assertRaises
must be placed in a context manager.
Here, the test case will run the code within the with
block and check to make sure it raises the specified exception: KeyError
: (src)
def test_missing_name(self):
fleet = Fleet()
with self.assertRaises(KeyError):
fleet.lookup("bla")
Fixtures
setUp
is run before every test method, allowing a test fixture to be created to avoid repetitive code.
tearDown
is called after every method, which allows these resources to be released, even if the test case raises an exception. However, if it is setUp
that raises the exception, then neither the test case nor tearDown
will run. (src, src)
import unittest
from phonebook import PhoneBook
class PhoneBookTest(unittest.TestCase):
def test_lookup_by_name(self):
phonebook = PhoneBook()
phonebook.add("Bob", "12345")
number = phonebook.lookup("Bob")
self.assertEqual("12345", number)
def test_missing_name(self):
phonebook = PhoneBook()
with self.assertRaises(KeyError):
phonebook.lookup("missing")
@unittest.skip("WIP")
def test_empty_phonebook_is_consistent(self):
phonebook = PhoneBook()
self.assertTrue(phonebook.is_consistent())
import unittest
from phonebook import PhoneBook
class PhoneBookTest(unittest.TestCase):
def setUp(self) -> None:
self.phonebook = PhoneBook()
def tearDown(self) -> None:
self.phonebook.clear()
def test_lookup_by_name(self):
# phonebook = PhoneBook()
self.phonebook.add("Bob", "12345")
number = self.phonebook.lookup("Bob")
self.assertEqual("12345", number)
def test_missing_name(self):
# phonebook = PhoneBook()
with self.assertRaises(KeyError):
self.phonebook.lookup("missing")
@unittest.skip("WIP")
def test_empty_phonebook_is_consistent(self):
# phonebook = PhoneBook()
self.assertTrue(self.phonebook.is_consistent())
The @unittest.skip
decorator will tell the test runner to skip the decorated test case (src)
@unittest.skip("WIP")
def test_empty_phonebook_is_consistent(self):
phonebook = PhoneBook()
self.assertTrue(phonebook.is_consistent())
The command line entry point is made with a call to unittest.main()
, which executes the tests.
(src)
import unittest
from my_sum import sum
class TestSum(unittest.TestCase):
def test_list_int(self):
"""
Test that it can sum a list of integers
"""
data = [1, 2, 3]
result = sum(data)
self.assertEqual(result, 6)
if __name__ == '__main__':
unittest.main()
Integration tests
By convention, tests are put in their own directory as sibling to the main module ( in order to be able to import it ). Integration and unit tests should be organized separately.
.
├── project
│ └── __init__.py
└── tests
├── integration
└── unit
Run all integration tests within specified directory.
python -m unittest discover -s tests/integration
Tasks
Deserialize
import yaml
with open('./starships.yaml') as f:
starships = yaml.safe_load(f)
import json
with open('./starships.json') as f:
data=json.load(f)
Serialize
import yaml
with open('./starships.yaml','w') as f:
yaml.dump(starships, f)
import json
with open('/starships.json',"w") as f:
json.dump(data,f)
Modules
When learning unfamiliar packages and importing them in a demonstration script, care must be taken that the demonstration script does not have the same name as the package being studied. If so, attempting to import the package while in an interpreter within that directory will cause the interpreter to try importing the incomplete script and not the package.
When running a Python interpreter within this directory, the files "calc" and "main" can be imported as modules by specifying their names with no file extension.
.
├── calc.py
└── main.py
import calc # No errors
import main # No errors
import calc.py # Error
import main.py # Error
Glossary
- Method resolution order
- Method resolution order (MRO) is the order of base classes that are searched when using super().
It is accessed with
__mro__
, which returns a tuple of base classes in order of precedence, ending inobject
which is the root class of all classes. - Non-interactive debugging
- Non-interactive debugging is the most basic form of debugging, dependent on
print
orlog
statements placed within the body of code. - Type slot
- A type slot is any of a number of fields within each magic method, including
__new__()
,__init__()
, and__prepare__()
(which returns a dictionary-like object that's used as the local namespace for all code from the class body)