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 1else: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 = nameif contents == None:contents = []self.pouch_contents = contents
我们可以将其重写为:
def __init__(self, name, contents=None):self.name = nameself.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
<generator object <genexpr> 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
生成器表达式经常和sum、max和min之类的函数配合使用:
>>> 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] = Nonereturn 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 Trued[x] = Truereturn 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 Falsereturn 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(<class 'list'>, {'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对象,包含两个数字,即x和y。定义一个这样的类时,通常会从init方法和str方法开始:
class Point:def __init__(self, x=0, y=0):self.x = xself.y = ydef __str__(self,):return '(%g, %g)' % (self.x, self.y)
这里用了很多代码来传达很少的信息。Python提供了一个更简洁的方式来表达同一个意思:
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
第一个参数是你想要创建的类名。第二个参数是Point对象应当包含的属性的列表,以字符串表示。namedtuple的返回值是一个类对象:
>>> Point
<class '__main__.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 "<stdin>", line 1, in <module>
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 1if n == 0:return 0res = binomial_coeff(n-1, k) + binomial_coeff(n-1, k-1)return res
使用内嵌条件表达式来重写该函数。
注意:这个函数效率不高,因为它会不停地重复计算相同的值。可以通过使用备忘(memoizing,参见11.6节)来提高它的效率。但你可能会发现,使用条件表达式之后,添加备忘会变得比较困难。
本文摘自《像计算机科学家一样思考Python(第2版)》
第2版增加了如下几个新特性。
- 全书内容和辅助代码都更新到Python 3。
- 增加了几节,以及更多关于Web的细节,以帮助初学者通过浏览器就能开始运行Python,而不需要过早地面对安装Python的问题。
- 对于第4章的“turtle模块”,我把实现从以前自己开发的Swampy乌龟绘图包,改为使用更标准的Python模块turtle,它更容易安装,功能也更强大。
- 增加了新的一章“Python拾珍”(第19章),介绍Python提供的一些并不必需,但有时会很方便的特性。
全书共21章,详细介绍Python语言编程的方方面面。本书从最基本的编程概念开始讲起,包括语言的语法和语义,而且每个编程概念都有清晰的定义,引领读者循序渐进地学习变量、表达式、语句、函数和数据结构。书中还探讨了如何处理文件和数据库,如何理解对象、方法和面向对象编程,如何使用调试技巧来修正语法错误、运行时错误和语义错误。每一章都配有术语表和练习题,方便读者巩固所学的知识和技巧。此外,每一章都抽出一节来讲解如何调试程序。作者针对每章所专注的语言特性,或者相关的开发问题,总结了调试的方方面面。
Python拾珍:用这些功能写出更简洁、更可读或更高效的代码相关推荐
- 用集合return多个值_Python拾珍:用这些功能写出更简洁、更可读或更高效的代码
本章我会带领大家回顾那些遗漏的地方.Python提供了不少并不是完全必需的功能(不用它们也能写出好代码),但有时候,使用这些功能可以写出更简洁.更可读或者更高效的代码,甚至有时候三者兼得. 19.1 ...
- 详细讲解用Python列表for循环还有if判断语句写出1-100之内的素数,思路清晰语法准确代码简单易懂实用
详细讲解用Python列表for循环还有if判断语句写出1-100之内的素数,思路清晰语法准确代码简单易懂实用,当然还有一定技巧的算法渗透其中.请看视频: Python 代码实例,找出1-100之内的 ...
- iOS 【如何写出最简洁优雅的网络封装 Moya + RxSwift】
前言 Why Moya ? Alamofire可能是iOS Swift中最常用的HTTP networking library,用Alamofire可以抽象出NSURLSession和其中很多繁琐的细 ...
- python发音1001python发音-怎样才能写出 Pythonic 的代码 #P1001#
L = [ i*i fori inrange(5) ] forindex, data inenumerate(L, 1):print(index, ':', data) 去除 import 语句和列表 ...
- python编写表白程序_python如何写出表白程序
用python编表白程序的方法: 1.创建GUI窗口,实现代码的调用. 2.编写点击触发函数,实现表白程序. 具体代码如下: from tkinter import * from tkinter im ...
- python爬取拼多多爆款数据_利用风变Python快速爬取海量数据,写出10万+爆文
我是从事自媒体内容创作者,每天都用心创作,保证文章质量,然后希望能出爆文,但似乎事情没有那么简单--自己认为可以的内容和标题但推送出去后,展示量和阅读量都低得可怜,心想,每次都这么用心,但结果却这样, ...
- python简短语法_写出优雅简洁的 python 语法(二)函数传参
Python中函数传递参数的形式包含多种,可根据情况而定使用哪种方式.基本所有语言都有简介明了的语法去替代繁琐的代码,优雅的写法不仅能提升代码美观,更能提高提高开发效率,让代码更加易读. 位置传递,默 ...
- python中能用分支结构写出循环的算法吗_Python分支结构及循环结构
Python中分支结构: 1.在Python中,要构造分支结构可以使用if.elif和else关键字. 所谓关键字就是有特殊含义的单词,像if和else就是专门用于构造分支结构的关键字,很显然你不能够 ...
- 风变Python 15数据的读入以及写出以及数据的编码类型
先把今天做的结果先放上来吧,其他的内容后期复习的时候再补充好了.
最新文章
- parted磁盘分区与GPT
- [每天进步一点 -- 流水账]第1周
- java面向对象高级分层实例_实体类
- 计算机操作系统——经典进程的同步问题
- 总奖池120万!中国“马栏山”杯国际音视频算法大赛启动
- [COURSE_PTHE] 4. 枚举
- c语言常用转向的语句,java中类似于C语言的goto无条件转向语句的语法是什么?...
- 正则邮箱_正则表达式再进级
- JavaScript基础流程控制(3)
- Atitit 规范标准化的艺术 -----java jsr的实现 目录 1. Atitit jsr规范有多少个 407个。Jsr规范大全	1 2. Atitit jsr规范化分类 attilax总
- Python纯代码pdf批量快速转换成ppt
- android蓝牙协议栈bluedroid分析,安卓系统蓝牙协议栈 bluedroid 的使能
- 如何找到属于自己高效学习方法?
- 腾讯笔试题是c语言吗,2019腾讯校园招聘C语言笔试题和面试题答案
- 程序员用python给了女友一个七夕惊喜!
- JS 实战: Drag 点击拖曳效果
- 全局vue中修改字体样式 苹方字体 微软雅黑 亲测可用
- 2022年磺化工艺考试练习题及模拟考试
- 常用类:Java比较器
- 中国高铁票价世界最低
热门文章
- chrome 如何设置Google英文搜索
- web前端期末大作业 绿色环境保护(4个页面) HTML5网站模板农业展示网站 html5网页制作代码 html5网页设计作业代码 html制作网页案例代码
- Markdown *.MD 文件 技术文档 在SDL Trados Studio中翻译
- Android应用性能优化: 应用卡顿、ANR
- 炫酷樱花花瓣飘落 HTML(附源码)
- Android7.1.1系统,Toast的Exception: android.view.WindowManager$BadTokenException解决
- 怎么虚化照片背景?分享几种照片背景虚化方法
- 如何打造一流的质量保证部门?
- 大工20秋《计算机原理》在线作业3,南开20秋学期(1709、1803、1809、1903、1909、2003、2009 )《计算机原理》在线作业【100分答案】...
- 在win 7 上,让thinkpad T60 支持UltraNav