© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
M. IndenPython Challengeshttps://doi.org/10.1007/978-1-4842-7398-2_1

1. Introduction

Michael Inden1  
(1)
Zurich, Switzerland
 

Welcome to this workbook! Before you get started, I want to briefly outline what you can expect when reading it.

This book covers a broad range of practice-relevant topics, represented by exercises of different levels of difficulty. The exercises are (for the most part) independent of each other and can be solved in any order, depending on your mood or interest.

Besides the exercises, you will find the corresponding answers, including a short description of the algorithm used for the solution and the actual source code, which is commented on at essential points.

1.1 Structure of the Chapters

Each chapter shares the same structure, so you will quickly find your way around.

1.1.1 Introduction

Each chapter begins with an introduction to the topic to get you familiar with the subject area or get you in the right mood for the tasks that follow.

1.1.2 Exercises

The introduction is succeeded by a bunch of exercises and the following structure:

Task Each exercise first will have an assignment. In it, the desired functionality to be realized is described in a few sentences. Often a function signature is already included as a clue to the solution.

Examples Supplementary examples are almost always given for clarification with inputs and expected results. For some quite simple tasks, which mainly serve to get to know an API, examples are sometimes omitted.

Often, different value assignments of input parameter(s), as well as the expected result, are shown in a table.

Input A

Input B

Result

[1, 2, 4, 7, 8]

[2, 3, 7, 9]

[2, 7]

The following notation styles apply to the specifications:
  • “AB” represents textual specifications.

  • True/False stands for Boolean values.

  • 123 represent numeric values.

  • [ value1, value2, .... ] represents collections like sets or lists, but also arrays.

  • { key1 : value1, key2 : value2, ... } describes dictionaries.

1.1.3 Solutions

The part of solutions follows the structure described below.

Task definition and examples First, you find the task description again so that you don’t have to constantly flip back and forth between tasks and solutions. Instead, the description of solutions is self-contained.

Algorithm A description of the chosen algorithm follows. For didactics, I sometime present an erroneous way or a not-very-optimal solution to then uncover pitfalls and iteratively come to an improvement. In fact, one or the other brute force solution is sometimes even usable but offers optimization potentials. I then present corresponding, sometimes astonishingly simple, but often very effective improvements.

Python shortcut Sometimes the task explicitly excludes certain Python standard functionality for realizing the solution in order to penetrate a problem algorithmically. In practice, however, you should use the defaults. In the separate short section named “Python shortcut” I show how to make the solution shorter and more concise.

Examination Some of the tasks are quite easy or only serve to get used to syntax or API functionality. For this, it often seems sufficient to execute a few calls directly in the Python command line interpreter. That’s why I don’t use unit tests for this. The same applies for a graphical presentation of a solution, such as displaying a Sudoku board and if the corresponding unit test would probably be more difficult to understand.

However, the more complicated the algorithms become, the more sources of errors exist, such as wrong index values, an accidental or omitted negation, or an overlooked edge case. For this reason, it makes sense to check functionality with the help of unit tests. In this book, for reasons of space, this is only accomplished for important inputs. The companion resources contain over 80 unit tests with roughly 600 test cases, a pretty good start. Nevertheless, in practice, the amount of unit tests and test cases should be even more voluminous if possible.

1.2 Basic Structure of the PyCharm Project

The included PyCharm project closely follows the structure of the book. It offers a separate folder for each relevant chapter (those with exercises), such as ch02_math or ch08_recursion_advanced.

Some of the source code snippets from the respective introductions are located in the subfolder intro. The provided (sample) solutions are collected in their own subfolder named solutions and the modules are named according to the task as follows: ex<no>_<taskdescription>.py.

Sources Figure 1-1 shows an outline for Chapter 2.
Figure 1-1

Outline of Chapter 2 sources

Utility modules All the useful utility functions developed in the respective chapters are included in the provided PyCharm project in the form of utility modules. They are combined into a module xyz_utils, which resides in its own subdirectory util—for the chapter about mathematical tasks in the subdirectory ch02_math.util. The same applies to the other chapters and topics.

Test classes Figure 1-2 shows some associated tests.
Figure 1-2

Tests

HINT: INSTALLATION OF EXTERNAL LIBRARIES OR FRAMEWORKS
Some examples use special libraries that must be installed up front, like numpy, pytest, and others, using the pip tool (on Mac use pip3 instead of pip) as follows:
pip install pytest
pip install parameterized
pip install numpy

1.3 Basic Framework for Unit Tests with Pytest

To not exceed the scope of the book, the illustrated unit tests only show the test methods but not the test module and the imports. To provide you with a basic framework into which you can insert the test functions and as a starting point for your own experiments, a typical test module is as follows:
import pytest
from ch02_math.solutions.ex01_basics import calc,
    calc_sum_and_count_all_numbers_div_by_2_or_7_v2
@pytest.mark.parametrize("m, n, expected",
                         [(6, 7, 0), (3, 4, 6), (5, 5, 5)])
def test_calc(m, n, expected):
    assert calc(m, n) == expected
@pytest.mark.parametrize("n, expected",
                         [(3, {"sum": 2, "count": 1}),
                          (8, {"sum": 19, "count": 4}),
                          (15, {"sum": 63, "count": 8})])
def test_calc_sum_and_count_all_numbers_div_by_2_or_7(n, expected):
    assert calc_sum_and_count_all_numbers_div_by_2_or_7_v2(n) == expected

In addition to the import needed, this example shows parameterized tests that are extensively used since they allow testing multiple combinations of values in a simple way. For details, please see Appendix A.

1.4 Note on Programming Style

During discussions while writing this book the question came up if certain things should be made more compact. This is why I would like to mention in advance something about the programming style used in this book.

1.4.1 Thoughts on Source Code Compactness

The most important things for me when programming and especially for the implementations in this book are easy comprehensibility and a clear structure. This leads to simplified maintainability and changeability. Therefore, the shown implementations are programmed as understandable as possible. I like to favor this aspect in this book. In practice, it is often easier to live with a bit more verbosity than with bad maintainability but more compact programming.

1.4.2 Example 1

Let’s have a look at a small example for clarification. First, let’s examine the readable, easy-to-understand variant for inverting the contents of a string, which also shows very nicely the two important elements of recursive termination and descent:
def reverse_string(text):
    # recursive termination
    if len(text) <= 1:
        return text
    first_char = text[0]
    remaining = text[1:]
    # recursive descent
    return reverse_string(remaining) + first_char
The following much more compact variant does not offer these advantages:
def reverse_string_short(text):
    return text if len(text) <= 1 else
        reverse_string_short(text[1:]) + text[0]

Think briefly about in which of the two methods you feel safe making subsequent changes. And what if you want to add unit tests? How do you find suitable value sets?

1.4.3 Example 2

Let’s bring in another example to illustrate my point. It concerns the following function count_substrings() which is modeled after the standard count() function. The later counts the number of occurrences of one string in another, and for the two inputs “hellohe” and “he,” it returns the result 2.

First, we implement this reasonably straightforwardly as follows:
def count_substrings(text, value_to_find):
    # recursive termination
    if len(text) < len(value_to_find):
        return 0
    count = 0
    remaining = ""
    # does the text start with the search string?
    if text.startswith(value_to_find):
        # match: continue the search for the found
        # term after the location where it was found
        remaining = text[len(value_to_find):]
        count = 1
    else:
        # remove first character and search again
        remaining = text[1:]
    # recursive descent
    return count_substrings(remaining, value_to_find) + count
Let’s try to realize this compactly:
def count_substrings_short(text, value_to_find):
    return 0 if len(text) < len(value_to_find) else
        (1 if text.startswith(value_to_find) else 0) +
        count_substrings_short(text[1:], value_to_find)

Would you prefer to change this function or the one shown before?

By the way, the lower one still contains a subtle functional deviation! For the inputs of “XXXX” and “XX” the first variant always consumes the characters and finds two occurrences. The lower, however, moves only one character at a time and thus finds three occurrences.

Further, integrating the previously realized functionality of advancing by the whole search string into the second variant will lead to more obscure source code. On the other hand, you can easily shift by only one character by simply adjusting the upper text[len(value_to_find):] call and then even pull this functionality out of the if.

1.4.4 Decorators and Sanity Checks at the Beginning of Functions

To ensure stable programs, it is often a good idea to check the parameters of central functions for validity. These checks are adequate in the form of simple if statements. In Python, however, this can often be accomplished more elegantly with the help of decorators. To get started, please have a look at Appendix B.

1.4.5 Block Comments in Listings

Please note that there are various block comments in listings, which serve as orientation and for better understanding. It’s advisable to use such comments with caution, and it’s preferable to extract individual source code sections to methods or functions in practice. For the book’s examples, these comments serve as reference points because the introduced or presented facts are probably still new and unfamiliar to you as a reader.
# does the text start with the search string?
if text.startswith(value_to_find):
    # match: continue the search for the found
    # term after the location where it was found
    remaining = text[len(value_to_find):]
    count = 1
else:
    # remove first character and search again
    remaining = text[1:]

1.4.6 PEP 8 and the Zen of Python

Besides my already presented thoughts about the programming style, I would like to mention two things explicitly:
  • PEP 8 Coding Standard (PEP = Python Enhancement Proposal)

  • The Zen of Python—thoughts about Python

PEP 8 Coding Standard

The official coding standard is available online at www.python.org/dev/peps/pep-0008/ as PEP 8 . This is intended to help write clean, consistent, and understandable Python code. There is a tendency in the Python community to put more emphasis on pretty source code than in other languages. In general, make it work somehow is not a sustainable strategy, as I have also motivated.

However, there are a few things about which opinions may differ, for example the limitation of the line length to 79 characters. With today’s HiDPI monitors and resolutions beyond Full-HD, longer lines of around 120 characters are certainly possible. But a line should also not be too long—especially if you want to compare two versions of a file with each other; this can otherwise be annoying.

I may violate indentation hints for split lines to favor readability when optically appropriate. Additionally, I occasionally name lambdas, which usually encapsulate only a tiny piece of functionality and thus should not be named, for a better insight into how things work or express more clearly what was intended. The latter is reported as E731 do not assign a lambda expression, use a def. Please find more info in a moment.

The Zen of Python

Interestingly, the Python command line interpreter (indicated by >>> ) includes a built-in output of style guides, also known as the Zen of Python. This is obtained by a call to
>>> import this
The following output occurs:
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Tooling As mentioned, PyCharm offers itself as IDE and provides various hints for style and improvement possibilities directly in the editor. A configuration is possible under Preferences > Editor > Code Style > Python as well as Preferences > Editor > Inspections > Python. In particular, the latter gives you the option to enable PEP8 coding style violation.

Alternatively or complementary, you can install the tool flake8 as follows—here, and in the following text I always use $ to indicate input on the console (i.e., the Windows command prompt and the terminal on MacOS). Please remember to use pip3 on Mac insted of pip.
$ pip install flake8
This helps to uncover various potential problems and violations against PEP 8 if you call it as follows:
$ flake8 <mypythonmodule>.py mydirwithmodules ...
Sample run for the project sources Please find an example run that excludes the virtual environment of Python and ignores some potential problems, just showing the lambdas assignments mentioned before:
$ flake8 --exclude=venv --ignore=E501,F811,E126,E127,W504
./tests/ch02_math/ex09_armstrong_test.py:13:5: E731 do not assign a lambda expression, use a def
./ch03_recursion/intro/intro.py:137:5: E731 do not assign a lambda expression, use a def
./ch03_recursion/intro/intro.py:138:5: E731 do not assign a lambda expression, use a def
./ch03_recursion/solutions/ex01_fibonacci.py:56:5: E731 do not assign a lambda expression, use a def
./ch05_datastructures/intro/basics.py:41:1: E731 do not assign a lambda expression, use a def
./ch06_arrays/solutions/ex06_erase_and_fall_down.py:146:5: F841 local variable 'book_example' is assigned to but never used
./ch07_recursion_advanced/solutions/ex01_towers_of_hanoi.py:39:5: E731 do not assign a lambda expression, use a def
Just for your info, these checks are excluded:
  • E501 line too long (80 > 79 characters: As already stated, 79 characters per line are pretty few these days.

  • F811 redefinition of unused...from line ...: Samples sometimes redefine variables and functions.

  • E126 continuation line over-indented for hanging indent: Minor deviations from the standard to achieve a nicer layout.

  • E127 continuation line over-indented for visual indent: Minor deviations from the standard to achieve a nicer layout.

  • W504 line break after binary operator: Minor deviations from the standard to achieve a nicer layout.

HINT: SONAR AS AN ALTERNATIVE

There are other tools for checking your sources. Although somewhat more complex, for larger projects it is recommended to perform a static source code analysis using Sonar. For this, you must install Sonar and a Sonar Runner. In return, though, you get a nice overview as well as a history so that you can quickly recognize both positive and negative trends and take countermeasures if necessary.

1.4.7 More Information

For more information on how to write clean Python, see the following books:
  • Python Tricks: A Buffet of Awesome Python Features by Dan Bader [Bad17]

  • Mastering Python by Rick van Hattern [vH16]

1.5 Note on the Exercises

When solving the tasks, the goal is to deal with the appropriate algorithms and data structures. Python offers an extensive collection of functionalities, for example for calculating sums and minimums of lists or even more complex things like computing permutations.

Some of the tasks can be solved with the ready-made standard functionalities in a few lines. However, this is not the goal within this book, because the exercises serve the algorithmic understanding and the extension of your problem-solving strategies. By exploring and solving this yourself, you learn a lot in the process. Developing things yourself is only for training, not for practical use: please keep in mind that in real projects the standard functionality of Python should always be preferred and you should not dream of inventing something yourself for which there is already a ready-made solution. That’s why I often point out in a separate short section named “Python shortcut” a solution that uses standard Python functionality.

1.6 Trying Out the Examples and Solutions

Basically, I prefer to use as comprehensible constructs as possible instead of fancy syntax or API features of special Python versions. In many cases, you can simply copy the source code snippets shown into the Python command line interpreter and execute them. Alternatively, all relevant sources are provided in the PyCharm project that comes with the book. There, the programs may be launched by a main() function or checked by corresponding unit tests that are often available.

1.7 Let’s Go: Discovering the Python Challenge

So, enough of the preface! You are probably already excited about the first challenges through the exercises. I hope you will enjoy this book and gain some new insights while solving the exercises and experimenting with the algorithms.

If you need a refresher on pytest, decorators, or O-notation, you might want to take a look at the appendices first.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset