0%

网络编程

osi 七层,tcp/ip 五层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1 cs架构和bs架构

2 互联网

3 osi七层 tcp/ip五层
-物理层
-网线、光纤
-数据链路层
-网卡
-网络层
-路由器
-传输层(也叫运输层)
-四层路由器
-应用层(会话层、表示层、应用层)
http协议、ftp协议、websocket协议...

每一层功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1 物理层
-网线 ----> 1010101电信号 -----> 从网线中传输电信号
-8个比特位是1个字节(重要)
2 数据链路层
-数据帧: 一组电信号就是一个数据帧/数据报,有头和数据部分
-网卡: ----> mac地址 ----> 全球唯一 ----->网卡出厂---->烧在网卡上
-48位二进制 ----> 看到的都是16进制表示
-广播:
-局域网内通信,所有人都会接收到,通过mac地址确定是给谁的数据帧/数据报,如果不是自己的就不处理
-广播风暴
3 网络层
-跨局域网通信,需要经过网络层
-IP地址
-子网掩码
-ARP协议 : ip和mac的对照表
4 传输层
-端口: 0-65535, 一个应用程序可以监听多个端口,但是一个端口只能属于一个应用程序
-TCP协议: 可靠传输
-UDP协议: 不可靠传输

常用端口号

应用程序 FTP TFTP TELNET SMTP DNS HTTP SSH MYSQL
熟知端口 21,20 69 23 25 53 80 22 3306
传输层协议 TCP UDP TCP TCP UDP TCP TCP TCP

DNS: 把网址转换成IP地址
从浏览器发出去的数据,都是http协议,默认是80端口

TCP 三次握手四次挥手

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1 TCP:可靠传输:三次握手,四次挥手保证数据可靠

2 三次握手
-客户端向服务端发送链接请求 ----1
-服务端回复可以建立你,并且带着跟客户端建立链接的数据报 ----2
-客户端收到后,链接建好了 ----3
3 这个过程后就可以可靠传输数据

4 断开链接(四次挥手)
-客户端告诉服务器,要断开 ----1
-服务端收到,回复ok ----2
-服务端可能还有数据在传递,暂时还没断开-----等数据传完
-服务端告诉客户端,我要断了 ----3
-客户端收到,回复ok ----4

Socket层

1 Socket抽象层,从osi七层抽象出来的,抽象了网络层和传输层,跟语言无关,任何语言都会有socket的封装
2 专门给开发人员用的

基于TCP的socket套接字(重点)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(简易版本)
服务端
import socket
sever = socket.socket()
sever.bind(('192.168.11.123', 7777))
print('服务端启动完成,等待连接...')
sever.listen(5)
sock, addr = sever.accept()
while True:
data = sock.recv(1024)
print('客户端发来的信息', data.decode('utf-8'))
msg = input('请输入: ').strip()
if msg == 'q':
break
sock.send(msg.encode('utf-8'))
sock.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

(客户端)
import socket

client = socket.socket()
client.connect(('192.168.11.123', 7777))
while True:
msg = input('请输入: ').strip()
if msg =='q':
break
client.send(msg.encode('utf-8'))
data = client.recv(1024)
print('服务端发来的信息: ',data.decode('utf-8'))
client.close()

总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1 osi 七层模型 ----> 五层
-物理层
-10101000电信号----->网线、光纤
-数据链路层
-网卡---->mac地址,全球唯一
-广播: 局域网中通信
-网络层
-ip地址: ipv4,ipv6
-子网掩码+ip区分是不是属于同一个子网
-arp协议: ip和mac地址的映射表
-传输层(运输层)
-端口: 0 --- 65535
-端口是用来区分应用程序的
-TCP: 可靠传输,三次握手,四次挥手
-UDP: 不可靠传输
-应用层
-http,ftp
2 常用的端口(重要)
-80: http协议,基于TCP
-3306 :mysql
-53 DNS(域名解析)---->基于UDP
3 TCP/IP---->socket抽象层---->针对于程序员来写客户端,服务端的程序(更好写)
4 基于TCP的套接字(socket)的客户端和服务端

程序与用户交互

什么是与用户交互?

1
# 用户交互就是人往计算机中input/输入数据,计算机print/输出结果

为什么要与用户交互?

1
2
3
为了让计算机能够像人一样与用户沟通交流

比如,过去我们去银行取钱,用户需要把帐号密码告诉柜员,而现在,柜员被ATM机取代,ATM机就是一台计算机,所以用户同样需要将帐号密码告诉计算机,于是我们的程序中必须有相应的机制来控制计算机接收用户输入的内容,并且输出结果

如何与用户交互?

交互的本质就是输入、输出

输入input

1
2
3
4
5
# 在python3中input功能会等待用户的输入,用户输入任何内容,都存成字符串类型,然后赋值给等号左边的变量名
>>> username=input('请输入您的用户名:')
请输入您的用户名:jack # username = "jack"
>>> password=input('请输入您的密码:')
请输入您的密码:123 # password = "123"
1
2
3
4
5
6
7
8

# 了解知识:
# 1、在python2中存在一个raw_input功能与python3中的input功能一模一样
# 2、在python2中还存在一个input功能,需要用户输入一个明确的数据类型,输入什么类型就存成什么类型
>>> l=input('输入什么类型就存成什么类型: ')
输入什么类型就存成什么类型: [1,2,3]
>>> type(l)
<type 'list'>

输出print

1
2
3
4
5
6
7
8
9
10
>>> print('hello world')  # 只输出一个值
hello world
>>> print('first','second','third') # 一次性输出多个值,值用逗号隔开
first second third

# 默认print功能有一个end参数,该参数的默认值为"\n"(代表换行),可以将end参数的值改成任意其它字符
print("aaaa",end='')
print("bbbb",end='&')
print("cccc",end='@')
#整体输出结果为:aaaabbbb&cccc@

输出之格式化输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1 什么是格式化输出?
把一段字符串里面的某些内容替换掉之后再输出,就是格式化输出
2 为什么要格式化输出?
我们经常会输出具有某种固定格式的内容,比如:'亲爱的xxx你好!你xxx月的话费是xxx,余额是xxx‘,我们需要做的就是将xxx替换为具体的内容。
3 如何格式化输出?
# %s占位符:可以接收任意类型的值
# %d占位符:只能接收数字
>>> print('亲爱的%s你好!你%s月的话费是%d,余额是%d' %('tony',12,103,11))
亲爱的tony你好!你12月的话费是103,余额是11

# 练习1:接收用户输入,打印成指定格式
name = input('your name: ')
age = input('your age: ') #用户输入18,会存成字符串18,无法传给%d
print('My name is %s,my age is %s' %(name,age))

# 练习2:用户输入姓名、年龄、工作、爱好 ,然后打印成以下格式
------------ info of Tony -----------
Name : Tony
Age : 22
Sex : male
Job : Teacher
------------- end -----------------

基本运算符

算术运算符

img

比较运算符

img

赋值运算符

python语法中除了有=号这种简单的赋值运算外,还支持增量赋值、链式赋值、交叉赋值、解压赋值,这些赋值运算符存在的意义都是为了让我们的代码看起来更加精简。我们以x=9,y=2为例先来介绍一下增量赋值

增量赋值

img

链式赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
如果我们想把同一个值同时赋值给多个变量名,可以这么做

>>> z=10
>>> y=z
>>> x=y
>>> x,y,z
(10, 10, 10)

链式赋值指的是可以用一行代码搞定这件事

>>> x=y=z=10
>>> x,y,z
(10, 10, 10)

交叉赋值

1
2
3
4
我们定义两个变量m与n

>>> m=10
>>> n=20
1
2
3
4
5
6
如果我们想将m与n的值交换过来,可以这么做
>>> temp=m
>>> m=n
>>> n=temp
>>> m,n
(20, 10)
1
2
3
4
5
6
交叉赋值指的是一行代码可以搞定这件事
>>> m=10
>>> n=20
>>> m,n=n,m # 交叉赋值
>>> m,n
(20, 10)

解压赋值

1
2
3
4
5
6
7
8
9
10
如果我们想把列表中的多个值取出来依次赋值给多个变量名,可以这么做
>>> nums=[11,22,33,44,55]
>>>
>>> a=nums[0]
>>> b=nums[1]
>>> c=nums[2]
>>> d=nums[3]
>>> e=nums[4]
>>> a,b,c,d,e
(11, 22, 33, 44, 55)
1
2
3
4
解压赋值指的是一行代码可以搞定这件事
>>> a,b,c,d,e=nums # nums包含多个值,就好比一个压缩包,解压赋值因此得名
>>> a,b,c,d,e
(11, 22, 33, 44, 55)
1
2
3
4
5
6
7
8
9
10
11
12
注意,上述解压赋值,等号左边的变量名个数必须与右面包含值的个数相同,否则会报错
#1、变量名少了
>>> a,b=nums
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

#2、变量名多了
>>> a,b,c,d,e,f=nums
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected 6, got 5)
1
2
3
4
但如果我们只想取头尾的几个值,可以用*_匹配
>>> a,b,*_=nums
>>> a,b
(11, 22)

ps:字符串、字典、元组、集合类型都支持解压赋值

逻辑运算符

img

连续多个and

可以用and连接多个条件,会按照从左到右的顺序依次判断,一旦某一个条件为False,则无需再往右判断,可以立即判定最终结果就为False,只有在所有条件的结果都为True的情况下,最终结果才为True。

1
2
>>> 2 > 1 and 1 != 1 and True and 3 > 2 # 判断完第二个条件,就立即结束,得的最终结果为False
False

连续多个or

可以用or连接多个条件,会按照从左到右的顺序依次判断,一旦某一个条件为True,则无需再往右判断,可以立即判定最终结果就为True,只有在所有条件的结果都为False的情况下,最终结果才为False

1
2
>>> 2 > 1 or 1 != 1 or True or 3 > 2 # 判断完第一个条件,就立即结束,得的最终结果为True
True

优先级not>and>or

1
2
3
#1、三者的优先级关系:not>and>or,同一优先级默认从左往右计算。
>>> 3>4 and 4>3 or 1==3 and 'x' == 'x' or 3 >3
False
1
2
3
4
5
6
7
8
9
10
11
#2、最好使用括号来区别优先级,其实意义与上面的一样
'''
原理为:
(1) not的优先级最高,就是把紧跟其后的那个条件结果取反,所以not与紧跟其后的条件不可分割

(2) 如果语句中全部是用and连接,或者全部用or连接,那么按照从左到右的顺序依次计算即可

(3) 如果语句中既有and也有or,那么先用括号把and的左右两个条件给括起来,然后再进行运算
'''
>>> (3>4 and 4>3) or (1==3 and 'x' == 'x') or 3 >3
False
1
2
#3、短路运算:逻辑运算的结果一旦可以确定,那么就以当前处计算到的值作为最终结果返回
>>> 10 and 0 or '' and 0 or 'abc' or 'egon' == 'dsb' and 333 or 10 > 4
1
2
3
4
5
6
我们用括号来明确一下优先级
>>> (10 and 0) or ('' and 0) or 'abc' or ('egon' == 'dsb' and 333) or 10 > 4
短路: 0 '' 'abc'
假 假 真

返回: 'abc'
1
2
3
4
5
6
7
8
9
10
11
12
13
#4、短路运算面试题:
>>> 1 or 3
1
>>> 1 and 3
3
>>> 0 and 2 and 1
0
>>> 0 and 2 or 1
1
>>> 0 and 2 or 1 or 4
1
>>> 0 or False and 1
False

成员运算符

img

注意:虽然下述两种判断可以达到相同的效果,但我们推荐使用第二种格式,因为not in语义更加明确

1
2
3
4
5

>>> not 'lili' in ['jack','tom','robin']
True
>>> 'lili' not in ['jack','tom','robin']
True

身份运算符

img

需要强调的是:==双等号比较的是value是否相等,而is比较的是id是否相等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#1. id相同,内存地址必定相同,意味着type和value必定相同
#2. value相同type肯定相同,但id可能不同,如下
>>> x='Info Tony:18'
>>> y='Info Tony:18'
>>> id(x),id(y) # x与y的id不同,但是二者的值相同
(4327422640, 4327422256)


>>> x == y # 等号比较的是value
True
>>> type(x),type(y) # 值相同type肯定相同
(<class 'str'>, <class 'str'>)
>>> x is y # is比较的是idxy的值相等但id可以不同
False

img

内容概要

  • 继承介绍
  • 继承与抽象
  • 属性查找
  • 继承的实现原理
  • 派生与方法重用
  • 组合

内容详细

继承介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,新建的类可称为子类或派生类,父类又可称为基类或超类

class ParentClass1: #定义父类
pass

class ParentClass2: #定义父类
pass

class SubClass1(ParentClass1): #单继承
pass

class SubClass2(ParentClass1,ParentClass2): #多继承
pass
1
2
3
4
通过类的内置属性__bases__可以查看类继承的所有父类

>>> SubClass2.__bases__
(<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)
1
2
3
4
5
6
7
8


在Python2中有经典类与新式类之分,没有显式地继承object类的类,以及该类的子类,都是经典类,显式地继承object的类,以及该类的子类,都是新式类。而在Python3中,即使没有显式地继承object,也会默认继承该类,如下

>>> ParentClass1.__bases__
(<classobject'>,)
>>> ParentClass2.__bases__
(<class 'object'>,)
1
2
3
因而在Python3中统一都是新式类,关于经典类与新式类的区别,我们稍后讨论

提示:object类提供了一些常用内置方法的实现,如用来在打印对象时返回字符串的内置方法__str__

继承与抽象

要找出类与类之间的继承关系,需要先抽象,再继承,抽象即总结相似之处,总结对象之间的相似之处得到类,总结类与类之间的相似之处就可以得到父类:

img

img

1
2
3
4
5
6
7
8
9
10
11
12
基于上图我们可以看出类与类之间的继承指的是什么’是’什么的关系(比如人类,猪类,猴类都是动物类)。子类可以继承/遗传父类所有的属性,因而继承可以用来解决类与类之间的代码重用性问题。比如我们按照定义Student类的方式再定义一个Teacher类

class Teacher:
school='清华大学'

def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age

def teach(self):
print('%s is teaching' %self.name)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
类Teacher与Student之间存在重复的代码,老师与学生都是人类,所以我们可以得出如下继承关系,实现代码重用

class People:
school='清华大学'

def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age

class Student(People):
def choose(self):
print('%s is choosing a course' %self.name)

class Teacher(People):
def teach(self):
print('%s is teaching' %self.name)
1
2
3
4
5
Teacher类内并没有定义__init__方法,但是会从父类中找到__init__,因而仍然可以正常实例化,如下

>>> teacher1=Teacher('lili','male',18)
>>> teacher1.school,teacher1.name,teacher1.sex,teacher1.age
('清华大学', 'lili', 'male', 18)

属性查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
有了继承关系,对象在查找属性时,先从对象自己的__dict__中找,如果没有则去子类中找,然后再去父类中找...

>>> class Foo:
... def f1(self):
... print('Foo.f1')
... def f2(self):
... print('Foo.f2')
... self.f1()
...
>>> class Bar(Foo):
... def f1(self):
... print('Foo.f1')
...
>>> b=Bar()
>>> b.f2()
Foo.f2
Foo.f1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
b.f2()会在父类Foo中找到f2,先打印Foo.f2,然后执行到self.f1(),即b.f1(),仍会按照:对象本身->类Bar->父类Foo的顺序依次找下去,在类Bar中找到f1,因而打印结果为Foo.f1

父类如果不想让子类覆盖自己的方法,可以采用双下划线'开头'的方式将方法设置为私有的

>>> class Foo:
... def __f1(self): # 变形为_Foo__fa
... print('Foo.f1')
... def f2(self):
... print('Foo.f2')
... self.__f1() # 变形为self._Foo__fa,因而只会调用自己所在的类中的方法
...
>>> class Bar(Foo):
... def __f1(self): # 变形为_Bar__f1
... print('Foo.f1')
...
>>>
>>> b=Bar()
>>> b.f2() #在父类中找到f2方法,进而调用b._Foo__f1()方法,同样是在父类中找到该方法
Foo.f2
Foo.f1

继承的实现原理

菱形问题

大多数面向对象语言都不支持多继承,而在Python中,一个子类是可以同时继承多个父类的,这固然可以带来一个子类可以对多个不同父类加以重用的好处,但也有可能引发著名的 Diamond problem菱形问题(或称钻石问题,有时候也被称为“死亡钻石”),菱形其实就是对下面这种继承结构的形象比喻

img

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
A类在顶部,B类和C类分别位于其下方,D类在底部将两者连接在一起形成菱形。
这种继承结构下导致的问题称之为菱形问题:如果A中有一个方法,B和/或C都重写了该方法,而D没有重写它,那么D继承的是哪个版本的方法:B的还是C的?如下所示

class A(object):
def test(self):
print('from A')


class B(A):
def test(self):
print('from B')


class C(A):
def test(self):
print('from C')


class D(B,C):
pass


obj = D()
obj.test() # 结果为:from B
要想搞明白obj.test()是如何找到方法test的,需要了解python的继承实现原理

继承原理

1
2
3
4
python到底是如何实现继承的呢? 对于你定义的每一个类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有基类的线性顺序列表,如下

>>> D.mro() # 新式类内置了mro方法可以查看线性列表的内容,经典类没有该内置该方法
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
1
2
3
4
5
6
7
8
9
10
11
12
13


python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。 而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:

1.子类会先于父类被检查
2.多个父类会根据它们在列表中的顺序被检查
3.如果对下一个类存在两个合法的选择,选择第一个父类
所以obj.test()的查找顺序是,先从对象obj本身的属性里找方法test,没有找到,则参照属性查找的发起者(即obj)所处类D的MRO列表来依次检索,首先在类D中未找到,然后再B中找到方法test

ps:

1.由对象发起的属性查找,会从对象自身的属性里检索,没有则会按照对象的类.mro()规定的顺序依次找下去,
2.由类发起的属性查找,会按照当前类.mro()规定的顺序依次找下去,

深度优先和广度优先

多继承结构为非菱形结构,此时,会按照先找B这一条分支,然后再找C这一条分支,最后找D这一条分支的顺序直到找到我们想要的属性

img

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
class E:
def test(self):
print('from E')


class F:
def test(self):
print('from F')


class B(E):
def test(self):
print('from B')


class C(F):
def test(self):
print('from C')


class D:
def test(self):
print('from D')


class A(B, C, D):
# def test(self):
# print('from A')
pass


print(A.mro())
'''
[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class 'object'>]
'''

obj = A()
obj.test() # 结果为:from B
# 可依次注释上述类中的方法test来进行验证

如果继承关系为菱形结构,那么经典类与新式类会有不同MRO,分别对应属性的两种查找方式:深度优先和广度优先

img

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
class G: # 在python2中,未继承object的类及其子类,都是经典类
def test(self):
print('from G')

class E(G):
def test(self):
print('from E')

class F(G):
def test(self):
print('from F')

class B(E):
def test(self):
print('from B')

class C(F):
def test(self):
print('from C')

class D(G):
def test(self):
print('from D')

class A(B,C,D):
# def test(self):
# print('from A')
pass

obj = A()
obj.test() # 如上图,查找顺序为:obj->A->B->E->G->C->F->D->object
# 可依次注释上述类中的方法test来进行验证,注意请在python2.x中进行测试

img

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
class G(object):
def test(self):
print('from G')

class E(G):
def test(self):
print('from E')

class F(G):
def test(self):
print('from F')

class B(E):
def test(self):
print('from B')

class C(F):
def test(self):
print('from C')

class D(G):
def test(self):
print('from D')

class A(B,C,D):
# def test(self):
# print('from A')
pass

obj = A()
obj.test() # 如上图,查找顺序为:obj->A->B->E->C->F->D->G->object
# 可依次注释上述类中的方法test来进行验证

python mixins机制

一个子类可以同时继承多个父类,这样的设计常被人诟病,一来它有可能导致可恶的菱形问题,二来在人的世界观里继承应该是个”is-a”关系。 比如轿车类之所以可以继承交通工具类,是因为基于人的世界观,我们可以说:轿车是一个(“is-a”)交通工具,而在人的世界观里,一个物品不可能是多种不同的东西,因此多重继承在人的世界观里是说不通的,它仅仅只是代码层面的逻辑。不过有没有这种情况,一个类的确是需要继承多个类呢?

答案是有,我们还是拿交通工具来举例子:

民航飞机、直升飞机、轿车都是一个(is-a)交通工具,前两者都有一个功能是飞行fly,但是轿车没有,所以如下所示我们把飞行功能放到交通工具这个父类中是不合理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Vehicle:  # 交通工具
def fly(self):
'''
飞行功能相应的代码
'''
print("I am flying")


class CivilAircraft(Vehicle): # 民航飞机
pass


class Helicopter(Vehicle): # 直升飞机
pass


class Car(Vehicle): # 汽车并不会飞,但按照上述继承关系,汽车也能飞了
pass

但是如果民航飞机和直升机都各自写自己的飞行fly方法,又违背了代码尽可能重用的原则(如果以后飞行工具越来越多,那会重复代码将会越来越多)。

怎么办???为了尽可能地重用代码,那就只好在定义出一个飞行器的类,然后让民航飞机和直升飞机同时继承交通工具以及飞行器两个父类,这样就出现了多重继承。这时又违背了继承必须是”is-a”关系。这个难题该怎么解决?

不同的语言给出了不同的方法,让我们先来了解Java的处理方法。Java提供了接口interface功能,来实现多重继承:

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
// 抽象基类:交通工具类
public abstract class Vehicle {
}

// 接口:飞行器
public interface Flyable {
public void fly();
}

// 类:实现了飞行器接口的类,在该类中实现具体的fly方法,这样下面民航飞机与直升飞机在实现fly时直接重用即可
public class FlyableImpl implements Flyable {
public void fly() {
System.out.println("I am flying");
}
}



// 民航飞机,继承自交通工具类,并实现了飞行器接口
public class CivilAircraft extends Vehicle implements Flyable {
private Flyable flyable;

public CivilAircraft() {
flyable = new FlyableImpl();
}

public void fly() {
flyable.fly();
}
}

// 直升飞机,继承自交通工具类,并实现了飞行器接口
public class Helicopter extends Vehicle implements Flyable {
private Flyable flyable;

public Helicopter() {
flyable = new FlyableImpl();
}

public void fly() {
flyable.fly();
}
}

// 汽车,继承自交通工具类,
public class Car extends Vehicle {
}

现在我们的飞机同时具有了交通工具及飞行器两种属性,而且我们不需要重写飞行器中的飞行方法,同时我们没有破坏单一继承的原则。飞机就是一种交通工具,可飞行的能力是飞机的属性,通过继承接口来获取。

回到主题,Python语言可没有接口功能,但Python提供了Mixins机制,简单来说Mixins机制指的是子类混合(mixin)不同类的功能,而这些类采用统一的命名规范(例如Mixin后缀),以此标识这些类只是用来混合功能的,并不是用来标识子类的从属”is-a”关系的,所以Mixins机制本质仍是多继承,但同样遵守”is-a”关系,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Vehicle:  # 交通工具
pass


class FlyableMixin:
def fly(self):
'''
飞行功能相应的代码
'''
print("I am flying")


class CivilAircraft(FlyableMixin, Vehicle): # 民航飞机
pass


class Helicopter(FlyableMixin, Vehicle): # 直升飞机
pass


class Car(Vehicle): # 汽车
pass

# ps: 采用某种规范(如命名规范)来解决具体的问题是python惯用的套路

可以看到,上面的CivilAircraft、Helicopter类实现了多继承,不过它继承的第一个类我们起名为FlyableMixin,而不是Flyable,这个并不影响功能,但是会告诉后来读代码的人,这个类是一个Mixin类,表示混入(mix-in),这种命名方式就是用来明确地告诉别人(python语言惯用的手法),这个类是作为功能添加到子类中,而不是作为父类,它的作用同Java中的接口。所以从含义上理解,CivilAircraft、Helicopter类都只是一个Vehicle,而不是一个飞行器。

使用Mixin类实现多重继承要非常小心

首先它必须表示某一种功能,而不是某个物品,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀
其次它必须责任单一,如果有多个功能,那就写多个Mixin类,一个类可以继承多个Mixin,为了保证遵循继承的“is-a”原则,只能继承一个标识其归属含义的父类
然后,它不依赖于子类的实现
最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。(比如飞机照样可以载客,就是不能飞了)
Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性就会越差,并且更恶心的是,在继承的层级变多时,代码阅读者在定位某一个方法到底在何处调用时会晕头转向,如下

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
class Displayer:
def display(self, message):
print(message)


class LoggerMixin:
def log(self, message, filename='logfile.txt'):
with open(filename, 'a') as fh:
fh.write(message)

def display(self, message):
super().display(message) # super的用法请参考下一小节
self.log(message)


class MySubClass(LoggerMixin, Displayer):
def log(self, message):
super().log(message, filename='subclasslog.txt')


obj = MySubClass()
obj.display("This string will be shown and logged in subclasslog.txt")


# 属性查找的发起者是obj,所以会参照类MySubClass的MRO来检索属性

# [<class '__main__.MySubClass'>, <class '__main__.LoggerMixin'>, <class '__main__.Displayer'>, <class 'object'>]

# 1、首先会去对象obj的类MySubClass找方法display,没有则去类LoggerMixin中找,找到开始执行代码

# 2、执行LoggerMixin的第一行代码:执行super().display(message),参照MySubClass.mro(),super会去下一个类即类Displayer中找,找到display,开始执行代码,打印消息"This string will be shown and logged in subclasslog.txt"

# 3、执行LoggerMixin的第二行代码:self.log(message),self是对象obj,即obj.log(message),属性查找的发起者为obj,所以会按照其类MySubClass.mro(),即MySubClass->LoggerMixin->Displayer->object的顺序查找,在MySubClass中找到方法log,开始执行super().log(message, filename='subclasslog.txt'),super会按照MySubClass.mro()查找下一个类,在类LoggerMixin中找到log方法开始执行,最终将日志写入文件subclasslog.txt

派生与方法重用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
子类可以派生出自己的新属性,在进行属性查找时,子类中的属性名会优先于父类被查找

例如每个老师还有职称这一属性,我们就需要在teacher类中定义自己的__init__覆盖父类的
>>> class People:
... school='清华大学'
...
... def __init__(self,name,sex,age):
... self.name=name
... self.sex=sex
... self.age=age
...
>>> class Teacher(People):
... def __init__(self,name,sex,age,title): # 派生
... self.name=name
... self.sex=sex
... self.age=age
... self.title=title
... def teach(self):
... print('%s is teaching' %self.name)
...
>>> obj=Teacher('lili','female',28,'高级讲师') #只会找自己类中的__init__,并不会自动调用父类的
>>> obj.name,obj.sex,obj.age,obj.title
('lili', 'female', 28, '高级讲师')
1
2
3
4
5
6
7
8
9
很明显子类Teacher中__init__内的前三行又是在写重复代码,若想在子类派生出的方法内重用父类的功能,有两种实现方式

方法一:“指名道姓”地调用某一个类的函数
>>> class Teacher(People):
... def __init__(self,name,sex,age,title):
... People.__init__(self,name,age,sex) #调用的是函数,因而需要传入self
... self.title=title
... def teach(self):
... print('%s is teaching' %self.name)
1
2
3
4
5
6
7
8
9
10
方法二:super()

调用super()会得到一个特殊的对象,该对象专门用来引用父类的属性,且严格按照MRO规定的顺序向后查找
>>> class Teacher(People):
... def __init__(self,name,sex,age,title):
... super().__init__(name,age,sex) #调用的是绑定方法,自动传入self
... self.title=title
... def teach(self):
... print('%s is teaching' %self.name)
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
提示:在Python2中super的使用需要完整地写成super(自己的类名,self) ,而在python3中可以简写为super()。

这两种方式的区别是:    1 方式一是跟继承没有关系的    2 方式二的super()是依赖于继承的,并且即使没有直接继承关系,super()仍然会按照MRO继续往后查找
>>> #A没有继承B
... class A:
... def test(self):
... super().test()
...
>>> class B:
... def test(self):
... print('from B')
...
>>> class C(A,B):
... pass
...
>>> C.mro() # 在代码层面A并不是B的子类,但从MRO列表来看,属性查找时,就是按照顺序C->A->B->object,B就相当于A的“父类”
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,<classobject'>]
>>> obj=C()
>>> obj.test() # 属性查找的发起者是类C的对象obj,所以中途发生的属性查找都是参照C.mro()
1
2
3
obj.test()首先找到A下的test方法,执行super().test()会基于MRO列表(以C.mro()为准)当前所处的位置继续往后查找(),然后在B中找到了test方法并执行。

关于在子类中重用父类功能的这两种方式,使用任何一种都可以,但是在最新的代码中还是推荐使用super()

组合

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
# 在一个类中以另外一个类的对象作为数据属性,称为类的组合。组合与继承都是用来解决代码的重用性问题。不同的是:继承是一种“是”的关系,比如老师是人、学生是人,当类之间有很多相同的之处,应该使用继承;而组合则是一种“有”的关系,比如老师有生日,老师有多门课程,当类之间有显著不同,并且较小的类是较大的类所需要的组件时,应该使用组合,如下示例

class Course:
def __init__(self,name,period,price):
self.name=name
self.period=period
self.price=price
def tell_info(self):
print('<%s %s %s>' %(self.name,self.period,self.price))

class Date:
def __init__(self,year,mon,day):
self.year=year
self.mon=mon
self.day=day
def tell_birth(self):
print('<%s-%s-%s>' %(self.year,self.mon,self.day))

class People:
school='清华大学'
def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age

#Teacher类基于继承来重用People的代码,基于组合来重用Date类和Course类的代码
class Teacher(People): #老师是人
def __init__(self,name,sex,age,title,year,mon,day):
super().__init__(name,age,sex)
self.birth=Date(year,mon,day) #老师有生日
self.courses=[] #老师有课程,可以在实例化后,往该列表中添加Course类的对象
def teach(self):
print('%s is teaching' %self.name)


python=Course('python','3mons',3000.0)
linux=Course('linux','5mons',5000.0)
teacher1=Teacher('lili','female',28,'博士生导师',1990,3,23)

# teacher1有两门课程
teacher1.courses.append(python)
teacher1.courses.append(linux)

# 重用Date类的功能
teacher1.birth.tell_birth()

# 重用Course类的功能
for obj in teacher1.courses:
obj.tell_info()
此时对象teacher1集对象独有的属性、Teacher类中的内容、Course类中的内容于一身(都可以访问到),是一个高度整合的产物

'''
继承与组合的区别:
继承:满足is a的关系
组合:满足has a的关系
'''

内容概要

  • 形参与实参介绍
  • 形参与实参的具体使用

内容详细

形参与实参介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
函数的参数分为'形式参数''实际参数',简称'形参''实参'

形参 : 即在'定义函数阶段'时,括号内声明的参数,形参本质就是一个'变量名',用来接收外部传来的值

实参 : 即在'调用函数阶段'时,括号内传入的'值',值可以是常量、变量、表达式或三者的组合

# 1:实参是常量
res = my_min(1,2)

# 2:实参是变量
a = 1
b = 2
res = my_min(a, b)

# 3:实参是表达式
res = my_min(10*2,10*my_min(3,4))

# 4:实参可以是常量、变量、表达式的任意组合
a = 2
my_min(1,a,10*my_min(3,4))

'''
在调用有参函数时,实参(值)会赋值给形参(变量名)
'''

在python中,变量名与值只是单纯的绑定关系,而对函数来说,这种绑定关系只在函数调用时生效,在调用结束后解除

形参与实参的具体使用

位置参数

1
2
3
4
5
6
7
位置即顺序,位置参数指的是按顺序定义的参数,需要从两个角度去看:

1 在定义函数阶段时:按照从左到右的顺序依次定义形参,称为位置形参,凡是按照这种形式定义的形参都必须被传值

def register(name,age,sex): # 定义位置形参:name,age,sex 这三者都必须被传值
print('Name: %s Age: %s Sex: %s' % (name,age,sex))
register() # TypeError:缺少3个位置参数
1
2
3
4
5
6

2 在调用函数阶段时:按照从左到右的顺序依次定义实参,称为位置实参,凡是按照这种形式定义的实参会按照从左到右的顺序与形参一一对应

def register(name,age,sex): # 定义位置形参:name,age,sex 这三者都必须被传值
print('Name: %s Age: %s Sex: %s' % (name,age,sex))
register('egon',18,male) # 实参从左到右与形参一一对应

关键字参数

1
2
3
4
5
6
7
在调用函数时,实参可以是key = value的形式,称为关键字参数,凡是按照这种形式定义的实参,可以完全不按照从左到右的顺序定义,但仍能为指定的形参赋值


def register(name,age,sex): # 定义位置形参:name,age,sex 这三者都必须被传值
print('Name: %s Age: %s Sex: %s' % (name,age,sex))
register(sex = 'male',name = 'lili', age = 18)
>>> Name: lili Age: 18 Sex: male
1
2
3
4
5
6
注意:
在调用函数时,实参也可以是按位置传参或按关键字传参混合使用,但是'必须保证关键字参数在位置参数后面,且不可以对一个形参重复赋值'

>>> register('lili',sex='male',age=18) #正确使用
>>> register(name='lili',18,sex='male') #SyntaxError:关键字参数name=‘lili’在位置参数18之前
>>> register('lili',sex='male',age=18,name='jack') #TypeError:形参name被重复赋值

默认参数

1
2
3
4
5
6
在定义函数阶段时,就已经为形参赋值,这类形参称之为:默认参数,当函数有多个参数时,需要将值经常改变的参数定义成位置形参,而将值改变较少的参数定义成默认参数

例如:编写一个注册学生信息的函数,如果大火书学生的性别都为男,那完全可以将形参sex定义成默认参数

def register(name, age, sex = 'male'): # 默认sex的值为male
print('Name:%s Age:%s Sex:%s' %(name,age,sex))
1
2
3
4
5
6
这样在定义阶段时,就已经为参数sex赋值,意味着调用时可以不对sex赋值,这样就降低了函数调用的复杂度

register('tom', 17) # 大多数情况,无需为sex传值,默认为male
>>> Name: tom Age: 17 Sex: male
register('lili', 18, 'female') # 少数情况,可以为sex传值female
>>> Name: lili Age: 18 Sex: female
1
2
3
4
5
6
7
8
9
10
11
注意:
1 默认参数必须在位置参数之后
2 默认参数的值仅在函数定义阶段被赋值一次


x = 1
def foo(arg = x):
print(arg)
x = 5 # 定义阶段arg已经被赋值为1,此处的修改与默认参数arg没有任何关系
foo()
>>> 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


注意:
3 默认参数的值通常设为:不可变类型


def foo(n,arg = []):
arg.append(n)
return arg
foo(1)
>>> [1]
foo(2)
>>> [1, 2]
foo(3)
>>> [1, 2, 3] # 每一次调用都是在上一次的基础上向同一个列表增加值
1
2
3
4
5
6
7
8
9
10
11
12
13
14

修改后:

def foo(n,arg=None):
if arg is None:
arg=[]
arg.append(n)
return arg
foo(1)
>>> [1]
foo(2)
>>> [2]
foo(3)
>>> [3] # 脱裤子放屁 这样默认参数就没有了意义

可变长度的参数(与*的用法)

参数的长度可变指的是:在调用函数的时候,实参的个数可以不固定

在调用函数时:实参的定义无非是按位置传参或者按关键字传参两种形式,这就要求形参提供两种方案,来分别处理两种形式的可变长度的参数 ↓↓↓

可变长度的位置参数

1
2
3
4
5
6
7
8
9
10
11
12
13
如果在最后一个形参名前加 * 号,那么在调用函数时,溢出的位置实参,都会被 * 接收,以元组的形式保存下来赋值给该形参


def foo(x, y, z = 1, *args): # 在最后一个形参名args前加 * 号
print(x)
print(y)
print(z)
print(args)
foo(1,2,3,4,5,6,7) # 实参1,2,3按位置为形参x,y,z赋值,多余的位置实参4,5,6,7都被*接收,以元组的形式保存下来,赋值给args,即args = (4,5,6,7)
>>> 1
>>> 2
>>> 3
>>> (4, 5, 6, 7)
1
2
3
4
5
6
7
8
9
10
11
12

如果我们事先生成了一个列表,仍然是可以传值给*args的

def foo(x, y, *args):
print(x)
print(y)
print(args)
L = [3, 4, 5]
foo(1, 2, *L) # *L就相当于位置参数3,4,5,foo(1,2,*L)就等同于foo(1,2,(3,4,5))
>>> 1
>>> 2
>>> (3,4,5)
1
2
3
4
5
6
7
8
9
10
11
12
13


注意:
1 如果在传入L时没有加*,那L就只是一个普通的位置参数了
def foo(x, y, *args):
print(x)
print(y)
print(args)
L = [3, 4, 5]
foo(1, 2, L)
>>> 1
>>> 2
>>> (3,)
1
2
3
4
5
6
7
8
9
10
11
    2 如果形参为常规的参数(位置或默认),实参仍可以是*的形式
def foo(x,y,z=3):
print(x)
print(y)
print(z)
foo(*[1,2]) #等同于foo(1,2)
>>> 14

4、、
>>> 2
>>> 3
1
2
3
4
5
6
7
8
9

如果想要求多个值的和,*args就派上用场了
def add(*args):
res=0
for i in args:
res+=i
return res
add(1,2,3,4,5)
>>> 15

可变长度的关键字参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'''
如果在最后一个形参名前加 ** 号,那么在调用函数时,溢出的关键字参数,都会被 ** 接收,以字典的形式保存下来赋值给形参
'''

def foo(x, **kwargs): # 在最后一个参数kwargs前加**
print(x)
print(kwargs)
foo(y=2, x=1, z=3) # 溢出的关键字实参y=2,z=3都被**接收,以字典的形式保存下来,赋值给kwargs
>>> 1
>>> {'z' : 3, 'y' : 2}
如果我们事先生成了一个字典,仍然是可以传值给**kwargs的

def foo(x,y,**kwargs):
print(x)
print(y)
print(kwargs)
dict = {'a' : 1, 'b' : 2}
foo(1, 2, **dict) # **dic就相当于关键字参数a=1,b=2,foo(1, 2, **dict)等同foo(1,2,a=1,b=2)
>>> 1
>>> 2
>>> {'a' : 1, 'b' : 2}
1
2
3
4
5
6

'''
注意:
如果在传入dict时没有加**,那么dict就只是一个普通的位置参数了
'''
>>> foo(1,2,dic) #TypeError:函数foo只需要2个位置参数,但是传了3个
1
2
3
4
5
6
7
8
9
10
11

如果形参为常规参数(位置或默认),实参仍可以是**的形式
def foo(x,y,z=3):
print(x)
print(y)
print(z)

foo(**{'x':1,'y':2}) # 等同于foo(y=2,x=1)
>>> 1
>>> 2
>>> 3
1
2
3
4
5
6

如果我们要编写一个用户认证的函数,起初可能只基于用户名密码的验证就可以了,可以使用**kwargs为日后的扩展提供良好的环境,同时保持了函数的简洁性

def auth(user,password,**kwargs):
pass

命名关键字参数

1
2
3
4
5
6
7
8
9
在定义了**kwargs参数后,函数调用者就可以传入任意的关键字参数(key = value),如果函数体代码的执行需要依赖某个key,必须在函数内进行判断

def register(name,age,**kwargs):
if 'sex' in kwargs:
# 有sex参数
pass
if 'height' in kwargs:
# 有height参数
pass
1
2
3
4
5
6
7
8
想要限定函数,调用者必须以key = value的形式传值,python3提供了专门的语法:需要在定义形参时,用*作为一个分隔符号,*号之后的形参称为命名关键字参数,对于这类参数,在函数调用时,必须按照key = value的形式为其传值,
且必须被传值
>>> def register(name,age,*,sex,height): #sex,height为命名关键字参数
... pass
...
>>> register('lili',18,sex='male',height='1.8m') #正确使用
>>> register('lili',18,'male','1.8m') # TypeError:未使用关键字的形式为sex和height传值
>>> register('lili',18,height='1.8m') # TypeError没有为命名关键字参数height传值。
1
2
3
4
5
6
7
8

命名关键字参数也可以有默认值,从而简化调用

>>> def register(name,age,*,sex='male',height):
... print('Name:%s,Age:%s,Sex:%s,Height:%s' %(name,age,sex,height))
...
>>> register('lili',18,height='1.8m')
Name:lili,Age:18,Sex:male,Height:1.8m
1
2
3
4
5
6
7
8

需要强调的是:sex不是默认参数,height也不是位置参数,因为二者均在后,所以都是命名关键字参数,形参sex=’male’属于命名关键字参数的默认值,因而即便是放到形参height之前也不会有问题。另外,如果形参中已经有一个args了,命名关键字参数就不再需要一个单独的*作为分隔符号了

>>> def register(name,age,*args,sex='male',height):
... print('Name:%s,Age:%s,Args:%s,Sex:%s,Height:%s' %(name,age,args,sex,height))
...
>>> register('lili',18,1,2,3,height='1.8m') #sex与height仍为命名关键字参数
Name:lili,Age:18,Args:(1, 2, 3),Sex:male,Height:1.8m

组合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
综上所述所有参数可任意组合使用,但定义顺序必须是:位置参数、默认参数、*args、命名关键字参数、**kwargs

可变参数*args与关键字参数**kwargs通常是组合在一起使用的,如果一个函数的形参为*args与**kwargs,那么代表该函数可以接收任何形式、任意长度的参数

>>> def wrapper(*args,**kwargs):
... pass
...
在该函数内部还可以把接收到的参数传给另外一个函数(这在4.6小节装饰器的实现中大有用处)

>>> def func(x,y,z):
... print(x,y,z)
...
>>> def wrapper(*args,**kwargs):
... func(*args,**kwargs)
...
>>> wrapper(1,z=3,y=2)
1 2 3
1
2
3
4
5
6
7
8
9

按照上述写法,在为函数wrapper传参时,其实遵循的是函数func的参数规则,调用函数wrapper的过程分析如下:

1 位置实参1被接收,以元组的形式保存下来,赋值给args,即args=(1,),关键字实参z=3,y=2被*接收,以字典的形式保存下来,赋值给kwargs,即kwargs={'y': 2, 'z': 3}
2 执行func(args,kwargs),即func((1,),* {'y': 2, 'z': 3}),等同于func(1,z=3,y=2)

'''
提示: *args、**kwargs中的args和kwargs被替换成其他名字并无语法错误,但使用args、kwargs是约定俗成的。
'''

重点:

1
2
3
4
5
6
7
8
9
10
11
* 与 **

1 在形参中:
接受溢出的,属于汇总行为
* 会接受溢出的(位置)的值
** 会接受溢出的(关键字)的值

2 在实参中:
打散行为
* 后面可以跟'可以被for循环遍历的类型',* 会将其打散成位置实参
** 后面跟的只能是'字典类型',** 会将其打散成关键字实参

内容概要

  • 函数递归调用介绍
  • 回溯与递推

内容详细

函数递归调用介绍

1
2
3
4
5
6
7
8
函数不仅可以嵌套定义,还可以嵌套调用,即在调用一个函数的过程中,函数内部又调用另一个函数,而函数的递归调用指的是在调用一个函数的过程中又直接或间接的调用该函数本身

例如:直接调用

def f1():
print('>>> f1')
f1()
f1()

img

1
2
3
4
5
6
7
8
9
10
11
12
13
在调用f1的过程中,又调用f2,而在调用f2的过程中又调用f1,这就是间接调用函数f1本身

例如:间接调用

def f1():
print('from f1')
f2()

def f2():
print('from f2')
f1()

f1()

img

1
2
3
4
5
6
7
8
9
从上图可知:这两种情况下的递归调用都是一个无限循环的过程

但在python中,对函数的递归调用的深度做了限制,因此并不会进入无限循环,而是会抛出异常

要避免这种情况出现,就必须让递归调用在满足某个特定条件下终止

ps : #1. 可以使用sys.getrecursionlimit()去查看递归深度,默认值为1000,虽然可以使用sys.setrecursionlimit()去设定该值,但仍受限于主机操作系统栈大小的限制

#2. python不是一门函数式编程语言,无法对递归进行尾递归优化。

回溯与递推

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
例子:

某公司四个员工坐在一起,问第四个人薪水,他说比第三个人多1000,问第三个人薪水,第他说比第二个人多1000,问第二个人薪水,他说比第一个人多1000,最后第一人说自己每月5000,请问第四个人的薪水是多少?

思路解析:

要知道第四个人的月薪,就必须知道第三个人的,第三个人的又取决于第二个人的,第二个人的又取决于第一个人的,而且每一个员工都比前一个多一千,数学表达式即:

salary(4)=salary(3)+1000
salary(3)=salary(2)+1000
salary(2)=salary(1)+1000
salary(1)=5000
总结为:
salary(n)=salary(n-1)+1000 (n>1)
salary(1)=5000 (n=1)
1
2
3
4
5
6
7
8
9
10
11

很明显这是一个递归的过程,可以将该过程分为两个阶段:回溯与递推

# 1 回溯阶段:
要求第n个员工的薪水,需要回溯得到(n-1)个员工的薪水,以此类推,直到得到第一个员工的薪水,此时,salary(1)已知,因而不必再向前回溯了。

# 2 递推阶段:
从第一个员工的薪水可以推算出第二个员工的薪水(6000),从第二个员工的薪水可以推算出第三个员工的薪水(7000),以此类推,一直推算出第第四个员工的薪水(8000)为止,递归结束。

# 注意:递归一定要有一个结束条件,这里n=1就是结束条件。

img

1
2
3
4
5
6
7
8
9
10
11
12
13
代码实现:

def salary(n):
if n==1:
return 5000
return salary(n-1)+1000

s=salary(4)
print(s)

执行结果:

8000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

程序分析:

在未满足n == 1的条件时,一直进行递归调用,即一直回溯 而在满足n == 1的条件时,终止递归调用,即结束回溯,从而进入递推阶段,依次推导直到得到最终的结果。

递归本质就是在做重复的事情,所以理论上递归可以解决的问题循环也都可以解决,只不过在某些情况下,使用递归会更容易实现,比如有一个嵌套多层的列表,要求打印出所有的元素,代码实现如下

items=[[1,2],3,[4,[5,[6,7]]]]
def foo(items):
for i in items:
if isinstance(i,list): #满足未遍历完items以及if判断成立的条件时,一直进行递归调用
foo(i)
else:
print(i,end=' ')

foo(items) #打印结果1 2 3 4 5 6 7

使用递归,我们只需要分析出要重复执行的代码逻辑,然后提取进入下一次递归调用的条件或者说递归结束的条件即可,代码实现起来简洁清晰

内容概要

  • 反射
  • 内置方法

内容详细

反射

python是动态语言,而反射机制则被视为动态语言的关键
反射机制指的是:
在程序的运行状态中:
对于任意一个类,都可以知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意方法和属性
这种动态获取程序信息以及动态调用对象的功能称为反射机制

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
在python中实现反射非常简单:
1 在程序运行过程中,如果我们获取一个不知道存有何种属性的对象,若想操作其内部属性,可以先通过内置函数dir来获取任意一个类或者对象的属性列表,列表中全为字符串格式

>>> class People:
... def __init__(self,name,age,gender):
... self.name=name
... self.age=age
... self.gender=gender
...
>>> obj=People('egon',18,'male')
>>> dir(obj) # 列表中查看到的属性全为字符串
[......,'age', 'gender', 'name']

2 接下来就是想办法通过字符串来操作对象的属性了,这就涉及到内置函数hasattr、gatattr、setattrdelattr的使用了
-- python中一切皆对象,类和对象都可以被这四个函数操作,用法一样

class Teacher:
def __init__(self,full_name):
self.full_name =full_name

t=Teacher('Egon Lin')

# hasattr(object,'name')
hasattr(t,'full_name') # 按字符串'full_name'判断有无属性t.full_name

# getattr(object, 'name', default=None)
getattr(t,'full_name',None) # 等同于t.full_name,不存在该属性则返回默认值None

# setattr(x, 'y', v)
setattr(t,'age',18) # 等同于t.age=18

# delattr(x, 'y')
delattr(t,'age') # 等同于del t.age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

基于反射可以十分灵活地操作对象的属性

例如:将用户交互的结果反射到具体的功能执行

>>> class FtpServer:
... def serve_forever(self):
... while True:
... inp=input('input your cmd>>: ').strip()
... cmd,file=inp.split()
... if hasattr(self,cmd): # 根据用户输入的cmd,判断对象self有无对应的方法属性
... func=getattr(self,cmd) # 根据字符串cmd,获取对象self对应的方法属性
... func(file)
... def get(self,file):
... print('Downloading %s...' %file)
... def put(self,file):
... print('Uploading %s...' %file)
...
>>> server=FtpServer()
>>> server.serve_forever()
input your cmd>>: get a.txt
Downloading a.txt...
input your cmd>>: put a.txt
Uploading a.txt...

内置方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
python的Class机制内置了很多特殊的方法来帮助使用者高度定制自己的类

这些内置方法都是以双下划线开头和结尾的,会在满足某种条件时自动触发
我们以常用的__str__和__del__为例:

1 __str__方法会在对象被打印时自动触发,print功能打印的就是它的返回值,我们通常基于方法来定制对象的打印信息,该方法必须返回字符串类型

>>> class People:
... def __init__(self,name,age):
... self.name=name
... self.age=age
... def __str__(self):
... return '<Name:%s Age:%s>' %(self.name,self.age) #返回类型必须是字符串
...
>>> p=People('lili',18)
>>> print(p) #触发p.__str__(),拿到返回值后进行打印
<Name:lili Age:18>
1
2
3
4
5
6
7
8
9
10
      2 __del__会在对象被删除时自动触发 由于Python自带的垃圾回收机制会自动清理Python程序的资源,所以当一个对象只占用应用程序级资源时,完全没必要为对象定制__del__方法,但在产生一个对象的同时涉及到申请系统资源(比如系统打开的文件、网络连接等)的情况下,关于系统资源的回收,Python的垃圾回收机制便派不上用场了,需要我们为对象定制该方法,用来在对象被删除时自动触发回收系统资源的操作

lass MySQL:
def __init__(self,ip,port):
self.conn=connect(ip,port) # 伪代码,发起网络连接,需要占用系统资源
def __del__(self):
self.conn.close() # 关闭网络连接,回收系统资源

obj=MySQL('127.0.0.1',3306) # 在对象obj被删除时,自动触发obj.__del__()

内容概要

  • 包的介绍
  • 包的使用

内容详细

包介绍

1
2
3
4
5
6
7
8
9
10
11
随着模块数目的增多,把所有模块不加区分地放到一起也是极不合理的
于是python为我们提供了一种把模块组织到一起的方法 ---> 创建一个包
包就是一个含有__init__.py文件的文件夹,文件夹内可以组织子模块或自包 例:↓↓↓

pool/ #顶级包
├── __init__.py
├── futures #子包
│ ├── __init__.py
│ ├── process.py
│ └── thread.py
└── versions.py #子模块
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

需要强调的是

#1. 在python3中,即使包下没有__init__.py文件,import 包仍然不会报错,而在python2中,包下一定要有该文件,否则import 包报错

#2. 创建包的目的不是为了运行,而是被导入使用,记住,包只是模块的一种形式而已,包的本质就是一种模

接下来我们就以包pool为例来介绍包的使用,包内各文件内容如下

# process.py
class ProcessPoolExecutor:
def __init__(self,max_workers):
self.max_workers=max_workers

def submit(self):
print('ProcessPool submit')

# thread.py
class ThreadPoolExecutor:
def __init__(self, max_workers):
self.max_workers = max_workers

def submit(self):
print('ThreadPool submit')

# versions.py
def check():
print('check versions’)

# __init__.py文件内容均为空

包的使用

导入包与init.py

1
2
3
4
5
6
7
8
9
10
11
12
13
包属于模块的一种,因而包以及包内模块均是用来被导入使用的,而绝非被直接执行,首次导入包,同样会做三件事:

1 执行包下的__init__.py文件

2 产生一个新的名称空间用于存放__init__.py执行过程中产生的名字

3 在当前执行文件所在的名称空间中得到一个名字pool,该名字指向__init__.py的名称空间,例如http://pool.xxx和pool.yyy中的xxx和yyy都是来自于pool下的__init__.py,也就是说导入包时并不会导入包下所有的子模块与子包

import pool

pool.versions.check() #抛出异常AttributeError
pool.futures.process.ProcessPoolExecutor(3) #抛出异常AttributeError
pool.versions.check()要求pool下有名字versions,进而pool.versions下有名字check。pool.versions下已经有名字check了,所以问题出在pool下没有名字versions,这就需要在pool下的__init__.py中导入模块versions
1
2
3
4
5
6
7
8
9


强调

1.关于包相关的导入语句也分为importfrom ... import ...两种,但是无论哪种,无论在什么位置,在导入时都必须遵循一个原则:凡是在导入时带点的,点的左边都必须是一个包,否则非法。可以带有一连串的点,如import 顶级包.子包.子模块,但都必须遵循这个原则。但对于导入后,在使用时就没有这种限制了,点的左边可以是包,模块,函数,类(它们都可以用点的方式调用自己的属性)。

2、包A和包B下有同名模块也不会冲突,如A.a与B.a来自俩个命名空间

3import导入文件时,产生名称空间中的名字来源于文件,import 包,产生的名称空间的名字同样来源于文件,即包下的__init__.py,导入包本质就是在导入该文件

绝对导入与相对导入

1
2
3
4
5
6
针对包内的模块之间互相导入,导入的方式有两种

1、绝对导入:以顶级包为起始

#pool下的__init__.py
from pool import versions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2、相对导入:.代表当前文件所在的目录,..代表当前目录的上一级目录,依此类推

#pool下的__init__.py
from . import versions
同理,针对pool.futures.process.ProcessPoolExecutor(3),则需要

#操作pool下的__init__.py,保证pool.futures
from . import futures #或from pool import futures

#操作futrues下的__init__.py,保证pool.futures.process
from . import process #或from pool.futures import process
在包内使用相对导入还可以跨目录导入模块,比如thread.py中想引用versions.py的名字check

import也能使用绝对导入,导入过程中同样会依次执行包下的__init__.py,只是基于import导入的结果,使用时必须加上该前缀
1
2
3
4
5
6
7


1

import pool.futures #拿到名字pool.futures指向futures下的__init__.py

pool.futures.xxx #要求futures下的__init__.py中必须有名字xxx
1
2
3
4
5
6
7
8
9
10
2

import pool.futures.thread #拿到名字pool.futures.thread指向thread.py

thread_pool=pool.futures.thread.ThreadPoolExecutor(3)
thread_pool.submit()
相对导入只能用from module import symbol的形式,import ..versions语法是不对的,且symbol只能是一个明确的名字

from pool import futures.process #语法错误
from pool.futures import process #语法正确
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18


针对包内部模块之间的相互导入推荐使用相对导入,需要特别强调:

1、相对导入只能在包内部使用,用相对导入不同目录下的模块是非法的

2、无论是import还是from-import,但凡是在导入时带点的,点的左边必须是包,否则语法错误


总结包的使用需要牢记三点:

1、导包就是在导包下__init__.py文件

2、包内部的导入应该使用相对导入,相对导入也只能在包内部使用,而且...取上一级不能出包

3、 使用语句中的点代表的是访问属性m.n.x ----> 向m要n,向n要x
而导入语句中的点代表的是路径分隔符import a.b.c --> a/b/c,文件夹下a下有子文件夹b,文件夹b下有子文件或文件夹c
所以导入语句中点的左边必须是一个包

from 包 import *

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 在使用包时同样支持from pool.futures import * ,毫无疑问*代表的是futures下__init__.py中所有的名字,通用是用变量__all__来控制*代表的意思

#futures下的__init__.py
__all__=['process','thread']
最后说明一点,包内部的目录结构通常是包的开发者为了方便自己管理和维护代码而创建的,这种目录结构对包的使用者往往是无用的,此时通过操作__init__.py可以“隐藏”包内部的目录结构,降低使用难度,比如想要让使用者直接使用

import pool

pool.check()
pool.ProcessPoolExecutor(3)
pool.ThreadPoolExecutor(3)
需要操作pool下的__init__.py

from .versions import check
from .futures.process import ProcessPoolExecutor
from .futures.thread import ThreadPoolExecutor

变量

什么是变量?

1
# 变量就是可以变化的量,量指的是事物的状态,比如人的年龄、性别,游戏角色的等级、金钱等等

为什么要有变量?

1
2
3
# 为了让计算机能够像人一样去记忆事物的某种状态,并且状态是可以发生变化的
# 详细地说:
# 程序执行的本质就是一系列状态的变化,变是程序执行的直接体现,所以我们需要有一种机制能够反映或者说是保存下来程序执行时状态,以及状态的变化。

怎么使用变量?(先定义,后使用)

变量的定义和使用

变量的定义由三部分组成,↓↓↓

img

通过变量名即可引用到对应的值

1
2
# 通过变量名即可引用到值,我们可以结合print()功能将其打印出来
print(age) # 通过变量名age找到值18,然后执行print(18),输出:18

变量的命名规范

1
2
3
4
5
# 命名规范
1. 变量名只能是 字母、数字或下划线的任意组合
2. 变量名的第一个字符不能是数字
3. 关键字不能声明为变量名,常用关键字如下
['and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'exec', 'finally', 'for', 'from','global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while', 'with', 'yield']

变量的命名风格

1
2
3
4
5
6
# 风格一:驼峰体
AgeOfTony = 56
NumberOfStudents = 80
# 风格二:纯小写下划线(在python中,变量名的命名推荐使用该风格)
age_of_tony = 56
number_of_students = 80

变量的三大特性

1
2
3
4
5
6
7
8
#1、id
反应的是变量在内存中的唯一编号,内存地址不同id肯定不同

#2、type
变量值的类型

#3、value
变量值

常量

什么是常量

1
# 常量指在程序运行过程中不会改变的量

为什么要有常量

1
# 在程序运行过程中,有些值是固定的、不应该被改变,比如圆周率 3.141592653...

怎么使用常量

1
# 在Python中没有一个专门的语法定义常量,约定俗成是用全部大写的变量名表示常量。如:PI=3.14159。所以单从语法层面去讲,常量的使用与变量完全一致。

什么是垃圾回收机制?

垃圾回收机制(简称GC),是python解释器自带的一种机制,专门用来回收不可用的变量值所占用的内存空间

为什么要用垃圾回收机制?

1
# 程序运行过程中会申请大量的内存空间,而对于一些无用的内存空间,如果不及时清理的话会导致内存使用殆尽(内存溢出),导致程序崩溃,因此内存管理是一件重要且繁杂的事情,而python解释器自带的垃圾回收机制把程序员从繁杂的内存管理中解放出来

理解GC原理需要储备的知识

堆区和栈区

1
2
3
4
5
    在定义变量时,变量名与变量值都是需要储存的,分别对应内存中的两块区域,堆区与栈区

# 1 变量名与值的内存地址的关联关系存放于栈区

# 2 变量值存放于堆区,内存管理机制回收的则是堆区的内容

直接引用与间接引用

1
2
3
4
5
6
7
8
直接引用指的是从栈区出发直接引用到的内存地址

间接引用指的是从栈区出发引用到堆区后,再通过进一步引用才能到达的内存地址

例如↓↓↓
# l2 = [20, 30] 列表本身被变量名l2直接引用,包含的元素被列表间接引用
# x = 10 值10被变量名x直接引用
# l1 = [x, l2] 列表本身被变量名l1直接引用,包含的元素被列表间接引用

垃圾回收机制原理分析

1
python的GC模块只要运用了'引用计数'来跟踪和回收垃圾. 在引用计数的基础上,还可以通过'标记-清除'解决容器对象可能产生的循环引用的问题,并且通过'分代回收'以空间换取时间的方式来进一步提高垃圾回收的效率

引用计数

1
2
3
4
引用计数就是:变量值被变量名关联的次数
如:age = 18
变量值18被关联了一个变量名age,称之为引用计数为1
图解如下↓↓↓

img

1
2
3
4
引用计数增加:
age = 18 # 此时, 变量值18的引用计数为1
m = age # 此时, 把age的内存地址给了m,此时,m和age都关联了18,所以变量值18的引用计数为2
图解如下↓↓↓

img

1
2
3
4
引用计数减少:
age = 10 # 变量名age先与值18解除关联, 再与10建立了关联,变量值18的引用计数就为1
del m # del的意思是解除变量名m与变量值18的关联关系,此时,变量值18的引用计数就为0
图解如下↓↓↓

img

1
变量值18的引用计数一旦变为0,其占用的内存地址就应该被解释器的垃圾回收机制回收

问题1:循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
引用计数机制存在一个致命的弱点,即循环引用(也称交叉引用)
# 如下我们定义了两个列表,简称列表1与列表2,变量名l1指向列表1,变量名l2指向列表2
l1=['xxx'] # 列表1被引用一次,列表1的引用计数变为1
l2=['yyy'] # 列表2被引用一次,列表2的引用计数变为1
l1.append(l2) # 把列表2追加到l1中作为第二个元素,列表2的引用计数变为2
l2.append(l1) # 把列表1追加到l2中作为第二个元素,列表1的引用计数变为2

# l1与l2之间有相互引用
# l1 = ['xxx'的内存地址,列表2的内存地址]
# l2 = ['yyy'的内存地址,列表1的内存地址]
>>> l1
['xxx', ['yyy', [...]]]
>>> l2
['yyy', ['xxx', [...]]]
>>> l1[1][1][0]
'xxx'

1
2
3
4
5
6
循环引用会导致:值不被任何名字关联,但是值的引用计数不会为0,应该被回收但不能被回收 
例如:
del l1 # 列表1的引用计数减1,列表1的引用计数变为1
   del l2 # 列表2的引用计数减1,列表2的引用计数变为1
此时,只剩下列表1与列表2之间的相互引用

1
但此时两个列表的引用计数均不为0,但两个列表不再被任何其他对象关联,没有任何人可以再引用到它们,所以它俩占用的内存空间应该被回收,但由于相互引用的存在,每一个对象的引用计数都不为0,因此这些对象所占用的内存永远不会被释放,所以循环引用是致命的,这与手动进行内存管理所产生的内存泄漏毫无区别,所以python引入了'标记-清除''分代回收'来分别解决引用计数的循环引用带来的效率低的问题

解决方案:标记-清除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 容器对象(比如:listsetdictclassinstance)都可以包含对其他对象的引用,所以都可能产生循环引用。而“标记-清除”计数就是为了解决循环引用的问题。

标记/清除算法的做法是当应用程序可用的内存空间被耗尽的时,就会停止整个程序,然后进行两项工作,第一项则是标记,第二项则是清除

#1、标记
通俗地讲就是:
栈区相当于“根”,凡是从根出发可以访达(直接或间接引用)的,都称之为“有根之人”,有根之人当活,无根之人当死。

具体地:标记的过程其实就是,遍历所有的GC Roots对象(栈区中的所有内容或者线程都可以作为GC Roots对象),然后将所有GC Roots的对象可以直接或间接访问到的对象标记为存活的对象,其余的均为非存活对象,应该被清除。

#2、清除
清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。

基于上例的循环引用,当我们同时删除l1与l2时,会清理到栈区中l1与l2的内容以及直接引用关系
1
这样在启用标记清除算法时,从栈区出发,没有任何一条直接或间接引用可以访达l1与l2,即l1与l2成了“无根之人”,于是l1与l2都没有被标记为存活,二者会被清理掉,这样就解决了循环引用带来的内存泄漏问题。

问题2:效率问题

1
基于引用计数的回收机制,每次回收内存,都需要把所有对象的引用计数都遍历一遍,这是非常消耗时间的,于是引入了分代回收来提高回收效率,分代回收采用的是用“空间换时间”的策略。

解决方案:分代回收

分代:

1
2
3
4
5
分代回收的核心思想是:在历经多次扫描的情况下,都没有被回收的变量,gc机制就会认为,该变量是常用变量,gc对其扫描的频率会降低,具体实现原理如下:

分代指的是根据存活时间来为变量划分不同等级(也就是不同的代)

新定义的变量,放到新生代这个等级中,假设每隔1分钟扫描新生代一次,如果发现变量依然被引用,那么该对象的权重(权重本质就是个整数)加一,当变量的权重大于某个设定得值(假设为3),会将它移动到更高一级的青春代,青春代的gc扫描的频率低于新生代(扫描时间间隔更长),假设5分钟扫描青春代一次,这样每次gc需要扫描的变量的总个数就变少了,节省了扫描的总时间,接下来,青春代中的对象,也会以同样的方式被移动到老年代中。也就是等级(代)越高,被垃圾回收机制扫描的频率越低

回收:

1
回收依然是使用引用计数作为回收的依据
1
2
3
4
5
6
7
8
9
10
虽然分代回收可以起到提升效率的效果,但也存在一定的缺点:

#例如一个变量刚刚从新生代移入青春代,该变量的绑定关系就解除了,该变量应该被回收,但青春代的扫描频率低于新生代,这就到导致了应该被回收的垃圾没有得到及时地清理。

没有十全十美的方案:
毫无疑问,如果没有分代回收,即引用计数机制一直不停地对所有变量进行全体扫描,可以更及时地清理掉垃圾占用的内存,但这种一直不停地对所有变量进行全体扫描的方式效率极低,所以我们只能将二者中和。

综上
垃圾回收机制是在清理垃圾&释放内存的大背景下,允许分代回收以极小部分垃圾不会被及时释放为代价,以此换取引用计数整体扫描频率的降低,从而提升其性能,这是一种以空间换时间的解决方案目录

内容概要

  • 名称空间
  • 作用域

名称空间

1
2
3
4
5
6
名称空间就是---> 存放名字和对象映射/绑定关系(内存地址)的地方

例如:
对于x = 3,python会申请内存空间存放对象3,然后将名字x与3的绑定关系存放于名称空间中,del x ---> 表示清除该绑定关系

# 在程序执行期间最多会存在三种名称空间 ↓↓↓

(内建)内置名称空间

1
2
3
4
5
伴随python解释器的 启动/关闭 而 产生/回收  所以内置名称空间 ---> 是第一个被加载的名称空间(在第一阶段解释器启动时就产生) 

用来存放一些内置的名字,比如内置函数名:
max
<built-in function max> #built-in内置

全局名称空间

1
2
3
4
5
6
7
8
9
10
11
12
伴随python文件的开始执行/执行完毕 而 产生/回收  所以全局名称空间是 ---> 是第二个被加载的名称空间(在第三阶段执行python文件时),文件执行过程中产生的名字都会存放于该名称空间中,例如:

import sys # 模块名:sys
x = 1
if x == 1
y = 2 # 变量名
def foo(x): # 函数名foo
y = 1
def bar():
pass
Class Bar: # 类名
pass

局部名称空间

1
2
3
4
伴随函数的 调用/结束 而临时 产生/回收,函数的形参、函数内定义的名字都会被存放于该名称空间中

def foo(x):
y = 3 # 调用函数时,才会执行函数代码,名字x和y都存放于该函数的局部名称空间中
1
2
3
4
5
'''
名称空间的加载顺序是:内置名称空间 ---> 全局名称空间 ---> 局部名称空间

而查找顺序为:局部名称空间 ---> 全局名称空间 ---> 内置名称空间
'''

作用域

全局作用域与局部作用域

1
2
3
4
5
按照名字作用范围的不同可以将三个名称空间划分为两个区域:

1 全局作用域:位于全局名称空间、内置名称空间中的名字属于全局范围,该范围内的名字全局存活(除非被删除,否则在整个文件执行过程中存活)、全局有效(在任意位置都可以使用)

2 局部作用域:位于局部名称空间中的名字属于局部范围,该范围内的名字临时存活(即在函数调用时临时生成,函数调用结束后就释放)、局部有效(只能在函数内使用)

作用域与名字查找的优先级

1
2
3
4
5
6
7
在局部作用域查找名字时,起始位置是局部作用域,所以先查找局部名称空间,没有找到,再去全局作用域查找 (先查找全局名称空间,没有找到,再查找内置名称空间),最后都没有找到就会抛出异常

x = 100
def foo()
x=300 #在函数调用时产生局部作用域的名字x
foo()
print(x) #在全局找x,结果为100
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

'''
ps:
可以调用内置函数local(),globals() 来分别查看局部作用域和全局作用域的名字,然后由内而外一层层查找外部嵌套函数定义的作用域,没有找到,则查找全局作用域
'''
x=1
def outer():
x=2
def inner(): # 函数名inner属于outer这一层作用域的名字
x=3
print('inner x:%s' %x)

inner()
print('outer x:%s' %x)

outer()
#结果为
inner x:3
outer x:2
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
# 在函数内,无论嵌套多少层,都可以查看到全局作用域的名字,若要在函数内修改全局名称空间中名字的值,当值为不可变类型时,则需要用到global关键字

x=1
def foo():
global x #声明x为全局名称空间的名字
x=2
foo()
print(x) #结果为2

当实参的值为可变类型时,函数体内对该值的修改将直接反应到原值

num_list=[1,2,3]
def foo(nums):
nums.append(5)

foo(num_list)
print(num_list)
#结果为
[1, 2, 3, 5]
对于嵌套多层的函数,使用nonlocal关键字可以将名字声明为来自外部嵌套函数定义的作用域(非全局)



#结果
3

nonlocal x会从当前函数的外层函数开始一层层去查找名字x,若是一直到最外层函数都找不到,则会抛出异常。

总结:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
'''
名称空间namespaces:
存放名字与其对应的内存地址的地方
'''

# 名称空间分为三大类:
1 内置名称空间:存放的是python解释器自带的名字
生命周期:解释器启动则产生,解释器关闭则销毁

2 全局名称空间:存放的是顶级的名字
生命周期:python程序刚开始运行则立即产生,python程序结束销毁

3 局部名称空间:存放的是函数内的名字
生命周期:函数调用则产生,函数调用结束则销毁

'''
名字的访问优先级:
基于自己当前所在的位置向外一层一层查找:L-->E-->G-->B

名称空间的 "嵌套" 关系是在 函数定义阶段,扫描语法时生成的,与调用位置无关
'''