Python学习笔记

来源:互联网 发布:手机在线网页源码 编辑:程序博客网 时间:2024/06/08 10:41

Python语言简介

Python是著名的“龟叔”Guido van Rossum在1989年圣诞节期间,为打发无聊的圣诞节而编写的一种高级编程语言。完成同一个任务,C语言要写1000行代码,Java可能只需要写100行,而Python可能只要20行。

Python提供了非常完善的基础代码库,覆盖了网络、文件、GUI、数据库、文本等大量内容,被形象地称作“内置电池(batteries included)”。用Python开发,许多功能不必从零编写,直接使用现成的即可。除了内置的库外,Python还有大量的第三方库,也就是别人开发的,供你直接使用的东西。当然,如果你开发的代码通过很好的封装,也可以作为第三方库给别人使用。

总的来说,Python的哲学就是简单优雅,尽量写容易看明白的代码,尽量写少的代码。那么Python适合开发哪些类型的应用呢?

  • 网络应用,包括网站、后台服务等等
  • 日常需要的小工具,包括系统管理员需要的脚本任务等等
  • 把其他语言开发的程序再包装起来,方便使用

最后说说Python的缺点。第一个缺点就是运行速度慢,和C程序相比非常慢,因为Python是解释型语言,代码在执行时会一行一行地翻译成CPU能理解的机器码,这个翻译过程非常耗时;而C程序是运行前直接编译成CPU能执行的机器码,所以非常快。第二个缺点就是代码不能加密。如果要发布你的Python程序,实际上就是发布源代码。这一点跟C语言不同,C语言不用发布源代码,只需要把编译后的机器码(也就是你在Windows上常见的xxx.exe文件)发布出去。要从机器码反推出C代码是不可能的,所以,凡是编译型的语言,都没有这个问题,而解释型的语言,则必须把源码发布出去。

Linux系统下Python安装与运行

我使用的是Ubuntu 17.04版本,已经内嵌Python 2.7版本。获取较新的Python3版本,可使用如下命令:

sudo apt-get install python3

编译Python文件,只需命令行命令行进入文件所在目录,直接输入Python3 文件名 即可

Python简易上手:输入输出

让Python打印出指定的文字,可以用print()函数,然后把希望打印的文字用单引号或者双引号括起来,但不能混用单引号和双引号

print('hello world!')

如果要让用户从电脑输入一些字符,Python提供了一个input(),可以让用户输入字符串,并存放到一个变量里。

varName = input()

Python基本语法

Python的语法比较简单,采用缩进方式,写出来的代码风格如下:

a = 100if a >= 0:    print(a)else:    print(-a)

另外,以#开头的语句是注释。
缩进有利有弊。好处是强迫你写出格式化的代码。按照惯例,应该始终坚持使用4个空格的缩进; 另一个好处是强迫你写出缩进较少的代码,你会倾向于把一段很长的代码拆分成若干函数,从而得到缩进较少的代码。缩进的坏处就是“复制-粘贴”功能失效了。当你重构代码时,粘贴过去的代码必须重新检查缩进是否正确。此外,IDE很难像格式化Java代码那样格式化Python代码。
最后,Python程序是大小写敏感的,如果写错了大小写,程序会报错。

数据类型和变量

常见数据类型与C,JAVA类似,包括整数、浮点数、字符串、布尔变量等。此外Python中存在一个空值,用None表示,不能理解为0。

Python变量不仅可以是数字,还可以是任意数据类型,在程序中就是用一个变量名表示了,变量名必须是大小写英文、数字和_的组合,且不能用数字开头。
在Python中,等号=是赋值语句,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量,例如:

a = 123 # a是整数print(a)a = 'ABC' # a变为字符串print(a)

在Python中,通常用全部大写的变量名表示常量。例如PI = 3.14159265359。但事实上PI仍然是一个变量,Python根本没有任何机制保证PI不会被改变,所以,用全部大写的变量名表示常量只是一个习惯上的用法,如果一定要改变变量PI的值,也没人能拦住你。

Python字符串

在最新的Python 3版本中,字符串是以Unicode编码的,也就是说,Python的字符串支持多语言。

对于单个字符的编码,Python提供了ord()函数获取字符的整数表示,chr()函数把编码转换为对应的字符。由于Python的字符串类型是str,在内存中以Unicode表示,一个字符对应若干个字节。如果要在网络上传输,或者保存到磁盘上,就需要把str变为以字节为单位的bytes。反过来,如果我们从网络或磁盘上读取了字节流,那么读到的数据就是bytes。要把bytes变为str,就需要用decode()方法。

Python对bytes类型的数据用带b前缀的单引号或双引号表示:

x = b'ABC'

注意区分'ABC'b'ABC',前者是str,后者虽然内容显示得和前者一样,但bytes的每个字符都只占用一个字节。

要计算str包含多少个字符,使用len()函数计算的,如果换成bytes,len()函数就计算字节数

len('ABC') #返回3len('中文'.encode('utf-8')) #返回6

在Python中,采用的格式化方式和C语言是一致的,用%实现。在字符串内部,%s表示用字符串替换,%d表示用整数替换,有几个%?占位符,后面就跟几个变量或者值,顺序要对应好。如果只有一个%?,括号可以省略。如果字符串里面的%是一个普通字符,就需要转义,用%%来表示一个%。

'%.2f' % 3.1415926 #输出3.14

list和tuple

list是一种有序集合,可随时添加和删除其中的元素,形如:

[a, b, c, d]

用索引来访问list中每一个位置的元素,且索引是从0开始。当索引超出了范围时,Python会报一个IndexError错误,要确保索引不要越界,记得最后一个元素的索引是len(classmates) - 1。

如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素。以此类推,可以获取倒数第2个、倒数第3个。

往list中追加元素到末尾,使用append() 方法;删除末尾元素用pop()方法。两个方法均可以传入索引参数来对具体位置的元素进行操作。另外,可以直接对某个元素进行赋值,来实现修改。



tuple和list非常类似,也是一个有序列表,但是tuple一旦初始化就不能修改,即没有append()pop() 等方法,形如:

(a, b, c, d)

只有1个元素的tuple定义时必须加一个逗号,来消除歧义:(1, )

条件判断

Python种条件判断格式如下:

if <条件判断1>:    <执行1>elif <条件判断2>:    <执行2>elif <条件判断3>:    <执行3>else:    <执行4>

循环

Python的循环有两种。
一种是for…in循环,依次把list或tuple中的每个元素迭代出来:

names = ['Michael', 'Bob', 'Tracy']for name in names:    print(name)

在这里,生成一个整数范围的list有技巧,使用range()函数,可以生成一个整数序列,再通过list()函数可以转换为list。比如range(5)生成的序列是从0开始小于5的整数。

另一种是while循环,只要条件满足,就不断循环,条件不满足时退出循环:

sum = 0n = 99while n > 0:    sum = sum + n    n = n - 2print(sum)

dict和set

Python内置了字典:dict的支持,在其他语言中也称为map,使用键-值(key-value)存储,具有极快的查找速度。这种key-value存储方式,在放进去的时候,必须根据key算出value的存放位置,这样,取的时候才能根据key直接拿到value.

把数据放入dict的方法,除了初始化时指定外,还可以通过key放入:

 d['Adam'] = 67

由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉。

如果key不存在,dict就会报错。要避免key不存在的错误,有两种办法,一是通过in判断key是否存在;二是通过dict提供的get方法,如果key不存在,可以返回None,或者自己指定的value:

'Thomas' in d    #ord.get('Thomas', -1) #返回-1

要删除一个key,用pop(key)方法,对应的value也会从dict中删除。

和list比较,dict有以下几个特点:

  • 查找和插入的速度极快,不会随着key的增加而变慢;
  • 需要占用大量的内存,内存浪费多。

所以,dict是用空间来换取时间的一种方法。



set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key。

创建一个set,需要提供一个list作为输入集合:

s = set([1, 2, 3])

通过add(key)方法可以添加元素到set中,可以重复添加,但不会有效果。通过remove(key)方法可以删除元素。

函数定义

在Python中,定义一个函数要使用def语句,依次写出函数名、括号、括号中的参数和冒号:,然后,在缩进块中编写函数体,函数的返回值用return语句返回。以自定义一个求绝对值的my_abs函数为例:

def my_abs(x):    if x >= 0:        return x    else:        return -x

可以定义一个什么也不做的空函数,使用pass语句:

def nop():    pass

在函数调用检查参数时,如果参数个数不对,Python解释器会自动检查;如果参数类型不对,Python解释器就无法帮我们检查。所以,应该尽量保证函数定义足够完善,即在函数中加入对传入参数的有效性判断。

函数可以返回“多个值”,参考以下实例:

import mathdef move(x, y, step, angle=0):    nx = x + step * math.cos(angle)    ny = y - step * math.sin(angle)    return nx, nyx, y = move(100, 100, 60, math.pi / 6)print(x, y) #输出 151.96152422706632 70.0r = move(100, 100, 60, math.pi / 6)print(r) #输出 (151.96152422706632, 70.0)

可见,返回值是一个tuple。但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便。

函数参数

默认参数

使用默认参数,来简化函数的调用过程。例如计算函数的次幂,可以使用如下函数:

def power(x, n):    s = 1    while n > 0:        n = n - 1        s = s * x    return s

假如我们经常计算x的平方,便完全可以把第二个参数n的默认值设定为2,即def power(x, n=2)。这样,当我们调用power(x)时,相当于调用power(x, 2)。

设置默认参数时,有几点要注意:一是必选参数在前,默认参数在后,否则Python的解释器会报错;二是当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

可变参数

在Python函数中,还可以定义可变参数。顾名思义,可变参数就是传入的参数个数是可变的。参考以下函数定义:

def calc(*numbers):    sum = 0    for n in numbers:        sum = sum + n * n    return sum

定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:

calc(1, 2) #返回5calc() #返回0

如果已经有一个list或者tuple,Python允许在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:

nums = [1, 2, 3]calc(*nums)

关键字参数

而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict:

def person(name, age, **kw):    print('name:', name, 'age:', age, 'other:', kw)person('Michael', 30) #返回name: Michael age: 30 other: {}person('Adam', 45, gender='M', job='Engineer') #返回name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}

关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。**kw获得的是一个dict。

命名关键字参数

如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:

def person(name, age, *, city, job):    print(name, age, city, job)

和关键字参数**kw不同,命名关键字参数需要一个特殊星号分隔符,后面的参数被视为命名关键字参数。如果缺少,Python解释器将无法识别位置参数和命名关键字参数:

person('Jack', 24, city='Beijing', job='Engineer')#返回Jack 24 Beijing Engineer

命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错。

参数组合

参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

递归函数

在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数。计算阶乘n! = 1 x 2 x 3 x … x n,用函数fact(n)表示如下:

def fact(n):    if n==1:        return 1    return n * fact(n - 1)

递归函数的优点是定义简单,逻辑清晰。但使用递归函数需要注意防止栈溢出。

在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。

解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。例如针对此前的程序,可修改成:

def fact(n):    return fact_iter(n, 1)def fact_iter(num, product):    if num == 1:        return product    return fact_iter(num - 1, num * product)

return fact_iter(num - 1, num * product)仅返回递归函数本身,num - 1和num * product在函数调用前就会被计算,不影响函数调用。

遗憾的是,大多数编程语言没有针对尾递归做优化,Python解释器也没有做优化,所以,即使把上面的fact(n)函数改成尾递归方式,也会导致栈溢出。

Python高级特性

切片

取一个list或tuple的部分元素是非常常见的操作。

L[a:b]表示,从索引a开始取,直到索引b为止,但不包括索引b。即索引a,a+1,a+2,….b-a+1

比如,一个list如下:

 L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack'] L[0:3] #返回['Michael', 'Sarah', 'Tracy']

第一个索引是0,还可以省略。因此L[:3] 和 L[0:3]效果一致。

类似的,既然Python支持L[-1]取倒数第一个元素,那么它同样支持倒数切片,倒数第一个元素的索引是-1:

 L[-2:] #返回['Bob', 'Jack'] L[-2:-1] #返回['Bob']

甚至什么都不写,只写[:]就可以原样复制一个list。

如果是L[a:b:c],则说明ab区间里,每c个数取一个数。例如集合L为0~99所有整数,每5个取一个:

L[::5]#返回[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]

tuple也是一种list,唯一区别是tuple不可变。因此,tuple也可以用切片操作,只是操作的结果仍是tuple:

(0, 1, 2, 3, 4, 5)[:3] # 返回(0, 1, 2)

字符串’xxx’也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:

'ABCDEFG'[:3] #返回'ABC'

迭代

在Python中,迭代是通过for … in来完成的。Python的for循环不仅可以用在list或tuple上,还可以作用在其他可迭代对象上。只要是可迭代对象,无论有无下标,都可以迭代,比如dict就可以迭代:

d = {'a': 1, 'b': 2, 'c': 3}for key in d:    print(key)#返回acb

因为dict的存储不是按照list的方式顺序排列,所以,迭代出的结果顺序很可能不一样。

默认情况下,dict迭代的是key。如果要迭代value,可以用for value in d.values(),如果要同时迭代key和value,可以用for k, v in d.items()。

如何判断一个对象是可迭代对象呢?方法是通过collections模块的Iterable类型判断:

from collections import Iterableisinstance('abc', Iterable) # str是否可迭代,返回Trueisinstance([1,2,3], Iterable) # list是否可迭代,返回Trueisinstance(123, Iterable) # 整数是否可迭代,返回False

如果要对list实现类似Java那样的下标循环怎么办?Python内置的enumerate函数可以把一个list变成索引-元素对,这样就可以在for循环中同时迭代索引和元素本身:

for i, value in enumerate(['A', 'B', 'C']):    print(i, value)#返回0 A1 B2 C

列表生成式

列表生成式即List Comprehensions,是Python内置的非常简单却强大的可以用来创建list的生成式。

要生成[1x1, 2x2, 3x3, …, 10x10]:

[x * x for x in range(1, 11)]

把要生成的元素x * x放到前面,后面跟for循环,就可以把list创建出来。

for循环后面还可以加上if判断,这样我们就可以筛选出仅偶数的平方:

[x * x for x in range(1, 11) if x % 2 == 0]

还可以使用两层循环,可以生成全排列:

[m + n for m in 'ABC' for n in 'XYZ'] #返回['AX', 'AY', 'AZ', 'BX', 'BY', 'BZ', 'CX', 'CY', 'CZ']

生成器

通过列表生成式,我们可以直接创建一个列表。但是列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:

L = [x * x for x in range(10)]#返回[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]g = (x * x for x in range(10))#返回<generator object <genexpr> at 0x1022ef630>

使用for循环,迭代generator对象:

for n in g:     print(n)#返回0149162536496481

如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。以生成斐波那契数列为例:

def fib(max):    n, a, b = 0, 0, 1    while n < max:        yield b        a, b = b, a + b        n = n + 1    return 'done'

函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

迭代器

可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator。

生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator。

Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

Python的for循环本质上就是通过不断调用next()函数实现的。例如:

for x in [1, 2, 3, 4, 5]:    pass

等价于

# 首先获得Iterator对象:it = iter([1, 2, 3, 4, 5])# 循环:while True:    try:        # 获得下一个值:        x = next(it)    except StopIteration:        # 遇到StopIteration就退出循环        break

函数式编程

高阶函数

函数本身也可以赋值给变量,即:变量可以指向函数。

x = abs(-10)f = abs#此时f为<built-in function abs>f(-10) #返回10

对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数。

既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。一个简单的高阶函数示例:

def add(x, y, f):    return f(x) + f(y)

当我们调用add(-5, 6, abs)时,参数x,y和f分别接收-5,6和abs,根据函数定义,我们可以推导计算过程为:

x = -5y = 6f = absf(x) + f(y) ==> abs(-5) + abs(6) ==> 11return 11

这与实际调用结果一致。

map/reduce

map()函数接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。

比如我们有一个函数f(x)=x2,要把这个函数作用在一个list [1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map()实现如下:

 def f(x):     return x * x     r = map(f, [1, 2, 3, 4, 5, 6, 7, 8, 9]) list(r) #返回[1, 4, 9, 16, 25, 36, 49, 64, 81]



reduce把一个函数作用在一个序列[x1, x2, x3, …]上,这个函数必须接收两个参数,reduce把结果继续和序列的下一个元素做累积计算,其效果为:

reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4)

比方说对一个序列求和,就可以用reduce实现:

from functools import reducedef add(x, y):    return x + yreduce(add, [1, 3, 5, 7, 9]) #返回25

如果要把序列[1, 3, 5, 7, 9]变换成整数13579,同样使用reduce:

from functools import reducedef fn(x, y):    return x * 10 + ydef char2num(s):    return {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9}[s]reduce(fn, map(char2num, '13579')) #返回13579

filter

和map()类似,filter()也接收一个函数和一个序列。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是True还是False决定保留还是丢弃该元素。

例如,在一个list中,删掉偶数,只保留奇数,可以这么写:

def is_odd(n):    return n % 2 == 1list(filter(is_odd, [1, 2, 4, 5, 6, 9, 10, 15]))# 返回 [1, 5, 9, 15]

sorted

Python内置的sorted()函数就可以对list进行排序:

sorted([36, 5, -12, 9, -21]) #返回[-21, -12, 5, 9, 36]

此外,sorted()函数也是一个高阶函数,它还可以接收一个key函数来实现自定义的排序,例如按绝对值大小排序:

sorted([36, 5, -12, 9, -21], key=abs) #返回[5, 9, -12, -21, 36]

实现忽略大小写的排序:

sorted(['bob', 'about', 'Zoo', 'Credit'], key=str.lower)#返回['about', 'bob', 'Credit', 'Zoo']

返回函数

高阶函数除了可以接受函数作为参数外,还可以把函数作为结果值返回。

如果不需要立刻求和,而是在后面的代码中,根据需要再计算怎么办?可以不返回求和的结果,而是返回求和的函数:

def lazy_sum(*args):    def sum():        ax = 0        for n in args:            ax = ax + n        return ax    return sum

当我们调用lazy_sum()时,返回的并不是求和结果,而是求和函数。调用函数f时,才真正计算求和的结果:

f = lazy_sum(1, 3, 5, 7, 9)#返回 <function lazy_sum.<locals>.sum at 0x101c6ed90>f() #返回25

在这个例子中,我们在函数lazysum中又定义了函数sum,并且,内部函数sum可以引用外部函数lazy_sum的参数和局部变量,当lazysum返回函数sum时,相关参数和变量都保存在返回的函数中。这种结构称为闭包。

匿名函数

有些时候,不需要显式地定义函数,直接传入匿名函数更方便。

还是以map()函数为例,计算f(x)=x2时,除了定义一个f(x)的函数外,还可以直接传入匿名函数:

list(map(lambda x: x * x, [1, 2, 3, 4, 5, 6, 7, 8, 9]))#返回[1, 4, 9, 16, 25, 36, 49, 64, 81]

匿名函数有个限制,就是只能有一个表达式,不用写return,返回值就是该表达式的结果;好处则是,因为函数没有名字,不必担心函数名冲突。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:

f = lambda x: x * xf  #返回<function <lambda> at 0x101c6ef28>f(5) #返回25

装饰器

在代码运行期间动态增加功能的方式,称之为“装饰器”。在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。

偏函数

当函数的参数个数太多,需要简化时,使用functools.partial,把一个函数的某些参数给固定住(也就是设置默认值),返回一个新的函数,调用这个新函数会更简单。

int()函数可以把字符串转换为整数,提供额外的base参数,默认值为10。如果传入base参数,就可以做N进制的转换。假设要转换大量的二进制字符串,每次都传入int(x, base=2)非常麻烦,于是,我们想到,可以定义一个int2()的函数,默认把base=2传进去:

def int2(x, base=2):    return int(x, base)

这样转换二进制就很方便了。

functools.partial就是帮助我们创建一个偏函数的,不需要我们自己定义int2(),可以直接使用下面的代码创建一个新的函数int2:

import functoolsint2 = functools.partial(int, base=2)int2('1000000') #返回64

创建偏函数时,实际上可以接收函数对象、*args和**kw这3个参数。当传入:

int2 = functools.partial(int, base=2)

实际上固定了int()函数的关键字参数base。相当于:

kw = { 'base': 2 }int('10010', **kw)

模块

为了编写可维护的代码,我们把很多函数分组,分别放到不同的文件里,这样,每个文件包含的代码就相对较少,很多编程语言都采用这种组织代码的方式。在Python中,一个.py文件就称之为一个模块(Module)。

使用模块最大的好处是大大提高了代码的可维护性。其次,编写代码不必从零开始。当一个模块编写完毕,就可以被其他地方引用。我们在编写程序的时候,也经常引用其他模块,包括Python内置的模块和来自第三方的模块。

使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中,因此,我们自己在编写模块时,不必考虑名字会与其他模块冲突。

如果不同的人编写的模块名相同怎么办?为了避免模块名冲突,Python又引入了按目录来组织模块的方法,称为包(Package)。引入了包以后,只要顶层的包名不与别人冲突,那所有模块都不会与别人冲突。请注意,每一个包目录下面都会有一个 __init__.py的文件,这个文件是必须存在的,否则,Python就把这个目录当成普通目录,而不是一个包。

使用模块

Python本身就内置了很多非常有用的模块,只要安装完毕,这些模块就可以立刻使用。以内建的sys模块为例,编写一个hello的模块:

#!/usr/bin/env python3# -*- coding: utf-8 -*-' a test module '__author__ = 'Michael Liao'import sysdef test():    args = sys.argv    if len(args)==1:        print('Hello, world!')    elif len(args)==2:        print('Hello, %s!' % args[1])    else:        print('Too many arguments!')if __name__=='__main__':    test()

第1行和第2行是标准注释,第1行注释可以让这个hello.py文件直接在Unix/Linux/Mac上运行,第2行注释表示.py文件本身使用标准UTF-8编码;第4行是一个字符串,表示模块的文档注释,任何模块代码的第一个字符串都被视为模块的文档注释;第6行使用__author__变量把作者写进去,这样当你公开源代码后别人就可以瞻仰你的大名。以上就是Python模块的标准文件模板。

使用sys模块的第一步,就是导入该模块:

import sys

导入sys模块后,我们就有了变量sys指向该模块,利用sys这个变量,就可以访问sys模块的所有功能。

sys模块有一个argv变量,用list存储了命令行的所有参数。argv至少有一个元素,因为第一个参数永远是该.py文件的名称

最后两行代码中,当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。

命令行运行py文件:

$ python3 hello.pyHello, world!$ python hello.py MichaelHello, Michael!

如果启动Python交互环境,再导入hello模块:

import hellohello.test()


在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_前缀来实现的。Python并没有一种方法可以完全限制访问private函数或变量,但是,从编程习惯上不应该引用private函数或变量。

在Python中,安装第三方模块,是通过包管理工具pip完成的。

面向对象编程

面向对象的设计思想是从自然界中来的,因为在自然界中,类(Class)和实例(Instance)的概念是很自然的。Class是一种抽象概念,比如我们定义的Class——Student,是指学生这个概念,而实例(Instance)则是一个个具体的Student。

class后面紧接着是类名,即Student,类名通常是大写开头的单词,紧接着是(object),表示该类是从哪个类继承下来的。

在创建实例的时候,把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法,在创建实例的时候,就把name,score等属性绑上去:

class Student(object):    def __init__(self, name, score):        self.name = name        self.score = score

面向对象编程的一个重要特点就是数据封装。在上面的Student类中,每个实例就拥有各自的name和score这些数据。我们可以通过函数来访问这些数据,比如打印一个学生的成绩:

def print_score(std):    print('%s: %s' % (std.name, std.score))

在Class内部,可以有属性和方法,而外部代码可以通过直接调用实例变量的方法来操作数据,这样,就隐藏了内部的复杂逻辑。但是,从前面Student类的定义来看,外部代码还是可以自由地修改一个实例的name、score属性:如果要让内部属性不被外部访问,可以把属性的名称前加上两个下划线__,在Python中,实例的变量名如果以__开头,就变成了一个私有变量(private),只有内部可以访问,外部不能访问,所以,我们把Student类改一改:

class Student(object):    def __init__(self, name, score):        self.__name = name        self.__score = score    def print_score(self):        print('%s: %s' % (self.__name, self.__score))

但是如果外部代码要获取name和score怎么办?可以给Student类增加get_name和get_score这样的方法:

def get_name(self):        return self.__name    def get_score(self):        return self.__score

继承与多态

在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类、父类或超类。

比如,我们已经编写了一个名为Animal的class,有一个run()方法可以直接打印:

class Animal(object):    def run(self):        print('Animal is running...')

当我们需要编写Dog和Cat类时,就可以直接从Animal类继承:

class Dog(Animal):    passclass Cat(Animal):    pass

继承有什么好处?最大的好处是子类获得了父类的全部功能。由于Animial实现了run()方法,因此,Dog和Cat作为它的子类,什么事也没干,就自动拥有了run()方法:

dog = Dog()dog.run() #返回Animal is running...cat = Cat()cat.run() #返回Animal is running...

对于一个变量,我们只需要知道它是Animal类型,无需确切地知道它的子类型,就可以放心地调用run()方法,而具体调用的run()方法是作用在Animal、Dog、Cat还是Tortoise对象上,由运行时该对象的确切类型决定,这就是多态真正的威力:调用方只管调用,不管细节,而当我们新增一种Animal的子类时,只要确保run()方法编写正确,不用管原来的代码是如何调用的。这就是著名的“开闭”原则:
- 对扩展开放:允许新增Animal子类;
- 对修改封闭:不需要修改依赖Animal类型的run_twice()等函数。

最后,对于静态语言(例如Java)来说,如果需要传入Animal类型,则传入的对象必须是Animal类型或者它的子类,否则,将无法调用run()方法;对于Python这样的动态语言来说,则不一定需要传入Animal类型。我们只需要保证传入的对象有一个run()方法就可以了。这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。

获取对象信息

判断对象类型,使用type()函数:

type(123)#返回 <class 'int'>type('str')#返回 <class 'str'>type(None)#返回 <type(None) 'NoneType'>type(abs)#返回 <class 'builtin_function_or_method'>

对于class的继承关系来说,使用type()就很不方便。我们要判断class的类型,可以使用isinstance()函数。
如果继承关系是:object -> Animal -> Dog -> Husky,则:

h = Husky()isinstance(h, Dog) #返回 True

能用type()判断的基本类型也可以用isinstance()判断:

 isinstance('a', str) #返回True isinstance(b'a', bytes) #返回True

类属性

如果Student类本身需要绑定一个属性呢?可以直接在class中定义属性,这种属性是类属性,归Student类所有:

class Student(object):    name = 'Student'

当我们定义了一个类属性后,这个属性虽然归类所有,但类的所有实例都可以访问到。需要注意,由于实例属性优先级比类属性高,因此,它会屏蔽掉类的name属性。

面向对象高级编程

使用__slots__

想要限制实例的属性怎么办?比如,只允许对Student实例添加name和age属性。为了达到限制的目的,Python允许在定义class的时候,定义一个特殊的__slots__变量,来限制该class实例能添加的属性:

class Student(object):    __slots__ = ('name', 'age') # 用tuple定义允许绑定的属性名称s.score = 99#输出Traceback (most recent call last):  File "<stdin>", line 1, in <module>AttributeError: 'Student' object has no attribute 'score'

由于’score’没有被放到__slots__中,所以不能绑定score属性,试图绑定score将得到AttributeError的错误。

使用__slots__要注意,__slots__定义的属性仅对当前类实例起作用,对继承的子类是不起作用的。

使用@property

@property广泛应用在类的定义中,可以让调用者写出简短的代码,同时保证对参数进行必要的检查,这样,程序运行时就减少了出错的可能性。

Python内置的@property装饰器就是负责把一个方法变成属性调用的:

class Student(object):    @property    def score(self):        return self._score    @score.setter    def score(self, value):        if not isinstance(value, int):            raise ValueError('score must be an integer!')        if value < 0 or value > 100:            raise ValueError('score must between 0 ~ 100!')        self._score = value

把一个getter方法变成属性,只需要加上@property就可以了,此时,@property本身又创建了另一个装饰器@score.setter,负责把一个setter方法变成属性赋值,于是,我们就拥有一个可控的属性操作。

多重继承

我们要给动物再加上Runnable和Flyable的功能,只需要先定义好Runnable和Flyable的类:

class Runnable(object):    def run(self):        print('Running...')class Flyable(object):    def fly(self):        print('Flying...')

对于需要Runnable功能的动物,就多继承一个Runnable,例如Dog:

class Dog(Mammal, Runnable):    pass

以此类推。

在设计类的继承关系时,通常,主线都是单一继承下来的,例如,Ostrich继承自Bird。但是,如果需要“混入”额外的功能,通过多重继承就可以实现,比如,让Ostrich除了继承自Bird外,再同时继承Runnable。这种设计通常称之为MixIn。MixIn的目的就是给一个类增加多个功能,这样,在设计类的时候,我们优先考虑通过多重继承来组合多个MixIn的功能,而不是设计多层次的复杂的继承关系。

定制类

如果一个类想被用于for ... in循环,类似list或tuple那样,就必须实现一个__iter__()方法,该方法返回一个迭代对象,然后,Python的for循环就会不断调用该迭代对象的__next__()方法拿到循环的下一个值,直到遇到StopIteration错误时退出循环。以斐波那契数列为例,写一个Fib类,可以作用于for循环:

class Fib(object):    def __init__(self):        self.a, self.b = 0, 1 # 初始化两个计数器a,b    def __iter__(self):        return self # 实例本身就是迭代对象,故返回自己    def __next__(self):        self.a, self.b = self.b, self.a + self.b # 计算下一个值        if self.a > 100000: # 退出循环的条件            raise StopIteration()        return self.a # 返回下一个值

把Fib实例作用于for循环:

for n in Fib():...     print(n)...11235...4636875025

表现得像list那样按照下标取出元素,需要实现__getitem__()方法:

class Fib(object):    def __getitem__(self, n):        a, b = 1, 1        for x in range(n):            a, b = b, a + b        return a

此外,如果把对象看成dict,getitem()的参数也可能是一个可以作key的object,例如str。与之对应的是setitem()方法,把对象视作list或dict来对集合赋值。最后,还有一个delitem()方法,用于删除某个元素。

总之,通过上面的方法,我们自己定义的类表现得和Python自带的list、tuple、dict没什么区别,这完全归功于动态语言的“鸭子类型”,不需要强制继承某个接口。

Python还有另一个机制,那就是写一个__getattr__()方法,动态返回一个属性。例如:

class Student(object):    def __init__(self):        self.name = 'Michael'    def __getattr__(self, attr):        if attr=='score':            return 99

当调用不存在的属性时,比如score,Python解释器会试图调用getattr(self, ‘score’)来尝试获得属性,这样,我们就有机会返回score的值。


一个对象实例可以有自己的属性和方法,当我们调用实例方法时,我们用instance.method()来调用。能不能直接在实例本身上调用呢?在Python中,任何类,只需要定义一个__call__()方法,就可以直接对实例进行调用:

class Student(object):    def __init__(self, name):        self.name = name    def __call__(self):        print('My name is %s.' % self.name)s = Student('Michael')s() #返回 My name is Michael.

__call__()还可以定义参数。对实例进行直接调用就好比对一个函数进行调用一样,所以完全可以把对象看成函数,把函数看成对象。


当我们需要定义常量时,一种方法是为这样的枚举类型定义一个class类型,然后,每个常量都是class的一个唯一实例。Python提供了Enum类来实现这个功能:

from enum import EnumMonth = Enum('Month', ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'))

这样我们就获得了Month类型的枚举类,可以直接使用Month.Jan来引用一个常量,或者枚举它的所有成员:

for name, member in Month.__members__.items():    print(name, '=>', member, ',', member.value)

如果需要更精确地控制枚举类型,可以从Enum派生出自定义类:

from enum import Enum, unique@uniqueclass Weekday(Enum):    Sun = 0 # Sun的value被设定为0    Mon = 1    Tue = 2    Wed = 3    Thu = 4    Fri = 5    Sat = 6

@unique装饰器可以帮助我们检查保证没有重复值。

动态创建类

假设有如下Hello()类:

class Hello(object):    def hello(self, name='world'):        print('Hello, %s.' % name)

type()函数既可以返回一个对象的类型,又可以创建出新的类型,比如,我们可以通过type()函数创建出Hello类,而无需通过class Hello(object)...的定义:

def fn(self, name='world'): # 先定义函数print('Hello, %s.' % name)Hello = type('Hello', (object,), dict(hello=fn)) # 创建Hello classh = Hello()h.hello() # 返回Hello, world.

要创建一个class对象,type()函数依次传入3个参数:

  1. class的名称;
  2. 继承的父类集合,注意Python支持多重继承,如果只有一个父类,别忘了tuple的单元素写法;
  3. class的方法名称与函数绑定,这里我们把函数fn绑定到方法名hello上。

通过type()函数创建的类和直接写class是完全一样的,因为Python解释器遇到class定义时,仅仅是扫描一下class定义的语法,然后调用type()函数创建出class。

正常情况下,我们都用class Xxx…来定义类,但是,type()函数也允许我们动态创建出类来,也就是说,动态语言本身支持运行期动态创建类,这和静态语言有非常大的不同,要在静态语言运行期创建类,必须构造源代码字符串再调用编译器,或者借助一些工具生成字节码实现,本质上都是动态编译,会非常复杂。

错误处理

异常处理

高级语言通常都内置了一套try...except...finally...的错误处理机制,Python也不例外。
用一个例子来看看try的机制:

try:    print('try...')    r = 10 / 0    print('result:', r)except ZeroDivisionError as e:    print('except:', e)finally:    print('finally...')print('END')

当我们认为某些代码可能会出错时,就可以用try来运行这段代码,如果执行出错,则后续代码不会继续执行,而是直接跳转至错误处理代码,即except语句块,执行完except后,如果有finally语句块,则执行finally语句块,至此,执行完毕。

此外,如果没有错误发生,可以在except语句块后面加一个else,当没有错误发生时,会自动执行else语句:

try:    print('try...')    r = 10 / int('2')    print('result:', r)except ValueError as e:    print('ValueError:', e)except ZeroDivisionError as e:    print('ZeroDivisionError:', e)else:    print('no error!')finally:    print('finally...')print('END')

Python的错误其实也是class,所有的错误类型都继承自BaseException,所以在使用except时需要注意的是,它不但捕获该类型的错误,还把其子类也“一网打尽”。

因为错误是class,捕获一个错误就是捕获到该class的一个实例。因此,错误并不是凭空产生的,而是有意创建并抛出的。Python的内置函数会抛出很多类型的错误,我们自己编写的函数也可以抛出错误。

调试

第一种方法简单直接粗暴有效,就是用print()把可能有问题的变量打印出来看看。

第二种,凡是用print()来辅助查看的地方,都可以用断言(assert)来替代:

def foo(s):    n = int(s)    assert n != 0, 'n is zero!'    return 10 / ndef main():    foo('0')

assert的意思是,表达式n != 0应该是True,否则,根据程序运行的逻辑,后面的代码肯定会出错。
如果断言失败,assert语句本身就会抛出AssertionError

print()替换为logging是第三种方式,和assert比,logging不会抛出错误,而且可以输出到文件:

import loggings = '0'n = int(s)logging.info('n = %d' % n)print(10 / n)

logging.info()就可以输出一段文本。

import logging之后添加一行配置再试试:

import logginglogging.basicConfig(level=logging.INFO)

看到输出了:

$ python3 err.pyINFO:root:n = 0Traceback (most recent call last):  File "err.py", line 8, in <module>    print(10 / n)ZeroDivisionError: division by zero

这就是logging的好处,它允许你指定记录信息的级别,有debuginfowarningerror等几个级别,当我们指定level=INFO时,logging.debug就不起作用了。同理,指定level=WARNING后,debuginfo就不起作用了。这样一来,你可以放心地输出不同级别的信息,也不用删除,最后统一控制输出哪个级别的信息。

第四种方式是启动Python的调试器pdb,让程序以单步方式运行,可以随时查看运行状态。程序如下:

# err.pys = '0'n = int(s)print(10 / n)

启动pdb:

$ python3 -m pdb err.py> /Users/michael/Github/learn-python3/samples/debug/err.py(2)<module>()-> s = '0'

以参数-m pdb启动后,pdb定位到下一步要执行的代码-> s = ‘0’。输入命令l来查看代码:

(Pdb) l  1     # err.py  2  -> s = '0'  3     n = int(s)  4     print(10 / n)

输入命令n可以单步执行代码:

(Pdb) n> /Users/michael/Github/learn-python3/samples/debug/err.py(3)<module>()-> n = int(s)(Pdb) n> /Users/michael/Github/learn-python3/samples/debug/err.py(4)<module>()-> print(10 / n)

任何时候都可以输入命令p 变量名来查看变量:

(Pdb) p s'0'(Pdb) p n0

输入命令q结束调试,退出程序:

(Pdb) q

单元测试

单元测试是用来对一个模块、一个函数或者一个类来进行正确性检验的测试工作。如果单元测试通过,说明我们测试的这个函数能够正常工作。如果单元测试不通过,要么函数有bug,要么测试条件输入不正确,总之,需要修复使单元测试能够通过。这种以测试为驱动的开发模式最大的好处就是确保一个程序模块的行为符合我们设计的测试用例。在将来修改的时候,可以极大程度地保证该模块行为仍然是正确的。

正则表达式

正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。

在正则表达式中,如果直接给出字符,就是精确匹配。用\d可以匹配一个数字,\w可以匹配一个字母或数字,所以:

  • ‘00\d’可以匹配’007’,但无法匹配’00A’;

  • ‘\d\d\d’可以匹配’010’;

  • ‘\w\w\d’可以匹配’py3’;

.可以匹配任意字符,所以’py.’可以匹配’pyc’、’pyo’、’py!’等等。

来看一个复杂的例子:\d{3}\s+\d{3,8}

  • \d{3}表示匹配3个数字,例如’010’;
  • \s可以匹配一个空格(也包括Tab等空白符),所以\s+表示至少有一个空格,例如匹配’ ‘,’ ‘等;
  • \d{3,8}表示3-8个数字,例如’1234567’

要做更精确地匹配,可以用[]表示范围:

  • [0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线;
  • [0-9a-zA-Z\_]+可以匹配至少由一个数字、字母或者下划线组成的字符串,比如’a100’,’0_Z’,’Py3000’等等;
  • [a-zA-Z\_][0-9a-zA-Z\_]*可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量;
  • [a-zA-Z\_][0-9a-zA-Z\_]{0, 19}更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
  • A|B可以匹配A或B,所以(P|p)ython可以匹配’Python’或者’python’。

  • ^表示行的开头,^\d表示必须以数字开头。

  • $表示行的结束,\d$表示必须以数字结束。

Python提供re模块,包含所有正则表达式的功能。强烈建议使用Python的r前缀,就不用考虑转义的问题了:

s = r'ABC\-001' # Python的字符串# 对应的正则表达式字符串不变:# 'ABC\-001'

判断正则表达式是否匹配:

import rere.match(r'^\d{3}\-\d{3,8}$', '010-12345')#返回<_sre.SRE_Match object; span=(0, 9), match='010-12345'>re.match(r'^\d{3}\-\d{3,8}$', '010 12345')#返回None

match()方法判断是否匹配,如果匹配成功,返回一个Match对象,否则返回None

正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。比如:

^(\d{3})-(\d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码。注意到group(0)永远是原始字符串,group(1)、group(2)……表示第1、2、……个子串。

正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0:

re.match(r'^(\d+)(0*)$', '102300').groups()#返回('102300', '')

由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。

必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配:

re.match(r'^(\d+?)(0*)$', '102300').groups()#返回('1023', '00')

图形界面

Python自带的库是支持Tk的Tkinter,使用Tkinter,无需安装任何包,就可以直接使用。

我们编写的Python代码会调用内置的Tkinter,Tkinter封装了访问Tk的接口;Tk是一个图形库,支持多个操作系统,使用Tcl语言开发;Tk会调用操作系统提供的本地GUI接口,完成最终的GUI。所以,我们的代码只需要调用Tkinter提供的接口就可以了。

GUI版本的“Hello, world!”:

from tkinter import * #导入Tkinter包的所有内容:class Application(Frame): #从Frame派生一个Application类,这是所有Widget的父容器:    def __init__(self, master=None):        Frame.__init__(self, master)        self.pack()        self.createWidgets()    def createWidgets(self):        self.helloLabel = Label(self, text='Hello, world!')        self.helloLabel.pack()        self.quitButton = Button(self, text='Quit', command=self.quit)        self.quitButton.pack()#实例化Applicationapp = Application()# 设置窗口标题:app.master.title('Hello World')# 主消息循环:app.mainloop()

在GUI中,每个Button、Label、输入框等,都是一个Widget。Frame则是可以容纳其他Widget的Widget,所有的Widget组合起来就是一棵树。

pack()方法把Widget加入到父容器中,并实现布局。pack()是最简单的布局,grid()可以实现更复杂的布局。

createWidgets()方法中,我们创建一个Label和一个Button,当Button被点击时,触发self.quit()使程序退出。

效果图如下:

这里写图片描述

我们再对这个GUI程序改进一下,加入一个文本框,让用户可以输入文本,然后点按钮后,弹出消息对话框。

from tkinter import *import tkinter.messagebox as messageboxclass Application(Frame):    def __init__(self, master=None):        Frame.__init__(self, master)        self.pack()        self.createWidgets()    def createWidgets(self):        self.nameInput = Entry(self)        self.nameInput.pack()        self.alertButton = Button(self, text='Hello', command=self.hello)        self.alertButton.pack()    def hello(self):        name = self.nameInput.get() or 'world'        messagebox.showinfo('Message', 'Hello, %s' % name)app = Application()# 设置窗口标题:app.master.title('Hello World')# 主消息循环:app.mainloop()

当用户点击按钮时,触发hello(),通过self.nameInput.get()获得用户输入的文本后,使用tkMessageBox.showinfo()可以弹出消息对话框。

效果图如下:

这里写图片描述

网络编程

为了把全世界的所有不同类型的计算机都连接起来,就必须规定一套全球通用的协议,为了实现互联网这个目标,互联网协议簇(Internet Protocol Suite)就是通用协议标准。最重要的两个协议是TCP和IP协议,所以,大家把互联网的协议简称TCP/IP协议。

通信的时候,双方必须知道对方的标识,好比发邮件必须知道对方的邮件地址。互联网上每个计算机的唯一标识就是IP地址,类似123.123.123.123。如果一台计算机同时接入到两个或更多的网络,比如路由器,它就会有两个或多个IP地址,所以,IP地址对应的实际上是计算机的网络接口,通常是网卡。

IP地址实际上是一个32位整数(称为IPv4),以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示,目的是便于阅读。

IPv6地址实际上是一个128位整数,它是目前使用的IPv4的升级版,以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334

TCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接,保证数据包按顺序到达。TCP协议会通过握手建立连接,然后,对每个IP包编号,确保对方按顺序收到,如果包丢掉了,就自动重发。

许多常用的更高级的协议都是建立在TCP协议基础上的,比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。

原创粉丝点击