Tutorial for Xpert Basic Users ============================== This is a quick primer, mainly for Xpert Basic users moving to MicroPython. Python is a clean, simple and elegant language which provides fairly direct equivalents to most BASIC statements, but for more advanced users also goes far beyond that. MicroPython is a subset of the Python language that tries to implement as much of Python 3.4 as will reasonably fit onto an Embedded Microprocessor. Over time, they've added some features from more recent versions of Python as well (at least where they found it appropriate).  Python makes an excellent language for writing general purpose applications and is a widely used, modern, and powerful language. For this primer we will focus on general programming constructs that would be familiar to Basic user's. For further reading, Python has a tutorial you can find here: https://docs.python.org/3.4/tutorial/, and more detailed documentation here: https://docs.python.org/3.4/reference/index.html. The homepage for the MicroPython project is located here: https://micropython.org/ Interactive mode ---------------- In the early days of Basic, you could enter commands interactively. MicroPython too can be run interactively. The result of every expression is printed automatically. >>> indicates the python prompt. Pressing **tab** will even automatically complete commands, and **up-arrow** can be used to recall previous commands: >>> a = 4 >>> a 4 >>> a + 1 5 Getting started --------------- Here's the familiar hello world program in Xpert Basic and in Python:: A=MsgBox("Hello World") becomes:: print("Hello World") note that Python keywords are lowercase. Assignment in Basic:: a = 4.5 b = 3 c = "Hello World" is very much like assignment in Python:: a = 4.5 b = 3 c = "Hello World" A Python variable can hold a value of any type, even nothing:: d = None None is a special value that is different from zero or false or the empty string. This is useful in many situations. Consider, for example, a function which gets a number from the user and returns it. How should it indicate that the user has finished the list of numbers? Traditionally, a BASIC programmer might return zero or -1 to indicate this, but using None will prevent confusion if the user really does enter the reserved number. Input is a function, not a statement, in Python:: number = input("Enter a number:") WHILE LOOP ---------- You can also enter multi-line statements, such as loops, at the prompt. The '...' prompt indicates that it's waiting for more input. Enter a blank line to complete the statement:: >>> x = 1 >>> while x < 10: ... print x ... x = x + 1 ... 1 2 3 [etc] BASIC programmers normally indent their code to show loops clearly. However, BASIC itself ignores the indentation and uses the **End Loop** statement to detect the end of a **Do While** loop. Python uses the indentation directly, so no end markers are needed. Python has a **break** statement that can be used to exit from a loop at any point similar to Basic's **Exit Do** statement:: Finished = False Do While Not finished REM Do stuff... If problem Then Exit Do End If REM More stuff... End Loop a = MsgBox("Done") becomes:: finished = False while not finished: # Do stuff... if problem: break # More stuff... print "Done" - Python uses # instead of REM for comments DO ... LOOP UNTIL ----------------- Python doesn't have DO UNTIL loops. These can normally be done directly as while loops, but if not, use break for the end condition. 'while True' is an infinite loop, as in BASIC:: Do REM Stuff Loop Until finished becomes:: while True: # Stuff if finished(): break IF...THEN...ELSE...ENDIF ------------------------ If statements are very similar. The use of indentation means that there is no difference between single line and multiline statements. Note the ':'; this starts all nested blocks. The 'else' must go at the start of a new line (this prevents any confusion as to which IF the ELSE goes with, as is possible in BASIC). We can put more than one statement on a line in Python:: if x > 0 and x < 10: print("OK") else: print("Out of range!") but isn't this easier to read:: if x > 0 and x < 10: print("OK") else: print("Out of range!") Like BASIC, Python considers 0 to be false, and any other integer to be true. Unlike BASIC, Python also allows non-integers::   >>> if "hello": print "yes" ... yes >>> if None: print "yes" ... - An empty string is false, a non-empty one true. - None is also considered to be false. FUNCTION -------- Here's a simple function that doubles its input in Basic:: Function double(x) double = 2 * x End Function And here's the same in Python:: def double(x): return 2 * x SUB --- Here's a simple subroutine that says hello in Basic:: Sub hello A = MsgBox("Hello World") End Sub Call hello And here's the same in Python:: >>> def hello(): ... print("Hello World") >>> hello() Hello World There is no difference in Python between a function and a procedure. A procedure is just a function that doesn't return anything:: >>> a = hello() Hello World >>> print(a) None - To be more precise, a procedure is a function that returns the special value \`None'. - You need the 'print' statement to print None; the interpreter skips it to save space. ARRAY() ------- Python uses lists rather than arrays. They are rather more flexible (you can sort them, insert or delete entries, for example). Xpert Basic allows any data type to be stored in an array:: a = Array("red", "green", "blue", 5.6) and so do Python lists:: >>> a = ["red", "green", "blue"] >>> a ['red', 'green', 'blue'] >>> a[0] 'red' >>> a[1] 'green' >>> a[2] 'blue' >>> a[1] = "yellow" >>> a[1] 'yellow' >>> del a[1] >>> a ['yellow', 'blue'] You can use them much like Xpert Basic arrays. You can even mix data types:: >>> a = ["red", "green", "blue"] >>> a[1] = 4 >>> a ['red', 4, 'blue'] Negative indices count backwards from the end. This is very useful:: >>> a = ["red", "green", "blue"] >>> a[1:3] [4, 'blue'] >>> a[1:] [4, 'blue'] >>> a[:1] ['red'] >>> a[:-1] ['red', 4] - You can specify a start (inclusive) and end (exclusive) index to create a 'slice' of the original list. If you leave off one limit, it goes from the beginning or end. - [:-1] means 'all but the last element', while [1:] means 'all but the first' - Python and Basic arrays both start with index 0 LEFT, RIGHT, MID ---------------- Python has none of these. Instead, you can treat a string as a list of characters and use the indexing and slicing notation as for lists:: >>> b = "Hello World" >>> b[0] # Left(b, 1) 'H' >>> b[1] # Mid(b, 2, 1) 'e' >>> b[-1] # Right(b, 1) 'd' >>> b[-2] # Mid(b, Len(b)-1, 1) 'l' >>> b[1:] # Mid(b, 2) 'ello World' >>> b[:-1] # Left(b, Len(b)-1) 'Hello Worl' >>> b[2:4] # Mid(b, 3, 2) 'll' >>> b = b[:5] + " My" + b[5:] # Mid(b, 5) = " My" >>> b 'Hello My World' - Python strings starts start at index 0, Basic strings start at index 1. - negative indices work from the end of the string, so b[-2] refers to the 2nd to last character in b FOR...NEXT ---------- Python's for loops are completely different to BASIC's. Instead of providing start, end and step values, you provide a list to loop over. For instance in Basic we'd iterate over each index in an array:: a = Array("red", "green", "blue") For x = 0 to UBound(a) a=MsgBox("The next color is " + a[x]) Next x whereas in Python we would just iterate over each item in an array:: >>> for x in ["red", "green", "blue"]: ... print("The next color is ", x) ... The next color is red The next color is green The next color is blue You can get the BASIC behaviour by using the range() function to create a list of numbers to loop over, but you'll find you almost never need to use this. For instance, to count the commas in a string, we might write the following in Basic:: a = "Hello,Bye,Fred" count = 0 For c = 1 TO Len(a) If Mid(a, c, 1) = "," Then count = count + 1 Next c a = MsgBox("String contains " + count + " commas") becomes:: a = "Hello,Bye,Fred" count = 0 for c in a: if c == ",": count += 1 print("String contains", count, "commas")  - No need to loop over indices and then extract the element at that index. Just loop over the list instead. - Use '==' for comparisons instead of '='. Python won't let you use '=' in an IF statement, so no risk of C-style errors. - += notation is the same - In fact, python provides a better way to count, as we shall see later. ON ERROR GOTO... ERR -------------------- Python error handling is much simpler. For instance, here's a Basic function that needs to check for a division by zero error:: Const BE_DIVIDE_BY_ZERO = 25 Function Reciprocal(number) On Error Goto 0 Reciprocal = 1.0/number If Err = BE_DIVIDE_BY_ZERO Then Reciprocal = 0.0 End If On Error Resume Next End Function becomes:: def Reciprocal(number) try: answer = 1.0/number except ZeroDivisionError: answer = 0.0 - Any error in a 'try' block jumps to the 'except' clause. - You can provide multiple except clauses for different types of errors. If you don't provide a default handler ('except:') then the error is passed up to the enclosing 'try' block (possibly in a calling function) until it is either handled or there are no more try blocks. In that case, Python prints out the error, giving not only the line number of the problem, but showing the call in each function leading to it. In fact, you should almost never use \ **except** on its own since unexpected errors should be reported with all their details. Only try to catch errors you want to handle specially (such as division by zero above). OBJECTS ------- Time to come clean... Python is an object oriented language. This is a good thing, but requires a little adjustment to appreciate fully. Now, consider variables in BASIC. We think of a variable as a box with a label (the variable's name) and a value inside it. In an OO (Object Oriented) language, the values exist outside of any boxes on their own. Variables merely 'point to' values. The difference is, two variables can point to the same value. Consider:: >>> a = [1,2,3] >>> b = [1,2,3] >>> c = b The final result can be visualized like this:: a ----> [1,2,3] b ----> [1,2,3] <---- c That is, we have created two lists. 'a' points to the first, while 'b' and 'c' point to the second. The '==' test in python checks if two arrays contain equal values, while the 'is' test checks if 'two' values are actually one:: >>> a == b True >>> b == c True >>> a is b False >>> b is c True The difference is important when you change something:: >>> b[1] = 42 >>> a [1,2,3] >>> b [1,42,3] >>> c [1,42,3] On the other hand, if you assign something to 'b' (rather than modifying the thing 'b' points to), you make 'b' point at that instead:: >>> b = [4,5,6] a ----> [1,2,3] c ----> [1,42,3] b ----> [4,5,6] You might be a bit worried about this: >>> a = 1 >>> b = a >>> a += 1 >>> b [ what goes here? ] Don't worry. After assigning to 'b', 'a' and 'b' do indeed point at the same object (the number 1):: a ----> 1 <---- b However, incrementing 'a' has the effect of creating a new number, and making 'a' point at that:: a ----> 2 b ----> 1 You cannot change the number which 'a' points to in-place (numbers are thus said to be 'immutable'). Strings are also immutable. This means you can pass lists (arrays) to functions very quickly (because you're just making another pointer to the same array). If you want to copy a list, use the slicing notation to copy the whole thing:: >>> a = [1,2,3] >>> b = a[:] a ----> [1,2,3] b ----> [1,2,3] GARBAGE COLLECTION ------------------ OK, so we can create lots of new objects. How do we get rid of them again? The answer is simple: when it's no longer possible to refer to an object, Python will free it for you:: >>> a = [1, 2, 3] >>> a = None The first line creates three number objects and a list, and makes a pointer to the list:: a ----> [1, 2, 3] The second line makes a pointer to the None object:: a ----> None Since there's no way to get to the list or the objects it contains, they will all be freed when garbage collection occurs.  If we wanted to eliminate 'a' completely such that even the name no longer exists, we can delete it:: >>> del a >>> a NameError: name 'a' is not defined We can even force garbage collection to occur:: import gc gc.collect() Objects and methods ------------------- Because Basic has only a few types, it tends to represent everything else with numbers. File handles are an example; window handles another. Python tends to create new types for everything. We have already seen a few types. You can use a type like a function, to create a new object of that type. Eg:: >>> a = int("4") >>> a 4 >>> b = float(4) >>> b 4.0 >>> c = str(4) >>> c "4" >>> d = list() >>> d [] Basic has a global selection of functions, and the names must be selected carefully to avoid clashes. Consider:: account = Acount_New() Call Account_Credit(account, 100) a=MsgBox("Now has" + Account_check(account)) door = Door_New() Call Door_Close(door) Call Account_Close(account) Because Python keeps track of types (and allows new ones to be created), it can automatically work out which of several functions to use, by keeping the functions 'inside the type'. Such functions are called 'methods':: account = Account() account.Credit(100) print("Now has", account.check()) door = Door() door.Close() account.Close() - Here 'Door' and 'Account' are types (like 'int' or 'str'), while 'account' and 'door' are values of that type (like 4 or "Hello"). Notice how we use the type to create new objects ('instances') of that type. - Python knows that 'door' is a Door, so 'door.Close()' calls the close function for doors, whereas 'account.Close()' calls the close function for accounts. Most objects in Python have many methods. Here are a few for strings:: >>> a = "Hello" >>> a.upper() "HELLO" >>> a.lower() "hello" >>> " spaces ".strip() "spaces" >>> a = "Hello,Bye,Fred" >>> print("String contains", a.count(","), "commas") String contains 2 commas - Strings are immutable, so the above methods return a \ **new** string, leaving the original alone. In mutable objects, such as lists, some methods will modify the object in place. While we're on the subject of strings, I should point out that you can use either single or double quotes, as desired::   >>> a = "Hello" >>> a = 'Hello' >>> a = '"Hello", she said.' >>> a = '''In python, " and ' can be used to quote strings'''  - Triple quotes of either type can also be used, and these can span multiple lines.  - Strings can be any length; you can read an entire file into a string if you want. SELECT ... CASE --------------- There is no SELECT CASE statement. You can use **if** for a direct Basic translation:: if x == 1: print("One") elif x == 2: print("Two") elif x == 3: print("Three") else: print("Many") However, as with FOR loops, you'll generally find that Python makes such things unnecessary. Consider:: For creature_number = 0 TO number_of_creatures - 1 Select Case TypeOf[creature_number] Case Elf Move_Elf(creature_number) Case Goblin Move_Goblin(creature_number) Case Dragon Move_Dragon(creature_number) ... End Select Next creature_number might be written in Python as:: for creature in creatures: creature.Move() As long as we create a new type for each kind of creature, Python will call the correct 'move' function for each creature automatically! USER ERRORS ----------- It's not possible to create your own error in Xpert Basic, but in Python exceptions are not limited to just the system:: def UserFunction(): raise Exception("User error!") Exception is a type, so this creates a new Exception object and raises it as an error. It can be caught in a try block, as shown below:: try: UserFunction() except Exception: print("failed") When you know how to create your own types, you can use that to create different types of error (any object can be raised as an error, even a string, but there are conventions...). OPEN FILES ---------- This:: f = FreeFile Open "MyFile" For Input As f becomes:: >>> f = open("MyFile") or   >>> f = open("MyFile", "r") - a file is a type, like 'str' or 'int' - 'r' means 'for reading' You can use the file type's methods to read from it:: >>> f.readline() 'The first line\\r\\n' >>> f.readline() 'The second line\\r\\n' >>> f.readline() 'The end\\r\\n' >>> f.readline() '' Each line read includes the newline character ('\\n') and possibly a carriage return ('\\r'), so you can tell the difference between a blank line ('\\n' or '\\r\\n') and the end of the file (''). More commonly, you'll use a file object as the sequence in a list. In this case, the loop iterates over the lines:: my_data = open('MyFile') for line in my_data: print("Next line is", line) my_data.close() Or, indeed:: for line in open('MyFile'): print("Next line is", line) - When the loop finishes, the file object is unreachable (since we never assigned it to a variable). Therefore, the garbage collector frees it, closing the file for us automatically. - Earlier, I said that for loops take a list to work on. In fact, a for loop takes an \ *iterator* (anything that can generate a sequence of values). Therefore, lists, files and even user-defined types can be used in for loops. For writing, pass 'w' to the constructor rather than 'r' (the type used to create a new object)::   f = FreeFile Open "Report.txt" For Output As f Print f, "Hello World" Close f   becomes::   f = open("Report.txt", 'w') f.write("Hello World\\r\\n") f.close() or::   f = open("Report.txt", 'w') print("Hello World\\r", file=f) f.close()  When performing file I/O with print it will automatically append the newline character ('\\n') to the output, but if the file is intended to be read by a Microsoft Windows system, you'll need to add a carriage return ('\\r') character as well as shown in the example. Unix and Linux based systems use just the newline character ('\\n') to mark the end of lines.  LIBRARIES --------- Libraries (or modules) contain functions we can use:: >>> import math >>> math.cos(0) 1.0 - import loads routines from another file (called a 'module') - You access a module's functions using the same notation as an object's methods. This avoids name conflicts. For example, most math functions exist in both normal and 'complex' forms:: >>> import cmath, math >>> math.cos(0) 1.0 >>> cmath.cos(0) (1-0j) You can also import functions directly:: >>> from math import cos, sin >>> cos(0) 1.0 >>> sin(0) 0.0 You can use import to split your own programs into several files. Name the files with a '.py' extension, and use import without the extension to load the file. HELP ---- Use the help function to find out about anything in Python. You can get help on functions, types and modules!   >>> import math >>> help(math) >>> help(math.cos) >>> help(str) - MicroPython's help system provides primarily names and types, whereas desktop Python's help provides more thorough documentation. Notice how we are passing apparently abstract ideas as function arguments! In fact, all these things are real Python values, and can be assigned just like anything else:: >>> import math >>> print(math.cos(0)) 1.0 >>> print(math.cos) >>> print math >>> a = math.cos >>> a(0) 1.0 AND, OR, XOR, ^ --------------- Most python operators are the same as in BASIC. The bit-wise operators are different, however. XOR is written ^ in python, AND as & and OR as \| (and ^ becomes \*\*):: >>> 6 & 3 # (6 And 3) 2 >>> 6 \| 3 # (6 Or 3) 7 >>> 6 ^ 3 # (6 Xor 3) 5 >>> 6 ** 3 # (6 ^ 3) 216 >>> 6 ** 333 # (??? Basic can't do this ???) 1331577...4415616 - Basic integers are limited in range to roughly +/- 2 billion, but this is not the case for MicroPython which can handle arbitrarily large integers. BASIC also uses the bitwise operators AND and OR for boolean operations. Use the normal python 'and' and 'or' operators for this. These are 'short-circuit' operators; they only evaluate as far as they need to to get the result:: >>> 0 and 1/0 0 - There is no divide by zero error, because Python only evaluates as far as the 0 before realizing the result is going to be false. STATIC AND DIM -------------- Python doesn't have STATIC or DIM; any variable assigned in a function is automatically considered to be a local variable, and any variable defined outside of a function is considered to be a global variable. The keyword 'global' must be used to indicate that a variable is NOT local:: >>> def inc_a(): ... global a ... a += 1 ... >>> a = 1 >>> inc_a() >>> print a 2 - When you use a variable without assigning to it, Python uses the variable assigned to in the closest enclosing lexical scope.  PASSING BY REFERENCE -------------------- Xpert Basic supports "passing by reference" which means if you pass a variable to a subroutine, and the subroutine changes the variable, then the variable passed in will change too. So, for instance in the example below we create a function that will increment a variable as long as it's less than 10:: Function Increment(test) If test < 10 Then test = test + 1 Increment = True Else Increment = False End If End Function   a = 2   If Increment(a) Then ' a will now be 3 End If Python, however, passes variables by value. If the variable is a list, we can change the list, or add an item to the list, but any change to the variable itself will not affect the original variable. So, how can we get around this? How can we implement an Increment function? Do we have to put our variable in a list? Fortunately, not. The key is to take advantage of the fact that Python can return multiple values like we do in this example:: def Increment(test): if test < 10: return True, test+1 else: return False, test   a = 2   ok, a = Increment(a) if ok: ' a will now be 3 Why would the Python designers not implement a feature that can be so useful? The reason is because passing by reference has become frowned upon due to how easy it is to introduce an obscure bug in a program if the programmer forgets about the "side effects" of a subroutine or function. Fortunately, Python doesn't just leave us with this lofty goal without also giving us tools to help realize them. Still not convinced? Here's an example where "passing by reference" can cause a bug:: Sub PumpOn(channel) ' pulse the pump on channel Digital 1, channel, True Sleep 0.1 Digital 1, channel, False End Sub Sub PumpOff(channel) ' the pump off channel always comes after the pump on channel channel = channel + 1 Digital 1, channel, True Sleep 0.1 Digital 1, channel, False End Sub ' Pulse the pump on channel 2 ten times: For i = 1 To 10 Call PumpOn(2) Sleep 5 Call PumpOff(2) Next i What's the problem? Well, nothing yet, but there is a bug sleeping in the code. If we decided later we didn't want to hardcode the pump channel as 2 and switched to using a variable instead, then each call to PumpOff would increment the new variable by side-effect and the code would no longer work. Future reading -------------- A few other interesting bits to look out for in Python: - The % operator can be used with a string to insert formatted values. Eg:: >>> "Pi is %.3f (to three decimal places)" % math.pi 'Pi is 3.142 (to three decimal places)' - The 'class' keyword lets you create your own types. Vital to take advantage of python properly:: >>> class Point: ... def __init__(self, x, y): ... self.x = x ... self.y = y ... >>> p1 = Point(3, 4) >>> p2 = Point(7, 8) >>> p1.x 3 >>> p2.y 8 - There is a very useful ``dict`` type. It holds a set of elements, each with a ``key`` and a ``value`` and can look up a value from a key in constant time (no matter how big the dictionary gets). Very useful. - The ``assert`` statement checks that something is true, and raises an exception if not. Sprinkle generously around your code to check for programming errors. - Everything is an object! Functions, modules, classes can all be assigned to variables just like other values. You can pass a function to another function, or get one back. Consider this:: >>> def double(x): return 2 * x ... >>> map(double, [1,2,3]) [2,4,6] The ``map`` function applies a function to every item in a list. In this case we doubled every value in the list [1,2,3]. Credits ------- Contents inspired by and adapted from: http://rox.sourceforge.net/desktop/book/export/html/44.html