The IPython -- being rebranded as Jupyter -- notebook interface is becoming a standard for a number of languages other than Python: Julia, Scala, R, Haskell, bash are all getting their kernels in IPython. Since Python allows you to call MATLAB anyway, you can also use the notebook interface for MATLAB if you wish so.
Many features of IPython are independent of the underlying language. The so-called magic functions make it extremely powerful. This are prefixed by a percentage sign. For a quick reference, try
In [1]:
%quickref
For instance, you can benchmark the speed of a function:
In [2]:
%timeit range(1000000)
You also have direct access to the operating system calls:
In [13]:
!uname -a
You can, of course, always access the result of the previous command:
In [14]:
print(_)
Some other cool features to explore: LaTeX export of notebooks with support for Bibtex -- also works for HTML -- and launching parallel computations in Python interpreters distributed across a cluster.
In [15]:
from __future__ import print_function, division
We also want nicely formatted output:
In [2]:
from sympy.interactive import printing
printing.init_printing(use_latex='mathjax')
Then we bulk-import everything we might need:
In [3]:
from sympy import *
We define some symbols:
In [4]:
x, y = symbols('x y')
In [5]:
sin(pi*x)/cos(pi*y)
Out[5]:
It renders nicely as $\frac{\sin\pi x}{\cos\pi x}$. Now a symbolic integral:
In [6]:
integrate(pi*sin(x*y), x)
Out[6]:
Evaluate this for a particular value of x and y:
In [9]:
integrate(pi*sin(x*y), x).subs([(x, pi), (y, 1)])
Out[9]:
SymPy, just like Mathematica, keeps results symbolic as long as possible. If you want a numerical result, you specifically have to request it:
In [25]:
N(integrate(pi*sin(x*y), x).subs([(x, pi), (y, 1)]))
Out[25]:
The quantum physics module is especially helpful. The noncommutative algebra is better than the respective package in Mathematica, especially when it comes to non-Hermitian variables:
In [7]:
from sympy.physics.quantum import *
X = HermitianOperator('X')
Y = Operator('Y')
Dagger(X*Y)
Out[7]:
We can easily define Hamiltonians. For instance, the Hubbard model on a chain is as follows:
In [31]:
t = 1.0
U = 4.0
n_sites = 2
cu = [Operator("%s_%s_u" % ("c", i + 1)) for i in range(n_sites)]
cd = [Operator("%s_%s_d" % ("c", i + 1)) for i in range(n_sites)]
hamiltonian = sum(U*Dagger(cu[r])*cu[r]*Dagger(cd[r])*cd[r] for r in range(n_sites))
hamiltonian += sum(-t*(Dagger(cu[r])*cu[r+1]+Dagger(cu[r+1])*cu[r]
+Dagger(cd[r])*cd[r+1]+Dagger(cd[r+1])*cd[r]) for r in range(n_sites-1))
expand(hamiltonian)
Out[31]:
Python was retrofitted with some elements of functional programming, mainly building on ideas coming from Haskell. It nevertheless remains an object-oriented language, but it is highly opportunistic. This approach is a lot like Mathematica, which is quintessentially functional, but you can follow any programming paradigm when using it.
List comprehensions are probably the most used construct in Python from functional programming. In fact, it is considered a Pythonesque way of doing things. It is a quick way of generating transformed lists from other lists: list comprehension is a simple map function in disguise.
In [22]:
[i**2 for i in range(5)]
Out[22]:
The expression on the left inside the list comprehension is similar to a pure function and we can have more complex forms:
In [26]:
[i*j for i in range(1, 4) for j in range(1, 4)]
Out[26]:
We can also include conditionals in the list comprehension:
In [27]:
[i for i in range(30) if i%3 == 0]
Out[27]:
It is easy to emulate the MapIndex function of Mathematica:
In [29]:
[[a, i] for i, a in enumerate([sqrt(2), pi, x])]
Out[29]:
List comprehension does not actually return a list, it returns an iterator. Iterators are essentially generating functions for list and they are very important to functional programming in Python. Iterators have a next() function to retrieve subsequent elements, making them very easy to loop over. For instance:
In [16]:
l = iter([1, 2, 3])
next(l)
Out[16]:
This is, of course, not very useful, as we could have just used the list itself. List comprehensions are a more sensible way of getting iterators. Another way of creating an iterator is by defining a function that returns values through yield rather than through return. This allows an internal state for the function and lets it continue where it left it off. For example:
In [17]:
def squares(N):
for i in range(N):
yield(i**2)
In [19]:
for j in squares(5):
print(j)
There is no shortage of useful examples for using iterators. Take combinatoric functions, for instance:
In [24]:
import itertools
for combination in itertools.combinations([1, 2, 3, 4, 5], 2):
print(combination)