Conditional expressions

import math

x = float(input())
if x > 0:
    y = math.log(x)
else:
    y = float('nan') # a special floating-point value that represents “Not a Number”. 
print(y)

A more concise expression:

y = math.log(x) if x > 0 else float('nan')

Another example:

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)
def factorial(n):
    return 1 if n == 0 else n * factorial(n-1)

List comprehensions

def capitalize_all(t):
    res = []
    for s in t:
        res.append(s.capitalize())
    return res
def capitalize_all(t):
    return [s.capitalize() for s in t]

List comprehensions can also be used for filtering.

def only_upper(t):
    res = []
    for s in t:
        if s.isupper():
            res.append(s)
    return res
def only_upper(t):
    return [s for s in t if s.isupper()]

Note: list comprehensions are harder to debug because you can’t put a print statement inside the loop. I suggest that you use them only if the computation is simple enough that you are likely to get it right the first time. And for beginners that means never.

Generator expressions

It's easy to create a list using list comprehension. However the size of list is limited due to RAM. It's difficult to operate on a list of 1 million elements.

Generator allows generating elements while calculating

https://wiki.python.org/moin/Generators

L = [x * x for x in range(5)]
L
g = (x * x for x in range(5))
g
next(g)
next(g)
next(g)
next(g)
next(g)
next(g)

Another way to write generator: yield

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

f = fib(4)
f
next(f)
next(f)
next(f)
next(f)
next(f)

any and all

Python provides a built-in function, any, that takes a sequence of boolean values and returns True if any of the values are True. It works on lists:

any([False, False, True])
any(letter == 's' for letter in 'babson')

Python provides another built-in function, all, that returns True if every element of the sequence is True.

all([False, True, True])
all([True, True, True])
all(letter == 'w' for letter in 'www')

Sets

Each element in a set is unique.

s = set([1, 2, 3, 3])
s
s.add(4)
s
s.add(4)
s

set works like set in math.

s1 = set([1, 2, 3])
s2 = set([2, 3, 4])
s1 & s2
s1 | s2

Exercise 01

  1. Using set, rewrite function subtract in analyze_book.py in chapter 13.
  2. Using set, rewrite function avoids and uses_only in chapter 9.

Counters

A Counter is like a set, except that if an element appears more than once, the Counter keeps track of how many times it appears.

from collections import Counter
count = Counter('babson')
count
count['a']
count['c']

Named tuples

Many simple objects are basically collections of related values. For example, the Point object contains two numbers, x and y. When you define a class like this, you usually start with an init method and a str method:

class Point:

    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __str__(self):
        return '(%g, %g)' % (self.x, self.y)

This is a lot of code to convey a small amount of information. Python provides a more concise way to say the same thing:

from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
Point

By using namedtuple, Point automatically provides methods like __init__ and __str__ so you don’t have to write them.

To create a Point object, you use the Point class as a function:

p = Point(1, 2)
p
p.x, p.y

namedtuple provide a quick way to define simple classes. The drawback is that simple classes don’t always stay simple. You might decide later that you want to add methods to a named tuple. In that case, you could define a new class that inherits from the namedtuple:

class Pointier(Point):
    # add more methods here

Gathering keyword args

We've learned how to write a function that gathers its arguments into a tuple:

def printall(*args):
    print(args)
    
printall(1, 2.0, '3')

But the * operator doesn’t gather keyword arguments:

printall(1, 2.0, third='3')

To gather keyword arguments, you can use the ** operator:

def printall(*args, **kwargs):
    print(args, kwargs)
    
printall(1, 2.0, third='3')