王子子的成长之路

王子子


  • 首页

  • 历程

  • 学习

  • 随笔

  • 关于
王子子的成长之路

不忘初心,也不在乎什么始终

发表于 2017-06-26 | 分类于 随便

一直有不要把兴趣和事业混为一谈的说法,我仔细看过,都很有道理。

想象过一些人痛心疾首的望着我说,“你在这有天分,你不做是在浪费你的才华!”这时我就呵呵一笑,“我的才华还有很多,不差浪费这一点”。

好在我的兴趣不需要什么卓越的智商和天分,就是门手艺而已。而手艺是需要磨练的,没时间磨练还搞什么手艺。

好比你爱上一个姑娘,要是两情相悦最好。可要是单恋一枝花,你也没什么办法。

离开舒适区,回到起点,做喜欢的事情,要是摔到头破血流–也没什么办法不是。

王子子的成长之路

机器学习实战读书笔记 -- 朴素贝叶斯

发表于 2017-06-18 | 分类于 学习

最近太忙,实在没时间充电了,翻出一篇一年前的学习笔记先顶一下,虽然当今机器学习框架已经封装的非常好了,但是了解何时使用和算法里发生了什么也是很重要的。

简介

用python的文本处理能力将文档切分成词向量,对文档进行分类和过滤,最终将结果转换成人可以理解的信息。
优点:在数据较少的情况下仍然有效,可以处理多类别问题。
缺点:对于输入数据的准备方式较为敏感。
使用数据类型:标称型数据。

流程

朴素贝叶斯的一般流程:
1.收集数据:可以使用任何方法。
2.准备数据:需要数值型或者布尔型数据。
3.分析数据:有大量特征时,绘制特征作用不大,此时使用直方图效果更好。
4.训练算法:计算不同的独立特征的条件概率。
5.测试算法:计算错误率。
6.使用算法:一个常见的朴素贝叶斯应用是文档分类。可以在任意的分类场景中使用朴素贝叶斯分类器。

使用情况分类

决策树和朴素贝叶斯的使用情况分类:
决策树适合标准的离散化数据,并且对数值型数据的处理难以处理。
朴素贝叶斯的要求相对较低。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#-*- encoding:utf-8 -*-
from numpy import *
def loadDataSet():
postingList=[['my','dog','has','flea','problems','help','please'],
['maybe','not','take','him','to','dog','park','stupid',],
['my','dalmation','is','so','cute','I','love','him'],
['stop','posting','stupid','worthless','garbage'],
['mr','licks','ate','my','steak','how','to','stop','him'],
['quit','buying','worthless','dog','food','stupid']]
#1 代表侮辱性文字,0代表正常言论
classVec=[0,1,0,1,0,1]
return postingList,classVec
def createVocabList(dataSet):
# 取唯一值
vocabSet=set([])
for document in dataSet:
vocabSet=vocabSet|set(document)
return list(vocabSet)
def setOfWords2Vec(vocabList,inputSet):
# 参数1唯一值 当前列表参数2原值,出现在当前词组出现在总词库中的索引
returnVec=[0]*len(vocabList)
for word in inputSet:
if word in vocabList:
returnVec[vocabList.index(word)]=1
else:
print "the word %s is not in my Vocablary!"%word
return returnVec
def trainNB0(trainMatrix,trainCategory):
#只针对二分类问题
numTrainDocs=len(trainMatrix) # 统计有几个训练用词组文档
numWords=len(trainMatrix[0]) # 词库数量
pAbusive=sum(trainCategory)/float(numTrainDocs) # 侮辱性词组数量和词组数量的比值
p0Num=ones(numWords);p1Num=ones(numWords) #p0Num,p1Num等于总词库数量个1
p0Denom=2.0;p1Denom=2.0
for i in range(numTrainDocs): # 以i遍历每个词组
if trainCategory[i]==1: # 如果是侮辱性词组
p1Num+=trainMatrix[i] # 每个词在总词库副本中的计数+1
p1Denom+=sum(trainMatrix[i]) #侮辱性词组中单词的总数
else:
p0Num+=trainMatrix[i] # 每个词在总词库中的计数+1
p0Denom+=sum(trainMatrix[i]) # 非侮辱性词组中单词的总数
p1Vect=log(p1Num/p1Denom) # 侮辱性词在词库中的占比,避免下溢出,采取自然底数
p0Vect=log(p0Num/p0Denom) # 非侮辱词在词库中的占比
return p0Vect,p1Vect,pAbusive
def classifyNB(vec2Classify,p0Vec,p1Vec,pClass1):
p1=sum(vec2Classify*p1Vec)+log(pClass1) # 出现词组的位置与总字库内词组向量相乘,加自然底数中侮辱性词库在词库中的占比
p0=sum(vec2Classify*p0Vec)+log(1.0-pClass1)
# 权重越大,说明可能性越高
if p1>p0:
return 1
else:
return 0
def testingNB():
listOPosts,listClasses=loadDataSet() # 原文档和词组是否为侮辱性词汇的定义
myVocabList=createVocabList(listOPosts) # 总词库,每个词都是唯一的
trainMat=[]
for postinDoc in listOPosts: # 形成一个二维数组,内有每个在总词库,文档单词在库中的索引
trainMat.append(setOfWords2Vec(myVocabList,postinDoc))
p0V,p1V,pAb=trainNB0(array(trainMat),array(listClasses)) # 返回每个词组的侮辱性语言概率,非侮辱性语言概率,侮辱性语言再词库中所占比
testEntry=['love','my','dalmation']#测试词汇
thisDoc=array(setOfWords2Vec(myVocabList,testEntry)) # 添加测试词组,返回测试词组个总词库索引
print testEntry,'classified as:',classifyNB(thisDoc, p0V, p1V, pAb)
testEntry=['stupid','garbage']
thisDoc=array(setOfWords2Vec(myVocabList,testEntry))
print testEntry,'classified as:',classifyNB(thisDoc,p0V,p1V,pAb)
if __name__=='__main__':
testingNB()

总结

朴素贝叶斯算法是充分的利用概率原理,用概率的方法分类一个对象的所属类别,对多个类型的对象都可以使用。
算法首先收集了所有训练的词组(数字),通过对总体(比如说文章)的的判断训练,让机器清楚每个的词组出现在总体中分类的概率,用对数的方式防止向下溢出,最终整合概率进行分类。

王子子的成长之路

改善 Python 程序的 91 个建议读书笔记 3

发表于 2017-06-08 | 分类于 学习

建议 41:使用argparse处理命令行参数

处理命令行参数可以使用argsparse,也推荐更方便更高级的docopt进行处理。
docopt是根据常见的帮助信息定义了一套领域特定语言(DSL),并通过这个DSL Parser参数生成处理命令行参数的代码。

建议 42:使用pandas处理大型CSV文件

pandas作为python三大科学运算库之一的使用。

建议 43:一般情况下使用ElementTree解析xml格式文件

使用Beautifulsoup更好

建议 44:理解模块pickle优劣

序列化,简单来说就是把内存中的数据结构在不丢失其身份和类型信息的情况下转成对象的文本或二进制表示的过程。同类支持序列化的模块有pickle,json,marshal和shelve。

pickle是最通用的序列化模块,我们应该优先使用c语言实现的cPickle,速度比pickle快1000倍,区别是cPickle不能被继承。

pickle主要通过dump和load两种方法序列化与反序列化(存储与读取)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
import cPickle as pickle
# 序列化
my_data= {"name":"Python","type":"Language"}
fp = open("picklefile.dat","wb")
pickle.dump(my_data, fp)
fp.close
# 反序列化
fp = open("picklefile.dat", "rb")
out = pickle.load(fp)
```
pickle模块的优点:
1. 接口简单,容易使用。
2. 存储格式有平台通用型,在Linux和Windouws都可以使用,兼容性好。
3. 支持数据类型广泛,除了常规项,还包含能通过类的\__dict__或\__getstate__()方法返回的对象。
4. pickle是可扩展的,对于不可序列化的对象,也可以通过特殊方法来返回示例在被pickle时的状态。
5. 能够自动维护对象间的引用
pickle模块的限制:
* pickle不能保证操作的原子性。当错误发生时,可能部分数据已经被保存;如果对象处于深递归状态,那么可能超过python的最大递归深度,可以通过sys.setrecursionlimit()进行扩展。
* pickle存在安全性问题,为乳清提供了可能。
* pickle协议是python特定的,不同语言之间数据内容可能难以保障。
简单来说,对于需要存储的对象,使用pickle,另外很重要的一点,**dat文件用pickle模块来读**。
### 建议 45:序列化的另一个不错的选择 -- JSON
cJson比python自身的json要快250倍
JSON的优势:
1. 使用简单,支持多种数据类型(集合、列表、字典、关联数组等等)
2. 存储格式可读性更友好,易于修改
3. 支持跨平台跨语言操作,所占空间更小
4. 具有较强扩展性
json的速度比pickle略慢。
**json不支持序列化dateime**
### 建议 46:使用 traceback 获取栈信息
当发生异常,开发人员往往需要看到现场信息,trackback 模块可以满足这个需求,先列几个常用的:
```python
traceback.print_exc() # 打印错误类型、值和具体的trace信息
traceback.print_exception(type, value, traceback[, limit[, file]]) # 前三个参数的值可以从sys.exc_info()
raceback.print_exc([limit[, file]]) # 同上,不需要传入那么多参数
traceback.format_exc([limit]) # 同 print_exc(),返回的是字符串
traceback.extract_stack([file, [, limit]]) # 从当前栈中提取 trace 信息
```
traceback 模块获取异常相关的数据是通过sys.exc_info()得到的,该函数返回异常类型type、异常value、调用和堆栈信息traceback组成的元组。
同时 inspect 模块也提供了获取 traceback 对象的接口。
### 建议 47:使用 logging 记录日志信息
仅仅将信息输出到控制台是远远不够的,更为常见的是使用日志保存程序运行过程中的相关信息,如运行时间、描述信息以及错误或者异常发生时候的特定上下文信息。Python 提供 logging 模块提供了日志功能。
常规日志设置:
```python
logging.basicConfig(
filename='%s.log' % self.table_name,
level=logging.DEBUG,
format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
datefmt='%a, %d %b %Y %H:%M:%S')
```
logging是线程安全的,不支持多进程写入同一个子文件,对多个进程需要配置不同的日志文件。
### 建议 48:使用 threading 模块编写多线程程序
(python3中,使用threadpool线程池模块比较省心)
由于 GIL 的存在,让 Python 多线程编程在多核处理器中无法发挥优势,但在一些使用场景下使用多线程仍然比较好,如等待外部资源返回,或建立反应灵活的用户界面,或多用户程序等。
Python3 提供了两个模块:_thread和threading。_thread提供了底层的多线程支持,使用比较复杂,下面我们重点说说threading。
Python 多线程支持用两种方式来创建线程:一种通过继承 Thread 类,重写它的run()方法;另一种是创建一个 threading.Thread 对象,在它的初始化函数__init__()中将可调用对象作为参数传入。
threading模块中不仅有 Lock 指令锁,RLock 可重入指令锁,还支持条件变量 Condition、信号量 Semaphore、BoundedSemaphore 以及 Event 事件等。
下面有一个比较经典的例子来理解多线程:
```python
import threading
from time import ctime,sleep
def music(func):
for i in range(2):
print("I was listening to %s. %s" % (func,ctime()))
sleep(1) # 程序休眠 1 秒
def move(func):
for i in range(2):
print("I was at the %s! %s" % (func,ctime()))
sleep(5)
threads = []
t1 = threading.Thread(target=music,args=('爱情买卖',))
threads.append(t1)
t2 = threading.Thread(target=move,args=('阿凡达',))
threads.append(t2)
if __name__ == '__main__':
for t in threads:
t.setDaemon(True) # 声明线程为守护线程
t.start()
#3
print("all over %s" % ctime())
```
以下是运行结果:
```python
I was listening to 爱情买卖. Tue Apr 4 17:57:02 2017
I was at the 阿凡达! Tue Apr 4 17:57:02 2017
all over Tue Apr 4 17:57:02 2017
```
分析:threading 模块支持线程守护,我们可以通过setDaemon()来设置线程的daemon属性,当其属性为True时,表明主线程的退出可以不用等待子线程完成,反之,daemon属性为False时所有的非守护线程结束后主线程才会结束,那运行结果为:
```python
I was listening to 爱情买卖. Tue Apr 4 18:05:26 2017
I was at the 阿凡达! Tue Apr 4 18:05:26 2017
all over Tue Apr 4 18:05:26 2017
I was listening to 爱情买卖. Tue Apr 4 18:05:27 2017
I was at the 阿凡达! Tue Apr 4 18:05:31 2017
```
继续修改代码,当我们在#3处加入t.join(),此方法能够阻塞当前上下文环境,直到调用该方法的线程终止或到达指定的 timeout,此时在运行程序:
```python
I was listening to 爱情买卖. Tue Apr 4 18:08:15 2017
I was at the 阿凡达! Tue Apr 4 18:08:15 2017
I was listening to 爱情买卖. Tue Apr 4 18:08:16 2017
I was at the 阿凡达! Tue Apr 4 18:08:20 2017
all over Tue Apr 4 18:08:25 2017
```
当我们把music函数的休眠时间改为 4 秒,再次运行程序:
```python
I was listening to 爱情买卖. Tue Apr 4 18:11:16 2017
I was at the 阿凡达! Tue Apr 4 18:11:16 2017
I was listening to 爱情买卖. Tue Apr 4 18:11:20 2017
I was at the 阿凡达! Tue Apr 4 18:11:21 2017
all over Tue Apr 4 18:11:26 2017
```
此时我们就可以发现多线程的威力了,music虽然增加了 3 秒,然而总的运行时间仍然为 10 秒。
### 建议 49:使用 Queue 使多线程编程更加安全
(同47,使用threadingpool)
线程间的同步和互斥,线程间数据的共享等这些都是涉及线程安全要考虑的问题。纵然 Python 中提供了众多的同步和互斥机制,如 mutex、condition、event 等,但同步和互斥本身就不是一个容易的话题,稍有不慎就会陷入死锁状态或者威胁线程安全。
如何保证线程安全呢?我们先来看看 Python 中的 Queue 模块:
* Queue.Queue(maxsize):先进先出,maxsize 为队列大小,其值为非正数的时候为无限循环队列
* Queue.LifoQueue(maxsize):后进先出,相当于栈
* Queue.PriorityQueue(maxsize):优先级队列
以上队列所支持的方法:
* Queue.qsize():返回近似的队列大小。当该值 > 0 的时候并不保证并发执行的时候 get() 方法不被阻塞,同样,对于 put() 方法有效。
* Queue.empty():队列为空的时候返回 True,否则返回 False
* Queue.full():当设定了队列大小的情况下,如果队列满则返回 True,否则返回 False
* Queue.put(item[, block[, timeout]]):往队列中添加元素 item,block 设置为 False 的时候,如果队列满则抛出 Full 异常。如果 block 设置为 True,timeout 为 None 的时候则会一直等待直到有空位置,否则会根据 timeout 的设定超时后抛出 Full 异常
* Queue.put_nowait(item):等于 put(item, False).block 设置为 False 的时候,如果队列空则抛出 Empty 异常。如果 block 设置为 True、timeout 为 None 的时候则会一直等到有元素可用,否则会根据 timeout 的设定超时后抛出 Empty 异常
* Queue.get([block[, timeout]]):从队列中删除元素并返回该元素的值
* Queue.get_nowait():等价于 get(False)
* Queue.task_done():发送信号表明入列任务已经完成,经常在消费者线程中用到
* Queue.join():阻塞直至队列中所有的元素处理完毕
首先 Queue 中的队列和 collections.deque 所表示的队列并不一样,前者用于不同线程之间的通信,内部实现了线程的锁机制,后者是数据结构上的概念,支持 in 方法。
Queue 模块实现了多个生产者多个消费者的队列,当多线程之间需要信息安全的交换的时候特别有用,因此这个模块实现了所需要的锁原语,为 Python 多线程编程提供了有力的支持,它是线程安全的。
先来看一个简单的例子:
```python
import os
import Queue
import threading
import urllib2
class DownloadThread(threading.Thead):
def __init__(self, queue):
threading.Thread.__init__(self)
self.queue = queue
def run(self):
while True:
url = self.queue.get()
print('{0} begin download {1}...'.format(self.name, url))
self.download_file(url)
self.queque.task_done()
print('{0} download completed!!!'.format(self.name))
def download_file(self, url):
urlhandler = urllib2.urlopen(url)
fname = os.path.basename(url) + '.html'
with open(fname, 'wb') as f:
while True:
chunk = urlhandler.read(1024)
if not chunk: break
f.write(chunk)
if __name__ == '__main__':
urls = ['http://wiki.python.org/moin/WebProgramming',
'https://www.createspace.com/3611970',
'http://wiki.python.org/moin/Documentation'
]
queue = Queue.Queue()
for i range(5):
t = DownloadThread(queue)
t.setDaemon(True)
t.start()
for url in urls:
queue.put(url)
queue.join()
```
## 第 5 章 设计模式
### 建议 50:利用模块实现单例模式
单例模式可以保证徐彤中一个类只有一个实例且该实例易被外界访问,常用来使用XxxManager之类的功能。
满足单例模式的 3 个需求:
* 只能有一个实例
* 必须自行创建这个实例
* 必须自行向整个系统提供这个实例
模块采用的其实是天然的单例的实现方式,在入口文件导入:
* 所有的变量都会绑定到模块
* 模块只初始化一次
* import 机制是线程安全的,保证了在并发状态下模块也只是一个实例
```python
# World.py
import Sun
def run():
while True:
Sun.rise()
Sun.set()
# main.py
import World
World.run()
```
此外,Borg模式可以创造任意数量实例,并保证状态共享。
### 建议 51:用 mixin 模式让程序更加灵活
模板方法模式就是在一个方法中定义一个算法的骨架,并将一些实现步骤延迟到子类中。模板方法可以使子类在不改变算法结构的情况下,重新定义算法中的某些步骤。
```python
class UseSimpleTeapot(object):
def get_teapot(self):
return SimpleTeapot()
class UseKungfuTeapot(object):
def get_teapot(self):
return KungfuTeapot()
class OfficePeople(People, UseSimpleTeapot): pass
class HomePeople(People, UseSimpleTeapot): pass
class Boss(People, UseKungfuTeapot): pass
def simple_tea_people():
people = People()
people.__base__ += (UseSimpleTeapot,)
return people
def coffee_people():
people = People()
people.__base__ += (UseCoffeepot,)
def tea_and_coffee_people():
people = People()
people.__base__ += (UseSimpleTeapot, UserCoffeepot,)
return people
def boss():
people = People()
people.__base__ += (KungfuTeapot, UseCoffeepot, )
return people
```
代码的原理在于每个类都有一个__bases__属性,它是一个元组,用来存放所有的基类,作为动态语言,Python 中的基类可以在运行中可以动态改变。所以当我们向其中增加新的基类时,这个类就拥有了新的方法,这就是混入mixin。
利用这个技术我们可以在不修改代码的情况下就可以完成需求:
```python
import mixins # 把员工需求定义在 Mixin 中放在 mixins 模块
def staff():
people = People()
bases = []
for i in config.checked():
bases.append(getattr(maxins, i))
people.__base__ += tuple(bases)
return people
```
### 建议 52:用发布订阅模式实现松耦合
发布订阅模式是一种编程模式,消息的发送者不会发送其消息给特定的接收者,而是将发布的消息分为不同的类别直接发布,并不关注订阅者是谁。而订阅者可以对一个或多个类别感兴趣,且只接收感兴趣的消息,并且不关注是哪个发布者发布的消息。要实现这个模式,就需要一个中间代理人。 Broker,它维护着发布者和订阅者的关系,订阅者把感兴趣的主题告诉它,而发布者的信息也通过它路由到各个订阅者处。
```python
from collections import defaultdict
route_table = defaultdict(list)
def sub(topic, callback):
if callback in route_table[topic]:
return
route_table[topic].append(callback)
def pub(topic, *args, **kw):
for func in route_table[topic]:
func(*args, **kw)
```
将以上代码放在 Broker.py 的模块,省去了各种参数检测、优先处理、取消订阅的需求,只向我们展示发布订阅模式的基础实现:
```python
import Broker
def greeting(name):
print('Hello, {}'.format(name))
Broker.sub('greet', greeting)
Broker.pub('greet', 'LaiYonghao')

因为python-message的消息订阅默认是全局性的,所以有可能产生名字冲突。

建议 53:用状态模式美化代码

所谓状态模式,就是当一个对象的内在状态改变时允许改变其行为,但这个对象看起来像是改变了其类。

简单的状态模式有其缺点:

  • 查询对象的当前状态很麻烦
  • 状态切换时需要对原状态做一些清扫工作,而对新状态做初始化工作,因每个状态需要做的事情不同,全部写在切换状态的代码中必然重复

这时候我们可以使用 Python-state 来解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from state import curr, switch, stateful, State, behavior
@stateful
class People(object):
class Workday(State):
default = True
@behavior # 相当于staticmethod
def day(self): # 这里的self并不是Python的关键字,而是有助于我们理解状态类的宿主是People的实例
print('work hard')
class Weekend(State):
@behavior
def day(self):
print('play harder')
people = People()
while True:
for i in range(1, 8):
if i == 6:
switch(people, People.Weekend)
if i == 1:
switch(people, People.Workday)
people.day()

@statefule装饰器重载了被修饰的类的__getattr__()从而使得 People 的实例能够调用当前状态类的方法,同时被修饰的类的实例是带有状态的,能够使用curr()查询当前状态,也可以使用switch()进行状态切换,默认的状态是通过类定义的 default 属性标识,default = True的类成为默认状态。

状态类 Workday 和 Weekend 继承自 State 类,从其派生的子类可以使用__begin__和__end___状态转换协议,自定义进入和离开当前状态时对宿主的初始化和清理工作。

下面是一个真实业务的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
@stateful
class User(object):
class NeedSignin(State):
default = True
@behavior
def signin(self, user, pwd):
...
switch(self, Player.Signin)
class Signin(State):
@behavior
def move(self, dst): ...
@behavior
def atk(self, other): ...

第 6 章 内部机制

建议 54:理解 built-in objects

Python 中一切皆对象,在新式类中,object 是所有内建类型的基类,用户自定义的类可以继承自 object 也可继承自内建类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [1]: class TestNewClass:
...: __metaclass__ = type
...:
In [2]: type(TestNewClass)
Out[2]: type
In [3]: TestNewClass.__bases__
Out[3]: (object,)
In [4]: a = TestNewClass()
In [5]: type(a)
Out[5]: __main__.TestNewClass
In [6]: a.__class__
Out[6]: __main__.TestNewClass

新式类支持 property 和描述符特性,作为新式类的祖先,Object 类还定义了一些特殊方法:new()、init()、delattr()、getattribute()、setattr()、hash()、repr()、str()等。

建议 55:init()不是构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A(object):
def __new__(cls, *args, **kw):
print(cls)
print(args)
print(kw)
print('----------')
instance = object.__new__(cls, *args, **kw)
print(instance)
def __init__(self, a, b):
print('init gets called')
print('self is {}'.format(self))
self.a, self.b = a, b
a1 = A(1, 2)
print(a1.a)
print(a1.b)

运行结果:

1
2
3
4
5
6
7
8
9
10
<class '__main__.A'>
(1, 2)
{}
----------
Traceback (most recent call last):
File "test.py", line 19, in <module>
a1 = A(1, 2)
File "test.py", line 13, in __new__
instance = object.__new__(cls, *args, **kw)
TypeError: object() takes no parameters

从结果中我们可以看出,程序输出了__new__()调用所产生的输出,并抛出了异常。于是我们知道,原来__new__()才是真正创建实例,是类的构造方法,而__init__()是在类的对象创建好之后进行变量的初始化。上面程序抛出异常是因为在__new__()中没有显式返回对象,a1此时为None,当去访问实例属性时就抛出了异常。

根据官方文档,我们可以总结以下几点:

  • object.new(cls[, args…]):其中 cls 代表类,args 为参数列表,为静态方法
  • object.init(self[, args…]):其中 self 代表实例对象,args 为参数列表,为实例方法
  • 控制实例创建的时候可使用 new() ,而控制实例初始化的时候使用 init()
  • new()需要返回类的对象,当返回类的对象时将会自动调用__init__()进行初始化,没有对象返回,则__init__()不会被调用。init() 方法不需要显示返回,默认为 None,否则会在运行时抛出 TypeError
  • 但当子类继承自不可变类型,如 str、int、unicode 或者 tuple 的时候,往往需要覆盖__new__()
  • 覆盖 new() 和 init() 的时候这两个方法的参数必须保持一致,如果不一致将导致异常
    下面我们来总结需要覆盖__new__()的几种特殊情况:
  • 当类继承不可变类型且默认的 new() 方法不能满足需求的时候
  • 用来实现工厂模式或者单例模式或者进行元类编程,使用__new__()来控制对象创建
  • 作为用来初始化的 init() 方法在多继承的情况下,子类的 init()方法如果不显式调用父类的 init() 方法,则父类的 init() 方法不会被调用;通过super(子类, self).init()显式调用父类的初始化方法;对于多继承的情况,我们可以通过迭代子类的 bases 属性中的内容来逐一调用父类的初始化方法

分别来看例子加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 创建一个集合能够将任何以空格隔开的字符串变为集合中的元素
class UserSet(frozenset):
def __new__(cls, *args):
if args and isinstance(args[0], str):
args = (args[0].split(), ) + args[1:]
return super(UserSet, cls).__new__(cls, *args)
# 一个工厂类根据传入的参量决定创建出哪一种产品类的实例
class Shape(object):
def __init__(object):
pass
def draw(self):
pass
class Triangle(Shape):
def __init__(self):
print("I am a triangle")
def draw(self):
print("I am drawing triangle")
class Rectangle(Shape):
def __init__(self):
print("I am a rectangle")
def draw(self):
print("I am drawing triangle")
class Trapezoid(Shape):
def __init__(self):
print("I am a trapezoid")
def draw(self):
print("I am drawing triangle")
class Diamond(Shape):
def __init__(self):
print("I am a diamond")
def draw(self):
print("I am drawing triangle")
class ShapeFactory(object):
shapes = {'triangle': Triangle, 'rectangle': Rectangle, 'trapzoid': Trapezoid, 'diamond': Diamond}
def __new__(cls, name):
if name in ShapeFactory.shapes.keys():
print('creating a new shape {}'.format(name))
return ShapeFactory.shapes[name]()
else:
print('creating a new shape {}'.format(name))
return Shape()

建议 56:理解名字查找机制

在 Python 中所谓的变量其实都是名字,这些名字指向一个或多个 Python 对象。这些名字都存在于一个表中(命名空间),我们称之为局部变量,调用locals()可以查看:

1
2
3
4
>>> locals()
{'__package__': None, '__spec__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__doc__': None, '__name__': '__main__', '__builtins__': <module 'builtins' (built-in)>}
>>> globals()
{'__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__builtins__': <module 'builtins' (built-in)>, '__package__': None, '__doc__': None, '__spec__': None, '__name__': '__main__'}

Python 中的作用域分为:

  • 局部作用域: 一般来说函数的每次调用都会创建一个新的本地作用域, 拥有新的命名空间
  • 全局作用域: 定义在 Python 模块文件中的变量名拥有全局作用域, 即在一个文件的顶层的变量名仅在这个文件内可见
  • 嵌套作用域: 多重函数嵌套时才会考虑, 即使使用 global 进行申明也不能达到目的, 其结果最终是在嵌套的函数所在的命名空间中创建了一个新的变量
  • 内置作用域: 通过标准库中的__builtin__实现的 当访问一个变量的时候,其查找顺序遵循变量解析机制 LEGB 法则,即依次搜索 4 个作用域:局部作用域、嵌套作用域、全局作用域以及内置作用域,并在第一个找到的地方停止搜寻,如果没有搜到,则会抛出异常。

Python 3 中引入了 nonlocal 关键字:

1
2
3
4
5
6
7
8
def foo(x):
a = x
def bar():
nonlocal a
b = a * 2
a = b + 1
print(a)
return bar

建议 57: 为什么需要 self 参数

在类中当定义实例方法的时候需要将第一个参数显式声明为self, 而调用时不需要传入该参数, 我们通过self.x访问实例变量, self.m()访问实例方法:

1
2
3
4
5
6
7
8
9
10
11
class SelfTest(object):
def __init__(self.name):
self.name = name
def showself(self):
print('self here is {}'.format(self))
def display(self):
self.showself()
print('The name is: {}'.format(self.name))
st = SelfTest('instance self')
st.display()
print('{}'.format(st))

运行结果:

1
2
3
self here is <__main__.SelfTest object at 0x7f440c53ba58>
The name is: instance self
<__main__.SelfTest object at 0x7f440c53ba58>

从中可以发现, self 表示实例对象本身, 即 SelfTest 类的对象在内存中的地址. self 是对对象 st 本身的引用, 我们在调用实例方法时也可以直接传入实例对象: SelfTest.display(st). 同时 self 或 cls 并不是 Python 的关键字, 可以替换成其它的名称。

Python 中为什么需要 self 呢:

  1. 借鉴了其他语言的特征
  2. Python 语言本身的动态性决定了使用 self 能够带来一定便利
  3. 在存在同名的局部变量以及实例变量的情况下使用 self 使得实例变量更容易被区分

Python 属于一级对象语言, 我们有好几种方法可以引用类方法:

1
2
A.__dict__["m"]
A.m.__func__

Python 的哲学是:显示优于隐式(Explicit is better than implicit)。

建议 58: 理解 MRO 与多继承

古典类与新式类所采取的 MRO (Method Resolution Order, 方法解析顺序) 的实现方式存在差异。

古典类是按照多继承申明的顺序形成继承树结构, 自顶向下采用深度优先的搜索顺序. 而新式类采用的是 C3 MRO 搜索方法, 在新式类通过__mro__得到 MRO 的搜索顺序, C3 MRO 的算法描述如下:

假定,C1C2…CN 表示类 C1 到 CN 的序列,其中序列头部元素(head)=C1,序列尾部(tail)定义 = C2…CN;

C 继承的基类自左向右分别表示为 B1,B2…BN

L[C] 表示 C 的线性继承关系,其中 L[object] = object。

算法具体过程如下:

L[C(B1…BN)] = C + merge(L[B1] … L[BN], B1 … BN)

其中 merge 方法的计算规则如下:在 L[B1]…L[BN],B1…BN 中,取 L[B1] 的 head,如果该元素不在 L[B2]…L[BN],B1…BN 的尾部序列中,则添加该元素到 C 的线性继承序列中,同时将该元素从所有列表中删除(该头元素也叫 good head),否则取 L[B2] 的 head。继续相同的判断,直到整个列表为空或者没有办法找到任何符合要求的头元素(此时,将引发一个异常)。

菱形继承是我们在多继承设计的时候需要尽量避免的一个问题。

建议 59: 理解描述符机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
In [1]: class MyClass(object):
...: class_attr = 1
...:
# 每一个类都有一个__dict__属性, 包含它的所有属性
In [2]: MyClass.__dict__
Out[2]:
mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
'class_attr': 1})
In [3]: my_instance = MyClass()
# 每一个实例也相应有一个实例属性, 我们通过实例访问一个属性时,
# 它首先会尝试在实例属性中查找, 找不到会到类属性中查找
In [4]: my_instance.__dict__
Out[4]: {}
# 实例访问类属性
In [5]: my_instance.class_attr
Out[5]: 1
# 如果通过实例增加一个属性,只能改变此实例的属性
In [6]: my_instance.inst_attr = 'china'
In [7]: my_instance.__dict__
Out[7]: {'inst_attr': 'china'}
# 对于类属性而言并没有丝毫变化
In [8]: MyClass.__dict__
Out[8]:
mappingproxy({'__dict__': <attribute '__dict__' of 'MyClass' objects>,
'__doc__': None,
'__module__': '__main__',
'__weakref__': <attribute '__weakref__' of 'MyClass' objects>,
'class_attr': 1})
# 我们可以动态地给类增加一个属性
In [9]: MyClass.class_attr2 = 100
In [10]: my_instance.class_attr2
Out[10]: 100
# 但Python的内置类型并不能随意地为它增加属性或方法

.操作符封装了对实例属性和类属性两种不同属性进行查找的细节。

但是如果是访问方法呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In [1]: class MyClass(object):
...: def my_method(self):
...: print('my_method')
...:
In [2]: MyClass.__dict__['my_method']
Out[2]: <function __main__.MyClass.my_method>
In [3]: MyClass.my_method
Out[3]: <function __main__.MyClass.my_method>
In [4]: type(MyClass.my_method)
Out[4]: function
In [5]: type(MyClass.__dict__['my_method'])
Out[5]: function

根据通过实例访问属性和根据类访问属性的不同,有以下两种情况:

  • 一种是通过实例访问,比如代码 obj.x,如果 x 是一个描述符,那么 getattribute() 会返回 type(obj).dict[‘x’].get(obj, type(obj)) 结果,即:type(obj) 获取 obj 的类型;type(obj).dict[‘x’] 返回的是一个描述符,这里有一个试探和判断的过程;最后调用这个描述符的 get() 方法。

  • 另一个是通过类访问的情况,比如代码 cls.x,则会被 getattribute()转换为 cls.dict[‘x’].get(None, cls)。

    描述符协议是一个 Duck Typing 的协议,而每一个函数都有 get 方法,也就是说其他每一个函数都是描述符。所有对属性, 方法进行修饰的方案往往都用到了描述符, 如classmethod, staticmethod, property等, 以下是property的参考实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Property(object):
    "Emulate PyProperty_Type() in Objects/descrobject.c"
    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
    self.fget = fget
    self.fset = fset
    self.fdel = fdel
    self.__doc__ = doc
    def __get__(self, obj, objtype=None):
    if obj is None:
    return self
    if self.fget is None:
    raise AttributeError, "unreadable attribute"
    return self.fget(obj)
    def __set__(self, obj, value):
    if self.fset is None:
    raise AttributeError, "can't set attribute"
    self.fset(obj, value)
    def __delete__(self, obj):
    if self.fdel is None:
    raise AttributeError, "can't delete attribute"
    self.fdel(obj)

建议 60:区别__getattr__()和__getattribute__()方法

以上两种方法可以对实例属性进行获取和拦截:

  • getattr(self, name):适用于属性在实例中以及对应的类的基类以及祖先类中都不存在;
  • getattribute(self, name):对于所有属性的访问都会调用该方法。

但访问不存在的实例属性时,会由内部方法__getattribute__()抛出一个 AttributeError 异常,也就是说只要涉及实例属性的访问就会调用该方法,它要么返回实际的值,要么抛出异常。详情请参考。

那么__getattr__()在什么时候调用呢:

  • 属性不在实例的__dict__中;
  • 属性不在其基类以及祖先类的__dict__中;
  • 触发AttributeError异常时(注意,不仅仅是__getattribute__()方法的AttributeError异常,property 中定义的get()方法抛出异常的时候也会调用该方法)。

当这两个方法同时被定义的时候,要么在__getattribute__()中显式调用,要么触发AttributeError异常,否则__getattr__()永远不会被调用。

我们知道 property 也能控制属性的访问,如果一个类中如果定义了 property、getattribute()以及__getattr__()来对属性进行访问控制,会最先搜索__getattribute__()方法,由于 property 对象并不存在于 dict 中,因此并不能返回该方法,此时会搜索 property 中的get()方法;当 property 中的set()方法对属性进行修改并再次访问 property 的get()方法会抛出异常,这时会触发__getattr__()的调用。

getattribute()总会被调用,而__getattr__()只有在__getattribute__()中引发异常的情况下调用。

王子子的成长之路

改善 Python 程序的 91 个建议读书笔记 2

发表于 2017-05-26 | 分类于 学习

建议 21: i+=1 不等于 ++i

++i 合法,但是无效

建议 22:使用 with 自动关闭资源

对于打开的资源我们记得关闭它,如文件、数据库连接等,Python 提供了一种简单优雅的解决方案:with。

with的实现得益于一个称为上下文管理器(context manager)的东西,它定义程序运行时需要建立的上下文,处理程序的进入和退出,实现了上下文管理协议,即对象中定义了__enter__()和__exit__(),任何实现了上下文协议的对象都可以称为一个上下文管理器:

  • enter():返回运行时上下文相关的对象
  • exit(exception_type, exception_value, traceback):退出运行时的上下文,处理异常、清理现场等

包含with语句的代码块执行过程如下:

1
2
3
4
5
6
7
8
>>> with open('test.txt', 'w') as f:
... f.write('test')
...
4
>>> f.__enter__
<built-in method __enter__ of _io.TextIOWrapper object at 0x7f1b967aaa68>
>>> f.__exit__
<built-in method __exit__ of _io.TextIOWrapper object at 0x7f1b967aaa68>
  1. 计算表达式的值,返回一个上下文管理器对象。
  2. 加载上下文管理器对象的__exit__()以备后用。
  3. 调用上下文管理器对象的__enter__()。
  4. 将__enter__()的返回值赋给目标对象。
  5. 执行代码块,正常结束调用__exit__(),其返回值直接忽略,如果发生异常,会调用__exit__()并将异常类型、值及 traceback 作为参数传递给__exit__(),exit()返回值为 false 异常将会重新抛出,返回值为 true 异常将被挂起,程序继续执行。

Python 还提供 contextlib 模块,通过 Generator 实现,其中的 contextmanager 作为装饰器来提供一种针对函数级别上的上下文管理器,可以直接作用于函数/对象而不必关心__enter__()和__exit__()的实现。

推荐文章

建议 23:使用 else 子句简化循环(异常处理)

python 的 else 子句在循环正常结束和循环条件不成立时被执行,由 break 语句中断时不执行,同样,我们可以利用这颗语法糖作用在 while 和 try…except 中。

建议 24:遵循异常处理的几点基本原则

异常处理的几点原则:
1. 注意异常的粒度,不推荐在 try 中放入过多的代码。 2. 谨慎使用单独的 except 语句处理所有异常,最好能定位具体的异常。
3. 注意异常捕获的顺序,在适合的层次处理异常,Python 是按内建异常类的继承结构处理异常的,所以推荐的做法是将继承结构中子类异常在前抛出,父类异常在后抛出。
4. 使用更为友好的异常信息,遵守异常参数的规范。

建议 25:避免 finally 中可能发生的陷阱

当 finally 执行完毕时,之前临时保存的异常将会再次被抛出,但如果 finally 语句中产生了新的异常或执行了 return 或 break 语句,那么临时保存的异常将会被丢失,从而异常被屏蔽。 在实际开发中不推荐 finally 中使用 return 语句进行返回。

建议 26:深入理解 None,正确判断对象是否为空

(None被判断为False,但是空集不等于None)
类型FalseTrue布尔False (与0等价)True (与1等价)字符串“”( 空字符串)非空字符串,例如 " “,”blog“数值0, 0.0非0的数值,例如:1, 0.1, -1, 2容器[], (), {}, set()至少有一个元素的容器对象,例如:[0], (None,), [’’]NoneNone非None对象。

>>> id(None)  
10743840  
>>> a = None  
>>> id(a)  
10743840  
>>> l = []  
>>> if l is not None:       # 判断逻辑 l 不为空  
...     print('l is {}'.format(l))  
... else:  
...     print('l is empty')  
...   
l is []  
>>> if l:   # #3 正确的判断形式  
...     print('Do something...')  
... else:  
...     print('Do other thing...')  
...   
Do other thing...  
  

执行中会调用__nonzero__()来判断自身对象是否为空并返回0/1或True/False,如果没有定义该方法,Python 将调用__len__()进行判断,返回 0 表示为空。如果一个类既没有定义__len__()又没有定义__nonzero__(),该类实例用 if 判断为True。

建议 27:连接字符串优先使用 join 而不是 +

连接字符串使用join将使程序性能更佳,原因是使用每次使用 + 都需要格外分一块内存去存储结果。

建议 28:格式化字符串时尽量使用 .format 而不是 %

format方法总结 使用 format 格式化字符串有以下好处:

  • format更为灵活,参数顺序和格式不必完全相同
  • format更为方便的作为参数传递(例如支持列表的索引操作)
  • %最终会被format取代
  • %容易抛出异常,而format则不会(未尝是好事)

建议 29:区别对待可变对象和不可变对象

Python 中一切皆对象,每个对象都有一个唯一的标识符(id)、类型(type)和值。数字、字符串、元组属于不可变对象,字典、列表、字节数组属于可变对象。

默认参数在初始化时仅仅被评估一次,以后直接使用第一次评估的结果,course 指向的是 list 的地址,每次操作的实际上是 list 所指向的具体列表,所以对于可变对象的更改会直接影响原对象。

最好的方法是传入None作为默认参数,在创建对象的时候动态生成列表。

>>> list1 = ['a', 'b', 'c']  
>>> list2 = list1  
>>> list1.append('d')  
>>> list2  
['a', 'b', 'c', 'd']  
>>> list3 = list1[:]  # 可变对象的切片操作相当于浅拷贝  
>>> list3.remove('a')  
>>> list3  
['b', 'c', 'd']  
>>> list1  
['a', 'b', 'c', 'd']  

建议 30:[]、() 和 {} 一致的容器初始化形式

使用列表解析、字典解析、元组解析等替代for循环
解析式有以下好处:

  • 代码更清晰、简洁
  • 效率更高、速度更快
  • (代码更加pythonic)

建议 31:记住函数传参既不是传值也不是传引用

正确的说法是传对象(call by object)或传对象的引用(call-by-object-reference),函数参数在传递过程中将整个对象传入,对可变对象的修改在函数外部以及内部都可见,对不可变对象的”修改“往往是通过生成一个新对象然是赋值实现的。

建议 32:警惕默认参数潜在的问题

其中就是默认参数如果是可变对象,在调用者和被调用者之间是共享的。
所以默认值使用可以使用数字、字符串、元组。
不可以使用字典、列表、字节数组。

import time  
# 对当前系统时间进行处理  
def report(when=time.time): # 而不是when=time.time()  
    pass  
  

建议 33:慎用变长参数

原因如下:
1. 使用过于灵活,导致函数签名不够清晰,存在多种调用方式
2. 使用*args和**kw简化函数定义就意味着函数可以有更好的实现方法

使用场景:
1. 为函数添加一个装饰器
2. 参数数目不确定
3. 实现函数的多态或子类需要调用父类的某些方法时

建议 34:深入理解 str() 和repr() 的区别

(str方法面向用户更为友好,repr解释更加清晰)
总结几点:

  1. str()面向用户,返回用户友好和可读性强的字符串类型;repr()面向 Python 解释器或开发人员,返回 Python 解释器内部的含义。
  2. 解释器中输入a默认调用repr(),而print(a)默认调用str()。
  3. repr()返回值一般可以用eval()还原对象:obj == eval(repr(obj))。
  4. 以上两个方法分别调用内建的__str__()和__repr__(),一般来说类中都应该定义__repr__(),但当可读性比准确性更为重要时应该考虑__str__(),用户实现__repr__()方法的时候最好保证其返回值可以用eval()是对象还原。

建议 35:分清 staticmethod 和 classmethod 的适用场景

(需要返回类的实例时,或需要动态生成对应类的类变量,使用classmethod,方法不跟实例与类相关(不适用self和cls),定义为静态方法(工具方法))

调用类方法装饰器的修饰器的方法,会隐式地传入该对象所对应的类,可以动态生成对应的类的类变量,同时如果我们期望根据不同的类型返回对应的类的实例,类方法才是正确的解决方案。

反观静态方法,当我们所定义的方法既不跟特定的实例相关也不跟特定的类相关,可以将其定义为静态方法,这样使我们的代码能够有效地组织起来,提高可维护性。

当然,也可以考虑定义一个模块,将一组的方法放入其中,通过模块来访问。

第 4 章 库

建议 36:掌握字符串的基本用法

# 小技巧:Python 遇到未闭合的小括号会自动将多行代码拼接为一行  
>>> s = ('SELECT * '  
...      'FROM table '  
...      'WHERE field="value"')  
>>> s  
'SELECT * FROM table WHERE field="value"'  
# Python2 中使用 basestring 正确判断一个变量是否是字符串  
# 性质判断  
isalnum() isalpha() isdigit() islower() isupper() isspace() istitle()  
# 查找替换  
startswith(prefix[, start[, end]]) endswith(suffix[, start[, end]]) # prefix参数可以接收 tuple 类型的实参  
count(sub[, start[, end]]) find(sub[, start[, end]]) index(sub[, start[, end]])  
rfind(sub[, start[, end]]) rindex(sub[, start[, end]]) replace(old, new[, count])   # count是指的替换次数,不指定就全部替换  
# 切分  
partition(sep) rpartition(sep) splitlines([keepends]) split([sep, [, maxsplit]]) rsplit([sep[, maxsplit]])  # partition 返回一个3个元素的元组对象  
# 变形  
lower() upper() capitalize() swapcase() title()  
# 删减填充  
strip([chars]) lstrip([chars]) rstrip([chars]) # 没有提供chars默认是空白符,由string.whitespace 常量定义  
center(width[, fillchar]) ljuct(width[, fillchar]) rjust(width[, fillchar])  
zfill(width) expandtabs([tabszie])  
  

下面来介绍一些易混淆的地方:

>>> '  hello world'.split()  
['hello', 'world']  
>>> '  hello world'.split(' ')  
['', '', 'hello', 'world']  
>>> 'hello wORld'.title()  
'Hello World'  
>>> import string  
>>> string.capwords(' hello world!')  
'Hello World!'  
>>> string.whitespace  
' \t\n\r\x0b\x0c'  
  

建议 37:按需选择 sort() 或者 sorted()

(sort方法是原地操作,sorted是复制操作,不需要保留源列表用sort)

# 函数原型  
sorted(iterable[, cmp[, key[, reverse]]])   # 返回一个排序后的列表  
s.sort([cmp[, key[, reverse]]])             # 直接修改原列表,返回为None  
>>> persons = [{'name': 'Jon', 'age': 32}, {'name': 'Alan', 'age': 50}, {'name': 'Bob', 'age': 23}]  
>>> sorted(persons, key=lambda x: (x['name'], -x['age']))  
[{'name': 'Alan', 'age': 50}, {'name': 'Bob', 'age': 23}, {'name': 'Jon', 'age': 32}]  
>>> a = (1, 2, 4, 2, 3)  
>>> sorted(a)  
[1, 2, 2, 3, 4]  
  

所以如果实际过程中需要保留原有列表,可以使用sorted()。sort()不需要复制原有列表,消耗内存较小,效率较高。同时传入参数key比传入参数cmp效率要高,cmp传入的函数在整个排序过程中会调用多次,而key针对每个元素仅作一次处理。

建议 38:使用 copy 模块深拷贝对象

(对可变对象需要真正意义上的复制时使用copy.deepcopy,这种需求情况还是比较少见)

  • 浅拷贝(shallow copy):构造一个新的复合对象并将从原对象中发现的引用插入该对象中。工厂函数、切片操作、copy 模块中的 copy 操作都是浅拷贝

  • 深拷贝(deep copy):针对引用所指向的对象继续执行拷贝,因此产生的对象不受其它引用对象操作的影响。深拷贝需要依赖 copy 模块的 deepcopy() 操作

在 python 中,标识一个对象唯一身份的是:对象的id(内存地址),对象类型,对象值,而浅拷贝就是创建一个具有相同类型,相同值但不同id的新对象。因此使用浅拷贝的典型使用场景是:对象自身发生改变的同时需要保持对象中的值完全相同,比如 list 排序:

def sorted_list(olist, key=None):  
    copied_list = copy.copy(olist)  
    copied_list.sort(key=key)  
    return copied_list  
a = [3, 2, 1]       # [3, 2, 1]  
b = sorted_list(a)  # [1, 2, 3]  
  

深拷贝不仅仅拷贝了原始对象自身,也对其包含的值进行拷贝,它会递归的查找对象中包含的其他对象的引用,来完成更深层次拷贝。因此,深拷贝产生的副本可以随意修改而不需要担心会引起原始值的改变:

>>> a = [1, 2]  
>>> b = [a, a]  
>>> b  
[[1, 2], [1, 2]]  
>>> from copy import deepcopy  
>>> c = deepcopy(b)  
>>> id(b[0]) == id(c[0])  
False  
>>> id(b[0]) == id(b[1])  
True  
>>> c  
[[1, 2], [1, 2]]  
>>> c[0].append(3)  
>>> c  
[[1, 2, 3], [1, 2, 3]]  
  

使用 copy 和 deepcopy 可以完成对一个对象拷贝的定制。

参考博文

建议 39: 使用 Counter 进行计数统计

(需要计数统计时,使用Counter)
常见的计数统计可以使用dict、defaultdict、set和list,不过 Python 提供了一个更优雅的方式:

>>> from collections import Counter  
>>> some_data = {'a', '2', 2, 3, 5, 'c', '7', 4, 5, 'd', 'b'}  
>>> Counter(some_data)  
Counter({'7',: 1, 2: 1, 3: 1, 4: 1, 5: 1, '2': 1, 'b': 1, 'a': 1, 'd': 1, 'c': 1})  
  

Counter 类属于字典类的子类,是一个容器对象,用来统计散列对象,支持+、-、&、|,其中&和|分别返回两个 Counter 对象各元素的最小值和最大值。

# 初始化  
Counter('success')  
Counter(s=3, c=2, e=1, u=1)  
Counter({'s': 3, 'c': 2, 'u': 1, 'e': 1})  
# 常用方法  
list(Counter(some_data).elements())     # 获取 key 值  
Counter(some_data).most_common(2)       # 前 N 个出现频率最高的元素以及对应的次数  
(Counter(some_data))['y']               # 访问不存在的元素返回 0  
c = Counter('success')  
c.update('successfully')                # 更新统计值  
c.subtract('successfully')              # 统计数相减,允许为0或为负  
  

建议 40:深入掌握 ConfigParser

(啥程序都需要配置,要搞懂配置库)
几乎所有的应用程序都会读取配置文件,ini是一种比较常见的文件格式:

[section1]  
option1=0  

Python 提供标准库 ConfigParser 来支持它:

import ConfigParser  
conf = ConfigParser.ConfigParser()  
conf.read('example.conf')  
print(conf.get('section1', 'in_default'))  
  

再来看个SQLAlchemy配置文件的例子:

[DEFAULT]  
conn_str = %(dbn)s://%(user)s:%(pw)s@%(host)s:%(port)s/%(db)s  
dbn = mysql  
user = root  
host = localhost  
port = 3306  
[db1]  
user = aaa  
pw = ppp  
db = example  
[db2]  
host = 192.168.0.110  
pw = www  
db = example  
  
import ConfigParser  
conf = ConfigParser.ConfigParser()  
conf.read('format.conf')  
print(conf.get('db1', 'conn_str'))  
print(conf.get('db2', 'conn_str'))  
王子子的成长之路

解决不了问题就崩了心态太不应该

发表于 2017-05-19 | 分类于 随笔

对于大多数事情来说,失败是常态,无法掌控事情的发展是常态,缺乏安全感的弊端就是恐惧面对这些。问题是越不去面对,挫折固然少了,无法掌控的事情却更多了。要学会坦然面对无法掌控的事情,磨练出更优秀的自己。

12…4
王子子

王子子

天生极客

18 日志
4 分类
GitHub 知乎
Friends
  • andrewwang
© 2017 王子子
由 Hexo 强力驱动
主题 - NexT.Pisces