Remove Python antipatterns, moved to Python repo
This commit is contained in:
parent
c1878d9826
commit
e24a5a7973
@ -1,248 +0,0 @@
|
|||||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
|
||||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
|
||||||
## Table of Contents
|
|
||||||
|
|
||||||
- [Python Antipatterns](#python-antipatterns)
|
|
||||||
- [Redundant type checking](#redundant-type-checking)
|
|
||||||
- [Restricting version in setup.py dependencies](#restricting-version-in-setuppy-dependencies)
|
|
||||||
- [Unwieldy if... else instead of dict](#unwieldy-if-else-instead-of-dict)
|
|
||||||
- [Overreliance on kwargs](#overreliance-on-kwargs)
|
|
||||||
- [Overreliance on list/dict comprehensions](#overreliance-on-listdict-comprehensions)
|
|
||||||
- [Mutable default arguments](#mutable-default-arguments)
|
|
||||||
- [Using `is` to compare objects](#using-is-to-compare-objects)
|
|
||||||
- [Instantiating exception with a dict](#instantiating-exception-with-a-dict)
|
|
||||||
- [Not strictly pinning all packages](#not-strictly-pinning-all-packages)
|
|
||||||
- [Reference](#reference)
|
|
||||||
|
|
||||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
|
||||||
|
|
||||||
# Python Antipatterns
|
|
||||||
|
|
||||||
## Redundant type checking
|
|
||||||
|
|
||||||
Bad:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def toast(bread):
|
|
||||||
if bread is None:
|
|
||||||
raise TypeError('Need bread to toast.')
|
|
||||||
if bread.is_toastable:
|
|
||||||
toaster.toast(bread)
|
|
||||||
```
|
|
||||||
|
|
||||||
In this case, checking against `None` is totally useless because in the next
|
|
||||||
line, `bread.is_toastable` would raise `AttributeError: 'NoneType' object has no attribute 'is_toastable'`. This is not a general rule, but in this case
|
|
||||||
I would definitely argue that adding the type checks hurts readability and adds
|
|
||||||
very little value to the function.
|
|
||||||
|
|
||||||
Good:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def toast(bread):
|
|
||||||
if bread.is_toastable:
|
|
||||||
toaster.toast(bread)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Restricting version in setup.py dependencies
|
|
||||||
|
|
||||||
Read those articles first:
|
|
||||||
|
|
||||||
- [setup.py vs.
|
|
||||||
requirements.txt](https://caremad.io/2013/07/setup-vs-requirement/)
|
|
||||||
- [Pin Your Packages](http://nvie.com/posts/pin-your-packages/)
|
|
||||||
- [Better Package Management](http://nvie.com/posts/better-package-management/)
|
|
||||||
|
|
||||||
**Summary: The main point is that `setup.py` should not specify explicit version
|
|
||||||
requirements (good: `flask`, bad: `flask==1.1.1`).**
|
|
||||||
|
|
||||||
In a few words, if library `lib1` requires `flask==1.1.1` and library `lib2`
|
|
||||||
requires `flask==1.1.2`, then you'll have a conflict and won't be able to use
|
|
||||||
them both in application `app`.
|
|
||||||
|
|
||||||
Yet in 99.999% of the cases, you don't need a specific version of flask, so:
|
|
||||||
|
|
||||||
- `lib1` should just require `flask` in `setup.py` (no version specified, not
|
|
||||||
even with inequality operators: `flask<=2` is bad for instance)
|
|
||||||
- `lib2` should just require `flask` in `setup.py` (same)
|
|
||||||
|
|
||||||
`app` will be happy using `lib1` and `lib2` with whatever version of `flask` it
|
|
||||||
wants.
|
|
||||||
|
|
||||||
`app`'s `requirements.txt` should be as specific as possible, ideally
|
|
||||||
strictly pinning (`==`) every single dependency. This way the app's stability
|
|
||||||
will be very predictable, because always the same packages version will be
|
|
||||||
installed.
|
|
||||||
|
|
||||||
Usually apps only use `requirements.txt`, not `setup.py`, because `pip install -r requirements.txt` is used when deploying.
|
|
||||||
|
|
||||||
The only exception for pinning a dependency in a library is in case of a known
|
|
||||||
incompatibility, but again this should be a very temporary move, because that
|
|
||||||
will prevent people from upgrading.
|
|
||||||
|
|
||||||
Ruby has a pretty similar dichotomy with [Gemspec and
|
|
||||||
gemfile](http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/).
|
|
||||||
|
|
||||||
## Unwieldy if... else instead of dict
|
|
||||||
|
|
||||||
Bad:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import operator as op
|
|
||||||
|
|
||||||
|
|
||||||
def get_operator(value):
|
|
||||||
"""Return operator function based on string.
|
|
||||||
|
|
||||||
e.g. ``get_operator('+')`` returns a function which takes two arguments
|
|
||||||
and return the sum of them.
|
|
||||||
"""
|
|
||||||
if value == '+':
|
|
||||||
return op.add
|
|
||||||
elif value == '-':
|
|
||||||
return op.sub
|
|
||||||
elif value == '*':
|
|
||||||
return op.mul
|
|
||||||
elif value == '/':
|
|
||||||
return op.div
|
|
||||||
else:
|
|
||||||
raise ValueError('Unknown operator %s' % value)
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: the operator module is a standard library modules that defines base
|
|
||||||
operator functions. For instance `operator.add(1, 1) == 2`.
|
|
||||||
|
|
||||||
This huge switch-like expression will soon become really difficult to read and
|
|
||||||
maintain. A more pythonic way is to use a dict to store the mapping.
|
|
||||||
|
|
||||||
Another reason is that to get 100% line and branch coverage, you will have to
|
|
||||||
create as many tests as you have mappings. Yet you could consider that the
|
|
||||||
value to operator mapping is part of the codebase's configuration, not its
|
|
||||||
behavior, and thus shouldn't be tested.
|
|
||||||
|
|
||||||
Good:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import operator as op
|
|
||||||
|
|
||||||
OPERATORS = {
|
|
||||||
'+': op.add,
|
|
||||||
'-': op.sub,
|
|
||||||
'*': op.mul,
|
|
||||||
'/': op.div,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_operator(value):
|
|
||||||
"""Return operator function based on string."""
|
|
||||||
operator = OPERATORS.get(value)
|
|
||||||
if operator:
|
|
||||||
return operator
|
|
||||||
else:
|
|
||||||
raise ValueError('Unknown operator %s' % value)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Overreliance on kwargs
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Overreliance on list/dict comprehensions
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Mutable default arguments
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Using `is` to compare objects
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
[Why you should almost never use “is” in
|
|
||||||
Python](http://blog.lerner.co.il/why-you-should-almost-never-use-is-in-python/)
|
|
||||||
|
|
||||||
## Instantiating exception with a dict
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def main():
|
|
||||||
raise Exception({'msg': 'This is not a toaster', 'level': 'error'})
|
|
||||||
```
|
|
||||||
|
|
||||||
Why is this an antipattern? Exception are meant to be read by human beings.
|
|
||||||
Thus, their first argument should be a human-readable error message, like this:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class NotAToasterException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
raise NotAToasterException('This is not a toaster')
|
|
||||||
```
|
|
||||||
|
|
||||||
Most tools expect this, most importantly
|
|
||||||
[Sentry](https://getsentry.com/welcome/) which is a tool used to get alerts
|
|
||||||
when exception are raised in production. An `Exception`'s message should be
|
|
||||||
short so that it can be displayed on a single line.
|
|
||||||
|
|
||||||
If you need to attach custom metadata to an exception, the proper way is to
|
|
||||||
have a custom constructor:
|
|
||||||
|
|
||||||
```python
|
|
||||||
class NotAToasterException(Exception):
|
|
||||||
|
|
||||||
def __init__(self, message, level):
|
|
||||||
super(NotAToasterException, self).__init__(message)
|
|
||||||
self.message = message
|
|
||||||
self.level = level
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
raise NotAToasterException('This is not a toaster', 'error')
|
|
||||||
```
|
|
||||||
|
|
||||||
## Not strictly pinning all packages
|
|
||||||
|
|
||||||
Example of bad `requirements.txt` file:
|
|
||||||
|
|
||||||
```
|
|
||||||
sqlalchemy
|
|
||||||
flask
|
|
||||||
```
|
|
||||||
|
|
||||||
Another example of a bad `requirements.txt` file:
|
|
||||||
|
|
||||||
```
|
|
||||||
sqlalchemy>=0.9
|
|
||||||
flask>0.10
|
|
||||||
```
|
|
||||||
|
|
||||||
Good:
|
|
||||||
|
|
||||||
```
|
|
||||||
Flask==0.10.1
|
|
||||||
Jinja2==2.8
|
|
||||||
MarkupSafe==0.23
|
|
||||||
SQLAlchemy==1.0.12
|
|
||||||
Werkzeug==0.11.4
|
|
||||||
itsdangerous==0.24
|
|
||||||
```
|
|
||||||
|
|
||||||
This ensures that there's absolutely no ambiguity as to which package will be installed on production. If you forget to mention even a single package, you will perhaps have a different version on your testing/dev environment, and in your production environment.
|
|
||||||
|
|
||||||
The proper way to update a package and its dependency is to use another tool, for instance [pip-tools](https://github.com/nvie/pip-tools). If you have multiple applications and you want to mass update a package, then you'll have to write a script to do so. Keep things simple and explicit, then use scripts on top of it to instrument your processes.
|
|
||||||
|
|
||||||
**Reference**
|
|
||||||
|
|
||||||
- [Pin Your Packages](http://nvie.com/posts/pin-your-packages/)
|
|
||||||
|
|
||||||
# Reference
|
|
||||||
|
|
||||||
- [Pythonic Pitfalls](http://nafiulis.me/potential-pythonic-pitfalls.html)
|
|
||||||
- [Python Patterns](https://github.com/faif/python-patterns)
|
|
||||||
- [The Little Book of Python
|
|
||||||
Anti-Patterns](http://docs.quantifiedcode.com/python-anti-patterns/)
|
|
||||||
- [How to make mistakes in
|
|
||||||
Python](http://www.oreilly.com/programming/free/files/how-to-make-mistakes-in-python.pdf)
|
|
||||||
G
|
|
Loading…
Reference in New Issue
Block a user