A115 Software Engineering

Bespoke cloud-based software platforms powering UK commerce since 2010

Re-introduction to Python - part 7. Tips and shortcuts (and a Python challenge)

Let's begin this lesson with an exercise. A slightly more difficult problem, which you should be able to solve using only material we have covered so far. Not much self-learning is required for this one, and no clever tricks. However, as with any requirements, you should read the text of the challenge very carefully and make sure you know exactly what the terminology means.

Write a Python function, which takes two arguments. The first argument (let's call it nums) is a list of integer numbers. The second argument (let's call it target) is a single integer number. Your function should return a tuple with the indices of two numbers in nums such that when you add those two numbers together you get target. You may NOT use the same element twice for the addition.

Assume that the numbers provided will be such that exactly one solution exists. You can return the two indices in the answer in any order.

Example: if nums is [2, 7, 11, 15] and target is 9, your function should return the tuple (0, 1). This is because the element at position 0 (which is 2) plus the element at position 1 (which is 7) add up to the value of target (which is 9). And this is the only solution - although (1, 0) will also be accepted as a valid answer, because as per the requirements, the order doesn't matter.

Go on and give it a try.

By the way, if you find you are forgetting things you used to know, don't be too hard on yourself. Like with any language (whether computer language or natural human language) - unless you practice and use it all the time, you are going to forget. It's just how the brain works, and you wouldn't want it to be any other way. But in the absence of real-life projects, doing exercises is a great way to keep in shape and get better. There are many on-line sites full of delightful and challenging Python coding exercises and contests. Choose one to register an account with and whenever you have some spare time, practice solving problems to stay sharp.

Here is a small list of "rule of thumb" tips that will help you in your career as a software engineer. None of these are laws, of course, and once you know what you are doing, you could (and probably should) break any number of these. But if you stick to them as much as possible in general (and particularly early on, while you are still building up your coding habits) - you will find that you are naturally producing high-quality, maintainable code.

  1. Write less code. Think more and do less. Understand the concept of leverage. Always ask "what is the most high-impact things I can do?" In other words, how can I get the most valuable results with the least amount of effort? You will rarely find a perfect answer, but just asking the questions is often enough to give you an edge. It's a frame of mind thing.
  2. Try to keep things smaller. Smaller projects tend to be better. Smaller packages, smaller interfaces, smaller functions are usually better. Make sure if another competent dev looks at your code, they can encompass the entirety of your "thing" in their mind. This makes everything else easier. Of course, sometimes things can't be any smaller than they need to be and that's fine. But my number one complaint when reviewing the code of junior developers is "too much code". I've often been able to help them replace screens worth of code with just a few lines. To be able to do this, you need to be really well familiar with the features of the language and the standard library.
  3. Worth repeating one more time: Strive to keep functions concise. Bigger functions create bigger problems. Keep function signatures clean, readable and consistent. This helps create team habits and expectations which increase productivity.
  4. Don't be afraid to delete code. Whether it is old, unused code, or ugly code you can make better - go on and delete it. This feels terribly scary to most developers (even experienced ones), but it is easier once you realise that with modern revision control tools like Git, no code is ever lost. If you mess up, you can always go back to an earlier revision and recover the code you deleted.
  5. Fewer conditionals = better code. Code that uses a lot of if statements (or other conditional logic) has many branches, which makes it difficult to follow, to reason about, to debug and to test.
  6. Avoid shadowing / redefining / overloading variables (or other names) because it makes your code confusing (both to humans and to computer tools analysing your code). Be particularly careful to avoid shadowing built-in keywords or library names (e.g. type, id, min, max, next, etc.) See further discussion at the end of this lesson.

While most of my advice above boils down to some variation of "try to write less code", it's also important to balance this against readability. When I say "write less code", I don't mean "come up with clever tricks to save a character or two here and there". Because that just makes your code less readable. What I mean is, have a think to see if there's a way to just not do what you're thinking about doing, and perhaps do something else instead, which would require less code.

The following are a few other things to keep in mind (some are specific to Python, but most are applicable to many other similar languages as well). You'll notice a common theme here - the examples below are mostly "clever tricks" which developers use to make their code more concise, but that's usually at the expense of degraded readability. You should be familiar with these shortcuts, because you have to be able to recognise them when you see them in other people's code - and you might also want to use them yourself sometimes, but my general advice is to steer clear of them and use the more verbose alternatives.

ASSIGNMENT AS VALUE: The assignment expression (e.g. a = 5) assigns a value to a named variable, but it also returns the value. Some people use this fact to write code like: v2 = v1 = 5 Reading from right to left, this assigns the value 5 to the variable v1, but that assignment itself also has the value of 5, and this value is then assigned to v2 as well. It's a shortcut. But it is not a very good one, because people reading it may forget about one or the other of the effects of the expression. It's better to use the more verbose:

v1 = 5
v2 = 5

Or, if the value is quite complex and you don't want to repeat yourself (which is usually a good idea - research the DRY principle in programming):

v1 = 4*sum(-float(k%4 - 2)/k for k in range(1, 2*10000+1, 2))
v2 = v1

Traditionally, the and and or operators are used for logical conjunction and disjunction, respectively. Due to short-circuiting, they can also be used for conditional execution.

BOOLEAN LOGIC USED FOR CONTROL FLOW: Traditionally, the and and or operators are used for logical conjunction and disjunction, respectively (boolean logic). However, in many languages, Python included, there is this concept of "short-circuiting" logical operations.

For example, if you have the code a = (5/2) or (5/0), the value of a will be set to 2.5, but more importantly, the second part of the or expression (5/0) will not even be considered by the interpreter. This happens to be lucky in this case, because as you probably know, programming languages typically refuse to divide a number by 0 - in Python you will get a ZeroDivisionError if you try to evaluate 5/0. But because of the short-circuiting property of the or operator, Python doesn't even attempt to do the illegal division. As soon as it calculates 5/2, which is a "Truthy" value in Python, it knows that "True or Anything" is always True in boolean logic, so it just returns the Truthy value of 2.5 right away and ignores the rest of the expression.

Because of this short-circuiting property of or and and, developers sometimes use these operators to implement conditional execution (i.e. where you would typically use the if statement more explicitly). So sometimes you might see code like this:

has_beans and count_beans()

The meaning of this code is: only execute the count_beans() function is the value of has_beans is Truthy (otherwise, we know that False and Anything is False, so short-cut and don't bother counting the beans). This is, of course, equivalent to:

if has_beans is True:
    count_beans()

In your code, you should prefer the latter, because it is clearer and more readable. Likewise:

has_beans or gather_beans()

is better written as:

if has_beans is not True:
    gather_beans()

OPERATOR PRECEDENCE: You might be familiar with the concepts of "associativity" and "precedence" of operations from algebra. It's worth researching those terms in the context of computer programming. The idea is similar - some operators take higher precedence than others, so they are applied first. Some operators associate with the value to their left, while others with the value to their right, so this also affects the order of applying operations in a complex mathematical or logical expression. Python has many built-in operators with various precedence and associativity rules. Most programmers don't know all of these rules - only the most common ones. Like in algebra, we can utilise parentheses for added clarity. For example:

if a and b or c:

is equivalent to:

if (a and b) or c:

because and has higher precedence than or, so it gets evaluated first. While most programmers are familiar with this one, it's still a good idea to opt for the more explicit expression, using the parentheses. In general, it is a good idea to get into the habit of placing parentheses around each logical group in any complex logical (or mathematical) expression.

UNDETECTABLE TRAILING COMMAS: In Python, assigning a comma-separated list of values to a variable creates a tuple. E.g.:

v1 = 3, 4

The type of v1 is now tuple and it's value is (3, 4). It is important to know that a trailing comma at the end of these values is ignored. So:

v2 = 2,

is also a tuple with the value (2,)

This is a surprisingly common source of difficult to detect errors, which can take hours to debug - or may lurk in the code for days, months or years before causing a seemingly "mysterious" problem.

To avoid this, it is always preferable to define tuples explicitly, as in:

v1 = (3, 4)
v2 = (2, )

Unfortunately, the case of a single value followed by a misplaced comma is not so easy to guard against. This can happen accidentally, for example when the value was first assigned to one of the keys of a dictionary, but was later changed to be an independent variable - forgetting to remove the comma. Few IDEs or static analysis tools would issue a warning about that comma. The flake8-commas extension of the flake8 checker does this (research these tools and get familiar with their usage.)

REPURPOSED (SHADOW) LABELS: When a variable or function (or something else) is named in a way that shadows an already existing label in the same namespace, the resulting code can be confusing, difficult to follow, and prone to errors. In Python, there are certain keywords and commonly used library functions with names one sometimes wishes to use for other purposes. For example, it is common to want to label a variable next, or id, or list. Unfortunately, in each of these cases you would be repurposing (shadowing) an existing Python keyword, simultaneously confusing the reader and opening the door to future trouble in follow-up code, when the same name may be used with the intention to invoke the original keyword. Avoid this.