Python3 is cool and you should use it

From wiki.python.org:

Short version: Python 2.x is legacy, Python 3.x is the present and future of the language

  • why the change?
  • biggest changes
  • cool features
  • cool libraries

Biggest changes

  • unicode everywhere
  • print function
  • yield from
  • function annotations
  • exceptions

New awesome features in standard library!

functools

  • lru_cache
  • single_dispatch

Last recently used cache


In [1]:
import functools
import urllib


@functools.lru_cache(maxsize=32)
def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = 'http://www.python.org/dev/peps/pep-%04d/' % num
    try:
        with urllib.request.urlopen(resource) as s:
            return s.read()
    except urllib.error.HTTPError:
        return 'Not Found'

In [2]:
for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
    pep = get_pep(n)
    print(n, len(pep))

get_pep.cache_info()


8 71449
290 36622
308 29850
320 19440
8 71449
218 19918
320 19440
279 20886
289 27641
320 19440
9991 9
Out[2]:
CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)

In [3]:
@functools.lru_cache(maxsize=None)
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

print([fib(n) for n in range(16)])


[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]

In [4]:
fib.cache_info()


Out[4]:
CacheInfo(hits=28, misses=16, maxsize=None, currsize=16)

Single dispatch


In [5]:
from functools import singledispatch


@singledispatch
def fun(arg, verbose=False):
    if verbose:
        print("Let me just say,", end=" ")
    print(arg)

To add overloaded implementations to the function, use the register() attribute of the generic function. It is a decorator, taking a type parameter and decorating a function implementing the operation for that type:


In [6]:
@fun.register(int)
def _(arg, verbose=False):
    if verbose:
        print("Strength in numbers, eh?", end=" ")
    print(arg)

    
@fun.register(list)
def _(arg, verbose=False):
    if verbose:
        print("Enumerate this:")
    for i, elem in enumerate(arg):
        print(i, elem)

To enable registering lambdas and pre-existing functions, the register() attribute can be used in a functional form:


In [7]:
def nothing(arg, verbose=False):
    print("Nothing.")

fun.register(type(None), nothing)


Out[7]:
<function __main__.nothing>

In [8]:
fun("Hello, world.")


Hello, world.

In [9]:
fun("test.", verbose=True)


Let me just say, test.

In [10]:
fun(42, verbose=True)


Strength in numbers, eh? 42

In [11]:
fun(['spam', 'spam', 'eggs', 'spam'], verbose=True)


Enumerate this:
0 spam
1 spam
2 eggs
3 spam

No more annoying .pyc files laying around

They are now stored in __pychache__ directory with additional info about compilation, etc

Better memory management

Default division doesn't truncate


In [12]:
2 / 3


Out[12]:
0.6666666666666666

Cool exceptions!

Implicit:


In [ ]:
try:
    v = {}['a']
except KeyError as e:
    raise ValueError('failed')

Explicit:


In [ ]:
try:
    v = {}['a']
except KeyError as e:
    raise ValueError('failed') from e

Doesn't work in IPython

but I'm working on that:

github issue

New modules in standard library:

and more.

Enum!


In [14]:
from enum import Enum

class Mood(Enum):
    funky = 1
    happy = 3
    
    def describe(self):
       # self is the member here
        return self.name, self.value

    def __str__(self):
        return 'my custom str! {0}'.format(self.value)

    @classmethod
    def favorite_mood(cls):
        # cls here is the enumeration
        return cls.happy

In [15]:
print(Mood.favorite_mood())
print(Mood.happy.describe())
str(Mood.funky)


my custom str! 3
('happy', 3)
Out[15]:
'my custom str! 1'

In [16]:
class Planet(Enum):
    MERCURY = (3.303e+23, 2.4397e6)
    VENUS   = (4.869e+24, 6.0518e6)
    EARTH   = (5.976e+24, 6.37814e6)
    MARS    = (6.421e+23, 3.3972e6)
    JUPITER = (1.9e+27,   7.1492e7)
    SATURN  = (5.688e+26, 6.0268e7)
    URANUS  = (8.686e+25, 2.5559e7)
    NEPTUNE = (1.024e+26, 2.4746e7)
    def __init__(self, mass, radius):
        self.mass = mass       # in kilograms
        self.radius = radius   # in meters
    @property
    def surface_gravity(self):
        # universal gravitational constant  (m3 kg-1 s-2)
        G = 6.67300E-11
        return G * self.mass / (self.radius * self.radius)

In [17]:
Planet.EARTH.value


Out[17]:
(5.976e+24, 6378140.0)

In [18]:
Planet.EARTH.surface_gravity


Out[18]:
9.802652743337129

In [19]:
import statistics

scores = [12, 13, 88, 12, 61, 32]

print(statistics.mean(scores))
print(statistics.stdev(scores))
print(statistics.mode(scores))


36.333333333333336
31.702786418021155
12

In [20]:
print(statistics.mode(range(5)))


---------------------------------------------------------------------------
StatisticsError                           Traceback (most recent call last)
<ipython-input-20-af5dc1939414> in <module>()
----> 1 print(statistics.mode(range(5)))

/usr/lib/python3.4/statistics.py in mode(data)
    432     elif table:
    433         raise StatisticsError(
--> 434                 'no unique mode; found %d equally common values' % len(table)
    435                 )
    436     else:

StatisticsError: no unique mode; found 5 equally common values

Asyncio


In [21]:
import asyncio

@asyncio.coroutine
def greet_every_two_seconds_three_times():
    for _ in range(3):
        print('Hello World')
        yield from asyncio.sleep(2)

loop = asyncio.get_event_loop()
loop.run_until_complete(greet_every_two_seconds_three_times())


Hello World
Hello World
Hello World

In [22]:
import asyncio

@asyncio.coroutine
def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    yield from asyncio.sleep(1.0)
    return x + y

@asyncio.coroutine
def print_sum(x, y):
    result = yield from compute(x, y)
    print("%s + %s = %s" % (x, y, result))

loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))


Compute 1 + 2 ...
1 + 2 = 3


In [23]:
import asyncio

@asyncio.coroutine
def factorial(name, number):
    f = 1
    for i in range(2, number+1):
        print("Task %s: Compute factorial(%s)..." % (name, i))
        yield from asyncio.sleep(1)
        f *= i
    print("Task %s: factorial(%s) = %s" % (name, number, f))

tasks = [
    asyncio.Task(factorial("A", 2)),
    asyncio.Task(factorial("B", 3)),
    asyncio.Task(factorial("C", 4))]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
loop.close()


Task A: Compute factorial(2)...
Task B: Compute factorial(2)...
Task C: Compute factorial(2)...
Task A: factorial(2) = 2
Task B: Compute factorial(3)...
Task C: Compute factorial(3)...
Task B: factorial(3) = 6
Task C: Compute factorial(4)...
Task C: factorial(4) = 24

yield from


In [24]:
yield from [1, 2, 'banana!']


  File "<ipython-input-24-76bdf12a82f9>", line 1
    yield from [1, 2, 'banana!']
                                ^
SyntaxError: 'yield' outside function

In [28]:
def show_me_yield():
    yield from range(2)
    yield "Surprise!"
    yield from sorted(range(2), reverse=True)

In [29]:
for value in show_me_yield():
    print(value)


0
1
Surprise!
1
0

extended unpacking


In [30]:
first, *rest = range(5)

In [31]:
first


Out[31]:
0

In [32]:
rest


Out[32]:
[1, 2, 3, 4]

In [33]:
first, *middle, last = range(5)

In [34]:
middle


Out[34]:
[1, 2, 3]

In [35]:
last


Out[35]:
4

In [36]:
first, *middle, least_but_not_last, last = range(5)

In [37]:
least_but_not_last


Out[37]:
3

Function annotations


In [38]:
def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9):
    print(a, b, c)

In [39]:
foo(1, 2, 3)


1 2 3

In [40]:
foo.__annotations__


Out[40]:
{'a': 'x', 'c': list, 'b': 11, 'return': 9}

In [41]:
foo.__annotations__['a']


Out[41]:
'x'

Real life example of function annotations


In [25]:
!pip install pytest pytest-quickcheck


Requirement already satisfied (use --upgrade to upgrade): pytest in ./lib/python3.4/site-packages
Requirement already satisfied (use --upgrade to upgrade): pytest-quickcheck in ./lib/python3.4/site-packages
Requirement already satisfied (use --upgrade to upgrade): py>=1.4.20 in ./lib/python3.4/site-packages (from pytest)
Requirement already satisfied (use --upgrade to upgrade): distribute in ./lib/python3.4/site-packages (from pytest-quickcheck)
Requirement already satisfied (use --upgrade to upgrade): setuptools>=0.7 in ./lib/python3.4/site-packages (from distribute->pytest-quickcheck)
Cleaning up...

In [26]:
%%writefile test.py

import pytest


MAX_NUMBER = 4


@pytest.mark.randomize(min_num=0, max_num=MAX_NUMBER, ncalls=12)
def test_generate_adding_ints(i1: int, i2: int):
    assert i1 + i2 <= 2 * MAX_NUMBER


Overwriting test.py

In [27]:
!python -m pytest test.py


============================= test session starts ==============================
platform linux -- Python 3.4.0 -- py-1.4.20 -- pytest-2.5.2
plugins: quickcheck
collected 144 items 

test.py ................................................................................................................................................

========================== 144 passed in 0.22 seconds ==========================

Other important changes

  • super is now super()
  • print as a function
  • new string formatting

Further reading & materials

Q & A