今年带了一个 Python 班,收获不少,对自己新的教学方法进行了实践,同学们的学习热情很高,但是也发现一些问题。5个月的教学让我终于搞明白了,为什么有的人能学好 Python 而有的同学就学不好。

我想请问大家扪心自问一些之前有自学过 Python 么?如果学过那大家思考一下为什么没学好呢?
只是 Python 没学好,还是学什么都这样呢?甚至于游戏也没别人玩的好?

如果学什么都这样,那么就是学习方法的问题了。

常见的 Python 学习问题:

  • 用不上
  • 没学过,不知如何入手
  • 不好坚持

其实这些问题也都是常见的问题,我们学习任何东西都会碰到这个问题,那么我们一一做一些解答。

用不上

其实学习一门技能,你有了需求就肯定能用上,用不上其实是一个学习的伪命题,如果你没有需求,我其实是不建议你学习编程的 这时间去玩游戏去多痛快,都能上王者了。虽然编程能力越来越有趋势成为像开车(真开车)一样的基本技能了。还有一个方法就是把这件事情定义一个逼格比较高的目标,比如我是一个运维,学好 Python 我想去做运维开发,工资翻番,泡到女神等等,只要你想不到没有你做不到。举个我的例子:我之前是一名运维工程师,每周需要通宵一次上线,最后熬到自己心脏不舒服。但是我不是一个甘愿寂寞的人,我有了把整个上线流程自动化起来的想法,然后我就去学了 Python,当时也非常苦逼早上5点起来看视频看书做联系,1个月后,我就可以写简单脚本了,最后经过种种的重复练习,终于掌握了 Django 开发能力实现了可视化自动部署的需求,随着能力的增长,之后就跳槽了,工资double。其实整个学习过程有3个月之久,当时自己也是走了不少弯路,如果有牛人指点应该能更快的入门的。

没学过,不知如何入手

这是个经典的问题 毕竟对一个陌生的领域我们一开始的表现都是懵逼的。有时候对于眼前暂时的笨拙和困惑,我们要有更强的包容心,以及一种更淡定的态度。 现在研究表明,学习一门新课程确实是需要一个过程的,比如你们知道的一万小时理论。所以对于没学过编程的同学,来说可能是有一点困难的。

其实想解决问题是有方法的,答案一定在某一本书里。

那我们如何挑一本书呢?李笑来老师告诉过我们一个非常好的方法,如下是对其的引用:

母亲教我的最为实用的东西之一,就是如何选书。方法真的特别简单。知识类的图书(国外叫 Non Fiction),按以下几个标准就行,挑到烂书的概率可以很轻松被降低:

  • 版次:优选版次两次以上的书籍;
  • 作者:优选该领域里知名作者的书籍;
  • 出版社:优选知名出版社的书籍。
  • 书后没有参考文献的,或参考文献寥寥无几的,不买。

给大家分享一个我的例子,比如我想学创业、经济学、管理学、我都会去找相应顶尖专家的书籍和课程,现在课程也比较丰富有音频和视频,并且我们还有评分的网站(豆瓣),让我们挑选东西简直易如反掌。

所以想要学好,还是得找行业的牛人,看他们的作品。

关于坚持

其实真正让你坚持不下去的原因是你没有快速的得到有效的正向反馈,所以很难坚持下去。对于学习一门新知识,你要接受自己暂时的笨拙和困惑,制定一个良好的长期计划,别急于求成,你要知道学习这个事是一个长期的积累的过程,没有一定的积累是不可能有正向反馈的。所以要给一个自己坚持下去的理由。比如做这件事对于自己的成长是怎样的。

读书的过程就是享受,享受哪些作者帮我们解决问题,看清世界。只有抱着这种心态,可能你才有机会“坚持下去”。

到底有没有捷径?

肯定有!

我先问大家一个问题,我们都很爱玩游戏,那么游戏里面什么最重要?
对是的,这个问题想不明白,可能你游戏玩的都不如人家好。
答案是地图。
你只有对地图非常的熟悉才能更好把控,游戏里面的所有资源。
哎呀,像我们这种爱学习的人连游戏也都思考的是学习,哈哈。这个也就跟我们学习很像,你要找到学习的关键性路径。才能快速的把握好一门学科的所有知识,不要进入到一个细节里,这样你永远学不好。

基于二八定律找到关键必要的20% 知识点,如果自己还并未锻炼出这种能力,那么尽快找一个这个学科的老师快速入门吧,我们的时间真的很宝贵。

文件

数据持久化最简单的类型是普通文件,有时也叫平面文件(flat file)。它仅仅是在一个文件名下的字节流,把数据从一个文件读入内存,然后从内存写入文件。Python很容易实现这些文件操作,它模仿熟悉和流行的 unix 系统的操作。

读写一个文件之前需要打开它:

1
data = open(filename, mode)

下面是对该 open() 调用的简单解释:

  • data 是 open() 返回的文件对象;
  • filename 是该文件的字符串名;
  • mode 是指明文件类型和操作的字符串。

mode 的第一个字母表明对其的操作。

  • r 以只读方式打开文件。文件的指针将会放在文件的开头。这是默认模式。
  • r+ 可读可写,不会创建不存在的文件。如果直接写文件,则从顶部开始写,覆盖之前此位置的内容,如果先读后写,则会在文件最后追加内容。
  • w 表示写模式。若果文件不存在则新创建,如果该文件已存在则将其覆盖。
  • w+ 打开一个文件用于读写。如果该文件已存在则将其覆盖。如果该文件不存在,创建新文件。
  • x 表示在文件不存在的情况下新创建并写文件。
  • a 打开一个文件用于追加。如果该文件已存在,文件指针将会放在文件的结尾。也就是说,新的内容将会被写入到已有内容之后。如果该文件不存在,创建新文件进行写入。
  • a+ 打开一个文件用于读写。如果该文件已存在,文件指针将会放在文件的结尾。文件打开时会是追加模式。如果该文件不存在,创建新文件用于读写。
    mode 的第二个字母是文件类型:

  • t(或者省略) 代表文本类型。

  • b 代表二进制文件。

打开文件之后就可以调用函数来读写数据,最后需要关闭文件。

使用 write() 写文本文件

1
2
3
>>> poem = "日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。"
>>> len(poem)
32

将整首诗写到libai.txt中:

1
2
3
4
>>> fout = open('libai.txt', 'wt')
>>> fout.write(poem)
32
fout.close()

函数 write() 返回写入文件的字节数。和 print() 一样, 他没有增加空格或者换行符。同样,你也可以在一个文本文件中使用 print()

1
2
3
>>> fout = open('libai.txt', 'w')
>>> print(poem, file=fout)
>>> fout.close()

这就产生了一个问题:到底是用是 write() 还是 print()? print() 默认会在每个参数后面添加空格,在每行结束处添加换行。在之前的例子中, libai.txt 中默认添加了一个换行。为了使 print() 与 write() 有同样的输出,传入下面两个参数:

  • sep 分隔符:默认是一个空格 ‘ ‘
  • end 结束字符:默认是一个换行符 ‘\n’

除非自定义参数,否则 print() 会使用默认参数。在这里,我们通过空字符串替换 print() 添加的所有多余输出:

1
2
3
>>> fout = open('libai.txt', 'w')
>>> print(poem, file=fout, sep='', end='')
>>> fout.close()

如果字符串非常大,可以将数据分块,直到所有字符被写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> fout = open('libai.txt', 'w')
>>> size = len(poem)
>>> size
32
>>> offset = 0
>>> chunk = 10
>>> while True:
... if offset > size:
... break
... fout.write(poem[offset:offset+chunk])
... offset += chunk
...
>>> fout.close()

第一次写 10 个字符,4次写完,32个字符。

如果 libai.txt 文件已经存在,使用模式 x 可以避免重写文件:

1
2
3
4
5
6
>>> font = open('libai.txt', 'xt')
Traceback (most recent call last):
File "<ipython-input-108-c2dbf65c7612>", line 1, in <module>
font = open('libai.txt', 'xt')
FileExistsError: [Errno 17] File exists: 'libai.txt'
>>>

使用 read()、readline() 或者 readlines() 读取文本文件

你可以按照下面的示例那样,使用不带参数的 read() 函数一次读入文件的所有内容。但在读入文件时要格外注意,1GB的文件会用到相同大小的内存。

1
2
3
4
5
6
7
>>> fin = open('libai.txt', 'rt')
>>> poem = fin.read()
>>> poem
'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'
>>> fin.close()
>>> len(poem)
32

同样也可以设置最大的读入字符数限制 read() 函数一次返回的大小。下面一次读入10个字符,然后把每一快拼接成原来的字符串 poem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> poem = ''
>>> fin = open('libai.txt', 'rt')
>>> chunk = 10
>>> while True:
... fragment = fin.read(chunk)
... if not fragment:
... break
... poem += fragment
...
>>> fin.close()
>>> len(poem)
32
>>> poem
'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'

读到结尾之后,再次调用 read() 会返回空字符串(‘’), if not fragment 条件被判断为 False。此时会跳出 while True 的循环。 当然, 你也能使用 readline() 每次读入文件的一行。 在下面的例子中,通过追加每一行拼接成原来的字符串 poem:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> poem = ''
>>> fin = open('libai.txt', 'rt')
>>> while True:
... line = fin.readline()
... if not line:
... break
... poem += line
...
>>> fin.close()
>>> len(poem)
32
>>> poem
'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'

对于一个文本文件,即使空行也有1字符长度(换行符’\n’),自然会返回 True。当文件读取结束后,readline() (类似 read() ) 同样会返回空字符串,也被 while True 判断为 False。

读取文本文件最简单的方式是使用一个迭代器(iterator),它会每次返回一行。这和之前的例子类似,但代码会更短:

1
2
3
4
5
6
7
8
9
10
>>> poem = ''
>>> fin = open('libai.txt', 'rt')
>>> for line in fin:
... poem += line
...
>>> fin.close()
>>> len(poem)
32
>>> poem
'日照香炉生紫烟,遥看瀑布挂前川。飞流直下三千尺,疑是银河落九天。'

前面所有的示例最终都返回单个字符串 poem。 函数 readlines() 调用时每次读取一行,并返回单行字符串的列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>>> poem = '''日照香炉生紫烟,
... 遥看瀑布挂前川。
... 飞流直下三千尺,
... 疑是银河落九天。
... '''

>>> fin = open('libai.txt', 'wt')
>>> fin.write(poem)
36
>>> fin.close()

>>> fin = open('libai.txt', 'rt')
>>> lines = fin.readlines()
>>> fin.close()
>>> lines
['日照香炉生紫烟,\n', '遥看瀑布挂前川。\n', '飞流直下三千尺,\n', '疑是银河落九天。\n']
>>> print(len(lines), 'lines read')
4 lines read
>>> for line in lines:
... print(line, end='')
...
日照香炉生紫烟,
遥看瀑布挂前川。
飞流直下三千尺,
疑是银河落九天。

使用 write() 写二进制文件

如果文件模式字符串中包含 ‘b’, 那么文件会以二进制模式打开,这种情况下,读写的是字节而不是字符串。

我们手边没有二进制格式的诗,所以直接在 0~255 产生 256 字节的值:

1
2
3
>>> bdata = bytes(range(0, 256))
>>> len(bdata)
256

以二进制模式打开文件,并且一次写入所有的数据:

1
2
3
4
5
>>> fout = open('bfile', 'wb')
>>> fout.write(bdata)
256
>>> fout.close()
>>>

再次,write() 返回到写入的字节数。

对于文本,也可以分块写二进制数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> fout = open('bfile', 'wb')
>>> size = len(bdata)
>>> offset = 0
>>> chunk = 100
>>> while True:
... if offset > size:
... break
... fout.write(bdata[offset:offset+chunk])
... offset += chunk
...
100
100
56
>>> fout.close()

使用 read() 读取二进制文件

1
2
3
4
5
>>> fin = open('bfile', 'rb')
>>> bdata = fin.read()
>>> len(bdata)
256
>>> fin.close()

使用 with 自动关闭文件

如果你忘记关闭已经打开的一个文件,在该文件对象不再被引用之后 Python 会关掉此文件。这也意味着在一个函数中打开文件,并没有及时关闭它,但是在函数结束时会被关掉。然而你可能会在一直运行中的函数或者程序的主要部分打开一个文件,应该强制剩下的所有写操作完成后再关闭文件。

Python的上下文管理器(context manager)会清理一些资源,例如打开的文件。它的形式为 with expression as variable:

1
2
>>> with open('libai.txt', 'wt') as fout:
>>> ... fout.write(poem) ...

完成上下文管理器的代码后,文件会自动关闭。

使用 seek() 改变位置

无论是读或者写文件,Python都会跟踪文件中的位置。函数 tell() 返回距离文件开始处的字节偏移量。函数 seek() 允许跳转到文件其他字节偏移量的位置。这意味着可以不用从头读取文件的没一个字节,直接跳转到最后位置并制度一个字节也是可行的。

对于这个例子,使用之前写过的 256 字节的二进制文件 ‘bfile’:

1
2
3
>>> fin = open('bfile', 'rb')
>>> fin.tell()
0

使用 seek() 读取文件结束前最后一个字节:

1
2
>>> fin.seek(255)
255

一直读到文件结束:

1
2
3
4
5
>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255

seek() 同样返回当前的偏移量。

用第二个参数调用函数 seek(): seek(offset, origin)

  • 如果 origin 等于0 (默认为0),从头偏移 offset 个字节;
  • 如果 origin 等于1,从当前位置处偏移 offset 个字节;
  • 如果 origin 等于2,距离最后结尾处偏移 offset 个字节。

这些值也在标准库 os 模块中被定义:

1
2
3
4
5
6
7
>>> import os
>>> os.SEEK_SET
0
>>> os.SEEK_CUR
1
>>> os.SEEK_END
2

所以,我们可以用不通的方法读取最后一个字节:

1
>>> fin = open('bfile', 'rb')

文件结尾前的一个字节:

1
2
3
4
>>> fin.seek(-1, 2)
255
>>> fin.tell()
255

一直读到文件结尾:

1
2
3
4
5
>>> bddata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255

在调用 seek() 函数时不需要额外调用 tell()。前面的例子只是想说明两个函数都可以返回同样的偏移量。

下面是从文件的当前位置寻找:

1
>>> fin = open('bfile', 'rb')

接下来的例子返回最后两个字节:

1
2
3
4
>>> fin.seek(254, 0)
254
>>> fin.tell()
254

在此基础上前进一个字节:

1
2
3
4
>>> fin.seek(1, 1)
255
>>> fin.tell()
255

这些函数对于二进制文件都是极其重要的。当文件是 ASCII 编码(每个字符一个字节)时,也可以使用它们,但是计算偏移量会是一个麻烦事。其实,这些都取决于文件的编码格式,最流行的编码格式(例如 UTF-8)每个字符的字节数都不尽相同。

异常

Python使用被称为异常的特殊对象来管理程序执行期间发生的错误。每当发生让Python不知 所措的错误时,它都会创建一个异常对象。如果你编写了处理该异常的代码,程序将继续运行; 如果你未对异常进行处理,程序将停止,并显示一个traceback,其中包含有关异常的报告。
异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作,同时告 诉Python发生异常时怎么办。使用了try-except代码块时,即便出现异常,程序也将继续运行: 显示你编写的友好的错误消息,而不是令用户迷惑的traceback。

使用 try-except 代码块

当你认为可能发生了错误时,可编写一个try-except代码来处理可能引发的异常。你让 Python尝试运行一些代码,并告诉它如果这些代码引发了指定的异常,该怎么办。

try 语句有两种主要形式: try-except 和 try-finally . 这两个语句是互斥的, 也就是说你 只能使用其中的一种. 一个 try 语句可以对应一个或多个 except 子句, 但只能对应一个 finally 子句, 或是一个 try-except-finally 复合语句.

处理ZeroDivisionError异常的try-except代码 类似于下面这样:

1
2
3
4
5
6
>>> try:
... print(5/0)
... except ZeroDivisionError:
... print("You can't divide by zero!")
...
You can't divide by zero!

我们将导致错误的代码行print(5/0)放在了一个try代码中。如果try代码中的代码运行起来没有问题,Python将跳过except代码; 如果try代码中的代码导致了错误,Python将查找这样的except代码块,并运行其中的代码,即其中指定的错误与引发的错误相同。
在这个示例中,try代码中的代码引发了ZeroDivisionError异常,因此Python指出了该如何解决问题的except代码块,并运行其中的代码。这样,用户看到的是一条 好的错误消息,而不是traceback:

1
You can't divide by zero!

如果try-except代码后面还有其他代码,程序将接着运行,因为已经告诉了Python如何处理这种错误。

使用异常避免崩溃

发生错误时,如果程序还有工作没有完成,妥善地处理错误就尤其重要。这种情况经常会出现在要求用户提供输入的程序中;如果程序能够妥善地处理无效输入,就能再提示用户提供有效输入,而不至于崩溃。

下面来创建一个只执行除法运算的简单计算器:

1
2
3
4
5
6
7
8
9
10
11
12
13
 1 # coding=utf-8
2 print("Give me two numbers, and I'll divide them.")
3 print("Enter 'q' to quit.")
4
5 while True:
6 first_number = input("\nFirst number: ")
7 if first_number == 'q':
8 break
9 second_number = input("Second number: ")
10 if second_number == 'q':
11 break
12 answer = int(first_number) / int(second_number)
13 print(answer)

在第6行,这个程序提示用户输入一个数字,并将其存储到变量first_number中; 如果用户输入的不是表示退出的q,就再提示用户输入一个数字,并将其存储到变量second_number中(见第9行 )。 接下来,我们计算这两个数字的商 (即answer,见12行)。这个程序没有采取任何处理错误的措施,因此让它执行除数为0的除法运算时,它将崩溃:

1
2
3
4
5
6
7
8
9
10
Give me two numbers, and I'll divide them.
Enter 'q' to quit.

First number: 5
Second number: 0
Traceback (most recent call last):
File "<ipython-input-208-1e61f850d337>", line 11, in <module>
answer = int(first_number) / int(second_number)
ZeroDivisionError: division by zero
>>>

程序崩溃可不好,但让用户看到 traceback 也不是好主意。不懂技术的用户会被它们搞糊涂, 而且如果用户 怀有恶意,他会通过traceback获悉你不希望他知道的信息。例如,他将知道你的程序文件的名称,还将看到部分不能正确运行的代码。有时候,训练有素的攻击者可根据这些信息判断出可对你的代码发起什么样的攻击。

else 代码块

通过将可能引发错误的代码放在try-except代码中,可提高这个程序抵御错误的能力。错误是执行除法运算的代码行导致的,因此我们需要将它放到try-except代码块中。这个示例还包含一个else代码块; 依赖于try代码块成功执行的代码都应放到else代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 1 # coding=utf-8
2 print("Give me two numbers, and I'll divide them.")
3 print("Enter 'q' to quit.")
4
5 while True:
6 first_number = input("\nFirst number: ")
7 if first_number == 'q':
8 break
9 second_number = input("Second number: ")
10 if second_number == 'q':
11 break
12 try:
13 answer = int(first_number) / int(second_number)
14 except ZeroDivisionError:
15 print("You can't divide by 0!")
16 else:
17 print(answer)

我们让Python尝试执行try代码块中的除法运算(见12行),这个代码块只包含可能导致错误的代码。依赖于try代码块成功执行的代码都放在else代码中; 在这个示例中,如果除法运算成功,我们就使用else代码块来打印结果(见16行)。
except代码块告诉Python,出现ZeroDivisionError异常时该怎么办(见14行 )。如果try代码因除零错误而失败,我们就打印一条友好的消息,告诉用户如何避免这种错误。程序将继续运行,用户根本看不到traceback:

1
2
3
4
5
6
7
8
9
10
11
12
13
Give me two numbers, and I'll divide them.
Enter 'q' to quit.

First number: 5
Second number: 0
You can't divide by 0!

First number: 5
Second number: 2
2.5

First number: q
>>>

try-except-else代码块的工作原理大致如下: Python尝试执行try代码块中的代码; 只有可能引发异常的代码才需要放在try语句中。有时候,有一些仅在try代码块成功执行时才需要运行的的代码; 这些代码应放在else代码中。except代码块告诉Python,如果它尝试运行try代码块中的代码时引发了指定的异常,该怎么办。
通过预测可能发生错误的代码,可编写健壮的程序,它们即便面临无效数据或缺少资源,也能继续运行,从而能够抵御无意的用户错误和恶意的攻击。

失败时不提示

使用 pass

决定报告那些错误

在什么情况下该向用户报告错误? 在什么情况下又应该在失败时不提示呢? 如果用户知道要分析哪些文件,他们可能希望在有文件没有分析时出现一条消息,将其中的原因告诉他们。 如果用户只想看到结果,而并不知道要分析哪些文件,可能就无需在有些文件不存在时告知他们。 向用户显示他不想看到的信息可能会降低程序的可用性。Python的错误处理结构让你能够细致地控制与用户分享错误信息的程度,要分享多少信息由你决定。

编写得很好且经过详尽测试的代码不容易出现内部错误,如语法或逻辑错误,但只要程序依赖于外部因素,如用户输入、存在指定的文件、有网络连接,就有可能出现异常。凭借经验可判断该在程序的什么地方包含异常处理 ,以及出现错误时该向用户提供多少相关的信息。

finally子句

finally 子句是无论异常是否发生,是否捕捉都会执行的一段代码. 你可以将 finally 仅仅配合 try 一起使用,也可以和 try-except(else 也是可选的)一起使用. 你可以用 finally 子句 与 try-except 或 try-except-else 一起使用.

下面是 try-except-else-finally 语法的示例:

1
2
3
4
5
6
7
8
9
10
try: 
A
except MyException1:
B1
except MyException2:
B2
else:
C
finally:
D

当然,无论如何,你都可以有不止一个的 except 子句,但最少有一个 except 语句,而 else 和 finally 都是可选的.A,B,C 和 D 是程序(代码块).程序会按预期的顺序执行.(注意:可能的顺序是 A-C-D[正常]或 A-B-D[异常]).无论异常发生在 A,B,和/或 C 都将执行 finally 块.

常见的异常

异常 描述
AssertionError assert(断言)语句失败
AttributeError 试图访问一个对象没有的属性,比如foo.x ,但是foo没有x这个属性。
IOError 输入/输出异常,基本上是无法打开文件。
ImportError 无法引入模块或者包,基本上是路径问题
IndentationError 语法错误,代码没有正确对齐
IndexError 下标索引超出序列边界,比如当x只有三个元素,却试图访问x[5]
KeyError 试图访问字典里不存在的键
KerboardInterrupt Ctrl + C 被按下
NameError 使用一个还未被赋值予对象的变量
SyntaxError Python代码非法,代码不能解释
TypeError 传入对象类型与要求的不符
UnboundLocalError 试图访问一个还未被设置的局部变量,基本上是由于另一个同名的全局变量,导致你以为正在访问它
ValueError 传入一个调用者不期望的值,即使值的类型是正确的

docker 配置部分

拉取 alpine

1
$ alpine docker pull alpine

查看当前机 下的 docker 镜像

1
$ docker images

创建容器

1
$ docker create -ti --name python -h python -p 8080:8080 -w /root alpine sh

启动容器

1
$ docker start python

进入容器

1
$ docker attach python

停止容器

1
$ docker stop python

删除容器

1
$ docker rm python

alpine 配置

更改下alpine镜像的源

1
echo "http://mirrors.aliyun.com/alpine/v3.4/main/" > /etc/apk/repositories
1
apk update

安装常用工具

1
$ apk add bash vim musl-dev gcc g++ python3 python3-dev

启动bash

1
$ bash

更新pip

1
$ pip3 install -U pip

查看 pip 版本号

1
2
$ pip -V
pip 9.0.1 from /usr/lib/python3.5/site-packages (python 3.5)

显示了全局的系统库路径

Python 项目管理思路

1.全局的只安装开发工具类,全局的保持最小化
2.建立虚拟隔离环境,各个项目管理自己的第三方依赖库

例如:
项目 a 依赖 python2
项目 b 依赖 python3

所以需要简历独立运行环境

python3 自带 venv, 但是不够好用,推荐第三方工具 virtualenv

virtualenv 提供了核心功能
virtualenvwrapper 提供了丰富功能,包装器

安装虚拟环境

docker 环境下依赖包检查不完整, 所以先安装pbr

1
pip install -U pbr

1
pip install -U virtualenvwrapper

查看安装的库

1
pip list

配置bashrc

1
2
3
4
5
6
7
vim ~/.bashrc

export PS1="\[\e[32m\]\u:\w \$\[\e[m\] " # 命令行提示符样式设置
export WORKON_HOME=/root/py/venv # 虚拟环境目录
export PROJECT_HOME=/root/py # 项目目录
export VIRTUALENVWRAPPER_PYTHON=`which python3` # 指定虚拟环境使用 python 版本
source /usr/bin/virtualenvwrapper.sh # 执行 vwrapper 脚本

让 bashrc 生效

1
source .bashrc

在数学概念中,变量(Variable)表示没有固定值,可改变的数。但从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。

编程语言允许你定义变量(variable)。所谓变量就是在程序中为了方便地引用内存中的值而为它取的名称。在 Python 中,我们用 = 来给一个变量赋值。

我们先来声明一个变量

1
2
3
>>> a = 7
>>> print(a)
7

Python 中的变量有一个非常重要的性质:它仅仅是一个名字。赋值操作并不会实际 复制值,它只是为数据对象取个相关的名字。名字是对对象的引用而不是对象本身。你可以把名字想象成贴在盒子上的标签

1
2
3
4
变量                         内存
+-------------------+
a +------------> | 7 |
+-------------------+

在Python中使用变量时,需要遵守一些规则和指南。违反这些规则将引发错误,而指南旨在 让你编写的代码更容易阅读和理解。请务必牢记下述有关变量的规则。

  • 变量名只能包含字母、数字和下划线。变量名可以字母或下划线打头,但不能以数字打 头,例如,可将变量命名为message_1,但不能将其命名为1_message。
  • 变量名不能包含空格,但可使用下划线来分隔其中的单词。例如,变量名greeting_message 可行,但变量名greeting message会引发错误。
  • 不要将Python关键字和函数名用作变量名,即不要使用Python保留用于特殊用途的单词

Python 关键字

下面的关键字都有特殊含义,如果你将它们用作变量名,将引发错误:

Python 内置函数

将内置函数名用作变量名时,不会导致错误,但将覆盖这些函数的行为:

注意 在Python2.7中,print是关键字而不是函数。另外,Python3没有内置函数unicode()。这 两个单词都不应用作变量名。

  • 变量名应既简短又具有描述性。例如,name比n好,student_name比s_n好,name_length 比length_of_persons_name好。
  • 慎用小写字母l和大写字母O,因为它们可能被人错看成数字1和0。 要创建良好的变量名,需要经过一定的实践,在程序复杂而有趣时尤其如此。随着你编写的程序越来越多,并开始阅读别人编写的代码,将越来越善于创建有意义的变量名。

1.1.2 列表

列表由一系列按特定顺序排列的元素组成。你可以创建包含字母表中所有字母、数字0~9或 所有家庭成员姓名的列表;也可以将任何东西加入列表中,其中的元素之间可以没有任何关系。 鉴于列表通常包含多个元素,给列表指定一个表示复数的名称(如names)是个不错的主意。

列表非常适合利用顺序和位置定位某一元素,尤其是当元素的顺序或内容经常发生改变时。与字符串不同,列表是可变的。你可以直接对原始列表进行修改:添加新元素、删除 或覆盖已有元素。在列表中,具有相同值的元素允许出现多次。

常用列表方法

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
>>> dir(list)

['__add__',
'__class__',
'__contains__',
'__delattr__',
'__delitem__',
'__dir__',
'__doc__',
'__eq__',
'__format__',
'__ge__',
'__getattribute__',
'__getitem__',
'__gt__',
'__hash__',
'__iadd__',
'__imul__',
'__init__',
'__iter__',
'__le__',
'__len__',
'__lt__',
'__mul__',
'__ne__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__reversed__',
'__rmul__',
'__setattr__',
'__setitem__',
'__sizeof__',
'__str__',
'__subclasshook__',
'append',
'clear',
'copy',
'count',
'extend',
'index',
'insert',
'pop',
'remove',
'reverse',
'sort']

append这个是向列表末尾追加一个元素,如下:

1
2
3
4
>>> numbers = [1,2,2,3,3,3]
>>> numbers.append(4)
>>> numbers
[1, 2, 2, 3, 3, 3, 4]

clear 清除一个列表中的所有元素

1
2
3
4
5
6
7
>>> n
[1, 2, 3, 4, 5]

>>> n.clear()

>>> n
[]

copy 复制一个列表

1
2
3
4
5
6
7
8
9
10
11
12
>>> n = [1,2,3,4,5]

>>> n.copy()
[1, 2, 3, 4, 5]

>>> n.copy()
[1, 2, 3, 4, 5]

>>> num = n.copy()

>>> num
[1, 2, 3, 4, 5]

count 计算某个元素在列表中出现的次数,如下:

1
2
3
4
5
6
7
8
9
10
>>> numbers
[1, 2, 2, 3, 3, 3, 4]
>>> numbers.count(1)
1
>>> numbers.count(2)
2
>>> numbers.count(3)
3
>>> numbers.count(4)
1

extend 直接向列表末尾一次性追加另一个列表,如下:

1
2
3
4
5
6
7
8
>>> numbers
[1, 2, 2, 3, 3, 3, 4]
>>> a = [4,4,4]
>>> numbers.extend(a)
>>> numbers
[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
>>> a
[4, 4, 4]

index 从列表中找出某个值第一个匹配的索引位置

1
2
3
4
5
6
7
8
9
10
>>> numbers
[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
>>> numbers.index(1)
0
>>> numbers.index(2)
1
>>> numbers.index(3)
3
>>> numbers.index(4)
6

insert 将对象插入列表中

1
2
3
4
5
>>> numbers
[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
>>> numbers.insert(0,0)
>>> numbers
[0, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

pop 移除列表中的最后一个元素,默认是最后一个

1
2
3
4
5
6
>>> numbers
[0, 1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
>>> numbers.pop()
4
>>> numbers
[0, 1, 2, 2, 3, 3, 3, 4, 4, 4]

remove 移除列表中第一个匹配的元素

1
2
3
4
5
6
7
8
>>> num
[1, 2, 1, 3, 2]
>>> num.remove(1)
>>> num
[2, 1, 3, 2]
>>> num.remove(2)
>>> num
[1, 3, 2]

reverse 将列表中的元素反向存放

1
2
3
4
5
>>> numbers
[0, 1, 2, 2, 3, 3, 3, 4, 4, 4]
>>> numbers.reverse()
>>> numbers
[4, 4, 4, 3, 3, 3, 2, 2, 1, 0]

sort 对列表排序,这个我比较喜欢,里面挺多好玩的东西,下面做个简单的介绍

默认排序:

1
2
3
4
>>> L = [1,4,3,2]
>>> L.sort()
>>> L
[1, 2, 3, 4]

自定义排序:

查看sort的使用方法

1
2
>>> L.sort.__doc__
'L.sort(key=None, reverse=False) -> None -- stable sort *IN PLACE*'

根据提供的key函数为元素产生一个键,列表的元素按照这个键值来排序

1
2
3
4
>>> x = ['abc','a','bc','abcd']
>>> x.sort(key=len)
>>> x
['a', 'bc', 'abc', 'abcd’]

反向排序

1
2
3
4
>>> n = [3, 1, 2, 5]
>>> n.sort(reverse=True)
>>> n
[5, 3, 2, 1]

使用 in 判断值是否存在

判断一个值是否存在于给定的列表中的许多方式,其中最具有 Python 风格的是使用 in:

1
2
3
4
5
6
>>> num = [1, 2, 3]
>>> 1 in num
True
>>> 5 in num
False
>>>

同一个值可能出现在列表的多个位置,但只要至少出现一次,in就会返回Ture

1
2
3
>>> num = [1, 1, 2, 3]
>>> 1 in num
True

使用 len() 获取长度

len() 可以返回列表长度:

1
2
3
>>> num = [1, 2, 3]
>>> len(num)
3

使用=赋值,使用copy()复制

如果将一个列表赋值给了多个变量,改变其中的任何一处造成其他变量对应的值也被修改,如下所示:

1
2
3
4
 >>> a = [1, 2, 3]
>>> a
[1, 2, 3]
>>> b = a
>>> b
[1, 2, 3]
>>> a[0] = 'surprise'
>>> a ['surprise', 2, 3]
>>> b
['surprise', 2, 3]

还记得解释变量的时候,那个贴标签的比喻吗?b与a实际上指向的是同一个对象,因此,无论我们是通过a还是b来修改列表的内容,其结果都会作用于双方:

1
2
3
>>> b
['surprise', 2, 3]
>>> b[0] = 'I love surprises' 
>>> b ['I love surprises', 2, 3]
>>> a ['I love surprises', 2, 3]

通过下面任意一种方法,都可以将一个列表的值复制到另一个新的列表中:

  • 列表 copy() 函数
  • list() 转换函数
  • 列表分片 [:]
1
2
3
4
5
>>> a = [1, 2, 3]
>>> b = a.copy()
>>> c = list(a)
>>> d = a[:]
>>> a[0] = 'one'
>>> a
['one', 2, 3]
>>> b [1, 2, 3] >>> c [1, 2, 3] >>> d [1, 2, 3]

b、c、d 都是a的复制:它们是自身带有值的新对象,与原始的a所指向的列表对象[1, 2, 3]没有任何关联。所以改变a不影响b、c、d的复制。

1. 内置类型

1.1 变量

1.2 布尔

表示真假的类型,仅包含 True 和 False 两种取值

数字 0、None,以及元素为空的容器类对象都可视作 bool False,反之为 True。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> bool(0)
False

>>> bool(None)
False

>>> bool("")
False

>>> bool([])
False

>>> bool({})
False

>>> bool(1)
True

>>> bool([1,2])
True

bool类型支持的运算符

1
2
3
>>> a and b  # 如果 a 和 b 都是 True,结果就是 Ture , 否则 False
>>> a or b # a 和 b 至少有一个是 True 时结果是 True, 否则 False
>>> not b # 如果 aFalse, 结果是 True, 如果 aTrue,结果是 False

1.3 数字

python本身支持整数以及浮点数。你可以对这些数字进行下表中的计算。

运算符 表述 示例 运算结果
+ 加法 1 + 1 2
- 减法 4 - 2 2
* 乘法 2 * 2 4
/ 浮点数除法 7 / 2 3.5
// 整数除法 7 // 2 3
/ 摸(求余) 7 % 3 1
** 2 ** 2 4

整数

任何仅含数字的序列在 Python 中都被认为是整数:

1
2
>>> 5
5

Python还支持运算次序,因此你可在同一个表达式中使用多种运算。你还可以使用括号来修 改运算次序,让Python按你指定的次序执行运算,如下所示:

1
2
3
>>> 2 + 3*4
14
>>> (2 + 3) * 4 20

在这些示例中,空格不影响Python计算表达式的方式,它们的存在旨在让你阅读代码时,能 迅速确定先执行哪些运算。

浮点数

Python将带小数点的数字都称为浮点数。大多数编程语言都使用了这个术语,它指出了这样一个事实:小数点可出现在数字的任何位置。每种编程语言都须细心设计,以妥善地处理浮点数, 确保不管小数点出现在什么位置,数字的行为都是正常的。

1
2
3
4
5
6
7
8
>>> 0.1 + 0.1
0.2
>>> 0.2 + 0.2
0.4
>>>2 * 0.1
0.2
>>>2 * 0.2
0.4

但需要注意的是,结果包含的小数位数可能是不确定的:

1
2
3
4
>>> 0.2 + 0.1 
0.30000000000000004
>>> 3 * 0.1
0.30000000000000004

所有语言都存在这种问题,没有什么可担心的。Python会尽力找到一种方式,以尽可能精确地表示结果,但鉴于计算机内部表示数字的方式,这在有些情况下很难。就现在而言,暂时忽略 多余的小数位数即可。

1.4 字符串

字符串是由多个字符组成的序列。在Python中,用引号括起的都是字符串,字符串定义简单自由,可以是单引号、双引号或者三引号。但是个人建议使用双引号表示字符串,用单引号表示字符,和其他语言习惯保持一致。字符串是不可变序列(immutable, sequence)类型,默认存储 Unicode 文本。 python3 不再使用 str 处理二进制字节数据,改为使用 bytes 和 bytearray,前者同为不可变类型。

1
2
3
4
5
6
>>> s = "abc汉字"
>>> >>> len(s)
5

>>> print(ascii(s))
'abc\u6c49\u5b57'

内置函数 ascii 将目标转换为可打印 ASCII 字符组成的字符串。

构建字符串字面量很容易,单引号、双引号,以及跨行的三个引号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>> "ab'c"           # 双引号。
"ab'c"

>>> 'ab"c' # 单引号。
'ab"c'

>>> 'ab\'c' # 引号转义。
"ab'c"

>>> """ # 多行,也可以用三个单引号。
... a
... b
... c"""

'\na\nb\nc'

>>> "a" "b" 'c' # 自动合并多个相邻字符串。
'abc'

可在字面量前添加特殊指示符。

1
2
3
4
5
6
7
8
>>> r"abc\nd"       # raw string,禁用转义。
'abc\\nd'

>>> type(b"abc")
<class 'bytes'>

>>> type(u"abc")
<class 'str'>

str() 类型转换

1
2
>>> str(2.2)
'2.2'

合并字符串

format

1
2
>>> "python培训哪家强:{}".format('京峰教育')
'python培训哪家强:京峰教育'
1
2
>>> "python培训哪家强:{}, 京峰教育谁最帅? {}".format('京峰教育', '斌哥')
'python培训哪家强:京峰教育, 京峰教育谁最帅? 斌哥'
1
2
>>> "python培训哪家强:{0}, 京峰教育谁最帅? {1}".format('京峰教育', '斌哥')
'python培训哪家强:京峰教育, 京峰教育谁最帅? 斌哥'

+

1
2
>>> '京峰' +  '教育'
'京峰教育'

split() 分割

1
2
3
4
>>> s = 'a,b,c'

>>> s.split(',')
['a', 'b', 'c']

join() 合并

1
2
3
4
5
6
7
>>> l = s.split(',')

>>> l
['a', 'b', 'c']

>>> ','.join(l)
'a,b,c'

find 查找子串

查找到返回该子串在原字符串中的索引位置,如果无法找到,find方法会返回值-1

1
2
3
4
5
6
>>> s = "abc"
>>> s.find('a')
0
>>> s.find('d')
-1
>>>

1.5 列表

1.6 元组

与列表类似,元组也是由任意类型元素组成的序列。与列表不同的是,元组是不可改变的,这意味着一但元组被定义,将无法再进行增加、删除或者修改元素等操作。因此元组就像一个常量列表。从行为上看,元组(tuple)像是列表的只读版本。 但在内在实现上有根本不同,元组的只读性使其拥有更好的内存效率和性能。除无法修改外,其普通特征和列表类似。 在需要传递 “不可变” 参数时,应鼓励用元组替代列表。 它是可哈希(hashaable)结构,可用作字典(dict)主键(key)

使用()创建元组

可以用()创建一个空元组:

1
2
3
4
5
>>> empty_tuple = ()
>>> empty_tuple
()
>>> type(empty_tuple)
tuple

创建包含一个或多个元素的元组时,没一个元素后面需要跟着一个逗号,即使只包含一个元素也不能忽略:

1
2
3
4
>>> num = '1',
>>> num
('1',)
>>>

如果创建的元组所包含的元素数量超过1,最后一个元素后面的逗号可以忽略:

1
2
3
>>> num = '1', '2', '3'
>>> num
('1', '2', '3')

Python的交互式解释器输出元组时会自动添加一堆圆括号。你并不需要这么做——定义元组真正靠的是每个元素的后缀逗号——但如果你习惯添加一对括号也无可厚非。可以用括号将所有元素包裹起来,这会使得程序更加清晰:

1
2
3
>>> num = ('1', '2', '3')
>>> num
('1', '2', '3')

可以一口气将元组赋值给多个变量:

1
2
3
4
5
6
7
8
>>> a, b, c = num
>>> a
'1'
>>> b
'2'
>>> c
'3'
>>>

这个过程称为元组解包

可以利用元组在一条语句中对多个变量的值进行交换,而不需借助临时变量:

1
2
3
4
5
6
7
8
>>> a = 1
>>> b = 2
>>> a, b = b, a
>>> a
2
>>> b
1
>>>

tuple() 函数可以用其他类型的数据来创建元组:

1
2
3
4
>>> num = [1, 2, 3]
>>> tuple(num)
(1, 2, 3)
>>>

元组与列表

在许多地方都可以用元组代替列表,但元组的方法函数与类表相比要少一些——元组没有 append() 、insert(),等等——因为一但创建元组变无法修改。既然列表更加灵活那为什么不在所有地方都是用列表呢?原因如下:

  • 元组占用的空间小
  • 你不会意外修改元组的值
  • 可以将元组用作字典的键(详细的后面会介绍)
  • 命名元组可以作为对象的代替
  • 函数的参数是以元组形式是传递的

1.7 字典

使用 {} 创建字典

1
2
3
>>> empty_dict = {}
>>> empty_dict
{}

使用 dict() 转换为字典

可以用 dict() 将包含双值子序列的序列转换成字典。

1
2
3
>>> lol = [ ['a', 'b'], ['c', 'd'], ['e', 'f'] ]
>>> dict(lol)
{'a': 'b', 'c': 'd', 'e': 'f'}

记住,字典中元素的顺序是无关紧要的,实际存储顺序可能取决于你添加元素的顺序。

双值元组列表:

1
2
3
>>> lot = [ ('a', 'b'), ('c', 'd'), ('e', 'f') ]
>>> dict(lot)
{'a': 'b', 'c': 'd', 'e': 'f'}

双字符串的字符串组成的列表:

1
2
3
>>> los = [ 'ab', 'cd', 'ef' ]
>>> dict(los)
{'a': 'b', 'c': 'd', 'e': 'f'}

双字符的字符串组成的元组:

1
2
3
>>> tos = ( 'ab', 'cd', 'ef' )
>>> dict(tos)
{'a': 'b', 'c': 'd', 'e': 'f'}

使用 [key] 添加或修改元素

1
2
3
4
>>> d = {"l1":1, "l2":2, "l3":3}
>>> d["l4"] = 4
>>> d
{'l2': 2, 'l1': 1, 'l3': 3, 'l4': 4}

使用 update() 合并字典

1
2
3
4
>>> d = {"l1":1, "l2":2, "l3":3}
>>> d.update({"l4":4})
>>> d
{'l2': 2, 'l1': 1, 'l3': 3, 'l4': 4}

使用 del 删除具有指定键的元素

1
2
3
4
>>> d = {"l1":1, "l2":2, "l3":3}
>>> del d["l1"]
>>> d
{'l2': 2, 'l3': 3}

使用 clear() 删除所有元素

1
2
3
4
>>> d = {"l1":1, "l2":2, "l3":3}
>>> d.clear()
>>> d
{}

使用 in 判断是否存在

如果你希望判断某一个键是否存在于一个字典中,可以使用 in 。

1
2
3
4
5
6
7
8
9
10
>>> d = {"l1":1, "l2":2, "l3":3}
>>> d
{'l2': 2, 'l1': 1, 'l3': 3}
>>> 1 in d
False
>>> "l1" in d
True
>>> "l4" in d
False
>>>

使用 [key] 获取元素

1
2
3
>>> d = {"l1":1, "l2":2, "l3":3}
>>> d.keys()
dict_keys(['l2', 'l1', 'l3'])

使用 values() 获取所有值

1
2
3
>>> d = {"l1":1, "l2":2, "l3":3}
>>> list( d.values() )
[2, 1, 3]

使用 items() 获取所有键值对

1
2
3
>>> d = {"l1":1, "l2":2, "l3":3}
>>> list(d.items())
[('l2', 2), ('l1', 1), ('l3', 3)]

使用=赋值,使用copy()复制

与列表一样,对字典内容进行修改会反应到所有与之相关联的变量名上:

1
2
3
4
5
6
7
8
>>> d = {"l1":1, "l2":2, "l3":3}
>>> d2 = d
>>> d.update({"l4":4})
>>> d
{'l2': 2, 'l1': 1, 'l3': 3, 'l4': 4}
>>> d2
{'l2': 2, 'l1': 1, 'l3': 3, 'l4': 4}
>>>

若想避免这种情况,可以使用 copy() 将字典复制到一个新的字典中:

1
2
3
4
5
6
7
8
9
10
11
>>> d
{'l2': 2, 'l1': 1, 'l3': 3, 'l4': 4}
>>> d2
{'l2': 2, 'l1': 1, 'l3': 3, 'l4': 4}
>>> d = {"l1":1, "l2":2, "l3":3}
>>> d2 = d.copy()
>>> d.update({"l4":4})
>>> d2
{'l1': 1, 'l2': 2, 'l3': 3}
>>> d
{'l2': 2, 'l1': 1, 'l3': 3, 'l4': 4}

两个列表转换为字典

1
2
3
4
5
6
>>> l1 = [1,2,3]
>>> l2 = ["one", "two", "there"]
>>> zip(l1,l2)
<zip object at 0x1022f1588>
>>> dict(zip(l1,l2))
{1: 'one', 2: 'two', 3: 'there'}

1.8 集合

集合就像舍弃了值,仅剩下的字典一样。键与键之间也不允许重复。如果你仅仅想知道某一个元素是否存在而不关心其他的,使用集合是个非常好的选择。如果需要为键附加其他信息的话建议使用字典。

使用 set() 创建集合

可以使用set()函数创建一个集合,或者用大括号将一系列一都好分开的值包裹起来:

1
2
3
4
5
6
>>> empty_set = set()
>>> empty_set
set()
>>> num_set ={1,2,3,4,5}
>>> num_set
{1, 2, 3, 4, 5}

与字典一样,集合是无序的。

{} 创建的是一个空字典,这仅仅是因为字典出现的比较早抢占了花括号。

使用set()将其他类型转换为集合

你可以利用已有的列表、字符串、元组或字典的内容来创建集合,其中重复的值会被丢弃。

首先来试着转换一个包含重复字母的字符串:

1
2
>>> set('letters')
{'l', 'r', 'e', 't', 's'}

注意,上面得到的集合中仅含有一个 ‘e’ 和一个 ‘t’,尽管字符串 ‘letters’ 里各自包含两个。

再试试用列表建立集合:

1
2
>>> set(['one', 'two', 'three'])
{'one', 'two', 'three'}

再看下元组:

1
2
>>> set(('one', 'two', 'three'))
{'one', 'two', 'three'}

当字典作为参数传入set()函数时,只有键会被使用:

1
2
>>> set( {'apple': 'red', 'orange': 'orange', 'cherry': 'red'} )
{'cherry', 'orange', 'apple'}

使用in测试值是否存在

1
2
3
4
5
6
7
>>> num_set = {'one', 'two', 'three'}
>>> num_set
{'one', 'two', 'three'}
>>> 'one' in num_set
True
>>> 'four' in num_set
False

添加删除数据

1
2
3
4
5
6
7
>>> num_set = {'one', 'two', 'three'}
>>> num_set.add('four')
>>> num_set
{'one', 'two', 'four', 'three'}
>>> num_set.remove('one')
>>> num_set
{'two', 'four', 'three'}

交集和并集

set可以看成数学意义上的无序和无重复元素的集合,因此,两个set可以做数学意义上的交集、并集等操作:

1
2
3
4
5
6
>>> s1 = set([1, 2, 3])
>>> s2 = set([2, 3, 4])
>>> s1 & s2
set([2, 3])
>>> s1 | s2
set([1, 2, 3, 4])

比较数据结构

列表

元组

字典

集合

建立大型数据结构

我们从最简单的布尔型、数字、字符串开始学习,到目前为止,学习了列表、元组集合以及字典等数据结构,你可以将这些内置的数据结构自由地组合成更大、更复杂的结构。试着从建立上三个不同的列表开始:

1
2
3
>>> num = [1, 2, 3]
>>> name = ["l1", "l2", "l3", "l4", "l5"]
>>> englist = ["one", "two", "three"]

可以把上面每一个列表当做一个元素,并建立一个元组:

1
2
3
4
>>> tol = num, name, englist
>>> tol
([1, 2, 3], ['l1', 'l2', 'l3', 'l4', 'l5'], ['one', 'two', 'three'])
>>>

同样,可以创建一个包含上面三个列表的列表:

1
2
3
>>> lol = [num, name, englist]
>>> lol
[[1, 2, 3], ['l1', 'l2', 'l3', 'l4', 'l5'], ['one', 'two', 'three']]

还可以创建以这三个列表为值的字典:

1
2
3
4
>>> dol = {'num': num, 'name':name, 'englist':englist}
>>> dol
{'englist': ['one', 'two', 'three'], 'name': ['l1', 'l2', 'l3', 'l4', 'l5'], 'num': [1, 2, 3]}
>>>

在创建自定义数据结构的过程中,唯一的限制来自于这些内置数据本身。比如字典的键必须为不可变对象,因此列表、字典和集合都不能作为字典的键,但元组可以作为字典的键。举个例子,我们可以通过 GPS 坐标(纬度,经度,海拔)定位感兴趣的位置:

1
2
3
>>> houses = {
... (44.79, -93.14, 285): 'My House',
(38.89, -77.03, 13): 'The White House' }

2. 代码格式

2.1. 注释

在大多数编程语言中,注释都是一项很有用的功能。随着程序越来越大、越来越复杂,就应在其中添加说明,对你解决问题的方法进行大致的阐述。注释让你能够使用自然语言在程序中添加说明。注释是程序中会被Python解释器忽略的一段文本。通过使用注释,可以解释和明确Python代码的功能,记录将来要修改的地方,甚至写下你想写的东西。在Python中使用#字符标记注释,从#开始到当前行结束的部分都是注释。你可以把注释作为单独一行:

单行注释:

hello.py

1
2
# 向大家问好
print("Hello Python people!")

Python解释器将忽略第一行,只执行第二行.

1
print("Hello Python people!")

多行注释:

1
2
3
4
5
6
7
8
9
10
#coding=utf-8

"""这是"nester.py"模块,提供了一个名为print_lol的函数,这个函数的作用是打印列表,其中有可能包含(也可能不包含)嵌套列表。"""
def print_lol(the_list):
"""这个函数取一个位置参数,名为"the_list",这个可以是任何python列表(也可以是包含嵌套列表的列表)。所指定的列表中的每个数据项(递归地)输出到屏幕上,各数据项各占一行。"""
for each_item in the_list:
if isinstance(each_item, list):
print_lol(each_item)
else:
print(each_item)

该编写什么样的注释?

编写注释的主要目的是阐述代码要做什么,以及是如何做的。在开发项目期间,你对各个部分如何协同工作了如指掌,但过段时间后,有些细节你可能不记得了。当然,你总是可以通过研 究代码来确定各个部分的工作原理,但通过编写注释,以清晰的自然语言对解决方案进行概述, 可节省很多时间。
要成为专业程序员或与其他程序员合作,就必须编写有意义的注释。当前,大多数软件都是合作编写的,编写者可能是同一家公司的多名员工,也可能是众多致力于同一个开源项目的人员。 训练有素的程序员都希望代码中包含注释,因此你最好从现在开始就在程序中添加描述性注释。 作为新手,最值得养成的习惯之一是,在代码中编写清晰、简洁的注释。
如果不确定是否要编写注释,就问问自己,找到合理的解决方案前,是否考虑了多个解决方案。如果答案是肯定的,就编写注释对你的解决方案进行说明吧。相比回过头去再添加注释,删除多余的注释要容易得多。从现在开始,本书的示例都将使用注释来阐述代码的工作原理。

2.2 python 之禅

2.3 pep8

3. 表达式

控制流

if

elif

条件表达式(即”三元操作符”)

三元运算符可以只需要一行完成条件判断和赋值操作

1
data = x if x < y else y

while

while 是Python中的循环语句. 事实它上是一个条件循环语句. 与 if 声明相比, 如果 if 后的条件为真, 就会执行一次相应的代码块. 而 while 中的代码块会一直循环执行, 直到循环条件不再为真.

语法:

1
2
while expression:
suite_to_repeat

while 循环的 suite_to_repeat 子句会一直循环执行, 直到 expression 值为布尔假. 这种 类型的循环机制常常用在计数循环中。

1
2
3
4
count = 0
while (count < 9):
print('the index is:', count)
count += 1

无限循环

你必须小心地使用 while 循环, 因为有可能它的条件永远不会为布尔假. 这样一来循环就永远不会结束. 这些”无限”的循环不一定是坏事, 许多通讯服务器的客户端/服务器系统就是通过它来工作的. 这取决于循环是否需要一直执行下去, 如果不是, 那么这个循环是否会结束; 也就是说, 条件表达式会不会计算后得到布尔假?

1
2
3
4
while True:
handle, indata = wait_for_client_connect()
outdata = process_request(indata)
ack_result_to_client(handle, outdata)

例如上边的代码就是故意被设置为无限循环的,因为 True 无论如何都不会变成 False. 这是因为服务器代码是用来等待客户端(可能通过网络)来连接的. 这些客户端向服务器发送请求, 服务器处理请求.
请求被处理后, 服务器将向客户端返回数据, 而此时客户端可能断开连接或是发送另一个请求. 对于服务器而言它已经完成了对这个客户端的任务, 它会返回最外层循环等待下一个连接.

while使用 else 语句

在 python 中,for … else 表示这样的意思,for 中的语句和普通的没有区别,else 中的语句会在循环正常执行完(即 for 不是通过 break 跳出而中断的)的情况下执行,while … else 也是一样。

1
2
3
4
5
6
count = 0
while count < 5:
print count, " is less than 5"
count = count + 1
else:
print count, " is not less than 5"

结果:

1
2
3
4
5
6
0 is less than 5
1 is less than 5
2 is less than 5
3 is less than 5
4 is less than 5
5 is not less than 5

for

Python 提供给我们的另一个循环机制就是 for 语句. 它提供了 Python 中最强大的循环结构. 它可以遍历序列成员。

for 循环会访问一个可迭代对象(例如序列或是迭代器)中的所有元素, 并在所有条目都处理过后结束循环. 它的语法如下:

1
for iter_var in iterable:
        suite_to_repeat

每次循环, iter_var 迭代变量被设置为可迭代对象(序列, 迭代器, 或者是其他支持迭代的对象)的当前元素, 提供给 suite_to_repeat 语句块使用.

else

for 循环也可以有 else 用于循环后处理(post-processing). 它和 while 循环中的 else 处理方式相同. 只要 for 循环是正常结束的(不是通过 break ), else 子句就会执行.

1
2
3
4
5
6
7
8
9
10
s = ["a", "b", "c", "d", "e"]
found = False
for c in s:
if c.find("c") != -1:
found = True
print("发现c项")
break

if not found:
print("没有发现c项")
1
2
3
4
5
6
7
s = ["a", "b", "c", "d", "e"]
for c in s:
if c.find("c") != -1 :
print("发现c项")
break
else:
print("没有c项")

enumerate

1
2
3
4
5
6
7
8
>>> l = ['a', 'b', 'c']
>>>
>>> for index, item in enumerate(l):
... print(index, item)
...
0 a
1 b
2 c

range()

内建函数 range() 可以把类似 foreach 的 for 循环变成你更加熟悉的语句. 例如从 0 到 10 计数, 或者从 10 到 100 一次递增 5 .

1
range(start, end, step=1)
1
2
3
4
5
6
7
8
>>> range(5)
range(0, 5)

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> list(range(0, 10, 2))
[0, 2, 4, 6, 8]

zip

定义:zip([iterable, …])

zip()是Python的一个内建函数,它接受一系列可迭代的对象作为参数,将对象中对应的元素打包成一个个tuple(元组),然后返回由这些tuples组成的list(列表)。若传入参数的长度不等,则返回list的长度和参数中长度最短的对象相同。利用*号操作符,可以将list unzip(解压),看下面的例子就明白了:

1
2
3
4
5
6
7
8
9
>>> a = [1,2,3]
>>> b = [4,5,6]
>>> c = [4,5,6,7,8]
>>> zipped = zip(a,b)
[(1, 4), (2, 5), (3, 6)]
>>> zip(a,c)
[(1, 4), (2, 5), (3, 6)]
>>> zip(*zipped)
[(1, 2, 3), (4, 5, 6)]

pass

空语句。

如果你在需要子语句块的地方不写任何语句, 解释器会提示你 语法错误. 因此, Python 提供了 pass 语句, 它不做任何事情 - 即 NOP , ( No OPeration , 无操作) 我们从汇编语言中借用这个概念. pass 同样也可作为开发中的小技巧, 标记你后来要完成的代码

break

Python 中的 break 语句可以结束当前循环然后跳转到下条语句, 类似 C 中的传统 break . 常用在当某个外部条件被触发(一般通过 if 语句检查), 需要立即从循环中退出时. break 语句可以用在 while 和 for 循环中.

continue

不管是 Python, C, Java 还是其它任何支持 continue 语句的结构化语言中, 一些初学者有这样的一个误解: continue 语句”立即启动循环的下一次迭代”. 实际上, 当遇到 continue 语句时, 程序会终止当前循环, 并忽略剩余的语句, 然后回到循环的顶端. 在开始下一次迭代前, 如果是条件循环, 我们将验证条件表达式. 如果是迭代循环, 我们将验证是否还有元素可以迭代. 只有在验证成功的情况下, 我们才会开始下一次迭代.

Python 里的 continue 语句和其他高级语言中的传统 continue 并没有什么不同. 它可以被用在 while 和 for 循环里. while 循环是条件性的, 而 for 循环是迭代的, 所以 continue 在开始下一次循环前要满足一些先决条件, 否则循环会正常结束.

1
2
3
4
5
6
7
8
>>> for s in 'hello':
... if s == 'l':
... continue
... print(s)
...
h
e
o

推导式

推导式是从一个或者多个迭代器快速简洁地创建数据结构的一种方法。他可以讲循环和条件判断结合,从而避免语法冗长的代码。会使用推导式有时可以说明你已经超过 Python初学者的水平。也就是说,使用推导式更像 Python 风格

列表推导式

你可以从1到5创建一个整数列表,每次增加一项:

1
2
3
4
5
6
7
8
>>> number_list = []
>>> number_list.append(1)
>>> number_list.append(2)
>>> number_list.append(3)
>>> number_list.append(4)
>>> number_list.append(5)
>>> number_list
[1, 2, 3, 4, 5]

或者,可以结合 range() 函数使用一个迭代器:

1
2
3
4
5
6
>>> number_list = []
>>> for number in range(1, 6):
... number_list.append(number)
...
>>> number_list
[1, 2, 3, 4, 5]

上面这些方法都是可行的Python代码,会得到相同的结果。然而,更像 Python 风格的创建列表方法是列表推导。语法如下:

1
[ expression for item in iterable ]

将通过列表推导创建一个整数列表:

1
2
3
>>> number_list = [number for number in range(1,6)]
>>> number_list
[1, 2, 3, 4, 5]

在第一行中,第一个 number 变量为列表生成值,也就是说,把循环的结果放在列表 number_list 中。 第二个 number 可以为表达式, 看下下面的例子:

1
2
3
>>> number_list = [number-1 for number in range(1,6)]
>>> number_list
[0, 1, 2, 3, 4]

列表推到把循环放在方括号内部。这种例子和之前碰到的不大一样,但却是更为常见的方式。同样,列表推导也可以像下面的例子加上条件表达式:

1
[expression for item in iterable if condition]

现在,通过推导创建一个在1到5之间的偶数列表(当 number % 2 为真时,代表奇数;为假时代表偶数):

1
2
3
>>> a_list = [number for number in range(1,6) if number % 2 == 1]
>>> a_list
[1, 3, 5]

正如很多嵌套循环一样,在对应的推导中会有多个for语句,我们先来看一个简单的嵌套循环例子:

1
2
3
4
5
6
7
8
9
10
11
12
>>> rows = range(1,4)
>>> cols = range(1,3)
... for row in rows:
... for col in cols:
... print(row, col)
...
1 1
1 2
2 1
2 2
3 1
3 2

使用一次推导,将结果赋值给变量 cells,使 row,col 成为元组:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> rows = range(1,4)
>>> cols = range(1,3)
>>> cells = [(row, col) for row in rows for col in cols]
>>> for cell in cells:
... print(cell)
...
(1, 1)
(1, 2)
(2, 1)
(2, 2)
(3, 1)
(3, 2)
>>>

另外,在对 cells 列表进行迭代时可以通过元组拆封将变量 row 和 col 的值分别取出:

1
2
3
4
5
6
7
8
9
>>> for row, col in cells:
... print(row, col)
...
1 1
1 2
2 1
2 2
3 1
3 2

其中,列表推导中 for row …和 for col …都可以有自己单独的 if 条件判断。

1
2
3
4
5
6
>>> rows = range(1,4)
>>> cols = range(1,3)
>>> cells = [(row, col) for row in rows if row % 2 == 1 for col in cols if col % 2 == 1]
>>> cells
[(1, 1), (3, 1)]
>>>

字典推导式

集合推导式

集合也不例外,同样有推导式。最简单的版本和之前的列表、字典推导类似:

1
{expression for expression in iterable }

也可以使用条件判断:

1
2
3
>>> a_set = {number for number in range(1,6) if number % 3 == 1}
>>> a_set
{1, 4}

id is ==

Python中的对象包含三要素:id、type、value
其中id返回一个对象的唯一标识,type标识对象的类型,value是对象的值
is判断的是a对象是否就是b对象,是通过id来判断的
==判断的是a对象的值是否和b对象的值相等,是通过value来判断的
如下代码或许可以帮助你理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> a = 1
>>> b = 1.0
>>> a is b
False
>>> a == b
True
>>> id(a)
12777000
>>> id(b)
14986000
>>> a = 1
>>> b = 1
>>> a is b
True
>>> a == b
True
>>> id(a)
12777000
>>> id(b)
12777000

就 CPython 而言,id 返回的就是运行期内存地址。因此这个标识属阶段性的,不能保证不被重复使用。 但对于其他实现来说,id 返回的未必就是内存地址。

4. 函数

代码复用的第一步是使用函数,它是命名的用于区分的代码段。函数可以接受任何数字或者其他类型的输入作为参数,并且返回数字或者其他类型的结果。

你可以使用函数做一下两件事情:

  • 定义函数
  • 调用函数

定义:

语句 def 在运行期创建函数对象,并与指定名字关联。

1
2
def func_name():
pass # 写入你的逻辑

参数

传入到函数的值称为参数。当调用含参数的函数时,这些参数的值会被复制给函数中的对应参数。

1
2
3
4
5
6
7
8
9
10
>>> def get_name(num):
... if num == 1:
... return '老大'
... elif num == 2:
... return '老二'
...
...
>>> name = get_name(1)
>>> name
'老大'

这个函数做了如下事情:

  • 把 1 赋值给函数的内部参数 num
  • 运行 if-elif 的逻辑链
  • 返回一个字符串
  • 将该字符串赋值给变量 name

一个函数可以接受任何数量(包括0)的任何类型的值作为输入变量,并且返回任何数量(包括0)的任何类型的结果。如果函数不显示调用 return 函数,那么会默认返回 None。

1
2
3
4
5
6
>>> def func_name():
... pass
...
>>> print(func_name())
None
>>>

None

None 是 Python 中一个特殊的值,虽然它不表示任何数据,但仍然具有重要的作用。 虽然 None 作为布尔值和 False 是一样的,但是它和 False 有很多差别。下面是一个例子:

1
2
3
4
5
6
7
>>> thing = None
>>> if thing:
... print("It's some thing")
... else:
... print("It's no thing")
...
It's no thing

为了区分 None 和布尔值 False , 使用 Python 的 is 操作符:

1
2
3
4
5
6
>>> if thing is None:
... print("It's nothing")
... else:
... print("It's something")
...
It's nothing

这虽然是一个微妙的区别,但是对于 Python 来说是很重要的。你需要把 None 和不含 任何值的空数据结构区分开来。0 值的整型 / 浮点型、空字符串(‘’)、空列表([])、 空元组((,))、空字典({})、空集合(set())都等价于 False,但是不等于 None。
现在,快速写一个函数,输出它的参数是否是 None:

1
2
3
4
5
6
7
>>> def is_none(thing):
... if thing is None:
... print("It's None")
... elif thing:
... print("It's True")
... else:
... print("It's False")

现在,运行一些测试函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>>> is_none(None)
It's None
>>> is_none(True)
It's True
>>> is_none(False)
It's False
>>> is_none(0)
It's False
>>> is_none(0.0)
It's False
>>> is_none(())
It's False
>>> is_none([])
It's False
>>> is_none({})
It's False
>>> is_none(set())
It's False

位置参数

Python 处理参数的方式要比其他语言更加灵活。其中,最熟悉的参数类型是位置参数,传入参数的值是按照顺序依次复制过去的。

1
2
3
4
5
6
7
8
9
10
>>> def name (n1, n2, n3):
... print('1', n1)
... print('2', n2)
... print('3', n3)
...
>>> name('老大', '老二', '老三')
1 老大
2 老二
3 老三
>>>

尽管这种方式很常见,但是位置参数的一个弊端是必须熟记没个位置的参数的含义。在调用函数name() 时误把最后一个参数当做第一个参数,会得到完全不同的结果:

1
2
3
4
5
>>> name('老二', '老大', '老三')
1 老二
2 老大
3 老三
>>>

关键字参数

为了避免位置参数带来的混乱,调用参数时可以指定对应的名字,甚至可以采用与函数定义不同的顺序调用:

1
2
3
4
5
>>> name(n2='老二', n1='老大', n3='老三')
1 老大
2 老二
3 老三
>>>

你也可以把位置参数和关键字参数混合起来。

1
2
3
4
>>> name('老大', n2='老二', n3='老三')
1 老大
2 老二
3 老三

如果同时出现两种参数形式,首先应该考虑的是位置参数。

1
2
3
4
5
>>> name(n2='老二', '老大', n3='老三')
File "<ipython-input-60-2a320a67eb2a>", line 1
name(n2='老二', '老大', n3='老三')
^
SyntaxError: positional argument follows keyword argument

指定默认参数

当调用方没有提供对应的参数值时,你可以指定默认参数值。

1
2
3
4
5
6
7
8
9
10
11
>>> def name (n2, n3, n1='老大'):
... print('1', n1)
... print('2', n2)
... print('3', n3)
...
...
>>> name('老二', '老三')
1 老大
2 老二
3 老三
>>>

默认参数值在函数被定义时已经计算出来,而不是在程序运行时。Python程序员经常犯的一个错误是把可变的数据类型(例如列表或者字典)当做默认参数值。

在函数 box() 在每次调用时,添加参数 arg 到一个空的列表 result, 然后打印输出一个单值列表。但存在一个问题:只有在第一次调用时列表是空的,第二次调用时就会存在之前调用的返回值:

1
2
3
4
5
6
7
8
>>> def box(arg, result=[]):
... result.append(arg)
... print(result)
...
>>> box('a')
['a']
>>> box('b')
['a', 'b']

如果写成下面的样子就会解决刚才的问题:

1
2
3
4
5
6
7
8
9
>>> def box(arg):
... result = []
... result.append(arg)
... return result
...
>>> box('a')
['a']
>>> box('b')
['b']

这样的修改也是为了表明第一次调用跳过一些操作:

1
2
3
4
5
6
7
8
9
10
>>> def box(arg, result=None):
... if result is None:
... result = []
... result.append(arg)
... print(result)
...
>>> box('a')
['a']
>>> box('b')
['b']

迭代器

在Python中,很多对象都是可以通过for语句来直接遍历的,例如list、string、dict等等,这些对象都可以被称为可迭代对象。至于说哪些对象是可以被迭代访问的,就要了解一下迭代器相关的知识了。

迭代器是一个可以记住遍历的位置的对象。迭代器对象要求支持迭代器协议的对象,在Python中,支持迭代器协议就是实现对象的iter()和next()方法。其中iter()方法返回迭代器对象本身;next()方法返回容器的下一个元素,在结尾时引发StopIteration异常。

迭代器有两个基本的方法:

1
__iter__()和next()方法

这两个方法是迭代器最基本的方法,一个用来获得迭代器对象,一个用来获取容器中的下一个元素。
对于可迭代对象,可以使用内建函数iter()来获取它的迭代器对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> l = [1,2,3,4]
>>> it = iter(l)
>>> it
<list_iterator object at 0x10715dcf8>
>>> next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
4
>>> next(it)
Traceback (most recent call last):
File "<ipython-input-60-2cdb14c0d4d6>", line 1, in <module>
next(it)
StopIteration

例子中,通过iter()方法获得了list的迭代器对象,然后就可以通过next()方法来访问list中的元素了。当容器中没有可访问的元素后,next()方法将会抛出一个StopIteration异常终止迭代器。

其实,当我们使用for语句的时候,for语句就会自动的通过iter()方法来获得迭代器对象,并且通过next()方法来获取下一个元素。

生成器

生成器是用来创建 Python 序列的一个对象。使用它可以迭代庞大的序列,且不需要再内存中创建和存储整个序列。通常,生成器是为迭代器产生数据的。(生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。)我们已经在之前学习过其中一个,就是 range(),来产生一系列整数。range()在 python 2 中返回一个列表,这也限制了它要进入内存空间。Python 2 中同样存在的生成器 xrange() 在 Python 3中成为标准的 range() 生成器。下面的例子累加从 1 到 100 的整数:

1
2
>>> sum(range(1, 101))
5050

每次迭代生成器,它会记录上一次调用的位置,并且返回下一个值。这一点和普通的函数是不一样的,一般函数都不记录之前一次调用,而且都会在函数的第一行开始执行。

如果你想创建一个比较大的序列,使用生成器推导的代码会很长,这是可以尝试写一个生成器函数。生成器函数和普通函数类似,但是它的返回值使用 yield 语句声明, 不是 return。下面编写我们自己的 range() 函数版本:

1
2
3
4
5
>>> def my_range(first=0, last=10, step=1):
... number = first
... while number < last:
... yield number
... number += step

这是一个普通的函数:

1
2
>>> my_range
<function my_range at 0x1070c4e18>

并且它返回的是一个生成器对象

1
2
3
4
>>> ranger = my_range(1, 5)
>>> ranger
<generator object my_range at 0x1070b5eb8>
>>>

可以对这个生成器对象进行迭代:

1
2
3
4
5
6
7
>>> for x in ranger:
... print(x)
...
1
2
3
4

在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回yield的值。并在下一次执行 next()方法时从当前位置继续运行。

装饰器

有时你需要在不改变源代码的情况下修改已经存在的函数。常见的例子是增加一句调试声明,已查看传入的参数。

装饰器本质上是一个函数。它把一个函数作为输入并且返回另一个函数。在装饰器中,通常使用下面这些 Python 技巧:

  • args 和 *kwargs
  • 闭包
  • 作为参数的函数

函数 document_it() 定义了一个装饰器,会实现如下功能:

  • 打印输出函数的名字和参数的值
  • 执行含有参数的函数
  • 打印输出结果
  • 返回修改后的函数

看下面代码:

1
2
3
4
5
6
7
8
9
10
>>> def document_it(func):
... def new_function(*args, **kwargs):
... print('Running function:', func.__name__)
... print('Positional arguments:', args)
... print('Keyword arguments:', kwargs)
... result = func(*args, **kwargs)
... print('Result:', result)
... return result
... return new_function
...

无论传入 document_it() 函数 func 是什么,装饰器都会返回一个新的函数,其中包含函数 document_it() 增加的额外语句。事实上,装饰器并不需要执行函数 func 中的代码,只是在结束前函数 document_it() 调用函数 func 以便得到 func 的返回结果和附加代码的结果。

那么,如何使用装饰器?当然,可以通过人工赋值:

1
2
3
4
5
6
7
8
9
10
11
12
>>> def add_ints(a, b):
... return a + b
...
>>> add_ints(3, 5)
8
>>> cooler_add_ints = document_it(add_ints) # 人工对装饰器赋值
>>> cooler_add_ints(3, 5)
Running function: add_ints
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 8
8

1
2
3
@decorator
def func():
pass

其解释器会解释成下面这样的语句:

1
func = decorator(func)

作为对前面人工装饰器赋值的替代,可以直接再要装饰的函数前添加装饰器名字 @decorator_name:

1
2
3
4
5
6
7
8
9
10
>>> @document_it
... def add_ints(a, b):
... return a + b
...
>>> add_ints(3, 5)
Running function: add_ints
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 8
8

同样一个函数可以有多个装饰器。下面,我们写一个对结果求平方的装饰器 square_it():

1
2
3
4
5
6
>>> def square_it(func):
... def new_function(*args, **kwargs):
... result = func(*args, **kwargs)
... return result * result
... return new_function
...

靠近函数定义(def 上面)的装饰器最先执行,然后一次执行上面的。任何顺序都会得到相同的最终结果。下面的例子中会看到中间步骤的变化:

1
2
3
4
5
6
7
8
9
10
11
>>> @document_it
... @square_it
... def add_ints(a, b):
... return a + b
...
>>> add_ints(3, 5)
Running function: new_function
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 64
64

交换两个装饰器的顺序:

1
2
3
4
5
6
7
8
9
10
11
>>> @square_it
... @document_it
... def add_ints(a, b):
... return a + b
...
>>> add_ints(3, 5)
Running function: add_ints
Positional arguments: (3, 5)
Keyword arguments: {}
Result: 8
64

命名空间和作用域

5.

6. 文件异常

7. 模块

8.标准库

Python 的一个显著特点是具有庞大的模块标准库,这些模块可以执行很多有用的任务,并且和核心 Python 语言分开以避免臃肿。我们开始写代码时,首先要检查是否存在想要的标准模块。在标准库中你会经常碰到一些”珍宝”!

关于标准库的学习资料:

一般标准库里涉及数据结构、网络、系统、数据库等模块。下面来学习一些常用的标准模块。

使用 setdefault() 和 defaultdic() 处理缺失的键

使用 Counter() 计数

使用有序字典 OrderedDict() 按键排序

双端队列:栈+队列

使用 itertools 迭代代码结构

使用 pprint() 友好输出

argparse 命令行解析

python中的命令行解析最简单最原始的方法是使用sys.argv来实现,更高级的可以使用argparse这个模块。argparse从python 2.7开始被加入到标准库中,所以如果你的python版本还在2.7以下,那么需要先手动安装。

基础课实战

密码管理器

  • 主密码(retry 3次 给一个提示) 加密
  • 保存密码
    • 密码 title
    • 真实的密码 (加密)
  • 自动生成安全密码 (用户可以输入长度)
  • 密码分类
    • web
    • 应用
    • 银行
    • 服务器
  • 获取密码
  • 密码文件的云备份

9.第三方库

运维常用的第三方库

psutil

系统性能信息模块
psutil是一个跨平台库,能够轻松的实现获取系统运行的进程和系统利用率(包括CPU、内存、磁盘、网络等)信息。它主要应用于系统监控,分析和限制系统资源及进程的管理。它实现了同等命令行工具提供的功能,如ps、top、lsof、netstat、ifconfig、who、df、kill、free、nice、ionice、iostat、iotop、uptime、pidof、tty、taskset、pmap等。目前支持32位和64位的Linux、Windows、OS X、FreeBSD和Sun Solaris等操作系统,支持从2.6 到 3.5的Python版本,目前最新版本为5.1.3。

项目地址:https://github.com/giampaolo/psutil

安装

1
pip install psutil

CPU

1
2
3
4
5
6
#显示所有逻辑CPU信息。
psutil.cpu_times(percpu=True)
#获取CPU的逻辑个数
psutil.cpu_count()
#获取物理cpu个数
psutil.cpu_count(logical=False)

Memory

1
2
3
4
5
6
7
8
#内存信息
psutil.virtual_memory()
#获取内存总数
psutil.virtual_memory().total
#获取空闲内存数
psutil.virtual_memory().free
#获取swap信息
psutil.swap_memory()

Disks

1
2
3
4
5
6
#磁盘信息
psutil.disk_partitions(all=True)
#获取分区参数
psutil.disk_usage('/')
#获取磁盘IO个数
psutil.disk_io_counters()

Network

1
2
3
4
#获取网络总的IO信息
psutil.net_io_counters()
#获取每个网络接口的io信息
psutil.net_io_counters(pernic=True)

Other system info

1
2
3
4
5
6
7
#返回当前用户登录信息
psutil.users()
#获取开机时间(以时间戳的方式)
psutil.boot_time()
#让人能看懂
import datetime
datetime.datetime.fromtimestamp(psutil.boot_time()).strftime("%Y-%m-%d %H:%M:%S")

Process management

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
# 进程信息
psutil.pids() #获取所有进程的PID
p = psutil.Process(2128) #实例化一个Process对象,参数为一个进程PID
p.name() #进程名
p.exe() #经常bin路径
p.cwd() #进程工作目录路径
p.status() #进程状态
p.create_time() #进程创建时间,时间戳格式
p.uids() #进程uid信息
p.gids() #进程gid信息
p.cpu_times() #进程cpu时间信息,包括use,system两个时间
p.cpu_affinity() #get进程CPU亲合度,如果设置了进程cpu的亲和度,降CPU号作为参数即可
p.memory_percent() #进程内存利用率
p.memory_info() #进程内存rss.vms信息
p.io_counters() #进程IO信息,包括读写IO数及字节数
p.connections() #返回打开进程socket的namedutples列表,包括fs、family、laddr等信息
p.num_threads() #进程开启的线程数

#popen类的使用
from subprocess import PIPE
#通过psutil的popen方法启动的应用程序,可以跟踪程序运行的所有相关信息
p = psutil.Popen(['/usr/bin/python','-c','print ("hello")'],stdout=PIPE)
p.name()
p.username()
p.communicate()
p.cpu_times()

Ipy

IP地址处理模块
IP地址规划是网络设计中非常重要的一个环节,规划的好坏会直接影响路由协议算法的效率,包括网络性能、可扩展性等方面,在这个过程当中,免不了要计算大量的IP地址,包括网段、网络掩码、广播地址、子网数、IP类型等。

官方地址: https://github.com/autocracy/python-ipy

安装

版本号 0.83

1
pip install IPy

IPy模块包含IP类,使用它可以方便处理绝大部分格式为IPv6及IPv4的网络和地址。比如通过version方法就可以区分出IPv4与IPv6,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from IPy import IP
IP('192.168.1.0/24').version()
IP('::1').version()

ip = IP('192.168.0.0/16')
ip.len() #输出192.168.0.0网段的ip个数
for x in ip:
print(x)

test_ip = IP('192.168.17.8')
test_ip.reverseName() #反向解析地址格式
test_ip.iptype() #192.168.17.8为私网类型'PRIVATE'
IP('8.8.8.8').iptype() #8.8.8.8 为公网类型
IP('8.8.8.8').int() #转换为整形格式
IP('8.8.8.8').strHex() #转换为16进制
IP('8.8.8.8').strBin() #转换为2进制
IP(0x8080808) #16进制转换成IP格式

IP方法也支持网络地址的转换,例如根据IP与掩码生产网段格式,如下:

1
2
3
4
5
6
7
>>> from IPy import IP
>>> IP('192.168.1.0').make_net('255.255.255.0')
IP('192.168.1.0/24')
>>> IP('192.168.1.0/255.255.255.0', make_net=True)
IP('192.168.1.0/24')
>>> IP('192.168.1.0-192.168.1.255', make_net=True)
IP('192.168.1.0/24')

也可以通过strNormal方法指定不同wantprefixlen参数值以定制不同输出类型的网段。输出类型为字符串,如下:

1
2
3
4
5
6
7
8
>>>IP('192.168.1.0/24').strNormal(0)
'192.168.1.0'
>>>IP('192.168.1.0/24').strNormal(1)
'192.168.1.0/24'
>>>IP('192.168.1.0/24').strNormal(2)
'192.168.1.0/255.255.255.0'
>>>IP('192.168.1.0/24').strNormal(3)
'192.168.1.0-192.168.1.255'

示例 根据输入的IP或子网返回网络、掩码、广播、反向解析、子网数、IP类型等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python
#-*- coding:utf-8 -*-

from IPy import IP

ip_s = input('请输入一个ip或 网段地址:')
ips = IP(ip_s)
if len(ips) > 1:
print('net: %s' % ips.net()) #输出网络地址
print('netmask: %s' % ips.netmask()) #输出网络地址掩码
print('broadcast: %s' % ips.broadcast()) #输出网络广播地址
print('reverse adress: %s' % ips.reverseName()[0]) #输出地址反向解析
print('subnet: %s' %len(ips)) #输出网络子网数
else: #为单个ip地址
print('reverse adress: %s' % ips.reverseName()[0])
print('十六进制地址:%s' % ips.strHex())
print('二进制地址:%s' % ips.strBin())
print('IP地址类型:%s' % ips.iptype())

dnspython

DNS处理模块
dnspython(http://www.dnspython.org/) 是Python实现的一个DNS工具包,它支持几乎所有的记录类型,可以用于查询、传输并动态更新ZONE信息,同时支持TSIG(事务签名)验证消息和EDNS0(扩展DNS)。在系统管理方面,我们可以利用其查询功能来实现DNS服务监控以及解析结果的校验,可以代替nslookup及dig等工具,轻松做到与现有平台的整合。

项目地址:https://github.com/rthalley/dnspython

安装:

1
pip install dnspython

最新版本 dnspython-1.15.0

dnspython模块提供了大量的DNS处理方法,最常用的方法是域名查询。dnspython提供了一个DNS解析器类—resolver,使用它的query方法来实现域名的查询功能。query方法的定义如下:

1
query(self, qname, rdtype=1, rdclass=1, tcp=False, source=None, raise_on_no_answer=True, source_port=0)
  • qname参数为查询的域名。
  • rdtype参数用来指定RR资源的类型,常用的有以下几种:
    • A记录,将主机名转换成IP地址;
    • MX记录,邮件交换记录,定义邮件服务器的域名;
    • CNAME记录,指别名记录,实现域名间的映射;
    • NS记录,标记区域的域名服务器及授权子域;
    • PTR记录,反向解析,与A记录相反,将IP转换成主机名;
    • SOA记录,SOA标记,一个起始授权区的定义。
  • rdclass参数用于指定网络类型,可选的值有IN、CH与HS,其中IN为默认,使用最广泛。tcp参数用于指定查询是否启用TCP协议,默认为False(不启用)。
  • source与source_port参数作为指定查询源地址与端口,默认值为查询设备IP地址和0。
  • raise_on_no_answer参数用于指定当查询无应答时是否触发异常,默认为True。

A记录查询

1
2
3
4
5
6
7
8
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import dns.resolver
domain = raw_input('Please input an domain: ') #输入域名地址
A = dns.resolver.query(domain, 'A') #指定查询类型为A记录
for i in A.response.answer: #通过response.answer方法获取查询回应信息
print(i)

mx记录查询

1
2
3
4
5
6
7
8
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import dns.resolver
domain = raw_input("Please input an domain:")
MX = dns.resolver.query(domain,'MX')#指定查询记录类型为mx
for i in MX:
print('MX preference=', i.preference, ' mail exchanger=', i.exchange)

NS记录查询

只限输入一级域名,baidu.com。

1
2
3
4
5
6
7
8
9
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import dns.resolver
domain = raw_input("Please input an domain:")
ns = dns.resolver.query(domain,'NS') #指定查询类型为NS记录
for i in ns.response.answer:
for j in i.items:
print j.to_text()

CNAME记录查询

1
2
3
4
5
6
7
8
9
#!/usr/bin/env python
#-*- coding:utf-8 -*-

import dns.resolver
domain = raw_input("Please input an domain:")
cname = dns.resolver.query(domain,'CNAME') #指定查询类型为CNAME记录
for i in cname.response.answer:
for j in i.items:
print j.to_text()

结果返回cname后的目标域名。

DNS域名轮询业务监控

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
#!/usr/bin/env python
# coding=utf-8

import dns.resolver
# import httplib
import http.client

iplist = [] # 定义域名IP列表变量
appdomain = "jingfengjiaoyu.com" # 定义业务域名

def get_iplist(domain=""): # 域名解析函数,解析成功IP将被追加到iplist
try:
A = dns.resolver.query(domain, 'A') # 解析A记录类型
except Exception as e:
print("dns resolver error:" + str(e))
return
for i in A.response.answer:
for j in i.items:
iplist.append(j.address) # 追加到iplist
return True


def checkip(ip):
checkurl = ip + ":80"
getcontent = ""
http.client.socket.setdefaulttimeout(5) # 定义http连接超时时间(5秒)
conn = http.client.HTTPConnection(checkurl) # 创建http连接对象

try:
conn.request("GET", "/", headers={"Host": appdomain}) # 发起URL请求,添
# 加host主机头
r = conn.getresponse()
getcontent = r.read(7) # 获取URL页面前15个字符,以便做可用性校验
print(getcontent)
finally:
if getcontent == "<html>": # 监控URL页的内容一般是事先定义好的,比如 “HTTP200”等
print(ip + " [OK]")
else:
print(ip + " [Error]") # 此处可放告警程序,可以是邮件、短信通知


if __name__ == "__main__":
if get_iplist(appdomain) and len(iplist) > 0: # 条件:域名解析正确且至少返回一个IP
for ip in iplist:
checkip(ip)
else:
print("dns resolver error.")

paramiko

模仿ssh登录执行命

官方网站:http://www.paramiko.org/
项目网站:https://github.com/paramiko/paramiko

安装

1
pip install paramiko

paramiko-2.1.2

连接服务器

用户名和密码连接

1
2
3
4
5
6
7
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) #作用是允许连接不在know_hosts文件中的主机
ssh.connect('139.199.228.59', 22, 'root', '!@#)(*WGK')
stdin,stdout,stderr = ssh.exec_command('df -h\nls')
stdout.read()
ssh.close()

上传和下载文件

1
2
3
4
5
6
7
8
9
10
import paramiko
import os,sys
t = paramiko.Transport(('192.168.17.248',22))
t.connect(username='root',password='123456')
sftp = paramiko.SFTPClient.from_transport(t)
#上传
sftp.put('D:\log.conf','/tmp/log.conf')
#下载
sftp.get('/tmp/ks-script-mZm5Oi','D:\ks-script-mZm5Oi')
t.close()

通过公私钥免密码SSH连接服务器

公私钥生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ssh-keygen -t rsa  # 生成密钥
ssh-copy-id -i ~/ssh/id_rsa.pub root@192.168.17.258 # 将本机的公钥复制到远程机器的authorized_keys文件中,ssh-copy-id也能让你有到远程机器的home, ~./ssh , 和 ~/.ssh/authorized_keys的权利

import paramiko

private_key_path = '/home/auto/.ssh/id_rsa'
key = paramiko.RSAKey.from_private_key_file(private_key_path)

ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('192.168.17.258 ', 22, 'root', key)

stdin, stdout, stderr = ssh.exec_command('df')
print stdout.read()
ssh.close();

paramiko sftp —— SSH上传和下载文件

1
2
3
4
5
6
7
8
9
10
11
12
import paramiko
#建立一个加密的管道
scp=paramiko.Transport(('192.168.0.102', 22))
#建立连接
scp.connect(username='root',password='361way')
#建立一个sftp客户端对象,通过ssh transport操作远程文件
sftp=paramiko.SFTPClient.from_transport(scp)
#Copy a remote file (remotepath) from the SFTP server to the local host
sftp.get('/root/testfile','/tmp/361way')
#Copy a local file (localpath) to the SFTP server as remotepath
sftp.put('/root/crash-6.1.6.tar.gz','/tmp/crash-6.1.6.tar.gz')
scp.close()

一个目录下多个文件上传下载的示例:

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
#!/usr/bin/env python
#-*- coding: utf-8 -*-
import paramiko,datetime,os
hostname='139.199.228.59'
username='root'
password='!@#)(*WGK1'
port=22
local_dir='/tmp/getfile'
remote_dir='/tmp/abc'
try:
t=paramiko.Transport((hostname,port))
t.connect(username=username,password=password)
sftp=paramiko.SFTPClient.from_transport(t)
files=sftp.listdir(remote_dir)
for f in files:
print ''
print '#########################################'
print 'Beginning to download file from %s %s ' % (hostname,datetime.datetime.now())
print 'Downloading file:',os.path.join(remote_dir,f)
sftp.get(os.path.join(remote_dir, f),os.path.join(local_dir, f))#下载
#sftp.put(os.path.join(local_dir, f),os.path.join(remote_dir, f))#上传
print 'Download file success %s ' % datetime.datetime.now()
print ''
print '##########################################'
t.close()
except Exception:
print "connect error!"

注:本处的目录下所有文件进行下载或上传的示例中,在遇到目录下还有嵌套的目录存在时,会将目录也当做文件进行处理,所以如果想要更加的完美的话,可以通过引入stat模块下的S_ISDIR方法进行处理

pexpect

pexpect 是linux下expect的python封装,通过pexpect我们可以实现对ssh、ftp、password、telnet等命令进行自动交互,无需人工达到自动化需求。

项目地址:https://github.com/pexpect/pexpect

安装:

1
pip install pexpect

登陆脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
# coding=utf-8


import pexpect

username = 'root'
ip = '139.199.228.59'
child.interact()mypassword = '!@#)(*WGK1'

child = pexpect.spawn('ssh {0}@{1}'.format(username, ip))
child.expect ('password:')
child.sendline (mypassword)
child.expect('$')
child.sendline('sudo -s')
child.expect (':')
child.sendline (mypassword)
child.expect('#')
child.sendline('ls -la')
child.expect('#')
print child.before # Print the result of the ls command.
child.interact() # Give control of the child to the user.
child.close()

fabric

Fabric是基于python2.5及以上版本实现的SSH命令行工具,简化了SSH应用程序部署及系统管理任务,他提供了系统基础的操作组件,可以实现本地或远程shell命令,包括命令执行、文件上传、下载及完整执行日志输出等功能。Fabric在paramiko的基础上做更高一层的封装,操作起来更加简单。
官网地址:http://www.fabfile.org/
项目地址:https://github.com/fabric/fabric/

2.0 支持 python3

fab的常用参数

1
Usage: fab [options] <command>[:arg1,arg2=val2,host=foo,hosts='h1;h2',...] ...
  • -l,显示定义好的任务函数名;
  • -f,指定fab入口文件,默认入口文件名为fabfile.py
  • -g,指定网关(中转)设备,比如堡垒机环境,填写堡垒机ip即可
  • -H,指定目标主机,多台主机用“,”号分隔;
  • -P,以异步并行方式运行多主机任务,默认为串行运行;
  • -R,指定role,以角色名区分不同业务组设备;
  • -t,设置设备连接超时时间(秒);
  • -T,设置远程主机命令执行超时时间(秒);
  • -w,当命令执行失败,发出告警,而非默认终止任务。

有时候我们可已不需要写一行python代码也可以完成远程操作,直接使用命令的形式,例如

1
fab -p 123456(密码) -H 192.168.1.21,192.168.1.22 -- 'uname -s'

fabric的环境变量

fabric的环境变量有很多,存放在一个字典中, fabric.state.env,而它包含在fabric.api中。 为了方便,我们一般使用env来指代环境变量。 env环境变量可以控制很多fabric的行为,一般通过env.xxx可以进行设置。

fabric默认使用本地用户通过ssh进行连接远程机器,不过你可以通过env.user变量进行覆盖。 当你进行ssh连接时,fabric会让你交互的让你输入远程机器密码,如果你设置了env.password变量,则就不需要交互的输入密码。 下面介绍一些常用的环境变量:

  • abort_on_prompts 设置是否运行在交互模式下,例如会提示输入密码之类,默认是false
  • connection_attempts fabric尝试连接到新服务器的次数,默认1次
  • cwd 目前的工作目录,一般用来确定cd命令的上下文环境
  • disable_known_hosts 默认是false,如果是true,则会跳过用户知道的hosts文件
  • exclude_hosts 指定一个主机列表,在fab执行时,忽略列表中的机器
  • fabfile 默认值是fabfile.py在fab命令执行时,会自动搜索这个文件执行。
  • host_string 当fabric连接远程机器执行run、put时,设置的user/host/port等
  • hosts 一个全局的host列表
  • keepalive 默认0 设置ssh的keepalive
  • loacl_user 一个只读的变量,包含了本地的系统用户,同user变量一样,但是user可以修改
  • parallel 默认false,如果是true则会并行的执行所有的task
  • pool_size 默认0 在使用parallel执行任务时设置的进程数
  • password ssh远程连接时使用的密码,也可以是在使用sudo时使用的密码
  • passwords 一个字典,可以为每一台机器设置一个密码,key是ip,value是密码
  • path 在使用run/sudo/local执行命令时设置的$PATH环境变量
  • port 设置主机的端口
  • roledefs 一个字典,设置主机名到规则组的映射
  • roles 一个全局的role列表
  • shell 默认是/bin/bash -1 -c 在执行run命令时,默认的shell环境
  • skip_bad_hosts 默认false,为ture时,会导致fab跳过无法连接的主机
  • sudo_prefix 默认值”sudo -S -p ‘%(sudo_prompt)s’ “ % env 执行sudo命令时调用的sudo环境
  • sudo_prompt 默认值”sudo password:”
  • timeout 默认10 网络连接的超时时间
  • user ssh使用哪个用户登录远程主机
  • local 执行本地命令,如: local(‘uname -s’)
  • lcd 切换本地目录,如: lcd(‘/home’)
  • cd 切换远程目录,如: cd(‘/data/logs’)
  • run 执行远程命令 如: run(‘free -m’)
  • sudo sudo方式执行命令,如sudo(‘/etc/init.d/httpd start’)
  • put 上传本地文件到远程主机,如put(‘/home/user.info’,/data/user.info’’)
  • get 从远程主机下载文件到本地,如get(‘/home/user.info’,/data/user.info’’)
  • prompt 获得用户输入信息,如: prompt(‘please input user:’)
  • confirm 获得提示信息确认, 如: confirm(‘Tests failed. Continue[Y/N]?’)
  • reboot 重启远程主机,如: reboot();
  • @task 函数修饰符,标识的函数为fab可调用的,非标记对fab不可见,纯业务逻辑。
  • @runs_once, 函数修饰符,标识的函数只会执行一次,不会受多台主机的影响。

学习资源:

http://www.pythondoc.com/pythontutorial3/index.html
http://python3-cookbook.readthedocs.io/zh_CN/latest/index.html

操作系统:mac OSX 10.12 或 centos 7
编辑器: vim、 sublime、atom
要求会翻墙

准备工具

centos:
http://mirror.bit.edu.cn/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-1611.iso

virtualbox:
https://www.virtualbox.org/wiki/Downloads

sublime:
http://www.sublimetext.com/3

SecureCRT:
http://www.xdowns.com/soft/softdown.asp?softid=23625

操作系统

下载 Centos
http://mirror.bit.edu.cn/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-1611.iso

虚拟机

如果不想直接安装Centos,建议使用虚拟机安装

virtualbox

https://www.virtualbox.org/wiki/Downloads
下载完后安装

安装增强功能:

在【设备】中选择安装【安装增强功能】,由于我们安装的是 minimal 最小化的centos,所以我们需要安装一些库来支持插件的安装

1
2
3
yum update # 更新软件
yum -y install gcc kernel-devel kenel-headers make bzip2 # 安装依赖库
reboot # 重启

挂载执行脚本

1
2
3
mount /dev/cdrom /mnt  # 挂载光驱到 mnt 目录下
cd /mnt # 进入到mnt目录
sh ./VBoxLinuxAdditions.run # 执行脚本,进行安装

安装完成后使用 reboot 重启,就已经生效

然后对现有的环境做一次快照,以便日后恢复。

python环境

版本: python3.5.2

centos os 默认使用的是 Python 2.7.5 , 我们需要使用 python3 走在时尚的最前沿,所以需要在系统中安装多个Python,但是又不能影响系统自带的 Python(比如yum 等等系统中好多程序是要依赖于系统本身的python的)。pyenv就是一个Ptyhon版本管理工具

安装工具

1. pyenv

安装 pyenv

linux:

centos 的配置

1
2
3
4
5
6
$ yum install readline readline-devel readline-static -y
$ yum install openssl openssl-devel openssl-static -y
$ yum install sqlite-devel -y
$ yum install bzip2-devel bzip2-libs -y

$ yum install patch vim git

给系统 python 安装 pip

1
2
3
$ yum -y install epel-release  # 安装 epel 扩展源
$ yum -y install python-pip
$ yum clean all # 清除 cache

系统 python 安装 virtualenvwrapper (为了做pyenv切换多个虚拟环境做兼容)

1
2
3
4
$ pip install virtualenvwrapper
$ vim ~/.bashrc
export WORKON_HOME=$HOME/.virtualenvs
source /usr/local/bin/virtualenvwrapper.sh

pyenv 安装

1
2
3
4
5
$ git clone git://github.com/yyuu/pyenv.git ~/.pyenv
$ echo 'export PYENV_ROOT="$HOME/.pyenv"'>> ~/.bashrc # 指明环境变量
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"'>> ~/.bashrc
$ echo 'eval "$(pyenv init -)"' >> ~/.bashrc # 开启shims and autocompletion
$ exec $SHELL -l # 重新启动shell让其生效

mac:

1
2
3
4
5
6
7
8
9
10
11
12
$ brew update
$ brew install pyenv //安装
$ brew upgrade pyenv //升级
$ echo 'export PYENV_ROOT="$HOME/.pyenv"'>> ~/.bash_profile # 指明环境变量
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"'>> ~/.bash_profile
$ echo 'eval "$(pyenv init -)"' >> ~/.bash_profile //只需要执行一次即可
$ vim ~/.bash_profile
if [[ -r /usr/local/bin/virtualenvwrapper.sh ]]; then
source /usr/local/bin/virtualenvwrapper.sh
else
echo "WARNING: Can't find virtualenvwrapper.sh"
fi

或者使用 pyenv-installer 脚本进行安装

查看可安装的版本

1
$ pyenv install --list

安装指定版本

1
$ pyenv install 3.5.2 -v

更新数据库

1
$ pyenv rehash

查看当前已安装的python版本

1
2
3
$ pyenv versions 
* system (set by /Users/ce/workspace/.python-version)
3.5.2

设置全局的python版本

1
2
3
4
$ pyenv global 3.5.2
$ pyenv versions
system
* 3.5.2 (set by /Users/ce/workspace/.python-version)

2. virtualenvwrapper

linux

1
2
$ pip install virtualenvwrapper
$ git clone https://github.com/yyuu/pyenv-virtualenvwrapper.git ~/.pyenv/plugins/pyenv-virtualenvwrapper

mac

1
2
$ pip install virtualenvwrapper
$ brew install pyenv-virtualenvwrapper

使用python3.5创建一个虚拟环境

1
2
3
4
$ mkvirtualenv env352 -p $(which python3.5) # 基于 python3 创建 env352虚拟环境
$ workon env352 # 切换到 env352 环境 (开发时使用)
$ pip install virtualenvwrapper # 为了兼容 pyenv 多虚拟环境
$ pyenv global system # 默认全局的 python 可以切换到 系统的python

项目地址:https://github.com/go-redis/redis

寻找入口

寻找入库,一般从 Quickstart 会给我们很多线索

1
2
3
4
5
6
7
8
9
10
11
func ExampleNewClient() {
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})

pong, err := client.Ping().Result()
fmt.Println(pong, err)
// Output: PONG <nil>
}

我们看到 创建client的时候调用 redis.NewClient 方法,我们不妨寻找一下这个函数

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
$ git clone https://github.com/go-redis/redis.git

$ cd redis

$ grep -r -n "NewClient" *.go [10:17:37]
bench_test.go:12: client := redis.NewClient(&redis.Options{
cluster.go:239: Client: NewClient(opt),
cluster_test.go:64: client := redis.NewClient(&redis.Options{
command_test.go:14: client = redis.NewClient(redisOptions())
commands_test.go:19: client = redis.NewClient(redisOptions())
example_test.go:15: client = redis.NewClient(&redis.Options{
example_test.go:26:func ExampleNewClient() {
example_test.go:27: client := redis.NewClient(&redis.Options{
iterator_test.go:47: client = redis.NewClient(redisOptions())
main_test.go:204: client := redis.NewClient(&redis.Options{
pipeline_test.go:17: client = redis.NewClient(redisOptions())
pool_test.go:17: client = redis.NewClient(redisOptions())
pubsub_test.go:19: client = redis.NewClient(redisOptions())
race_test.go:23: client = redis.NewClient(redisOptions())
race_test.go:114: client = redis.NewClient(redisOptions())
race_test.go:175: client := redis.NewClient(opt)
race_test.go:198: client := redis.NewClient(opt)
redis.go:167:// NewClient returns a client to the Redis Server specified by Options.
redis.go:168:func NewClient(opt *Options) *Client {
redis_test.go:17: client = redis.NewClient(redisOptions())
redis_test.go:40: custom := redis.NewClient(&redis.Options{
redis_test.go:109: db2 := redis.NewClient(&redis.Options{
redis_test.go:141: client = redis.NewClient(&redis.Options{
redis_test.go:195: client = redis.NewClient(redisOptions())
ring.go:157: ring.addClient(name, NewClient(clopt))
tx_test.go:17: client = redis.NewClient(redisOptions())

我们可以看到 redis.go 的168行有 NewClient 函数的定义

1
redis.go:168:func NewClient(opt *Options) *Client {

这样我们找到了一个入口

绘制项目地图

我们找查看一个项目,绘制这个项目的地图很重要

通过 redis.go 168行我们可以看到,输入的参数 Options 和 返回值 Client

我们看下 Optins 的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Options struct {
Network string
Addr string
Dialer func() (net.Conn, error)
Password string
DB int
MaxRetries int
DialTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
PoolSize int
PoolTimeout time.Duration
IdleTimeout time.Duration
IdleCheckFrequency time.Duration
ReadOnly bool
}

都是一些连接是用的参数,往下看有初始化函数

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
func (opt *Options) init() {
if opt.Network == "" {
opt.Network = "tcp"
}
if opt.Dialer == nil {
opt.Dialer = func() (net.Conn, error) {
return net.DialTimeout(opt.Network, opt.Addr, opt.DialTimeout)
}
}
if opt.PoolSize == 0 {
opt.PoolSize = 10
}
if opt.DialTimeout == 0 {
opt.DialTimeout = 5 * time.Second
}
if opt.ReadTimeout == 0 {
opt.ReadTimeout = 3 * time.Second
}
if opt.WriteTimeout == 0 {
opt.WriteTimeout = opt.ReadTimeout
}
if opt.PoolTimeout == 0 {
opt.PoolTimeout = opt.ReadTimeout + time.Second
}
if opt.IdleTimeout == 0 {
opt.IdleTimeout = 5 * time.Minute
}
if opt.IdleCheckFrequency == 0 {
opt.IdleCheckFrequency = time.Minute
}
}

对于参数提供了默认值

然后我们看一下 Clinet 的定义, redis.go 151行 :

1
2
3
4
5
6
type Client struct {
baseClient
cmdable
}

var _ Cmdable = (*Client)(nil)

Client 是由 baseClient 和 cmdable 组合而成。 从命名上来看 baseClient是保存一些基础不变的东西,cmdable可能是负责命令。

1
var _ Cmdable = (*Client)(nil)

这行的意思 Client 要实现 Cmdable 这样的接口,要不然编译器会报错。 从 Client的结构上看只有 cmdble才能实现这样的一个接口

我们看下 Cmdable 的源码:

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
type Cmdable interface {
Pipeline() *Pipeline
Pipelined(fn func(*Pipeline) error) ([]Cmder, error)

Echo(message interface{}) *StringCmd
Ping() *StatusCmd
Quit() *StatusCmd
Del(keys ...string) *IntCmd
Dump(key string) *StringCmd
Exists(key string) *BoolCmd
Expire(key string, expiration time.Duration) *BoolCmd
ExpireAt(key string, tm time.Time) *BoolCmd
Keys(pattern string) *StringSliceCmd
Migrate(host, port, key string, db int64, timeout time.Duration) *StatusCmd
Move(key string, db int64) *BoolCmd
ObjectRefCount(keys ...string) *IntCmd
ObjectEncoding(keys ...string) *StringCmd
ObjectIdleTime(keys ...string) *DurationCmd
Persist(key string) *BoolCmd
PExpire(key string, expiration time.Duration) *BoolCmd
PExpireAt(key string, tm time.Time) *BoolCmd
PTTL(key string) *DurationCmd
RandomKey() *StringCmd
Rename(key, newkey string) *StatusCmd
RenameNX(key, newkey string) *BoolCmd
Restore(key string, ttl time.Duration, value string) *StatusCmd
RestoreReplace(key string, ttl time.Duration, value string) *StatusCmd
Sort(key string, sort Sort) *StringSliceCmd
SortInterfaces(key string, sort Sort) *SliceCmd
TTL(key string) *DurationCmd
Type(key string) *StatusCmd
Scan(cursor uint64, match string, count int64) Scanner
SScan(key string, cursor uint64, match string, count int64) Scanner
HScan(key string, cursor uint64, match string, count int64) Scanner
ZScan(key string, cursor uint64, match string, count int64) Scanner
Append(key, value string) *IntCmd
BitCount(key string, bitCount *BitCount) *IntCmd
BitOpAnd(destKey string, keys ...string) *IntCmd
BitOpOr(destKey string, keys ...string) *IntCmd
BitOpXor(destKey string, keys ...string) *IntCmd
BitOpNot(destKey string, key string) *IntCmd
BitPos(key string, bit int64, pos ...int64) *IntCmd
Decr(key string) *IntCmd
DecrBy(key string, decrement int64) *IntCmd
Get(key string) *StringCmd
GetBit(key string, offset int64) *IntCmd
GetRange(key string, start, end int64) *StringCmd
GetSet(key string, value interface{}) *StringCmd
Incr(key string) *IntCmd
IncrBy(key string, value int64) *IntCmd
IncrByFloat(key string, value float64) *FloatCmd
MGet(keys ...string) *SliceCmd
MSet(pairs ...interface{}) *StatusCmd
MSetNX(pairs ...interface{}) *BoolCmd
Set(key string, value interface{}, expiration time.Duration) *StatusCmd
SetBit(key string, offset int64, value int) *IntCmd
SetNX(key string, value interface{}, expiration time.Duration) *BoolCmd
SetXX(key string, value interface{}, expiration time.Duration) *BoolCmd
SetRange(key string, offset int64, value string) *IntCmd
StrLen(key string) *IntCmd
HDel(key string, fields ...string) *IntCmd
HExists(key, field string) *BoolCmd
HGet(key, field string) *StringCmd
HGetAll(key string) *StringStringMapCmd
HIncrBy(key, field string, incr int64) *IntCmd
HIncrByFloat(key, field string, incr float64) *FloatCmd
HKeys(key string) *StringSliceCmd
HLen(key string) *IntCmd
HMGet(key string, fields ...string) *SliceCmd
HMSet(key string, fields map[string]string) *StatusCmd
HSet(key, field, value string) *BoolCmd
HSetNX(key, field, value string) *BoolCmd
HVals(key string) *StringSliceCmd
BLPop(timeout time.Duration, keys ...string) *StringSliceCmd
BRPop(timeout time.Duration, keys ...string) *StringSliceCmd
BRPopLPush(source, destination string, timeout time.Duration) *StringCmd
LIndex(key string, index int64) *StringCmd
LInsert(key, op string, pivot, value interface{}) *IntCmd
LInsertBefore(key string, pivot, value interface{}) *IntCmd
LInsertAfter(key string, pivot, value interface{}) *IntCmd
LLen(key string) *IntCmd
LPop(key string) *StringCmd
LPush(key string, values ...interface{}) *IntCmd
LPushX(key string, value interface{}) *IntCmd
LRange(key string, start, stop int64) *StringSliceCmd
LRem(key string, count int64, value interface{}) *IntCmd
LSet(key string, index int64, value interface{}) *StatusCmd
LTrim(key string, start, stop int64) *StatusCmd
RPop(key string) *StringCmd
RPopLPush(source, destination string) *StringCmd
RPush(key string, values ...interface{}) *IntCmd
RPushX(key string, value interface{}) *IntCmd
SAdd(key string, members ...interface{}) *IntCmd
SCard(key string) *IntCmd
SDiff(keys ...string) *StringSliceCmd
SDiffStore(destination string, keys ...string) *IntCmd
SInter(keys ...string) *StringSliceCmd
SInterStore(destination string, keys ...string) *IntCmd
SIsMember(key string, member interface{}) *BoolCmd
SMembers(key string) *StringSliceCmd
SMove(source, destination string, member interface{}) *BoolCmd
SPop(key string) *StringCmd
SPopN(key string, count int64) *StringSliceCmd
SRandMember(key string) *StringCmd
SRandMemberN(key string, count int64) *StringSliceCmd
SRem(key string, members ...interface{}) *IntCmd
SUnion(keys ...string) *StringSliceCmd
SUnionStore(destination string, keys ...string) *IntCmd
ZAdd(key string, members ...Z) *IntCmd
ZAddNX(key string, members ...Z) *IntCmd
ZAddXX(key string, members ...Z) *IntCmd
ZAddCh(key string, members ...Z) *IntCmd
ZAddNXCh(key string, members ...Z) *IntCmd
ZAddXXCh(key string, members ...Z) *IntCmd
ZIncr(key string, member Z) *FloatCmd
ZIncrNX(key string, member Z) *FloatCmd
ZIncrXX(key string, member Z) *FloatCmd
ZCard(key string) *IntCmd
ZCount(key, min, max string) *IntCmd
ZIncrBy(key string, increment float64, member string) *FloatCmd
ZInterStore(destination string, store ZStore, keys ...string) *IntCmd
ZRange(key string, start, stop int64) *StringSliceCmd
ZRangeWithScores(key string, start, stop int64) *ZSliceCmd
ZRangeByScore(key string, opt ZRangeBy) *StringSliceCmd
ZRangeByLex(key string, opt ZRangeBy) *StringSliceCmd
ZRangeByScoreWithScores(key string, opt ZRangeBy) *ZSliceCmd
ZRank(key, member string) *IntCmd
ZRem(key string, members ...interface{}) *IntCmd
ZRemRangeByRank(key string, start, stop int64) *IntCmd
ZRemRangeByScore(key, min, max string) *IntCmd
ZRevRange(key string, start, stop int64) *StringSliceCmd
ZRevRangeWithScores(key string, start, stop int64) *ZSliceCmd
ZRevRangeByScore(key string, opt ZRangeBy) *StringSliceCmd
ZRevRangeByLex(key string, opt ZRangeBy) *StringSliceCmd
ZRevRangeByScoreWithScores(key string, opt ZRangeBy) *ZSliceCmd
ZRevRank(key, member string) *IntCmd
ZScore(key, member string) *FloatCmd
ZUnionStore(dest string, store ZStore, keys ...string) *IntCmd
PFAdd(key string, els ...interface{}) *IntCmd
PFCount(keys ...string) *IntCmd
PFMerge(dest string, keys ...string) *StatusCmd
BgRewriteAOF() *StatusCmd
BgSave() *StatusCmd
ClientKill(ipPort string) *StatusCmd
ClientList() *StringCmd
ClientPause(dur time.Duration) *BoolCmd
ClientSetName(name string) *BoolCmd
ConfigGet(parameter string) *SliceCmd
ConfigResetStat() *StatusCmd
ConfigSet(parameter, value string) *StatusCmd
DbSize() *IntCmd
FlushAll() *StatusCmd
FlushDb() *StatusCmd
Info(section ...string) *StringCmd
LastSave() *IntCmd
Save() *StatusCmd
Shutdown() *StatusCmd
ShutdownSave() *StatusCmd
ShutdownNoSave() *StatusCmd
SlaveOf(host, port string) *StatusCmd
Time() *TimeCmd
Eval(script string, keys []string, args ...interface{}) *Cmd
EvalSha(sha1 string, keys []string, args ...interface{}) *Cmd
ScriptExists(scripts ...string) *BoolSliceCmd
ScriptFlush() *StatusCmd
ScriptKill() *StatusCmd
ScriptLoad(script string) *StringCmd
DebugObject(key string) *StringCmd
PubSubChannels(pattern string) *StringSliceCmd
PubSubNumSub(channels ...string) *StringIntMapCmd
PubSubNumPat() *IntCmd
ClusterSlots() *ClusterSlotsCmd
ClusterNodes() *StringCmd
ClusterMeet(host, port string) *StatusCmd
ClusterForget(nodeID string) *StatusCmd
ClusterReplicate(nodeID string) *StatusCmd
ClusterResetSoft() *StatusCmd
ClusterResetHard() *StatusCmd
ClusterInfo() *StringCmd
ClusterKeySlot(key string) *IntCmd
ClusterCountFailureReports(nodeID string) *IntCmd
ClusterCountKeysInSlot(slot int) *IntCmd
ClusterDelSlots(slots ...int) *StatusCmd
ClusterDelSlotsRange(min, max int) *StatusCmd
ClusterSaveConfig() *StatusCmd
ClusterSlaves(nodeID string) *StringSliceCmd
ClusterFailover() *StatusCmd
ClusterAddSlots(slots ...int) *StatusCmd
ClusterAddSlotsRange(min, max int) *StatusCmd
GeoAdd(key string, geoLocation ...*GeoLocation) *IntCmd
GeoPos(key string, members ...string) *GeoPosCmd
GeoRadius(key string, longitude, latitude float64, query *GeoRadiusQuery) *GeoLocationCmd
GeoRadiusByMember(key, member string, query *GeoRadiusQuery) *GeoLocationCmd
GeoDist(key string, member1, member2, unit string) *FloatCmd
GeoHash(key string, members ...string) *StringSliceCmd
Command() *CommandsInfoCmd
}

我们看到了 Cmdable 这个接口其实就是实现了 redis 命令的封装。

我们接着看 baseClient

1
2
3
4
5
6
type baseClient struct {
connPool pool.Pooler
opt *Options

onClose func() error // hook called when client is closed
}

baseClient 引用了连接池,

我们接着往下看 redis.go 这个文件

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
func (c *baseClient) Process(cmd Cmder) error {
for i := 0; i <= c.opt.MaxRetries; i++ {
if i > 0 {
cmd.reset()
}

cn, _, err := c.conn()
if err != nil {
cmd.setErr(err)
return err
}

readTimeout := cmd.readTimeout()
if readTimeout != nil {
cn.ReadTimeout = *readTimeout
} else {
cn.ReadTimeout = c.opt.ReadTimeout
}
cn.WriteTimeout = c.opt.WriteTimeout

if err := writeCmd(cn, cmd); err != nil {
c.putConn(cn, err, false)
cmd.setErr(err)
if err != nil && internal.IsRetryableError(err) {
continue
}
return err
}

err = cmd.readReply(cn)
c.putConn(cn, err, readTimeout != nil)
if err != nil && internal.IsRetryableError(err) {
continue
}

return err
}

return cmd.Err()
}

发现 baseClient 有个 Process 方法 这个应该是处理执行的过程 需要我们注意。

根据广度优先原则,我们再看 Client 上还有什么关联的内容

我们可以看到 redis.go 这个文件里面 还包含了 Pipeline 和 pubSub 两个函数

1
2
3
4
5
func (c *Client) Pipeline() *Pipeline {
}

func (c *Client) pubSub() *PubSub {
}

然后我们再看下,项目文件,还有什么我们并没有涉及到的

1
2
3
4
5
6
7
 $ ls                                                                                                                     
CHANGELOG.md cluster_client_test.go doc.go main_test.go pubsub.go ring.go tx.go
LICENSE cluster_test.go example_test.go options.go pubsub_test.go ring_test.go tx_test.go
Makefile command.go export_test.go parser.go race_test.go script.go
README.md command_test.go internal/ pipeline.go redis.go sentinel.go
bench_test.go commands.go iterator.go pipeline_test.go redis_test.go sentinel_test.go
cluster.go commands_test.go iterator_test.go pool_test.go result.go testdata/

答案就是 cluster,cluster 是连接 redis 集群的方式,会提供给用户使用

我们详细看一下 cluster 源码,cluster 里面有自己的 Options,值得注意的是,有 Node 逻辑

1
2
3
4
5
type clusterNode struct {
Client *Client
Latency time.Duration
loading time.Time
}

使用组合的方式把 Client 包装进来。

1
2


在web开发过程中,有些时候需要隐藏你的静态文件url,也有些时候需要频繁更替你的静态资源服务商,如果你的静态资源服务商(七牛、又拍云)发生了变更,然后你又要修改对外暴漏的url是非常不方便的。这时候我们使用一个统一的自己能控制的对外url对于我们非常方便,也算是变相的对外解耦,是个非常有用的办法。

基于以上的想法,我们来进行技术选型,由于现在大家写web都脱离不了nginx,所以就想在nginx层面上解决这个问题。不出所料,nginx 里的 x-accell-redirect 就可以实现此功能。下面我们使用nginx和django来实现此功能。

配置nginx

修改 nginx.conf 新增 location

1
2
3
4
5
6
location ~* ^/protected/ {
internal; # 只允许内部重定向
rewrite ^/protected/(.*?)\|(.*) /$2 break;
proxy_set_header Authorization $1; # 鉴权的东西放到header中 如果没有鉴权可以去掉,$1的值是服务端传过来的,详见后面
proxy_pass http://images.com; # 你的静态文件地址
}

保存,reload 。

django view

这里面使用了 django-rest-framework 来进行API的开发

1
2
3
4
5
6
7
8
class NginxAccel(APIView):

def get(self, request, img_name):
auth = image_auth() # 类似私有空间的验证
response = Response()
response["X-Accel-Redirect"] = "/protected/{0}|{1}".format(
auth, img_name)
return response

然后通过url来访问,鉴权成功,就可以访问到你要的图片。如果不需要鉴权,渠道auth的相关代码就可以。


现在开发环境原来越复杂,为了方便开发,让团队每个人的环境一致,最近使用docker进行打包image,发放给团队使用。

安装docker

本人使用mac,直接下载docker for mac 方便很多,其他os网上一搜一大把。so easy~!

下载images

1
2
3
4
5
6
[docker@docker-root ~]$ sudo docker pull alpine
[docker@docker-root ~]$ sudo docker pull ubuntu
[docker@docker-root ~]$ sudo docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
alpine latest 3e467a6273a3 2 days ago 4.793 MB
ubuntu latest 17b6a9e179d7 5 days ago 120.7 MB

启动

1
docker run -ti -h dev --net=host -v ~/workspace/sohu:/root/workspace -w /root develop:base /bin/bash

把本地目录 ~/workspace/sohu 映射到容器 /root/workspace 目录

自定义images

先更新下源,安装几个必备工具

1
2
apt-get update
apt-get install vim curl git wget

修改阿里云源

修改成阿里云源,加快安装软件的速度

1
2
3
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak #备份
sudo vim /etc/apt/sources.list #修改
sudo apt-get update #更新列表

阿里云源

1
2
3
4
5
6
7
8
9
10
deb http://mirrors.aliyun.com/ubuntu/ xenial main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ xenial main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse

python开发环境

安装工具和必备依赖

1
2
3
4
5
6
7
apt-get install gcc gdb binutils make git dstat sysstat htop curl wget
apt-get install libjpeg-dev
apt-get install net-tools
apt-get install libffi-dev
apt-get install bzip2
apt-get install libssl
apt-get install libssl-dev

如需要sqlit支持需要先装如下库,再安装python:

1
sudo apt-get install sqlite3 libsqlite3-dev

pyenv 安装

1
$ curl -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash

在 ~/.bashrc 中添加

1
2
3
export PATH="/root/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

查看可安装的版本

1
$ pyenv install --list

安装指定版本

1
$ pyenv install 3.5.2 -v

更新数据库

1
$ pyenv rehash

查看当前已安装的python版本

1
2
3
4
$ pyenv versions 
* system (set by /Users/ce/workspace/sohu/.python-version)
3.5.1
sohu351

设置全局的python版本

1
2
3
4
$ pyenv global 3.5.1
$ pyenv versions
system
* 3.5.1 (set by /Users/ce/workspace/sohu/.python-version)

安装virtualenv

1
2
$ pyenv global system  切换到系统python
$ pip install virtualenv

安装virtualenvwrapper

安装 virtualenvwrapper 并让pyenv支持

1
2
$ pip install virtualenvwrapper
$ git clone https://github.com/yyuu/pyenv-virtualenvwrapper.git ~/.pyenv/plugins/pyenv-virtualenvwrapper

使用python3.5创建一个虚拟环境

1
2
3
$ pyenv global 3.5.2
$ mkvirtualenv env2
$ workon env2

Go开发环境

编译go1.3需要的参数

1
CGO_ENABLED=0 ./make.bash

ToDo……

image 字符集修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ export LANG="en_US.UTF-8"

$ sudo locale-gen "en_US.UTF-8"
Generating locales...
en_US.UTF-8... done
Generation complete.

$ sudo dpkg-reconfigure locales
Generating locales...
en_US.UTF-8... up-to-date
Generation complete.

$ locale charmap
UTF-8

syslog

如果使用syslog,需要在启动的时候做一次目录映射

1
-v /dev/log:/dev/log

保存images

1
2
3
$ docker ps                                                                                                              [17:06:10]
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
111be6691cc8 c5940ba1089c "/bin/bash" 4 days ago Up 9 hours dev

把 image 中 c5940ba1089c 这个字段记住

1
docker commit c5940ba1089c develop:base  #进行保存

进行查看

1
2
3
4
5
6
 $ docker images                                                                                                          
REPOSITORY TAG IMAGE ID CREATED SIZE
develop base 5a893de95205 40 hours ago 1.87 GB
ubuntu latest 42118e3df429 9 weeks ago 124.8 MB
alpine latest 4e38e38c8ce0 3 months ago 4.799 MB
hello-world latest 690ed74de00f 11 months ago 960 B

私有仓库

查看如下文章 :

创建私有仓库