PP(MC)

  1. Table of contents
  2. Functions and methods (previous)
  3. Objects (next)

Control flow

  1. Top
  2. Logic
  3. Presence and identity
  4. Conditional
  5. Iteration
  6. Built-in functions
  7. Exceptions
  8. Recursion
  9. Exercises
  10. References

Interludes

  1. Short-circuit operators

Control flow

Logic

Truth

In order to determine the flow of operations, conditions must be tested at each branching of a program's flow. A condition, in natural language, could be that "a note be part of a scale", or that "the sum of a sequence of durations fit a musical measure". The tests could be formulated by the questions : "is the note Eb part of the scale C Major ?", and "for a measure of common time, do the note durations of this sequence sum to a whole note ?". In the Boolean logic of programming with Python, there are only two possible results when testing a condition : True and False. Continuing with the first example, the note Eb cannot both be part of the scale and not be part of the scale. We could say that the statement : "the note Eb is part of the C Major scale" is False, or that the statement : "the note G is part of the C Major scale" is True.

If you check the type of True or of False, you will find that they are of the built-in type bool (for Boolean). They are part of a handful of built-in constants.

>>> type(True)
bool

Relational operators

Objects can relate to one another in different ways. An object can be greater than, lesser than, or equivalent to another. The basic relational operators, in the same order, are >, <, and ==. We can use these operators to define conditions based on the relation between one object and another. Some examples :

>>> 60 > 48
True

>>> True == False
False

To these basic relational operators Python adds >= (greater than or equivalent), <= (lesser than or equivalent), != (not equivalent).

>>> 60 >= 48
True

>>> 60 <= 60
True

>>> True != False
True

Boolean operators

Boolean (sometimes called propositional) logic deals with expressions (also called propositions) that evaluate to either True or False. It is the basis of the mathematical and philosophical disciplines of Logic. There exists a Boolean algebra that can be used to form compound conditional expressions and the basic operators of this algebra are available in Python. They are : and (conjunction), or (disjunction), and not (negation). The unary operator not negates the truth-value of a condition :

>>> not True
False

>>> not (60 >= 48)
False

For an and expression to evaluate to True, both of its operands must evaluate to True :

>>> True and False
False

>>> True and True
True

>>> (60 >= 48) and (60 < 48)
False

An or expression will evaluate to True if either of its two operands evaluates to True :

>>> True or False
True

>>> False or False
False

>>> (60 >= 48) or (60 < 48)
True

Short-circuit operators

The following tables summarize the results of the possible combinations of operand truth values for each of the two binary boolean operators. The operands are represented by the letters p and q.

and (Conjunction)

p q p and q
True True True
True False False
False True False
False False False

or (Disjunction)

p q p or q
True True True
True False True
False True True
False False False

Presence and identity

in

We can build a conditional expression that asks : "is this object a part of that container ?" by using the in keyword. For example, to see if a MIDI note belongs to a scale, we can do the following :

>>> scale = {0, 2, 4, 5, 7, 9, 11}
>>> (50 % 12) in scale
True

First, we defined a scale as a set of pitch classes. We then brought the MIDI note down to its pitch class value (by using the % 12 operation) and checked if it was in the scale container.

is

There exists another way (besides ==) to evaluate something similar to equality in Python. An expression using the keyword is will evaluate to True if both of its operands are identical. Identical means that not only are they equivalent (which is what we verify when we use ==), but that they share the same identity. So far as Python is concerned, things are identical when they occupy the same space in memory. We haven't yet acquired the tools needed to demonstrate an example of is yielding a result that is different from ==, but we will start using is where appropriate. It is preferred as the more conventional way to build conditional expressions that compare an object to the bool or None types.

>>> pitches = None
>>> pitches is None
    True
>>> pitches is not None
    False

Conditional

if

if adds the question mark to the conditional, it asks the question. As is often the case in Python, the syntax is close to natural language. Given these assignments :

>>> durations = [1/4, 1/8, 1/8, 1/2]
>>> measure_duration = 4/4

An if condition can be expressed like this :

>>> if sum(durations) == measure_duration:
        print('The durations fit and fill the measure.')

In the example above, we are comparing for equivalence (using the relational operator ==) the value of sum(durations), which gives 1.0, with the value of measure_duration, which is also 1.0. The condition is met and the print() expression is executed.

If the condition was not met, the print() expression would not have been executed. We can catch a case that doesn't pass the condition by using else :

>>> if sum(durations) == measure_duration:
        print('The durations fit and fill the measure.')
    else:
        print('The durations do not fit the measure.')

If the condition is not met, the program will skip the first and instead execute the second print() expression.

We can further add to the construct by checking for more than one condition before passing to the default else clause by using elif (short for else, if) :

>>> if sum(durations) == measure_duration:
        print('The durations fit and fill the measure.')
    elif sum(durations) > measure_duration:
        print('The durations excede the measure.')
    elif sum(durations) < measure_duration:
        print("The durations don't fill the measure.")
    else:
        print('This will never execute.')

It is legitimate to claim that the else clause above, being useless because sum(durations) must be either ==, >, or < to measure_duration, should just be dropped and the final elif should be replaced by an else. It's fine to do so, but the final condition will have to be guessed by someone (including yourself in the future) reading your code. In this case, there's only a single expression between each of the conditions and it is trivial to imply the third condition if it isn't explicitly stated, but in the case of more extensive code it quickly becomes much less trivial. From the Zen of Python remember : explicit is better than implicit. It is better for readability to keep both the last elif as well as the final else clause.

Iteration

If there's one thing computers are good it, it's doing the same thing over and over again.

for, in

We can iterate over the items of a container using the for item in iterable construct. We can use this, for example, to transpose a pitch sequence like so :

>>> pitches = [43, 45, 47, 48, 50, 51, 53, 51, 50, 51]
>>> transposed = []
>>> for pitch in pitches:
        transposed.append(pitch + 5)
>>> print(transposed)
    [48, 50, 52, 53, 55, 56, 58, 56, 55, 56]

We began by creating a list of pitches, which we assigned to the variable pitches. We then created an empty list which we assigned to transposed. The next two lines show the use of the for item in iterable construct. We chose the name pitch to represent the item. This step is like assigning a variable which will be available in the scope of the construct, we could have chosen any name. The iterable in our case was the list pitches. Within the scope of the construct we have a single expression, which appends the transposed pitch to the transposed list we prepared on the second line of this example. The expressions in the scope are executed once for each of the items in the iterable container.

break

We can stop iterating using break. If, instead of quantizing pitches in the example above, we wanted to verify that the pitches were all part of the scale, we could iterate through the sequence of pitches until we find a pitch that is not part of the scale, print a warning, and stop iterating.

>>> for pitch in pitches:
        if (pitch % 12) not in scale:
            print('Not all pitches are part of the scale.')
            break

continue

Conversely, we can explicitly step forward to the next item in the iteration, skipping subsequent expressions, by using continue.

>>> for pitch in pitches:
        if (pitch % 12) in scale:
            continue
        print('A pitch was found that doesn't belong to the scale.')

This will print the warning each time a pitch that is not a member of the scale is found.

Comprehensions

The for item in iterable construct can be used many different ways, but the case demonstrated in the example given above is so common that there is a syntactical shortcut to achieve it. Creating a new list from the modified items of an iterable can be achieved using a list comprehension like so :

>>> transposed = [pitch + 5 for pitch in pitches]

The list comprehension construct [expression for item in iterable ] results in a new list created by applying an expression to each item for all the items in an iterable.

The same syntactical shortcut exists for dictionaries. Here is an example of a dictionary comprehension :

dict comp example

Notice that tuple unpacking also works within the comprehension !

Comprehensions can be further expanded with conditionals. The following example will transpose only those pitches from the initial pitches list that are part of the scale :

>>> scale = {0, 2, 4, 5, 7, 9, 11}
>>> pitches = [43, 45, 47, 48, 50, 51, 53, 51, 50, 51]
>>> transposed = [pitch + 5 for pitch in pitches if (pitch % 12) in scale]
>>> print(transposed)
    [48, 50, 52, 53, 55, 58, 55]

Finally, comprehensions can be nested :

>>> intervals = [0, 4, 7]
>>> pitches = [60, 65, 67, 60]
>>> chords = [[pitch + interval for interval in intervals] for pitch in pitches]
>>> print(chords)
    [[60, 64, 67], [65, 69, 72], [67, 71, 74], [60, 64, 67]]

The innermost comprehension is evaluated first, so in the example above : from the first pitch, a chord is created using each of the intervals sequentially, then the chord is added to chords, then another pitch is taken and another chord is created and added to chords, and so on until there are no more pitches to take. The equivalent code using the for item in iterable construct looks like this :

>>> chords = []
>>> for pitch in pitches:
        chord = []
        for interval in intervals:
            chord.append(pitch + interval)
        chords.append(chord)

The expressive power of comprehensions should, by now, have seriously impressed you. You'll probably want to place them everywhere in your code. If you find you're asking yourself the question "should I use a comprehension ?" I offer simple advice : use comprehensions only if the expression is dead simple. Your code should be easy to read and understand. Comprehensions can quickly become incomprehensible.

while

Here be dragons. If there's one thing computers are good it, it's doing the same thing over and over again (again), and they won't stop doing it until something tells them to stop. The while loop does exactly that, it does something over and over again as long as a given condition evaluates to True, and until the condition evaluates to False. If the condition evaluates to False to begin with, the expressions in the scope of the while loop are never executed. The following example uses a while loop to add semitones to a pitch until the resulting pitch is part of a scale :

>>> scale = {0, 2, 4, 5, 7, 9, 11}
>>> pitches = [43, 45, 47, 48, 50, 51, 53, 51, 50, 51]
>>> quantized = []
>>> for pitch in pitches:
        while (pitch % 12) not in scale:
            pitch = pitch + 1
        quantized.append(pitch)
>>> print(quantized)
    [43, 45, 47, 48, 50, 52, 53, 52, 50, 52]

When a while loop is used, there must to be a case in the scope that will cause the loop to stop, a terminating condition. The example above is a little risky, the loop will only stop once a pitch is found that fits the scale. We can be certain that if scale contains any integer smaller than 12, that by adding 1 repeatedly to any integer % 12 we will find a match. But if there is no integer smaller than 12 in the scale, we will never find a match and the loop will go on adding 1 for ever. In the present case, we can explicitly secure the loop by limiting the number of attempts to fit the pitch to the scale. Assuming we are operating in a 12 divisions of the octave system, we can be certain that if the scale is well constructed, that we reach a quantized pitch within 12 attempts (we will have tried all pitch classes in the system). Here is the application of this idea :

>>> for pitch in pitches:
        max_attempts = 12
        attempts = 0
        while ((pitch % 12) not in scale) and (attempts < max_attempts):
            pitch = pitch + 1
            attempts = attempts + 1
        quantized.append(pitch)

Built-in functions

map, filter

any, all

Exceptions

try, except, finally

Recursion

Recursively create an overtone series.

Recursively reduce a rhythmic structure

Exercises

  1. Write a function that checks if a list of durations fits a measure.

    def check_durations(durations, measure_duration):
        """Checks if a list of durations fits a measure.
    
        Durations should be represented as a tuple. A quarter note is (1, 4),
        an eighth is (1, 8), a dotted quarter is (3, 8), etc. A measure of common
        time is (4, 4), a 6/8 measure is (6, 8), etc.
    
        A duration tuple is returned, with a negative numerator if the durations do
        not fill the measure, a positive numerator if the durations exceed the
        measure, and a numerator of 0 if the durations fit.
    
        :param durations: Durations as described above.
        :type durations: list of tuple
        :param measure_duration: Measure duration tuple as described above.
        :type measure_duration: tuple
    
        :rtype: tuple
    
        **Examples**
    
        >>> check_durations([(1, 4), (1, 8), (1, 8), (1, 2)], (4, 4))
        (0, 8)
        >>> check_durations([(1, 4), (1, 8)], (3, 4))
        (-3, 8)
        >>> check_durations([(1, 2), (3, 8)], (3, 4))
        (1, 8)
        >>> check_durations([(1, 2), (1, 1)], (4, 4))
        (1, 2)
        """
        # Your code here

References