Python拾珍:用这些功能写出更简洁、更可读或更高效的代码

本章我会带领大家回顾那些遗漏的地方。Python提供了不少并不是完全必需的功能(不用它们也能写出好代码),但有时候,使用这些功能可以写出更简洁、更可读或者更高效的代码,甚至有时候三者兼得。

19.1 条件表达式

我们在5.4节中见过条件语句。条件语句通常用来从两个值中选择一个。例如:

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

这条语句检查x是否为正数。如果为正数,则计算math.log;如果为负数,math.log会抛出ValueError异常。为了避免程序停止,我们直接生成一个“NaN”,一个特殊的浮点数,代表“不是数”(Not A Number)。

我们可以用条件表达式来更简洁地写出这条语句:

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

这条语句几乎可以用英语直接读出来:“y gets log-x if x is greater than 0; otherwise it gets NaN”(Y的值在x大于0时是math.log(x),否则是NaN)。

递归函数有时候可以用条件表达式重写。例如,下面是factorial的一个递归版本:

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)

条件表达式的另一个用途是处理可选参数。例如,下面是GoodKangaroo的init方法(参见练习17-2):

def __init__(self, name, contents=None):
  self.name = name
  if contents == None:
    contents = []
  self.pouch_contents = contents

我们可以将其重写为:

def __init__(self, name, contents=None):
   self.name = name
   self.pouch_contents = [] if contents == None else contents

一般来说,如果条件语句的两个条件分支都只包含简单的返回或对同一变量进行赋值的表达式,那么这个语句可以转化为条件表达式。

19.2 列表理解

在10.7节中我们已经见过映射和过滤模式。例如,下面的函数接收一个字符串列表,将每个元素通过字符串方法capitalize进行映射,并返回一个新的字符串列表。:

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

我们可以用列表理解(list comprehension)把这个函数写得更紧凑:

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

上面的方括号操作符说明我们要构建一个新列表。方括号之内的表达式指定了列表的元素,而for子句则表示我们要遍历的序列。

列表理解的语法有一点粗糙的地方,因为里面的循环变量,即本例中的s,在表达式中出现在定义之前。

列表理解也可以用于过滤操作。例如,下面的函数选择列表t中的大写元素,并返回一个新列表:

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()]

对于简单表达式来说,列表理解更紧凑、更易于阅读,并且它们通常都比实现相同功能的循环更快,有时候甚至快很多。因此,如果你因为我没有早些提到它而恼怒,我表示十分理解。

但是我得辩解一下,列表理解更难以调试,因为你没法在循环内添加打印语句。我建议你只在计算简单到一次就能弄对的时候才使用它。对于初学者来说,这意味着从来不用。

19.3 生成器表达式

生成器表达式(generator expression)和列表理解类似,但是它使用圆括号,而不是方括号:

>>> g = (x**2 for x in range(5))
>>> g
 at 0x7f4c45a786c0>

结果是一个生成器对象,它知道该如何遍历值的序列。但它又和列表理解不同,它不会一次把结果都计算出来,而是等待请求。内置函数next会从生成器中获取下一个值:

>>> next(g)
0
>>> next(g)
1

当到达序列的结尾后,next会抛出一个StopIteration异常。可以使用for循环来遍历所有值:

>>> for val in g:
...   print(val)
4
9
16

生成器对象会跟踪记录访问序列的位置,所以for循环会从上一个next所在的位置继续。一旦生成器遍历结束,再访问它就会抛出StopException

>>> next(g)
StopIteration

生成器表达式经常和summaxmin之类的函数配合使用:

>>> sum(x**2 for x in range(5))
30

19.4 any和all

Python提供了一个内置函数any,它接收一个由布尔值组成的序列,并在其中任何值是True时返回True。它可以用于列表:

>>> any([False, False, True])
True

但它更常用于生成器表达式:

>>> any(letter == 't' for letter in 'monty')
True

上面这个例子用处不大,因为它做的事情和in表达式一样。但是我们可以用any来重写9.3节中的搜索函数。例如,我们可以将avoids函数重写为:

def avoids(word, forbidden):
   return not any(letter in forbidden for letter in word)

这个函数读起来几乎和英语一致:“word avoids forbidden if there are not any forbidden letters in word”(我们说一个word避免被禁止,是指word中没有任何被禁的字母)。

Python还提供了另一个内置函数all,它在序列中所有元素都是True时返回True。作为练习,请使用all重写9.3节中的uses_all函数。

19.5 集合

我曾在13.6节中使用字典来寻找在文档中出现但不属于一个单词列表的单词。我写的函数接收一个字典参数d1,其中包含文档中所有的单词作为键;以及另一个参数d2,包含单词列表。它返回一个字典,包含d1中所有不在d2之中的键:

def substract(d1, d2):
   res = dict()
   for key in d1:
     if key not in d2:
       res[key] = None
   return res

在这些字典中,值都是None,因为我们从来不用它们。因此,我们实际上浪费了一些存储空间。

Python还提供了另一个内置类型,称为集合(set),它表现得和没有值而只使用键集合的字典类似。向一个集合添加元素很快,检查集合成员也很快。集合还提供方法和操作符来进行常见的集合操作。

例如,集合减法可以使用方法difference或者操作符‘-’来实现。因此我们可以将substract函数重写为:

def substract(d1, d2):
   return set(d1) – set(d2)

结果是一个集合而不是字典,但是对于遍历之类的操作,表现是一样的。

本书中的一些练习可以用集合来更加简洁且高效地实现。例如,练习10-7中的has_duplicates函数,下面是使用字典来实现的一个解答:

def has_duplicates(t):
   d = {}
   for x in t:
     if x in d:
       return True
     d[x] = True
   return False

一个元素第一次出现的时候,把它加入到字典中。如果相同的元素再次出现时,函数就返回True

使用集合,我们可以这样写同一个函数:

def has_duplicates(t):
   return len(set(t)) < len(t)

一个元素在一个集合中只能出现一次,所以如果t中间的某个元素出现超过一次,那么变成集合后其长度会比t小。如果没有任何重复元素,那么集合的长度应当和t相同。

我们也可以使用集合来解决第9章中的一些练习。例如,下面是uses_only函数使用循环来实现的版本:

def uses_only(word, available):
   for letter in word:
     if letter not in available:
       return False
   return True

uses_only检查word中所有的字符是不是在available中出现。我们可以这样重写:

def uses_only(word, available):
   return set(word) <= set(available)

操作符<=检查一个集合是否是另一个集合的子集,包括两个集合相等的情况。这正好符合word中所有字符都出现在available中。

19.6 计数器

计数器(counter)和集合类似,不同之处在于,如果一个元素出现超过一次,计数器会记录它出现了多少次。如果你熟悉多重集(multiset)这个数学概念,就会发现计数器是多重集的一个自然的表达方式。

计数器定义在标准模块collections中,所以需要导入它再使用。可以用字符串、列表或者其他任何支持迭代访问的类型对象来初始化计数器:

>>> from collections import Counter
>>> count = Counter('parrot')
>>> count
Counter({'r':2, 't': 1, 'o': 1, 'p': 1, 'a': 1})

计数器有很多地方和字典相似。它们将每个键映射到其出现次数。和字典一样,键必须是可散列的。

但和字典不同的是,在访问计数器中不存在的元素时,它并不会抛出异常。相反,它会返回0

>>> count['d']
0

我们可以使用计数器来重写练习10-6中的is_anagram函数:

def is_anagram(word1, word2):
   return Counter(word1) == Counter(word2)

如果两个单词互为回文,则它们会包含相同的字母,且各个字母的计数相同,所以它们对应的计数器对象也会相等。

计数器提供方法和操作符来进行类似集合的操作,包括集合加法、减法、并集和交集。计算器还提供一个非常常用的方法most_common,它返回一个值-频率对的列表,按照最常见到最少见来排序:

>>> count = Counter('parrot')
>>> for val, freq in count, most_common(3):
...   print(val, freq)
r 2
p 1
a 1

19.7 defaultdict

collections模块还提供了defaultdict ,它和字典相似,不同的是,如果你访问一个不存在的键,它会自动创建一个新值。

创建一个defaultdict对象时,需要提供一个用于创建新值的函数。用来创建对象的函数有时被称为工厂(factory)函数。用于创建列表、集合以及其他类型对象的内置函数,都可以用作工厂函数:

>>> from collections import defaultdict
>>> d = defaultdict(list)

请注意,参数是list(一个类对象),而不是list()(一个新的列表)。你提供的函数直到访问不存在的键时,才会被调用的:

>>> t = d['new key']
>>> t
[]

新列表t也会加到字典中。所以,如果我们修改t,改动也会在d中体现:

>>> t.append('new value')
>>> d
defaultdict(, {'new key': ['new value']})

如果创建一个由列表组成的字典,使用defaultdict往往能够帮你写出更简洁的代码。在练习12-2的解答中,我创建了一个字典,将排序的字母字符串映射到可以由那些字母拼写出来的单词列表。例如,'opst'映射到列表['opts', 'post', 'pots', 'spot', 'stop', 'tops']。可以从http://thinkpython2.com/code/anagram_sets.py下载该解答。

下面是原始的代码:

def all_anagrams(filename):
   d = {}
   for line in open(filename):
     word = line.strip().lower()
     t = signature(word)
     if t not in d:
        d[t] = [word]
     else:
        d[t].append(word)
   return d

这个函数可以用setdefault简化,你可能在练习11-2中也用过:

def all_anagrams(filename):
   d = {}
   for line in open(filename):
     word = line.strip().lower()
     t = signature(word)
     d.setdefault(t, []).append(word)
   return d

但这个解决方案有一个缺点,它不管是否需要,每次都会新建一个列表。对于列表来说,这并不算大问题,但如果工厂函数非常复杂,就有可能成为问题了。

我们可以使用defaultdict来避免这个问题,并进一步简化代码:

def all_anagrams(filename):
   d = defaultdict(list)
   for line in open(filename):
     word = line.strip().lower()
     t = signature(word)
     d[t]. append(word)
   return d

在练习18-3的解答中,函数has_straightflush中使用了setdefault。可以从http:// thinkpython2.com/code/PokerHandSoln.py下载它。但这个解决方案的缺点是,不管是否必需,每次循环迭代都会创建一个新的Hand对象。作为练习,请使用defaultdict重写该函数。

19.8 命名元组

很多简单的对象其实都可以看作是几个相关值的集合。例如,第15章中定义的Point对象,包含两个数字,即xy。定义一个这样的类时,通常会从init方法和str方法开始:

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)

这里用了很多代码来传达很少的信息。Python提供了一个更简洁的方式来表达同一个意思:

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

第一个参数是你想要创建的类名。第二个参数是Point对象应当包含的属性的列表,以字符串表示。namedtuple的返回值是一个类对象:

>>> Point

这里Point类会自动提供__init____str__这样的方法,所以你不需要写它们。

要创建一个Point对象,可以把Point类当作函数来用:

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

init方法使用你提供的名字把实参值赋给属性。str方法会打印出Point对象及其属性的字符串表示。

可以使用名称来访问命名元组的元素:

>>> p.x, p.y
(1, 2)

也可以直接把它当作元组来处理:

>>> p[0], p[1]
(1, 2)

>>> x, y = p
>>> x, y
(1, 2)

命名元组提供了快速定义简单类的方法,但其缺点是简单的类并不会总保持简单。可能之后你需要给命名元组添加方法。如果那样,可以定义一个新类,继承当前的命名元组:

class Pointier(Point):
  # 在这里添加更多的方法

或者也可以直接切换成传统的类定义。

19.9 收集关键词参数

在12.4节中,我们见过如何编写函数将其参数收集成一个元组:

def printall(*args):
   print(args)

可以使用任意个数的按位实参(也就是说,不带名称的实参)来调用这个函数:

>>> printall(1, 2.0, '3')
(1, 2.0, '3')

但是*号操作符并不会收集关键词实参:

>>> printall(1, 2.0, third='3')
TypeError: printall() got an unexpected keyword argument 'third'

要收集关键词实参,可以使用**操作符:

def printall(*args, **kwargs):
  print(args, kwargs)

这里收集关键词形参可以任意命名,但kwargs是一个常见的选择。收集的结果是一个将关键词映射到值的字典:

>>> printall(1, 2.0, third='3')
(1, 2.0){'third': '3'}

如果有一个关键词到值的字典,就可以使用分散操作符**来调用函数:

>>> d = dict(x=1, y=2)
>>> Point(**d)
Point(x=1, y=2)

没有用分散操作符的话,函数会把d当作一个单独的按位实参,所以它会把d赋值给x,并因为没有提供y的赋值而报错:

>>> d = dict(x=1, y=2)
>>> Point(d)
Traceback (most recent call last):
  File "", line 1, in 
TypeError: __new__() missing 1 required positional argument: 'y'

当处理参数很多的函数时,创建和传递字典来指定常用的选项是非常有用的。

19.10 术语表

条件表达式(conditional expression):一个根据条件返回一个或两个值的表达式。

列表理解(list comprehension):一个以方框包含一个for循环,生成新列表的表达式。

生成器表达式(generator expression):一个以括号包含一个for循环,返回一个生成器对象的表达式。

多重集(multiset):一个用来表达从一个集合的元素到它们出现次数的映射的数学概念。

工厂函数(factory):一个用来创建对象,并常常当作参数使用的函数。

19.11 练习

练习19-1

下面的函数可以递归地计算二项式系数:

def binomial_coeff(n, k):
  """计算(n, k)的二项式系数.

  n: 试验次数
  k: 成功次数

  返回:int
  """
  if k == 0:
    return 1
  if n == 0:
    return 0

  res = binomial_coeff(n-1, k) + binomial_coeff(n-1, k-1)
  return res

使用内嵌条件表达式来重写该函数。

注意:这个函数效率不高,因为它会不停地重复计算相同的值。可以通过使用备忘(memoizing,参见11.6节)来提高它的效率。但你可能会发现,使用条件表达式之后,添加备忘会变得比较困难。

本文摘自《像计算机科学家一样思考Python(第2版)》

Python拾珍:用这些功能写出更简洁、更可读或更高效的代码

第2版增加了如下几个新特性。

全书共21章,详细介绍Python语言编程的方方面面。本书从最基本的编程概念开始讲起,包括语言的语法和语义,而且每个编程概念都有清晰的定义,引领读者循序渐进地学习变量、表达式、语句、函数和数据结构。书中还探讨了如何处理文件和数据库,如何理解对象、方法和面向对象编程,如何使用调试技巧来修正语法错误、运行时错误和语义错误。每一章都配有术语表和练习题,方便读者巩固所学的知识和技巧。此外,每一章都抽出一节来讲解如何调试程序。作者针对每章所专注的语言特性,或者相关的开发问题,总结了调试的方方面面。

展开阅读全文

页面更新:2024-03-30

标签:递归   遍历   高效   生成器   重写   表达式   语句   字典   函数   简洁   可读   元素   对象   条件   参数   操作   代码   功能   方法   列表

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top