1. 怎么能写出结构良好、可读的程序,你和其他人将能够很容易的重新使用它?


2. 基本结构块,如循环、函数以及赋值,是如何执行的?
3. Python 编程的陷阱有哪些,你怎么能避免它们吗?


4.1 回到基础

赋值


#并不会复制变量的内容,只有它的“引用对象”
a = ['wangsiji', 'liangxin']
b = a 
a[1] = "wang"
print(b)


#通过创建一个持有空链表的变量 empty,然后在下一行使用它 三次。
empty = []
nested = [empty, empty, empty]
print(nested)
nested[1].append("Python")
print(nested)
#改变链表中嵌套链表内的一个项目,它们全改变了。
#这是因为三个元素中的每一个实际上都只是一个内存中的同一链表的引用。

等式

#Python 提供了两种方法来检查一对项目是否相同。
#首先,我们创建一个链表,其中包含同一对象的 多个副本,证明它们不仅对于==完全相同,而且它们是同一个对象:
size = 5
python = ['Python']
snake_nest = [python] * size
print(snake_nest[0] == snake_nest[1] == snake_nest[2] == snake_nest[3] == snake_nest[4])
print(snake_nest[0] is snake_nest[1] is snake_nest[2] is snake_nest[3] is snake_nest[4])

#现在,让我们将一个新的 python 放入嵌套中。可以很容易地表明这些对象不完全相同。
import random
position = random.choice(range(size))
snake_nest[position] = ['Python']
print(snake_nest)
print(snake_nest[0] == snake_nest[1] == snake_nest[2] == snake_nest[3] == snake_nest[4])
print(snake_nest[0] is snake_nest[1] is snake_nest[2] is snake_nest[3] is snake_nest[4])

#现哪个位置包含闯入者,函数 id()使检测更加容易:
print([id(snake) for snake in snake_nest])

条件语句

#在 if 语句的条件部分,一个非空字符串或链表被判定为真,而一个空字符串或链表的 被判定为假。
#也就是说,我们不必在条件中写:if len(element) > 0:。
mixed = ['cat', '', ['dog'], []]
for element in mixed:
    if element:
        print(element)
        
#因为表达式中 if 子句条件满足,Python 就不会比较 elif 子句,所有程序永远不会输出2。
#相反,如果我们用一个 if 替换 elif,那么程序会输出 1 和 2。
#所以 elif 子句比单独的 if子句潜在的给我们更多信息;
#当它被判定为真时,告诉我们不仅条件满足而且前面的 if 子句的 条件不满足。
animals = ['cat', 'dog']
if 'cat' in animals:
    print(1)
elif 'dog' in animals:
    print(2)
    
#all()函数和 any()函数可以应用到一个链表(或其他序列),来检查是否全部或任一项 目满足一些条件:
sent = ['No', 'good', 'fish', 'goes', 'anywhere', 'without', 'a', 'porpoise', '.']
print(all(len(w) > 4 for w in sent))
print(any(len(w) > 4 for w in sent))

4.2 序列

#直接比较字符串、链表和元组,在各个类型上做索引、切片和长度操作:
raw = 'I turned off the spectroroute'
text = ['I', 'turned', 'off', 'the', 'spectroroute']
pair = (6, 'turned')
#索引
print(raw[2], text[3], pair[1])
#切片
print(raw[-3:], text[-3:], pair[-3:])
#长度操作
print(len(raw), len(text), len(pair))
#在一行代码中计算多个值,中间用逗号分隔。这些用逗号分隔的表达式其实就是元组——如果没有歧义,
#Python 允许我们忽略元组周围的括号。当我们输出一个元组时,括号始终显示。
#通过以这种方式使用元组,我们隐式的将这些项目聚 集在一起。

序列类型上的操作

for item in s    遍历 s 中的元素
for item in sorted(s)   按顺序遍历 s 中的元素
for item in set(s)    遍历 s 中的无重复的元素
for item in reversed(s)     按逆序遍历 s 中的元素
for item in set(s).difference(t)     遍历在集合 s 中不在集合 t 的元素
for item in random.shuffle(s)     按随机顺序遍历 s 中的元素

#要获得无重复的逆序排列的 s 的元素,可以使用 reversed(sorted(set(s)))。
#其他一些对象,如 FreqDist,也可以转换成一个序列(使用 list())且支持迭代:
import nltk
raw = 'Red lorry, yellow lorry, red lorry, yellow lorry.'
text = nltk.word_tokenize(raw)#列表
print(list(text))
fdist = nltk.FreqDist(text)#出现次数
print(list(fdist))
for key in fdist:
    print(fdist[key], end=" ")
    
    
    
#接下来的例子中,我们使用元组重新安排我们的链表中的内容。
#(可以省略括号,因为逗号比赋值的优先级更高。)
print()
words = ['I', 'turned', 'off', 'the', 'spectroroute']
words[2], words[3], words[4] = words[3], words[4], words[2]
print(words)
#这是一种地道和可读的移动链表内的项目的方式。
#它相当于下面的传统方式不使用元组做上述任务(注意这种方法需要一个临时变量 tmp)。

tmp = words[2]
words[2] = words[3]
words[3] = words[4]
words[4] = tmp

#Python 有序列处理函数,如 sorted()和 reversed(),
#它们重新 排列序列中的项目。也有修改序列结构的函数,可以很方便的处理语言。
#因此,zip()取两 个或两个以上的序列中的项目,将它们“压缩”打包成单个的配对链表。
#给定一个序列 s,enumerate(s)返回一个包含索引和索引处项目的配对。
words = ['I', 'turned', 'off', 'the', 'spectroroute']
tags = ['noun', 'verb', 'prep', 'det', 'noun']
print(list(zip(words, tags)))
print(list(enumerate(words)))
print(list(enumerate(tags)))


#对于一些 NLP 的任务,有必要将一个序列分割成两个或两个以上的部分。
#例如:我们可能需要用 90%的数据来“训练”一个系统,剩余 10%进行测试。
#要做到这一点,我们指定想要分割数据的位置,然后在这个位置分割序列。
text = nltk.corpus.nps_chat.words()
print(text)
cut = int(0.9 * len(text))
training_data, test_data = text[:cut], text[cut:]
print(text == training_data + test_data)
print(len(training_data) / len(test_data))
#让我们综合关于这三种类型的序列的知识,一起使用链表推导处理一个字符串中的词,按它们的长度排序。
words = 'I turned off the spectroroute'.split() 
print(words)
#使用链表推导建立一个元组的链表
#其中每个元组由一 个数字(词长)和这个词组成
wordlens = [(len(word), word) for word in words]
print(wordlens)
wordlens.sort()
print(wordlens)


#约定可以用下划线表示我们不会使用其值的变量
wordlens.sort()
print(' '.join(w for (_, w) in wordlens))

产生器表达式


text = '''"When I use a word," Humpty Dumpty said in rather a scornful tone,
           "it means just what I choose it to mean - neither more nor less."'''
print([w.lower() for w in nltk.word_tokenize(text)][:5])

#链表对象的存储空间必须在 max()的值被计 算之前分配。如果文本非常大的,这将会很慢
print(max([w.lower() for w in nltk.word_tokenize(text)]))
#数据流向调用它的函数。由于调用 的函数只是简单的要找最大值——按字典顺序排在最后的词——它可以处理数据流,而无需 存储迄今为止的最大值以外的任何值。
print(max(w.lower() for w in nltk.word_tokenize(text)))
#第二行使用了产生器表达式,在许多语言处理的案例中,产生器表达式会更高效

4.3 风格的问题



Python 代码风格

#代码布局中每个缩进级别应使用 4 个空格
#每行 应少于 80 个字符长,如果必要的话,你可以在圆括号、方括号或花括号内换行,因为 Pyth on 能够探测到该行与下一行是连续的
#如果你需要在圆括号、方括号或大括号中换行,通常可以添加额外的括号,也可以在行尾需要换行的地方添加一个反斜杠

过程风格与声明风格

计数器的一些合理用途

#在有些情况下,我们仍然要在链表推导中使用循环变量。例如:我们需要使用一个循环 变量中提取链表中连续重叠的 n-grams:
sent = ['The', 'dog', 'gave', 'John', 'the', 'newspaper']
n = 3
print([sent[i:i+n] for i in range(len(sent) - n + 1)])
# 确保循环变量范围的正确相当棘手的。因为这是 NLP中的常见操作,
#NLTK提供了支持函数bigrams(text)、trigrams(text)和一个更通用的ngrams(text, n)
print(list(nltk.ngrams(sent, n)))


#建立一个 m行n列的 数组,其中每个元素是一个集合,我们可以使用一个嵌套的链表推导:
m, n = 3, 7
array = [[set() for i in range(n)] for j in range(m)]
array[2][5].add('Alice')
print(array)
#请注意,由于我们前面所讨论的有关对象复制的原因,使用乘法做这项工作是不正确的。
array = [[set()] * n] * m
array[2][5].add(7)
print(array)

4.4 函数:结构化编程的基础

#函数提供了程序代码打包和重用的有效途径
#假设我们发现我们经常要从 HTML 文件读取文本。这包括以下几个步骤:打开文件,将它读入,规范化空白符号,剥离 HTML 标记。
#我们可以将这些步骤收集到一个函数中,并给它一个名 字,如 get_text()
import re
def get_text(file):
    """Read text from a file, normalizing whitespace and stripping HTML markup."""
    text = open(file).read()
    text = re.sub('\s+', '', text)
    text = re.sub(r'<.*?>', '', text)
    return text
#数定义包含一个字符串。函数定义内的第一个字符串被称为 docstri ng。
#它不仅为阅读代码的人记录函数的功能,也使从文件加载这段代码的程序员更容易访问:
help(get_text)

函数的输入和输出

#一个 Python 函数并不是一定需要有一个 return 语句。
#有些函数做它们的工作的同时会附带输出结果、修改文件或者更新参数的内容。这种函数在其他一些编程语言中被称为“过 程”)。
def repeat(msg, num):
    return ' '.join([msg]*num)
monty = "Monty Python"
print(repeat(monty, num=3))

参数传递

#赋值操作,而一个结构化对象的值是该对象的引用
#set_up()有两个参数,都在函数内部被修改。我们一开始将一个空字符串分配给 w,将一个空链表分配给 p。
#调用该函数后,w 没有变,而 p 改变了:
def set_up(word, properties):
    word = "lolcat"
    properties.append('noun')
    properties = 5
   
w = ""
p = []
print(set_up(w, p))
print(w)
print(p)
#注意,w 没有被函数改变。当我们调用 set_up(w, p)时,w(空字符串)的值被分配到一个新的变量 word。
# 在函数内部 word 值被修改。然而,这种变化并没有传播给 w

#当我们调用 set_up(w, p),p的值(一个空列表的引用)被分配到一个新的本地变量 properties,
#所以现在这两个变量引用相同的内存位 置。函数修改 properties,而这种变化也反映在 p 值上,正如我们所看到的。
#函数也分配 给 properties 一个新的值(数字 5);这并不能修改该内存位置上的内容,而是创建了一个 新的局部变量

变量的作用域

#Python不会强迫我们声明变量的类型,这允许我们定义参数类型灵活 的函数
def tag(word):
    #assert isinstance(word,basestring)
    "argument to tag() must be a string"
    if word in ['a', 'the', 'all']:
        return 'det'
    else:
        return 'noun'
print(tag('the'))
print(tag(1))

功能分解

#结构良好的程序通常都广泛使用函数。当一个程序代码块增长到超过 10-20 行,
#如果 将代码分成一个或多个函数,每一个有明确的目的,这将对可读性有很大的帮助。这类似于好文章被划分成段,每段话表示一个主要思想。

文档说明函数

4.5 更多关于函数

作为参数的函数

#Python 也允许我们传递一个函数作为另一个函数的参数。f现在,我们可以抽象出操作, 对相同数据进行不同操作

#正如下面的例子表示的,我们可以传递内置函数 len()或用户定 义的函数 last_letter()作为另一个函数的参数:
sent = ['Take', 'care', 'of', 'the', 'sense', ',', 'and', 'the', 'sounds', 'will', 'take', 'care', 'of', 'themselves', '.']
def extract_property(prop):
    return [prop(word) for word in sent]
def last_letter(word):
    return word[-1]

print(extract_property(len))
print(extract_property(last_letter))

#Python提供了更多的方式来定义函数作为其他函数的参数,即所谓的lambda表达式。
print(extract_property(lambda w: w[-1]))

#sorted()函数
print(sorted(sent))
print(sorted(sent, reverse=True))

累计函数

#这些函数以初始化一些存储开始,迭代和处理输入的数据,最后返回一些最终的对象
#做到这一点的一个标准的方式是初始化一个空链表,累计材料,然后返回这个链表

#函数 search1()
#累计输出到一个链表
def search1(substring, words):
    result = []
    for word in words:
        if substring in word:
            result.append(word)
    return result

print("search1:")
for item in search1('zz', nltk.corpus.brown.words()):
    print(item)
    break

#函数 search2()
#第一次调用此函数,它运行到 yield 语句然后停下来。 调用程序获得第一个词,没有任何必要的处理。
#一旦调用程序对另一个词做好准备,函数会 从停下来的地方继续执行,直到再次遇到 yield 语句。
#这种方法通常更有效,因为函数只产 生调用程序需要的数据,并不需要分配额外的内存来存储输出
def search2(substring, words):
    for word in words:
        if substring in word:
            yield word
                        
print("search2:")
for item in search2('zz', nltk.corpus.brown.words()):
    print(item)            
    break
    
    
#产生一个词链表的所有排列
#为了强制 permutations()函数产生所有它的输出,我们将它包装在 list()调用中
#递归
def permutations(seq):
    if len(seq) <= 1:
        yield seq
    else:
        for perm in permutations(seq[1:]):
            for i in range(len(perm) + 1):
                yield (perm[:i] + seq[0:1] + perm[i:])
print(list(permutations(['police', 'fish', 'buffalo'])))

高阶函数

#定义一个函数 is_content_word()开始,它检查一个词是否来自一个开放的实词类。
#使用此函数作为 offilter()的第一个参数,它对作为它的第二个参数的序列中的每 个项目运用该函数,只保留该函数返回 True 的项目
def is_content_word(word):
    return word.lower() not in ['a', 'of', 'the', 'and', 'will', ',', ',']
sent = ['Take', 'care', 'of', 'the', 'sense', ',', 'and', 'the', 'sounds', 'will', 'take', 'of', 'themeselves', '.']

print(list(filter(is_content_word, sent)))
print(' '.join(filter(is_content_word, sent)))
print([w for w in sent if is_content_word(w)])


#另一个高阶函数是 map(),将一个函数运用到一个序列中的每一项。
#找出布朗语 料库新闻部分中的句子的平均长度,后面跟着的是使用链表推导计算的等效版本:
#In Python 3, map returns an iterator not a list:
lengths = list(map(len, nltk.corpus.brown.sents(categories='news')))
print(sum(lengths) / len(lengths))

4.9 小结


Python 赋值和参数传递使用对象引用, 例如:如果 a 是一个链表,我们分配 b = a, 然后任何 a 上的操作都将修改 b,反之亦然。

is 操作测试是否两个对象是相同的内部对象,而==测试是否两个对象是相等的。两者 的区别和标识符与类型的区别相似。


#字符串、链表和元组是不同类型的序列对象,支持常见的操作如:索引、切片、l en()、 sorted()和使用 in 的成员测试。

我们可以通过以写方式打开文件来写入文本到一个文件:ofile = open('output.txt', 'w'),然后加入内容到文件:ofile.write("Monty Python"),最后关闭文件ofile.close()。

声明式的编程风格通常会产生更简洁更可读的代码;手动递增循环变量通常是不必要的。枚举一个序列,使用 enumerate()。

函数是一个重要的编程抽象,需要理解的关键概念有:参数传递、变量的范围和 docstrings。

函数作为一个命名空间:函数内部定义的名称在该函数外不可见,除非这些名称被宣布为是全局的

模块允许将材料与本地的文件逻辑的关联起来。一个模块作为一个命名空间:在一个模 块中定义的名称——如变量和函数——在其他模块中不可见,除非这些名称被导入。

动态规划是一种在 NLP 中广泛使用的算法设计技术,它存储以前的计算结果,以避免不必要的重复计算。