Showing posts with label Python Gotchas. Show all posts
Showing posts with label Python Gotchas. Show all posts

Monday, 9 November 2015

Python Gotcha #3 : Default Arguments, be careful

Python Gotchas - Default arguments

or how not to get caught out when you start using python

At first glance python seems very familiar, especially if you have used other procedural languages such as C or Java - but actually Python is different - in some cases very different, and those differences can trip you up as you progress along your python journey. In this series of occasional posts, I am going to cover some of those gotchas.

Default Arguments

Once you have spent any time with python, you will be very aware that the language supports the ability to define default values for function arguments.
>>> #---------------- Example 1
>>> def add(number, increment=1):
...    return number + increment
>>>
>>> add(10)
11
>>> add(10, 2)
12
 
This can be incredibly powerful feature which can dramatically improve readability of your code, by ensuring that you only need to specify an argument if you are doing something which is not usual or common. The example above is not a very good example of using default argument values.

For instance look at the str.find method (for finding one string in another); The method has both the start and stop arguments (if you need to search only part of the string), but both of those arguments have sensible default values so that in most cases you encounter,  when you want to search the entire string - all you need to do is provide a single argument :

>>> #---------------- Example 2
>>> stra = "This is a dead parrot, deceased, no-longer living."
>>> stra.find("e")   # Find the first 'e' in the string.
11
>>> stra.find("e", 12) # Find the first 'e' starting from the 11th character
24
>>> stra.find("e", 12,23) # Find the first "e" between character 12 & 23
-1


Say for instance that you write a function, which amongst other things, create and populates a file, and then sets the permissions on that file. Most of the time when your application calls the function you want the file permissions set to "rw" (read/write), but on some rare occasions you want your application to set the permission to "r" (read only). It would be a good idea to make the permissions argument have a default value of "rw" :

#---------------- Example 3
def write_file(data, file_name, permission="rw"):
    ...
    # Code to write file and set permission goes here
    return  0 # Success
 
write_file(data1, "data1.txt") # written with rw permission
...
write_file(data2, "data2.txt")
...
write_file(LicenseInfo, "LicenseReadme.txt", "r") # Don't want the license file to be changed


You can see how having a sensible default value makes the code readable, but not cluttered.

And now to the gotcha : There is a danger lurking here - which I will demonstrate in my next set of examples :

Imagine you are writing a system to record students as they enrol for courses at a college, and the first thing you need to do is to record the Student's name on a list - so you write a function as below :

#---------------- Example 4
>>> def RegisterStudent(student, existing_students=[]):
...    existing_students.append[student]
...    return existing_students
>>>
>>> class1 = RegisterStudent("John")
>>> class1
["John"]
>>> class1 = RegisterStudent("Mark", class1)
>>> class1
["John", Mark"]
>>> # ---------------------------- All Good so far


You intention is by having the default argument as an empty list, you can create multiple lists one for each course, and you can signify the creation of a new list by omitting the existing_students argument (since when it is a new list there are no existing students.

Now - lets try to create another student list for a second course, using the RegisterStudent function above

#---------------- Example 5
>>> class2 = RegisterStudent("Lucas") # This should work - shouldn't it ?
>>> # -------- Lets check
>>> class2
['John', 'Mark', 'Lucas'] # We have the names from class1 in our list too
>>> # -------- and even worse ?
>>> class1
['John', 'Mark', 'Lucas']
>>> class1 is class2   # They are the same list (the same object)
True


So clearly this does not work - but why not ? How did our two default lists, end up with the same object.

Looking at the definition of RegisterStudent in example 4, it would be reasonable to expect that if the 2nd argument (existing students) isn't provided then a new empty list would be created. However, that is not what happens, and the reality is a bit complicated - so stay with me :
  1. Compilation :  The compiler sees the item existing_students=[] for the first time during the compilation phase; and it creates a new empty list object, and associates that to existing_students argument (within the scope of the RegisterStudent function). 
  2. Execution : When the function is then called the interpreter checks if the existing_students argument has been provided, and if not, then the object which was created during step 2 is passed as into the function body as the existing_students argument.
  3. In the body of the function - when the code changes the existing_students list (by appending to it - and append is a change in place - i.e. no new object is created), this is a change to the object created by the compiler.
  4. The next time that the function is called with a missing existing_students argument, the interpret does everything in step 2, and the body of the function will be passed the changed list.
The summary of this is, that if you use a mutable value (list, dictionary, set etc) as the default argument in one of your functions, and then change that variable within your function, then you will actually be changing the value of the default argument which will be used when you call the function again : and often this is not what you want.

A better version of our RegisterStudent function :

#---------------- Example 6
>>> def RegisterStudent(student, existing_students=None): 
...    if existing_students is None:
...        existing_students = [] 
...    existing_students.append[student]
...    return existing_students
>>>
>>> class1 = RegisterStudent("John")
>>> class1
["John"]
>>> class1 = RegisterStudent("Mark", class1)
>>> class1
["John", Mark"]
>>> # ---------------------------- All Good so far
>>> class2 = RegisterStudent("Lucas")
>>> class2 
["Lucas"]
>>> class1
["John", "Mark"]
>>> class1 is class2
False


By using None (instead of []) we can avoid the issue with using mutable default arguments, and ensure that with the addition of a simple if statement, that whenever the existing_students argument is omitted in a function call - we get a brand new list to start adding to.

There are other ways of avoiding the mutable default argument issue - but the above method of using None is the method recommended even in the official documentation.

Note : The if statement can be made even simpler by using a conditional expression :

#---------------- Example 7
>>> def RegisterStudent(student, existing_students=None):
...    existing_students = existing_students if existing_students else []
...    existing_students.append[student]
...    return existing_students


This conditional expression works due to the rules that python uses to determine the Truth value of a value which isn't strictly True/False. For a list - if the list is None or [] then the Truth value is False, otherwise it is evaluated as True.

Note 2: If you are writing code that is sharing data between a number of different functions, then you would probably be better off investigating writing a class, which can hold the data, rather than pass the data around as arguments.

Functions as default arguments

It is also important to remember that this doesn't just happen when a list or dictionary is used as a default argument. You will also get a potentially unexpected results if you try to use a function call as a default argument. For instance it might seem logical to write code like the example below

#---------------- Example 8
>>> from datetime import datetime
>>> def log_message(msg, ts=datetime.now()):
...     """Create a log message in a known format, adding the time stamp to the message (defaulting to now)"""
...     return "Log : {} {}".format(ts, msg)


But as you might now have worked out, the ts=datetime.now() is evaluated only once (when the file is initially compiled, or is initially imported), and if we were to use the the log_message function, then it would create messages with the timestamp of the date/time of the import, every time it is called with the ts argument omitted, which is clearly not the expected functionality.

Thank fully we can use the same mechanism as in Examples 6 or 7 above (using a default argument of None) in order to get the expected functionality :

#---------------- Example 9
>>> from datetime import datetime
>>> def log_message(msg, ts=None):
...    """Create a log message in a known format, adding the time stamp to the message (defaulting to now)"""
...    ts = ts  if ts else datetime.now()
...    return "Log : {} {}".format(ts, msg)

And finally :

There is a case when using a mutable type (dictionary, list etc) as a default argument can be very useful.

Imagine writing a function that will generate the nth Fibonacci number :

>>> #---------------- Example 10
>>> def fib(n):
...     if n == 0:
...             return 0
...     if n == 1:
...             return 1
...     return fib(n-1) + fib(n-2)


This function certainly works, but it does have an issue : in that if you calculate a lot of different values, then you will recalculate many of the lower values multiple times. Since the Fibonacci series doesn't change - is there some way we can store the values we have already calculated, to make our code run faster ?

>>> #---------------- Example 11
>>> def fib2(n, cache={0:0, 1:1} ):
...     if n in cache:
...         return cache[n]
...     cache[n] =  fib2(n-1) + fib2(n-2)
...     return cache[n]


In this new function we now have a cache dictionary as a default argument, although the cache parameter is never used when we call the fib2 function, but as you can see as the function calculates new values, it adds them to the cache, and we know that the cache object is shared between multiple calls. The big time difference in the cached version arises from making multiple calls to a function is a lot slow than accessing one value from a dictionary.

This technique is memoization, and from the timings below, the speed improvement is considerable :

$ python -m timeit -n 1 -r 10 -s "import fib" "[fib.fib(n) for n in range(30)]"
10 loops, best of 10: 407 msec per loop
$ python -m timeit -n 1 -r 10 -s "import fib" "[fib.fib2(n) for n in range(30)]"
10 loops, best of 10: 5.01 usec per loop

Yes that is really 407 milli seconds (i.e. 0.4 seconds) compared to 5 micro seconds (i.e. 0.000005 seconds) - and the code is only building a list of the first 30 numbers - imagine the savings from building bigger lists.

A graph of the timings is illuminating - building a list of Fibonacci numbers from 0 to n.
As you can see the time taken as n increases exponentially for the non-memoized version (example 10), where-as the timing for the memoized function increases but at a far lower rate.

Beware though - in this case the optimization only works because we are building a list of values and as we attempt to calculate the higher values in the list, the lower values have already been calculated and stored. However, if we just called the memoized version to just calculate a single value, you may well find that it is similar timings or even slower than the non-memoized version.

Thursday, 29 October 2015

Python gotchas # 2 : Names, dynamic typing, is and ==

Python gotchas - Names vs Variables

or how not to get caught out when you start using python

At first glance python seems very familiar, especially if you have used other procedural languages such as C or Java - but actually Python is different - in some cases very different, and those differences can trip you up as you progress along your python journey. In this series of occasional posts, I am going to cover some of those gotchas. 

Names and variables 

In most other languages a variable holds a specific value, and a change to the variable changes the value that variable holds. That is true to a certain extent in Python, but critically there are some differences. Python doesn't have variables in the truest sense of the meaning - it has names and objects.
Everything in python is an object - numbers, strings, lists are all objects, as are the items in the list for instance, and names are simply references to those objects. (Classes and functions are also special types of object - I will cover that in more detail in a later blog post).

This process of making a name refer to an object is called binding, and a name is said to be bound to an object.

Take the following code :

################ Example 1
>>> var1 = 3
>>> var2 = var1
>>> id(var1) == id(var2)
True

This gives us two names (var1 and var2) both bound to the same object (The id function returns the object Id which the name is bound to). There is a int object with the value '3' to which both var1 and var2 are bound.

We can bind var1  to a different integer object - without affecting what var2 is bound to.

################ Example 2 
>>> var1 = 3
>>> var2 = var1
>>>id(var1) == id(var2)
True
>>> var1 = 4
>>> id(var1) == id(var2)
False
>>> print var1, var2
4 3

When you bind a name to an object the rules are simple - nothing is copied, the name is simply created and made to refer to that object - in terms of the C language - the name is a pointer to an object. The big difference is that in Python since every name is a reference you don't need a special syntax element to get to the object being refereed to.

When we increment our number :

################ Example 3
>>> var1 = 3
>>> var2 = var1
>>> id(var1) = id(var2) # bound to the same object ?
True
>>> var1 = var1 + 1
>>> id(var1) = id(var2) # bound to the same object ?
False
>>> print var1, var2
4 3

Here we change the value of the object that var1 is bound to, and var2 is left bound to it's previous object, while var1 is now bound to a new object.

So what happens when our names are bound to a list object, and we change the list - do we get two list objects :

################ Example 4
>>> l1 = [1,2,3,4]
>>> l2 = l1
>>> id(l2) == id(l1) # bound to the same object ?
True
>>> l1[0] = 4
>>> id(l2) == id(l1) # bound to the same object ?
True
>>> print l2
[4,2,3,4]

We can see that even after the change, the l2 name is still bound to the same object as l1, and therefore the change made to the object, is reflected in l1 and l2.

The difference between example 3 and example 4 is entirely down to whether the object is immutable :  lists (and other data structures such as dictionaries) are considered to be mutable - i.e. they can be changed without the creation of a new object, and therefore a change in the object is reflected across all the names bound to it.
In the case of integers and a number of other types, the objects are considered "immutable" (i.e. cannot be changed) and attempting to change the value (adding 1 to var1 in example 3 results in the creation of a new object - in this case new integer object (value 4) which var1 is then bound to.
(In fact when python starts up integer objects already exist for all values between -5 and 256 inclusive - as these values appear most regularly across most type of program, and having these objects ready saves time as your program runs).

So far the end result is probably similar to what you might expect : after all you can't redefine 3 to be 4, but you can change the content of a list.

The main tripping points are that for the mutable types there are many different way to change the object - and some don't result in the reflection one might expect :

>>> ################ Example 5
>>> l1 = [1,2,3]
>>> l2 = l1
>>> id(l1) == id(l2)
True 
>>> l1.append(4)   # append changes the object 'in place'
id(l1) == id(l2)
True
>>> print l1, l2
[1,2,3,4] [1,2,3,4]
>>> l1+=[5]       # this form of list addition also operates 'in place'
>>> id(l1) == id(l2)
True
>>> print l1, l2
[1,2,3,4,5] [1,2,3,4,5]

>>> ################ Example 6
>>> l1 = [1,2,3]
>>> l2 = l1
>>> id(l1) == id(l2)
True 
>>> l1 = l1 + [4] # This form of change creates a new object
>>> id(l1) == id(l2)
False 
>>> print l1, l2
[1,2,3,4] [1,2,3]

In example 6 - a new object is created (when the expression l1+[4] is evaluated), and since we have a new object, l2 is no longer bound to the same object as l1 - i.e. the reflection is broken.

The key phrase here is "in place" - many of the standard functions modify the object "in place" - i.e. modify the object without the creation of a new object, and this type of modification will always ensure that changes are reflected through all bound names, but there are equally many ways to apparently change the value of a mutable object which in fact will result in the creation of a new object - if in doubt you can always open your python interpreter and try it out - remember you can check the id values (or use the is operator discussed below).

Names and dynamic typing 

Unlike in C where you have to declare  what type a name/variable is before you use it, a name in Python can be bound to any object you want, and be bound to a different type of object later on - this is called dynamic typing - and is one of Python's greatest benefits - var1 can be an integer, and then a floating point number, and then a string. With that flexibility comes great strength, and also potential pitfalls.

In a complex application without clear boundaries it is easy for the developer to loose track of what type that variable should be at this point, and to try to do something which either results in an error or even worse a subtle bug, because for instance the value that should be an integer is actually a string :

>>> ################ Example 7
>>> var1 = 3
>>> print "Final value %"%(var1*3)
Final value 9
>>> ......
>>> ...... # Some time later
>>> var1 = "xxx"
>>> print "Final value %"%(var1*3)
Final value xxxxxxxxx

It is strongly suggested by the author that in any complex program you use sensible names for your values (not var1, var2 etc) - this naming will help prevent some of the worst of the challenges that Dynamic Typing can bring. 
There are also methods of testing what type of object a name is bound to, I will cover that in a future blog post.

And finally - a warning about 'is' and '=='

Many python beginners get confused about when to use the 'is' comparison and when to use '=='.

The '==' operator should be used when you are testing whether the two names have the same value - i.e. the objects they are bound to have the same value - for lists for instance this is whether the two lists have the same content in the same order - it is highly likely that you will use '==' far more often that you use 'is'.

The 'is' operator should be used when testing whether two names refer to the same object.

For trivial examples : integers between -5 and 256, and short string literals (less then 20 characters), then 'is' and '==' will return the same result (due to optimisations already mentioned) which can lead you into a false understanding of what the operators do. Using values outside those ranges will show the distinction :

>>> ################ Example 8
>>> a = 10*100
>>> b = 1000
>>> a is b        # bound to the same object ? equivalent to id(a) == id(b)
False             # NO
>>> a == b        # But definitely the same value
True
>>> l1 = [1,2,3,4]
>>> l2 = [1,2,3,4]
>>> l1 is l2      # Not bound to the same object 
False
>>> l1 == l2      # But again definitely equal in value
True

The 'is' operator is equivalent to comparing the id value.
It is expected that if two names are bound to the same object (var1 is var2) then the value comparison (==) will also be the same - this will always be the case with objects created by the standard library. As demonstrated in Example 8, the reverse is not true - if two names are not bound to the same object, their values could still be equal.

Sunday, 25 October 2015

Python gotchas # 1 : Indentation Tabs and Spaces

Python gotchas - Indentation

or how not to get caught out when you start using python

At first glance python seems very familiar, especially if you have used other procedural languages such as C or Java - but actually Python is different - in some cases very different, and those differences can trip you up as you progress along your python journey. In this series of occasional posts, I am going to cover some of those gotchas.

Indentation, Tabs and spaces 

As you no doubt already know by now, Python uses the end of a line (in most cases) to delimit statements and uses indentation to identify blocks of code in a loop or function, and Python allows you to use either Tabs or spaces to create your indentations and even mix both Tabs and spaces on the same line but there is a hidden gotcha.

Tabs and spaces look alike in most editors - but if you indent one line with Tabs - and the next with spaces - Python will complain with an IndentationError or a TabError even though the lines will seem to be perfectly aligned in your editor.

This can be such a problem that the official style guide (PEP 008) recommends only using spaces for new code, and setting your editor to insert 4 spaces when the tab key is pressed, and you should only use Tabs to stay compliant with existing code which you might be changing. Make sure you get your editor set up before you start coding - as finding and replacing those tab characters might be tricky when you have several 100 lines of code.

The use of indentation provides another thing to trip over if you are not wary. Since Indentation is important for structure the code you can change the behaviour of your code by indenting or un-indenting a line :

Example 1
flag = False
if flag :
    print "Flag is True"
print "Continuing"

Example 2
flag = False
if flag :
    print "Flag is True"
    print "Continuing"

Example 1 and 2 above are identical, apart from one having the last line indented, but Example 1 generates a line of output, and Example 2 doesn't. You can easily see how this could drastically change how your program behaves, and there isn't a error generated, as both of these are completely valid.

Special care should be taken if you copy and paste code into help sites such as stackoverflow which will often remove formatting, make your code invalid, and make it difficult for other people to help you