现在我们来看一些比较高级的 Python 特性,这些特性对开展数据工作特别有用。

每个 Python 列表都有一个 sort 方法可以恰当地排序。如果你不想弄乱你的列表,可以使用 sorted 函数,它会返回一个新列表:

x = [4,1,2,3]
y = sorted(x)       # 结果是[1,2,3,4],但x没有变
x.sort()            # x变为[1,2,3,4]

默认情况下,sort(和 sorted)基于元素之间的朴素比较从最小值到最大值对列表进行排序。

如果你想把元素按从最大值到最小值进行排序,可以指定参数 reverse=True。除了比较元素本身,你还可以通过指定键来对函数的结果进行比较:

# 通过绝对值对列表元素从最大到最小排序
x = sorted([-4,1,-2,3], key=abs, reverse=True) # 是[-4,3,-2,1]

# 从最高数到最低数排序单词和计数
wc = sorted(word_counts.items(),
            key=lambda (word, count): count,
            reverse=True)

我们有时可能会想把一个列表转换为另一个列表,例如只保留其中一些元素,或更改其中一些元素,或者同时做这两种变动。可以执行这种操作的 Python 技巧叫作列表解析(list comprehension):

even_numbers = [x for x in range(5) if x % 2 == 0]     # [0, 2, 4]
squares      = [x * x for x in range(5)]               # [0, 1, 4, 9, 16]
even_squares = [x * x for x in even_numbers]           # [0, 4, 16]

类似地,你也可以把列表转换为字典或集合:

square_dict = { x : x * x for x in range(5) }    # { 0:0, 1:1, 2:4, 3:9, 4:16}
square_set  = { x * x for x in [1, -1] }         # { 1 }

如果你不需要来自原列表中的值,常规的方式是使用下划线作为变量:

zeroes = [0 for _ in even_numbers]    # 和even_numbers有相同的长度

列表解析可以包括多个 for 语句:

pairs = [(x, y)
         for x in range(10)
         for y in range(10)]    # 100个对(0,0) (0,1) ... (9,8), (9,9)

其中后面的 for 语句可以使用前面的 for 语句的结果:

increasing_pairs = [(x, y)                       # 只考虑x < y的对
                    for x in range(10)           # range(lo, hi) 与之相等
                    for y in range(x + 1, 10)]   # [lo, lo + 1, ..., hi - 1]

我们会经常用到列表解析。

列表的一个问题是它很容易变得非常大。range(1000000) 能创建一个有 100 万个元素的列表:如果你需要每次只处理其中一个元素,这将会是极大的资源浪费(或会导致内存不足);如果你只需要前面的几个值,那对整个列表都进行计算也是一种浪费。

生成器(generator)是一种可以对其进行迭代(对我们来说,通常使用 for 语句)的程序,但是它的值只按需延迟(lazily)产生。

创建生成器的一种方法是使用函数和 yield 运算符:

def lazy_range(n):
    """a lazy version of range"""
    i = 0
    while i < n:
        yield i
        i += 1

下面的循环会每次消耗一个 yield 值直到一个也不剩:

for i in lazy_range(10):
    do_something_with(i)

(Python 确实有一个和 lazy_range 一样的函数,叫作 xrange,并且在 Python 3 中,range 函数本身就是延迟的。)这意味着,你甚至可以创建一个无限的序列:

def natural_numbers():
    """returns 1, 2, 3, ..."""
    n = 1
    while True:
        yield n
        n += 1

尽管在没有使用某种 break 逻辑语句时,你不应该做这种迭代的。

 延迟的缺点是,你只能通过生成器迭代一次。如果需要多次迭代某个对象,你就需要每次都重新创建一个生成器,或者使用列表。

第二种创建生成器的方法是使用包含在圆括号中的 for 语句解析:

lazy_evens_below_20 = (i for i in lazy_range(20) if i % 2 == 0)

前面提过,每个 dict 都有一个 items() 方法可以返回它的键值对的列表。更常见的做法是使用 iteritems() 方法:当我们在列表上迭代的时候它延迟 yield 为每次一个键值对。

当我们学习数据科学时,会经常需要生成随机数。可以使用 random 模块生成随机数:

import random

four_uniform_randoms = [random.random() for _ in range(4)]

# [0.8444218515250481,
#  0.7579544029403025,
#  0.420571580830845,
#  0.25891675029296335]
# random.random()生成在0-1之间均匀分布的随机数,是最常用的随机函数

random 模块实际上生成的是基于一种内部状态的确定性的伪随机数。如果你想得到可复生的结果,可以用 random.seed 生成随机数种子:

random.seed(10)          # 设置随机数种子为10
print random.random()    # 0.57140259469
random.seed(10)          # 重设随机数种子为10
print random.random()    # 再次得到0.57140259469

有时候我们用 random.randrange 生成随机数,它会取 1 到 2 个参数,并从对应的 range() 函数随机选择一个元素返回:

random.randrange(10)    # 从range(10) = [0, 1, ..., 9]中随机选取
random.randrange(3, 6)  # 从range(3, 6) = [3, 4, 5]中随机选取

还有其他一些比较方便的方法,如 Random.shuffle 可随机地重排列表中的元素:

up_to_ten = range(10)
random.shuffle(up_to_ten)
print up_to_ten
# [2, 5, 1, 9, 7, 3, 8, 6, 4, 0],(你的结果可能不同)

如果你需要从列表中随机取一个元素,可以使用 random.choice

my_best_friend = random.choice(["Alice", "Bob", "Charlie"])    # 对我来说是"Bob"

如果你需要不替换地(即不重复地)随机选择一个元素的样本,可以使用 random.sample

lottery_numbers = range(60)
winning_numbers = random.sample(lottery_numbers, 6)  # [16, 36, 10, 6, 25, 9]

选择一个允许替换的(即允许重复的)元素样本,只需多次调用 random.choice 即可:

four_with_replacement = [random.choice(range(10))
                         for _ in range(4)]
# [9, 4, 4, 2]

正则表达式提供了一种搜索文本的方法。它超乎想象地有用,但同时也相当复杂,以至于需要专门的书籍来讲解。之后的内容会频繁涉及正则表达式,届时我们再详述,这里只给出 Python 中如何使用正则表达式的例子:

import re

print all([                                 # 所有这些语句都为true,因为
    not re.match("a", "cat"),               # * 'cat'不以'a'开头
    re.search("a", "cat"),                  # * 'cat'里有一个字符'a'
    not re.search("c", "dog"),              # * 'dog'里没有字符'c'
    3 == len(re.split("[ab]", "carbs")),    # * 分割掉a,b,剩余长度为3
    "R-D-" == re.sub("[0-9]", "-", "R2D2")  # 用虚线进行位的替换
    ] # 打印True

就像许多语言一样,Python 允许你定义(class)。类可以封装对象和函数来对它们进行操作。有时候我们会用类来使代码更加干净整洁。解释类的用法的最简单方式可能是构建一个有超多注释的例子。

假设没有内置的 Python 集合,那我们可能会想到去创建自己的 Set 类。

我们创建的类会有什么样的行为呢?给定一个 Set,我们需要能在其中加入(add)项目,移除(remove)项目,以及检查其中是否包含(contains)某个值。我们把这些功能创建为成员(member)函数,意思是我们可以通过在 Set 对象后面加点(.)来访问它们:

# 按惯例,我们给下面的类起个PascalCase的名字
class Set:

    # 这些是成员函数
    # 每个函数都取第一个参数"self"(另一种惯例)
    # 它表示所用到的特别的集合对象

    def __init__(self, values=None):
        """This is the constructor.
        It gets called when you create a new Set.
        You would use it like
        s1 = Set()           # 空集合
        s2 = Set([1,2,2,3])  # 用值初始化"""

        self.dict = {} # Set的每一个实例都有自己的dict属性
                       # 我们会用这个属性来追踪成员关系
        if values is not None:
            for value in values:
                self.add(value)

    def __repr__(self):
        """this is the string representation of a Set object
        if you type it at the Python prompt or pass it to str()"""
        return "Set: " + str(self.dict.keys())

    # 通过成为self.dict中对应值为True的键,来表示成员关系
    def add(self, value):
        self.dict[value] = True

    # 如果它在字典中是一个键,那么在集合中就是一个值
    def contains(self, value):
        return value in self.dict

    def remove(self, value):
        del self.dict[value]

可以像下面这样来用上面的函数:

s = Set([1,2,3])
s.add(4)
print s.contains(4)       # True
s.remove(3)
print s.contains(3)       # False

在传递函数的时候,有时我们可能想部分地应用(或 curry)函数来创建新函数。下面是一个简单的例子,假设我们有一个含两个变量的函数:

def exp(base, power):
    return base ** power

我们想用它来创建一个单变量的函数 two_to_the。它的输入是一个幂次(power),输出的是 exp(2, power) 的结果。

当然,我们可以用 def 来实现,但它有时候使用起来并不太方便:

def two_to_the(power):
    return exp(2, power)

一个另辟蹊径的方法是使用 functools.partial

from functools import partial
two_to_the = partial(exp, 2)      # 现在是一个包含一个变量的函数
print two_to_the(3)               # 8

如果你为后面的参数指定了名字,也能用 partial 来填充这些参数:

square_of = partial(exp, power=2)
print square_of(3)                  # 9

如果你 curry 中间函数的参数,就会变得混乱起来,所以要努力避免这么做。

偶尔我们也会使用函数 mapreducefilter,它们为列表解析提供了函数式替换方案:

def double(x):
    return 2 * x

xs = [1, 2, 3, 4]
twice_xs = [double(x) for x in xs]         # [2, 4, 6, 8]
twice_xs = map(double, xs)                 # 和上面一样
list_doubler = partial(map, double)        # double了一个列表的*function*
twice_xs = list_doubler(xs)                # 同样是[2, 4, 6, 8]

如果你提供了多个列表,可以对带有多个参数的函数使用 map

def multiply(x, y): return x * y

products = map(multiply, [1, 2], [4, 5]) # [1 * 4, 2 * 5] = [4, 10]

类似地,filter 做了列表解析中 if 的工作:

def is_even(x):
    """True if x is even, False if x is odd"""
    return x % 2 == 0

x_evens = [x for x in xs if is_even(x)]       # [2, 4]
x_evens = filter(is_even, xs)                 # 和上面一样
list_evener = partial(filter, is_even)        # filter了一个列表的*function*
x_evens = list_evener(xs)                     # 同样是[2, 4]

reduce 结合了列表的头两个元素,它们的结果又结合了列表的第 3 个元素,这个结果之后又结合了第 4 个元素,依次下去,直到得到一个单独的结果:

x_product = reduce(multiply, xs)              # = 1 * 2 * 3 * 4 = 24
list_product = partial(reduce, multiply)      # reduce了一个列表的*function*
x_product = list_product(xs)                  # 同样是24

有时候,你可能想在一个列表上迭代,并且同时使用它的元素和元素的索引:

# 非Python用法
for i in range(len(documents)):
    document = documents[i]
    do_something(i, document)

# 也非Python用法
i = 0
for document in documents:
    do_something(i, document)
    i += 1

Python 惯用的解决方案是使用枚举(enumerate),它会产生 (index, element) 元组:

for i, document in enumerate(documents):
    do_something(i, document)

类似地,如果你只想要索引,则执行:

for i in range(len(documents)): do_something(i)     # 非Python用法
for i, _ in enumerate(documents): do_something(i)   # Python用法

我们会频繁用到枚举。

如果想把两个或多个列表压缩在一起,可以使用 zip 把多个列表转换为一个对应元素的元 组的单个列表中:

list1 = ['a', 'b', 'c']
list2 = [1, 2, 3]
zip(list1, list2)         # 是[('a', 1), ('b', 2), ('c', 3)]

如果列表的长度各异,zip 会在第一个列表结束时停止。

可以使用一种特殊的方法“解压”一个列表:

pairs = [('a', 1), ('b', 2), ('c', 3)]
letters, numbers = zip(*pairs)

其中的星号执行参数拆分(argument unpacking)。参数拆分使用 pairs 的元素作为独立的参数传给 zip。这就和调用以下函数的结果是一样的:

zip(('a', 1), ('b', 2), ('c', 3))

它返回 [('a','b','c')('1','2','3')]

你可以在任何函数上使用参数拆分:

def add(a, b): return a + b

add(1, 2)     # 返回3
add([1, 2])   # TypeError!
add(*[1, 2])  # 返回3

参数拆分并不是特别有用,但当我们用到它的时候,会觉得这是个不错的技巧。

假如我们想创建一个更高阶的函数,把某个函数 f 作为输入,并返回一个对任意输入都返回 f 值两倍的新函数:

def doubler(f):
    def g(x):
        return 2 * f(x)
    return g

这个函数在有些情况下可以实现:

def f1(x):
    return x + 1

g = doubler(f1)
print g(3)           # 8 (== ( 3 + 1) * 2)
print g(-1)          # 0 (== (-1 + 1) * 2)

但对于有多个参数的函数来说,就不适用:

def f2(x, y):
    return x + y

g = doubler(f2)
print g(1, 2)    # TypeError: g()只能有一个参数(给定了两个)

我们所需要的是一种指定一个可以取任意参数的函数的方法,利用参数拆分和一点点魔法就可以做到这一点:

def magic(*args, **kwargs):
    print "unnamed args:", args
    print "keyword args:", kwargs

magic(1, 2, key="word", key2="word2")

# 输出
# 未命名args: (1, 2)
# 关键词args: {'key2': 'word2', 'key': 'word'}

也就是说,当我们定义了这样一个函数时,args 是一个它的未命名参数的元组,而 kwargs 是一个它的已命名参数的 dict。反过来也适用,你可以使用一个 list(或者 tuple)和 dict 来给函数提供参数:

def other_way_magic(x, y, z):
    return x + y + z

x_y_list = [1, 2]
z_dict = { "z" : 3 }
print other_way_magic(*x_y_list, **z_dict)    # 6

参照以上例子,你可以充分利用这种技巧,随意发挥。我们将会只用它来创建可以将任意参数作为输入的高阶函数:

def doubler_correct(f):
    """works no matter what kind of inputs f expects"""
    def g(*args, **kwargs):
        """whatever arguments g is supplied, pass them through to f"""
        return 2 * f(*args, **kwargs)
    return g

g = doubler_correct(f2)
print g(1, 2) # 6

新员工的入职培训到此结束。哦,当然,不要浪费任何所学的东西。

30:00