摘要
本文内容转自网络,个人学习记录使用,请勿传播
内存结构图
计算机的内存空间作用:
- 存储数据
- 衡量计算机内存空间大小的单位:
- bit位:只可以存储一个二进制的数字
- byte字节:8bit
- kb,mb,gb
- 计算机内存空间的大小表示什么?
- 计算机内存空间越大存储数据的数值越大
- 8bit:2的8次方
- 计算中的内存空间会有两个默认的属性:
- 内存空间的大小
- 决定内存存储数据的大小
- 内存空间的地址
- 用来让cpu寻址的
- 内存空间的大小
引用
变量就是引用,引用就是变量
1
2a = 1
#变量a表示的是什么?a表示的是数据1对应内存空间的地址,因此我们说a引用了1
指向
- 如果一个变量引用了某块内存空间的地址,则该变量指向这块内存中的值
可变和不可变数据类型
python的8中内置类型
- 数字类型,布尔类型,字符串,列表,字典,元组,set集合,bytes(二进制类型)
可变类型:该类型的数据是一个在定义后被修改的
- 列表,字典 ,set
不可变类型:该类型的数据在定义后无法被修改
- 数字类型,布尔类型,字符串,元组,bytes
1
2a = 1 #定义
a = 2 #发生了什么?重新开辟了新的内存存储了2,然后使得a指向了新内存的地址- 如何区分数据是否为可变?
- 如果该类型提供了修改数据的操作就是可变类型,否则就是不可变类型
重要数据类型
- 字符串,列表,字典,set集合(去除重复元素),元组
数据类型存在的意义?
提高数据存储运算的性能和适当节省内存空间
在编程中,让计算机存储两个数据,分别是1和9999,这两个数据占据内存大小是否一样?
- 是一样的!1和9999都是占用了4个字节大小的内存空间。
回顾案例1:实现完整的猜数字游戏
规则如下:
- 可以不间断的进行猜数字游戏环节,找到猜对了,结束程序,猜不对,可以不断的进行游戏,并且需要提示用户猜打了还是猜小了。
- 拓展功能实现:
- 1.最后需要统计出,用户猜了多少次才猜对。
- 2.每一个用户的初始分数为100,每猜错一次扣5分,最后程序结束,统计用户的得分
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'''
规则如下:
- 可以不间断的进行猜数字游戏环节,找到猜对了,结束程序,猜不对,可以不断的进行游戏,并且需要提示用户猜打了还是猜小了。
- 拓展功能实现:
- 1.最后需要统计出,用户猜了多少次才猜对。
- 2.每一个用户的初始分数为100,每猜错一次扣5分,最后程序结束,统计用户的得分
'''
score = 100 #用户初始的分值
count = 0 #用来记录猜的次数
#写出大致的框架,然后补充细节
guess_num = 15 #提供了一个让用户猜的数字
#让用户去猜(实现和用户进行交互)
#input就是一个工具:获取用户从键盘上录入数据
while 1: #死循环:没有结束的循环
count += 1 #count = count + 1,使得猜的次数增加一个1
num = input('enter you guess num:')
num = int(num) #类型转换,将字符串的num转成数据类型的num
#判断:guess_num和num是否一样
if guess_num == num:#猜对了
print('猜对了,您一共猜的次数为:',count)
print('最终得分为:',score)
break #结束循环
elif guess_num < num:#猜大了
print('猜大了')
score -= 5
else:#猜小了
print('猜小了') #至此一局游戏结束
score -= 5
回顾案例2:算法实现
规则如下:有三个变量a,b,c,其值均为自然数,其中a2+b2=c2且a+b+c=1000,求出满足要求a,b,c的所有组合。
1
2
3
4
5for a in range(1001): #a的取值就是0-1000
for b in range(1001):#b的取值就是0-1000
c = 1000 - a - b #得到了c的取值
if a + b + c == 1000 and a**2 + b**2 == c**2:
print(a,b,c)
回顾案例3:算法实现
找寻出列表中存储元素的最大值,且将其移动到列表最后的位置
1
2
3
4
5
6
7alist = [2,8,61,7,6]
#如何找寻出列表中最大元素的值?需要两两元素比较
for i in range(len(alist)-1):#range(4)==>[0,1,2,3]
if alist[i] > alist[i+1]:
#两个元素交换位置
alist[i],alist[i+1] = alist[i+1],alist[i]
print(alist)
函数
引言
什么是函数?
- 前面在讲解Python数据类型的时候,我们已经接触过函数了。我们说,所谓的函数其实就是Python语言中的一种工具,基于该工具可以完成不同的具体操作。函数可以分为两种,一种是内置函数,另一种是自定义函数,我们可以这么理解:
- 内置函数:
- 内置函数其实就是Python语言的开发者已经给我们设计好的工具,我们可以直接使用这些已经被设计好的工具或者函数完成相关的操作
- 自定义函数:
- 当然,Python语言的开发者们也无法将我们在不同需求中要使用的操作都设计成不同的函数或者工具,那么我们也可以向开发者那样自行设计定制我们专属功能的工具函数。
- 案例:当你在野外露营的时候,如果想生火,如果你身上恰好带了打火机,则可以直接使用该工具自行完成生火操作,否则,你也可以自己利用现有环境下的资源自行制作取火工具。
那么,为什么要使用函数呢?
- 第一:
- 函数的使用可以重用代码,省去重复性代码的编写,提高代码的重复利用率。如果程序中需要多次使用某种特定的功能,那么只需要编写一个合适的函数就可以了。程序可以在任何需要的地方调用该函数,并且同一个函数可以在不同的程序中调用,就像我们经常使用的print()和input()函数一样。
- 第二:
- 函数能封装内部实现,保护内部数据。很多时候,我们把函数看做“黑盒子”,即对应一定的输入会产生特定的结果或返回某个对象。往往函数的使用者并不是函数的编写者,函数的使用者对黑盒子的内部行为并不需要考虑,可以把精力投入到自身业务逻辑的设计而不是函数的实现细节。只有函数的设计者或者说编写者,才需要考虑函数内部实现的细节,如何暴露对外的接口,返回什么样的数据,也就是API的设计。
- 第一:
函数基础
自定义函数的使用要经过两个过程
- 函数的定义(制定)
- 函数的调用(使用)
函数定义语法
1
2
3def 函数名(参数):
#内部代码
return 表达式1
2
3
4
5
6def myFunc(): #函数定义的时候,函数体是不会被执行的
#函数体
a = 1
b = 2
c = a + b
print('a和b的计算结果为:',c)
函数调用语法
函数编写出来就是给人调用的。
要调用一个函数,必须使用函数名后跟圆括号的方式才能调用函数。
调用的同时要根据函数的定义体,提供相应个数和类型的参数,每个参数之间用逗号分隔,否则就会报错。
1
myFunc() #函数调用,函数定义中的函数体才会被执行
函数定义规范使用
返回值
- return语句
- 当一个函数被调用结束后,该函数势必已经将一组操作执行结束了,如果在操作执行结束后,想要将一个结果返回给调用者,则就可以使用return语句实现。
1 | #返回一个表达式 |
1 | #不写return默认返回None(空) |
1 | #返回多个结果 |
1 | #return后面的代码无意义:程序执行到return语句后,表示函数调用结束 |
1 | def outer(): |
1 | #如何调用inner这个内部函数呢? |
1 | #返回函数调用(了解) |
- 闭包函数(简单了解即可):
- 首先给出闭包函数的必要条件:
- 闭包函数必须返回一个函数对象/内部函数名
- 闭包函数返回的那个函数必须引用外部变量,而返回的那个函数内部不一定要return返回值
- 首先给出闭包函数的必要条件:
函数参数
增加函数的通用性
1
2
3
4
5
6
7
8#定义一个函数可以计算出两个数据的和
def my_add():
num1 = 1
num2 = 2
return num1 + num2
result = my_add()
print(result)
#局限性:只可以计算指定两个数的和,无法实现具有较高的通用性1
2
3
4
5
6#定义一个函数可以计算出两个数据的和:具有更强的通用性
def my_add(num1,num2): #num1=5,num2=9
return num1 + num2
result = my_add(5,9)
print(result)绝大多数函数在定义的时候需要接收一定数量的参数,然后根据实际调用时提供的参数的不同,输出不同的结果。注意将函数内部的参数名字,定义得和外部变量的名字一样是一种不好的习惯,它容易混淆思维,甚至发生错误。
参数的两种称谓
- 形参(形式参数)
- 函数定义时,制定的参数叫做形参
- 实参(实际参数)
- 函数调用时,传递的参数叫做实参
- 而我们通常讨论的参数,指的都是形参
- 形参(形式参数)
参数传递
函数通常都有参数,用于将外部的实际数据传入函数内部进行处理。但是,在处理不同数据类型的参数时,会有不同的情况发生。这一切都是因为以下一点。
- Python的函数参数传递的是实际数据的内存地址。
例子1:不可变类型参数
1
2
3
4
5
6
7
8
9a = 1
def func(b):
b = 2
print('形参b的内存地址为:%s'%id(b))
print('函数内部的b为:',b)
print('变量a的内存地址为:',id(a))
func(a)
print('函数调用后,函数外部的变量a为:',a)
例子2:上面例子说的是不可变类型参数,如果是可变类型的,比如列表呢?
1
2
3
4
5
6
7
8
9a = [1,2,3]
def func(b):
b.append(4)
print('变量b的内存地址为:',id(b))
print('函数内部的b为:',b)
print('变量a的内存地址为:',id(a))
func(a)
print('函数外部的变量a为:',a)
调用函数时将列表对象a的地址传递给了函数内部的变量b。b.append(4)的时候,根据传进来的内存地址,找到[1,2,3]这个列表对象,在它的后面添加了4。
可以看出,此时的a和b实际指向了同一个对象。为什么会这样?因为最关键的b.append(4)这句代码,它不同于“=”赋值语句,不会创建新的变量,而列表作为可变类型,具有append方法,这个方法只是对列表的一种调用而已。因此,a和b实际还是同一个对象。
参数类型
- 参数的不同种类
- 定义函数时,参数的名字和位置确定下来,函数的接口就固定了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。Python函数的参数定义灵活度非常大。除了正常定义的位置参数外,还可以使用:
- 位置参数
- 默认参数
- 动态参数
- 定义函数时,参数的名字和位置确定下来,函数的接口就固定了。对于函数的调用者来说,只需要知道如何传递正确的参数,以及函数将返回什么样的值就够了,函数内部的复杂逻辑被封装起来,调用者无需了解。Python函数的参数定义灵活度非常大。除了正常定义的位置参数外,还可以使用:
位置参数
也叫必传参数或者顺序参数,是最重要的、也是必须在调用函数时明确提供的参数!位置参数必须按先后顺序,一一对应,个数不多不少的传递!
1
2
3
4
5
6
7def add(a,b,c):
return a+b+c
x = y = 5
r1 = add(x,y,x)
r2 = add(4,5,6)
print(r1,r2)上面例子中的a,b,c就是位置参数,我们在使用add(4, 5, 6)调用时,就是将4的地址传给a,5的传给b,6的传给c的一一对应传递。
- 类似add(4, 5, 6, 7)、add(4)这种“画蛇添足”、“缺胳膊少腿”和“嫁错郎”类型的调用都是错误的。
注意: Python在做函数参数传递的时候不会对数据类型进行检查,理论上你传什么类型都可以!
1
2
3
4def add(a,b,c):
return a+b+c
add(1,2,'haha')但是,上面的add函数,如果你传递了一个字符串和两个数字,结果是弹出异常,因为字符串无法和数字相加。
- 这就是Python的弱数据类型和动态语言的特点。在简单、方便的时候,需要你自己去实现数据类型检查。
默认参数
在函数定义时,如果给某个参数提供一个默认值,这个参数就变成了默认参数,不再是位置参数了。在调用函数的时候,我们可以给默认参数传递一个自定义的值,也可以使用默认值。
1
2
3
4
5def power(x,n=2): #x是位置参数,n是默认参数
return x * n
power(10)
power(10,4)上面例子中的n就是个默认参数。默认参数可以简化函数的调用,在为最常用的情况提供简便调用的同时,还可以在特殊情况时传递新的值。
默认参数的注意事项:
- 默认参数必须在位置参数后面!
- 使用参数名传递参数
1
2
3
4
5
6
7def student(name,sex,age,classroom='101',tel='1323333333',address='...'):
pass
#下述函数调动是否可以?
student('jack','male',17) #?
student('tom','male',18,'102','666','Beijing') #?
student('marry','female',18,'102',address='SH') #?
student('mary','female',address='Bj',18) #?首先先来看一道面试真题:输出程序运行后的结果
1
2
3
4
5
6
7def func(a=[]): #将可变类型数据作为了默认参数的默认值
a.append('A')
return a
print(func())
print(func())
print(func())解释:
- 因为Python函数体在被读入内存的时候,默认参数a指向的空列表对象就会被创建,并放在内存里了。因为默认参数a本身也是一个变量,保存了指向对象[]的地址。每次调用该函数,往a指向的列表里添加一个A。a没有变,始终保存的是指向列表的地址,变的是列表内的数据!
动态参数
顾名思义,动态参数就是传入的参数的个数是动态的,可以是1个、2个到任意个,还可以是0个。在不需要的时候,你完全可以忽略动态函数,不用给它传递任何值。
Python的动态参数有两种,分别是:
- *args
- **kwargs
- 这里面的关键是一个和两个星号的区别,而不是args和kwargs在名字上的区别
注意:
- 动态参数,必须放在所有的位置参数和默认参数后面!
*args
一个星号表示接收任意个参数。调用时,会将实际参数打包成一个元组传入形式参数。如果参数是个列表,会将整个列表当做一个参数传入。例如:
1
2
3
4
5def func(*args):#动态参数
print(args)
for param in args:
print(param) #param就是接收到的每一个参数
func(1,2,3)问题:
通过循环args,我们可以获得传递的每个参数。但是li这个列表,我们本意是让它内部的1,2,3分别当做参数传递进去,但实际情况是列表本身被当做一个整体给传递进去了。怎么办呢?
1
2
3
4
5
6
7def func(*args):
print(args)
for param in args:
print(param)
li = [1,2,3]
func(li)解决:
使用一个星号!调用函数,传递实参时,在列表前面添加一个星号就可以达到目的了。实际情况是,不光列表,任何序列类型数据对象,比如字符串、元组都可以通过这种方式将内部元素逐一作为参数,传递给函数。而字典,则会将所有的key逐一传递进去。
1
2
3
4
5
6
7def func(*args):
print(args)
for param in args:
print(param)
li = [1,2,3]
func(*li)
**kwargs
两个星表示接受键值对的动态参数,数量任意。调用的时候会将实际参数打包成字典。例如:
1
2
3
4def func(**kwargs):
print(kwargs)
func(k1=1,age=2,k3=2)
问题:
而如果我们这样传递一个字典dic呢?我们希望字典内的键值对能够像上面一样被逐一传入。
1
2
3
4
5
6
7
8
9def func(**kwargs):
print(kwargs)
dic = {
'k1':1,
'age':20,
'k3':3
}
func(d = dic)上述程序实际结果却是弹出错误,为什么?
解释:
- 因为这时候,我们其实是把dic当做一个位置参数传递给了func函数。而func函数并不接收任何位置函数。那怎么办呢?使用两个星号!
1
2
3
4
5
6
7
8
9def func(**kwargs):
print(kwargs)
dic = {
'k1':1,
'age':20,
'k3':3
}
func(**dic)有了前面一个星号的基础,这里我们应该很好理解了。两个星号能将字典内部的键值对逐一传入**kwargs。
万能参数
- 当args和**kwargs组合起来使用,理论上能接受任何形式和任意数量的参数,在很多代码中我们都能见到这种定义方式。需要注意的是,args必须出现在**kwargs之前。
变量的作用域
讲到了函数就必须介绍变量的作用域相关。
- 作用域指的是变量的有效范围。变量并不是在哪个位置都可以访问的,访问权限取决于这个变量是在哪里赋值的,也就是在哪个作用域内赋的值。变量在哪个作用域内赋值,则表示该变量的作用域就是该区域,变量只可以在其作用域指定区域被访问。
通常而言,在编程语言中,变量的作用域从代码结构形式来看,有块级、函数、类、模块、包等由小到大的级别。但是在Python中,没有块级作用域,也就是类似if语句块、for语句块、while语句块,with上下文管理器等等是不存在作用域概念的,他们等同于普通的语句。
核心:
通常,函数内部的变量无法被函数外部访问,但内部可以访问;类内部的变量无法被外部访问,但类的内部可以。通俗来讲,就是内部代码可以访问外部变量,而外部代码通常无法访问内部变量。
1
2
3
4
5
6
7#下列程序存在的问题是什么?
age = 10
def func():
name = 'bobo'
print(age,name)
func()
print(name)1
2
3
4
5
6
7def outer():
def inner():
print('i am inner')
inner()
return inner
inner()
全局变量和局部变量
局部变量
- 定义在函数内部的变量拥有一个局部作用域,被叫做局部变量
全局变量
- 定义在函数外的拥有全局作用域的变量,被称为全局变量。(类、模块等同理)
1 | num = 123 #? |
- 注意:
- 所谓的局部变量是相对的。局部变量也有可能是更小范围内的变量的外部变量。
1 | a = 1 #全局变量 |
global关键字
先来看一个例子
1
2
3
4
5
6
7
8
9
10total = 0 #全局变量
#函数定义
def plus(arg1,arg2): #arg1=10,arg2=20
#total是局部变量!只不过和外部的全局变量同名了而已!
total = arg1 + arg2 #total=30
print('函数内的变量total=',total)
return total
#函数调用
plus(10,20)
print('函数外部变量total=',total)很明显,函数plus内部通过total = arg1 + arg2语句,新建了一个局部变量total,它和外面的全局变量total是两码事。而如果我们,想要在函数内部修改外面的全局变量total呢?使用global关键字!
global:
- 指定当前变量使用外部的全局变量
1
2
3
4
5
6
7
8
9
10total = 0 #全局变量
#函数定义
def plus(arg1,arg2): #arg1=10,arg2=20
global total #应用外部全局变量的声明
total = arg1 + arg2 #total=30
print('函数内的变量total=',total)
return total
#函数调用
plus(10,20)
print('函数外部变量total=',total)
函数其他
我们将函数涉及到的其他几点进行讲解
- range()函数
- 递归函数
- 匿名函数
range函数
range函数是内置函数,无须特别导入,在任何地方都可以直接使用它,下面看一下具体用法:
1.提供一个数字参数,直接遍历数字:
1
2for item in range(5):
print(item)- 只给一个数字类型参数,range会遍历从0到参数减1的数字。要特别注意,range默认从0开始,到参数减1,也就是左闭右开的规则,这也是Python很多地方的规则,比如切片。
2.也可以指定遍历的区间:
1
2for item in range(1,5):
print(item)3.还可以指定步长,就像切片一样
1
2for item in range(1,11,3):
print(item)4.但更多的时候是结合range和len函数,遍历一个序列的索引
1
2
3alist = [1,2,3,4,5]
for i in range(len(alist)):
print(alist[i])5.指定步长为-1可以倒序遍历
1
2for i in range(6,1,-1):
print(i)
递归函数
如果一个函数在内部调用了自身,这个函数就被称为递归函数。
1
2
3
4
5def func():
print('正在调用func函数')
func()
func()What?函数可以自己调用自己?会不会进入死循环,永远退出不了?我们先看一个例子,典型的高斯求和问题,1+2+3+4+…+99+100,不使用递归的话,我们可以用循环,这么做:
1
2
3
4
5
6
7
8def sum_number(n):
sum = 0
for i in range(1,n+1):
sum += i
return sum
result = sum_number(5)
print(result)但如果使用递归函数来写,是这样的:
1
2
3
4
5
6def sum_number(n):
if n == 1:
return 1
return n + sum_number(n-1)
print(sum_number(5))- 分析一下代码,当n小于等于0的时候,直接给出和值为0,这句不能省。当n大于0时,结果是n加上sum_number(n-1)。这里的sum_number(n-1)又是一次sum_number函数的调用,不过参数的值变成了n-1,要得sum_number(n)到的值就必须等待sum_number(n-1)的值被计算出来,同样要得到sum_number(n-1)的值必须等待sum_number(n-2)的值,如此一路推算下去,直到sum_number(0),因为if语句的存在,它不需要等待sum_number(-1)的计算了,而是直接给出结果0。然后程序一路返回,直到回到最初的sum_number(n),并给出最终结果。
递归最核心的思想是:
- 每一次递归,整体问题都要比原来减小,并且递归到一定层次时,要能直接给出结果!
递归的优点
- 递归函数的优点是定义简单,代码量少,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
注意:
- 使用递归函数需要注意防止递归深度溢出,在Python中,通常情况下,这个深度是1000层,超过将抛出异常。在计算机中,函数递归调用是通过栈(stack)这种数据结构实现的,每当进入一个递归时,栈就会加一层,每当函数返回一次,栈就会减一层。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
1 | def sum_number(n): |
匿名函数
提问:
- 刚开始编程的同学可能会遇到一个头疼的问题,就是变量或者函数命名的问题,虽然命名规范很简单,但是如果涉及到的操作过多给每一个操作的函数都起一个高可读性的名称,必然会挖空你的英语词汇量。
当我们在创建函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。这省去了我们挖空心思为函数命名的麻烦,也能少写不少代码,很多编程语言都提供这一特性。匿名函数用好了,会有画龙点睛的效果,没用好,就容易“画虎不成反类犬”,需要我们在平时的代码过程中,多学、多看、多琢磨。
匿名函数的使用
- Python语言使用lambda关键字来创建匿名函数。
- 所谓匿名,即不再使用def语句这样标准的形式定义一个函数。
- lambda只是一个表达式,而不是一个代码块,函数体比def简单很多。
- 仅仅能在lambda表达式中封装有限的逻辑。
- 定义语法:
- 其形式通常是这样的:lambda 参数: 表达式
例子:它相当于下面的函数:
1
2
3
4
5
6
7
8def func(num):
if num > 0:
return 1
else:
return 0
#该匿名函数就等同于上面的有名函数
result = lambda x:1 if x > 0 else 0 #x就是匿名函数的参数,0和1就是返回值
print(result(1)) #调用匿名函数匿名函数只能有一个表达式,不用也不能写return语句,表达式的结果就是其返回值。