|
| 1 | +Clean(er) code |
| 2 | +============== |
| 3 | + |
| 4 | +Below are a few tips for writing clean(er) code. |
| 5 | + |
| 6 | +Code formatting |
| 7 | +--------------- |
| 8 | + |
| 9 | +Code formatting is important for readability. It allows you to write code that |
| 10 | +is easy to follow (for your future self as much as others), and (least importantly) |
| 11 | +adheres to the `PEP8 <https://www.python.org/dev/peps/pep-0008/>`_ standard. |
| 12 | + |
| 13 | +However, remembering all the rules and manually formatting your code is not |
| 14 | +how we want to spend our time as developers. To this end, there are several |
| 15 | +tools that can help us with this task. The ones we use are: |
| 16 | + |
| 17 | +* `Black <https://github.com/psf/black>`_ which will reformat your code |
| 18 | + in-place to conform to the PEP8 standard. |
| 19 | +* `Flake8 <https://flake8.pycqa.org/en/latest/>`__ which is a *linter* that |
| 20 | + will check your code for errors and style violations, but not reformat it. For |
| 21 | + example, for me it has identified code where I have unused variables or |
| 22 | + scripts / functions that are too long. |
| 23 | + |
| 24 | +While you can use these tools manually, it is much more convenient to use them |
| 25 | +as pre-commit hooks. This means that before you commit your code, these tools |
| 26 | +will be run automatically. If they find any errors, the commit will be aborted |
| 27 | +and you will have to fix the errors before you can commit again. Pre-commit |
| 28 | +helps you to automate this process and avoid commits that do not conform to |
| 29 | +the PEP8 standard (and commits that are just for formatting). |
| 30 | + |
| 31 | +A few files are needed to setup pre-commit hooks: |
| 32 | + |
| 33 | +* `.pre-commit-config.yaml <https://github.com/ebezzam/python-dev-tips/blob/main/.pre-commit-config.yaml>`_: This file contains the configuration for the |
| 34 | + pre-commit hooks. It specifies which tools to use, and how to use them. |
| 35 | +* `.flake8 <https://github.com/ebezzam/python-dev-tips/blob/main/.flake8>`_: This file contains the configuration for Flake8. It specifies |
| 36 | + e.g. which errors to ignore, and which line length to use. |
| 37 | +* `pyproject.toml <https://github.com/ebezzam/python-dev-tips/blob/main/pyproject.toml>`_: This file contains the configuration for Black. It |
| 38 | + specifies e.g. which line length to use. |
| 39 | + |
| 40 | +You can then install the pre-commit hooks for your project by running the |
| 41 | +following commands: |
| 42 | + |
| 43 | +.. code:: bash |
| 44 | +
|
| 45 | + # inside virtual environment |
| 46 | + (project_env) pip install pre-commit |
| 47 | + (project_env) pip install black |
| 48 | +
|
| 49 | + # Install git hooks |
| 50 | + (project_env) pre-commit install |
| 51 | + # pre-commit installed at .git/hooks/pre-commit |
| 52 | +
|
| 53 | +
|
| 54 | +Avoiding long ``if-else`` statements with object instantiation |
| 55 | +-------------------------------------------------------------- |
| 56 | + |
| 57 | +In :ref:`Reproducible examples<Reproducible examples>` we presented Hydra for separating configuration from code. |
| 58 | +Another cool feature of Hydra is `object instantiating <https://hydra.cc/docs/advanced/instantiate_objects/overview/>`_. |
| 59 | +Imagine you want to try different optimizers for your Deep Neural Network (DNN) or you want to try different DNNs in the same pipeline. |
| 60 | +Instead of doing ``if-else`` statements, you write one line of code and let Hydra choose the |
| 61 | +appropriate object class based on your configuration. See the script |
| 62 | +`examples/real_convolve.py <https://github.com/ebezzam/python-dev-tips/blob/main/examples/real_convolve.py>`_ |
| 63 | +for the example. |
| 64 | + |
| 65 | +.. code-block:: python |
| 66 | +
|
| 67 | + @hydra.main(version_base=None, config_path="configs", config_name="defaults") |
| 68 | + def main(config): |
| 69 | + # instantiate object from config |
| 70 | + signal = instantiate(config.signal) |
| 71 | + # application specific choice of object class |
| 72 | +
|
| 73 | +``instantiate`` function from ``hydra.utils`` allows you to define an object in a YAML file |
| 74 | +without being tied to a particular class. To do this, you need to define ``_target_`` in |
| 75 | +your config (see configs in ``configs/signal``) and object initialization arguments. Object class |
| 76 | +can be either defined in your project (``configs/signal/ExampleZeros``, ``configs/signal/ExampleCustom``) |
| 77 | +or taken from a package (``configs/signal/ExampleNumpy``). |
| 78 | + |
| 79 | +Note that here we use another Hydra feature: config grouping and splitting. Instead of writing |
| 80 | +configurations for all objects in the main config and copying configuration files, we create a sub-directory ``signal``, |
| 81 | +where all ``signal`` configs are defined. Now we can run the main config with the ``signal`` of |
| 82 | +our choice simply by specifying it in the command line. For example, ``python examples/real_convolve.py signal=ExampleNumpy`` |
| 83 | +or ``python examples/real_convolve.py signal=ExampleZeros``. |
| 84 | + |
| 85 | +If we need to define some of the arguments inside the code before creating an object, we can pass them directly to the ``instantiate`` function. |
| 86 | +For example, we did not define ``signal_len`` in the ``signal`` configuration file and passed it by hand: |
| 87 | +``signal = instantiate(config.signal, config.signal_len)``. This is especially useful when you have positional-only arguments |
| 88 | +like ``numpy.random.randn`` in our example. Note that we can both define arguments in the configuration file and pass new ones to ``instantiate`` like we did for |
| 89 | +``ExampleCustom``. |
| 90 | + |
| 91 | +Object instantiating is recursive, i.e. some of the arguments of the class can also be |
| 92 | +defined using ``_target_`` and they will be created automatically. For example, |
| 93 | +``python examples/real_convolve.py signal=ExampleCustom +signal/transform=power`` defines the ``transform`` argument of |
| 94 | +the ``ExampleCustom`` class as the ``PowerTransform`` class. The ``+signal/transform=power`` in the command line |
| 95 | +means adding the ``transform`` argument to the current ``signal`` configuration from the ``power.yaml`` config defined |
| 96 | +in ``configs/signal/transform``. That is, you can have sub-sub-directories. The default values from sub-sub-directories |
| 97 | +can also be changed in the command-line: ``python examples/real_convolve.py signal=ExampleCustom +signal/transform=power signal.transform.pow=3`` |
0 commit comments