语法
语句与缩进:
Python的标准语句不需要使用分号或逗号来表示语句结束,简简单单的换个行就表示本语句已经结束,下一句开始。
Python最具特色的语法就是使用缩进来表示代码块,不需要使用大括号({}),缩进的空格数是可变的,但是同一个代码块的语句必须包含相同的缩进空格数。
Python一行通常就是一条语句,一条语句通常也不会超过一行。其实,从语法层面,Python并没有完全禁止在一行中使用多条语句,也可以使用分号实现多条语句在一行(不推荐如此使用)
多行语句: 前面是多条语句在一行,但如果一条语句实在太长,也是可以占用多行的,可以使用反斜杠(\)来实现多行语句,在 [], {}, 或 () 中的多行语句,可以不需要使用反斜杠(\),直接回车,接着写。
变量与常量:
变量
每个变量在使用前都必须赋值,变量赋值以后才会被创建。
新的变量通过赋值的动作,创建并开辟内存空间,保存值。如果没有赋值而直接使用会抛出赋值前引用的异常或者未命名异常。
Python 中的变量不需要声明类型,可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量
这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。例如Java是静态语言,
最后,理解变量在计算机内存中的表示也非常重要。当我们写:
a = 'ABC'
时,Python解释器干了两件事情:
在内存中创建了一个'ABC'的字符串;
在内存中创建了一个名为a的变量,并把它指向'ABC'。
也可以把一个变量a赋值给另一个变量b,这个操作实际上是把变量b指向变量a所指向的数据
Python中,一切事物都是对象,变量引用的是对象或者说是对象在内存中的地址。
在Python中,变量本身没有数据类型的概念,通常所说的“变量类型”是变量所引用的对象的类型,或者说是变量的值的类型。
>>> a = 1
>>> a = "haha"
>>> a = [1, 2, 3]
>>> a = { "k1":"v1"}
例子中,变量a在创建的时候,赋予了值为1的整数类型,然后又被改成字符串“haha”,再又变成一个列表,最后是个字典。变量a在动态的改变,它的值分别是不同的数据类型,这是动态语言的特点。
Python允许同时为多个变量赋值。
例如:a = b = c = 1,最终大家都是1。
也可以同时为多个变量赋值,用逗号分隔,逐一对应。
例如:a, b, c = 1, 2, 3,最后a是1,b是2,c是3.
常量
所谓常量就是不能变的变量,比如常用的数学常数π就是一个常量。在Python中,通常用全部大写的变量名表示常量:
可变与不可变
上面我们讲了,str是不变对象,而list是可变对象。
对于可变对象,比如list,对list进行操作,list内部的内容是会变化的,比如:
>>> a = ['c', 'b', 'a']
>>> a.sort()
>>> a
['a', 'b', 'c']
而对于不可变对象,比如str,对str进行操作呢:
>>> a = 'abc'
>>> a.replace('a', 'A')
'Abc'
>>> a
'abc'
虽然字符串有个replace()方法,也确实变出了'Abc',但变量a最后仍是'abc',应该怎么理解呢?
我们先把代码改成下面这样:
>>> a = 'abc'
>>> b = a.replace('a', 'A')
>>> b
'Abc'
>>> a
'abc'
要始终牢记的是,a是变量,而'abc'才是字符串对象!有些时候,我们经常说,对象a的内容是'abc',但其实是指,a本身是一个变量,它指向的对象的内容才是'abc':
┌───┐ ┌───────┐
│ a │─────────────────>│ 'abc' │
└───┘ └───────┘
当我们调用a.replace('a', 'A')时,实际上调用方法replace是作用在字符串对象'abc'上的,而这个方法虽然名字叫replace,但却没有改变字符串'abc'的内容。相反,replace方法创建了一个新字符串'Abc'并返回,如果我们用变量b指向该新字符串,就容易理解了,变量a仍指向原有的字符串'abc',但变量b却指向新字符串'Abc'了:
┌───┐ ┌───────┐
│ a │─────────────────>│ 'abc' │
└───┘ └───────┘
┌───┐ ┌───────┐
│ b │─────────────────>│ 'Abc' │
└───┘ └───────┘
所以,对于不变对象来说,调用对象自身的任意方法,也不会改变该对象自身的内容。相反,这些方法会创建新的对象并返回,这样,就保证了不可变对象本身永远是不可变的。
切片
切出一部分数据
格式:[start:stop:step],其中start
是起始索引,stop
是结束索引(不包含),step
是步长,每个参数都可以省略,省略后的含义各有不同
取一个list或tuple的部分元素是非常常见的操作。比如,一个list如下:
>>> L = ['Michael', 'Sarah', 'Tracy', 'Bob', 'Jack']
>>> L[0:3]
['Michael', 'Sarah', 'Tracy']
- 省略步长
L[0:3]表示,从索引0开始取,直到索引3为止,但不包括索引3。即索引0,1,2,正好是3个元素。
- 如果第一个索引是0,还可以省略:
>>> L[:3]
['Michael', 'Sarah', 'Tracy']
- 也可以从索引1开始,取出2个元素出来:
>>> L[1:3]
- 前10个数,每两个取一个:
>>> L[:10:2]
[0, 2, 4, 6, 8]
- 所有数,每5个取一个:
>>> L[::5]
[0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
- 甚至什么都不写,只写[:]就可以原样复制一个list:
>>> L[:]
[0, 1, 2, 3, ..., 99]
- 类似的,既然Python支持L[-1]取倒数第一个元素,那么它同样支持倒数切片
字符串'xxx'也可以看成是一种list,每个元素就是一个字符。因此,字符串也可以用切片操作,只是操作结果仍是字符串:
>>> 'ABCDEFG'[:3]
'ABC'
>>> 'ABCDEFG'[::2]
'ACEG'
在很多编程语言中,针对字符串提供了很多各种截取函数(例如,substring),其实目的就是对字符串切片。Python没有针对字符串的截取函数,只需要切片一个操作就可以完成,非常简单。
文件加载
if __name__ == '__main__':
很多时候,我们经常在python程序中看到这么一行语句,这里简要解释一下:
首先,__name__是所有模块都会有的一个内置属性,一个模块的__name__值取决于你如何调用模块。假如你有一个test.py文件,如果在a.py文件中使用import导入这个模块import test.py,那么test.py模块的__name__属性的值就是test,不带路径或者文件扩展名。但是很多时候,模块或者说脚本会像一个标准的程序样直接运行,也就是类似python test.py这种方式,在这种情况下, __name__ 的值将是一个特别缺省值"__main__"。
根据上面的特性,可以用if __name__ == '__main__'来判断是否是在直接运行该py文件!如果是,那么if代码块下的语句就会被执行,如果不是,就不执行。该方法常用于对模块进行测试和调试,区分直接运行和被导入两种情况的不同执行方式!(直接写在文件的代码,和java类似,文件加载的时候执行)
我们通过下面的例子,脚本名为test.py,执行python test.py看看实际的顺序执行方式:
import os # 1
print('<[1]> time module start') # 2
class ClassOne():
print('<[2]> ClassOne body') # 3
def __init__(self): # 10
print('<[3]> ClassOne.__init__')
def __del__(self):
print('<[4]> ClassOne.__del__') # 101
def method_x(self): # 12
print('<[5]> ClassOne.method_x')
class ClassTwo(object):
print('<[6]> ClassTwo body') # 4
class ClassThree():
print('<[7]> ClassThree body') # 5
def method_y(self): # 16
print('<[8]> ClassThree.method_y')
class ClassFour(ClassThree):
print('<[9]> ClassFour body') # 6
def func():
print("<func> function func")
if __name__ == '__main__': # 7
print('<[11]> ClassOne tests', 30 * '.') # 8
one = ClassOne() # 9
one.method_x() # 11
print('<[12]> ClassThree tests', 30 * '.') # 13
three = ClassThree() # 14
three.method_y() # 15
print('<[13]> ClassFour tests', 30 * '.') # 17
four = ClassFour()
four.method_y()
print('<[14]> evaltime module end') # 100
首先执行#1的import语句
执行#2的打印语句
ClassOne、ClassThree和ClassFour的类定义执行过程中,分别打印#3、#4、#5、#6四句话,非方法内的其他代码块执行,但是其中的方法并不执行,仅仅是载入内存
碰到#7的if __name__ == '__main__':,判断为True,于是执行if内部的代码
执行#8的print语句
执行#9,实例化一个ClassOne的对象
执行#10的初始化方法,打印一条语句
返回执行#11的menthod_x调用
返回类的定义体,找到#12,执行方法,打印语句
再返回#13处,打印
执行#14的实例化
ClassThree没有自定义初始化方法,接着执行#15
回到类里找到#16的方法,执行打印语句
执行#17
......后面不再详述
执行完最后的#100的打印语句后,按理说程序应该终止退出了,但由于ClassOne这个类定义了__del__方法,还要在最后执行它内部的代码#101这条打印语句。
通过这个例子,相信你对Python的程序执行流程能够有一定的了解。其实这个过程,也是我们读别人代码的过程。
数据类型
数字类型
数字类型用于存储数学意义上的数值。
数字类型是不可变类型。所谓的不可变类型,指的是类型的值一旦有不同了,那么它就是一个全新的对象。数字1和2分别代表两个不同的对象,对变量重新赋值一个数字类型,会新建一个数字对象(此处不一定,不同版本的python和解释器实现可能不一样)。
整数(Int) :
通常被称为整型,是正或负整数,不带小数点。Python3的整型可以当作Long类型(更长的整型)使用,所以 Python3没有Python2的Long类型。
对于很大的数,例如10000000000,很难数清楚0的个数。Python允许在数字中间以_分隔,因此,写成10_000_000_000和10000000000是完全一样的。十六进制数也可以写成0xa1b2_c3d4。
小整数对象池:
Python初始化的时候会自动建立一个小整数对象池,方便我们调用,避免后期重复生成!这是一个包含262个指向整数对象的指针数组,范围是-5到256。也就是说比如整数10,即使我们在程序里没有创建它,其实在Python后台已经悄悄为我们创建了。
为什么要这样呢?我们都知道,在程序运行时,包括Python后台自己的运行环境中,会频繁使用这一范围内的整数,如果每需要一个,你就创建一个,那么无疑会增加很多开销。创建一个一直存在,永不销毁,随用随拿的小整数对象池,无疑是个比较实惠的做法。
除了小整数对象池,Python还有整数缓冲区的概念,也就是刚被删除的整数,不会被真正立刻删除回收,而是在后台缓冲一段时间,等待下一次的可能调用。
浮点
浮点数也就是小数,之所以称为浮点数,是因为按照科学记数法表示时,一个浮点数的小数点位置是可变的,比如,1.23x109和12.3x108是完全相等的。浮点数可以用数学写法,如1.23,3.14,-9.01,等等。但是对于很大或很小的浮点数,就必须用科学计数法表示,把10用e替代,1.23x109就是1.23e9,或者12.3e8,0.000012可以写成1.2e-5,等等。
字符串
字符串和编码
因为计算机只能处理数字,如果要处理文本,就必须先把文本转换为数字才能处理。因此引入了编码和解码,也就有了不同的编码方式
字符串的表示形式
python中单引号和双引号的作用完全相同。
使用三引号('''或""")可以指定一个多行字符串。
转义符 ‘\‘,用来特殊转义,例如\r\n,\\。它可以将引号转义为单纯的引号,没有任何作用的引号。
原生字符串: 通过在字符串前加r或R,如 r"this is a line with \n",表示这个字符串里的斜杠不需要转义,等同于自身。因此,例子中的\n会显示出来,并不是换行。
如果字符串里面有很多字符都需要转义,就需要加很多\,为了简化,Python还允许用r''表示''内部的字符串默认不转义,可以自己试试:
>>> print('\\\t\\')
\ \
>>> print(r'\\\t\\')
\\\t\\
字符串会自动串联,如“i" “love" “you"会被自动转换为”I love you”。
!Python不支持单字符类型,单字符在Python中也是作为一个字符串使用。
注释:
注释文档
在某些特定的位置,用三引号包括起来的部分,也被当做注释。但是,这种注释有专门的作用,用于为__doc__提供文档内容,这些内容可以通过现成的工具,自动收集起来,形成帮助文档。比如,函数和类的说明文档
字符串拼接
在Python中,字符串拼接的方式有多种,但在性能上它们有所不同。以下是几种常见的字符串拼接方式以及它们的大致性能排名(从高到低):
-
join() 方法
当需要拼接大量字符串时,
str.join()
方法通常是最快的方式。这是因为join()
方法在内部使用了一个优化过的循环来构建字符串,避免了多次内存分配和复制操作。python复制代码
parts = ["Hello", "World"]
result = "".join(parts)
-
f-string(字面量格式化字符串)
虽然 f-string 在可读性和易用性上表现出色,但在处理大量字符串拼接时,其性能可能不是最优的。然而,在较小的字符串拼接操作中,其性能差异可能并不显著。
python复制代码
s1 = "Hello"
s2 = "World"
result = f"{s1} {s2}"
-
format() 方法
str.format()
方法在性能上与 f-string 相似,但由于它较为冗长且不如 f-string 直观,所以在现代Python代码中较少使用。python复制代码
s1 = "Hello"
s2 = "World"
result = "{} {}".format(s1, s2)
-
% 运算符(旧式字符串格式化)
%
运算符是Python中较老的字符串格式化方法,其性能与str.format()
相似,但语法不如后者直观和灵活。python复制代码
s1 = "Hello"
s2 = "World"
result = "%s %s" % (s1, s2)
-
+ 运算符
使用
+
运算符连接字符串简单直观,但在处理大量字符串时效率较低,因为每次连接都会创建一个新的字符串对象。python复制代码
s1 = "Hello"
s2 = "World"
result = s1 + " " + s2
布尔
布尔值和布尔代数的表示完全一致,一个布尔值只有True、False两种值,要么是True,要么是False,在Python中,可以直接用True、False表示布尔值(请注意大小写),也可以通过布尔运算计算出来
看完上面的例子,你会发现很多想当然的结果居然是错的。0、0.0、-0.0、空字符串、空列表、空元组、空字典,这些都被判定为False。而-1、"False"也被判断为True。
list列表
Python内置的一种数据类型是列表:list。list是一种有序的集合,可以随时添加和删除其中的元素。
比如,列出班里所有同学的名字,就可以用一个list表示:
>>> classmates = ['Michael', 'Bob', 'Tracy']
>>> classmates
['Michael', 'Bob', 'Tracy']
如果要取最后一个元素,除了计算索引位置外,还可以用-1做索引,直接获取最后一个元素:
>>> classmates[-1]
'Tracy'
以此类推,可以获取倒数第2个、倒数第3个:
list里面的元素的数据类型也可以不同,比如:
>>> L = ['Apple', 123, True]
list元素也可以是另一个list,比如:
>>> s = ['python', 'java', ['asp', 'php'], 'scheme']
>>> len(s)
tuple元组
另一种有序列表叫元组:tuple。tuple和list非常类似,但是tuple一旦初始化就不能修改
>>> classmates = ('Michael', 'Bob', 'Tracy')
如果要定义一个空的tuple,可以写成():
>>> t = ()
>>> t
()
但是,要定义一个只有1个元素的tuple,如果你这么定义:
>>> t = (1)
>>> t
1
定义的不是tuple,是1这个数!这是因为括号()既可以表示tuple,又可以表示数学公式中的小括号,这就产生了歧义,因此,Python规定,这种情况下,按小括号进行计算,计算结果自然是1。
所以,只有1个元素的tuple定义时必须加一个逗号,,来消除歧义:
>>> t = (1,)
>>> t
(1,)Python在显示只有1个元素的tuple时,也会加一个逗号,,以免你误解成数学计算意义上的括号
最后来看一个“可变的”tuple:
>>> t = ('a', 'b', ['A', 'B'])
>>> t[2][0] = 'X'
>>> t[2][1] = 'Y'
>>> t
('a', 'b', ['X', 'Y'])
dict字典
如果用dict实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用Python写一个dict如下:
>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
dict可以用在需要高速查找的很多地方,在Python代码中几乎无处不在,正确使用dict非常重要,需要牢记的第一条就是dict的key必须是不可变对象(若是可改变对象,对象的值改变后,重新计算hash后无法找到在dict上的key了)。
set
set和dict类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在set中,没有重复的key
要创建一个set,需要提供一个list作为输入集合:
>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}
注意,传入的参数[1, 2, 3]是一个list,而显示的{1, 2, 3}只是告诉你这个set内部有1,2,3这3个元素,显示的顺序也不表示set是有序的。
运算符
算术运算符:
Python中,有3种除法,一种除法是/:
>>> 10 / 3
3.3333333333333335
/除法计算结果是浮点数,即使是两个整数恰好整除,结果也是浮点数:
>>> 9 / 3
3.0
还有一种除法是//,也称为地板除,只取整数部分,余数被抛弃:
>>> 10 // 3
3
Python还提供一个余数运算,可以得到两个整数相除的余数:
>>> 10 % 3
1
如果想同时得到商和余数,可以用这个方法:
>>> divmod(10,3)
(3, 1)
因为浮点数精度的问题,Python还存在一些计算方面的小问题,例如:
>>> 0.1+0.1+0.1-0.3
5.551115123125783e-17
什么?不是应该等于0吗?Python居然还有这么不为人知的一面?
要解决这个问题,可以导入decimal模块:
>>> from decimal import Decimal
>>> Decimal('0.1')+Decimal('0.1')+Decimal('0.1')-Decimal('0.3')
Decimal('0.0')
>>> Decimal('0.1') / Decimal('0.3')
Decimal('0.3333333333333333333333333333')
>>> from decimal import getcontext
>>> getcontext().prec = 4 #设置全局精度
>>> Decimal('0.1') / Decimal('0.3')
Decimal('0.3333')
注意其中提供的数字是用字符串的形式传递给Decimal的。
身份运算符
这也是Python的特色语法(全部都是小写字母)。
注意is与比较运算符“==”的区别,两者有根本上的区别,切记不可混用:
is用于判断两个变量的引用是否为同一个对象,而==用于判断变量引用的对象的值是否相等!
举个例子: 如果有两个人都叫张三。is比较的结果是false,因为他们是不同的两个人,==比较是True,因为他们都叫张三。
>>> 3.0 is 9/3
True
>>> id(3.0)
2434753652496
>>> id(9/3)
2434747931216
>>> 3.0 is 9/3
True
>>> a = 3.0
>>> 1333 is 3999/3
False
>>> 1333.0 is 3999/3
True
>>> 133333.0 is 399999/3
True
>>>
可见Python的内部机制:在没有创建任何变量的时候,比如3.0 is 9/3 ,为了减少开销和内存使用,9/3直接复用了3.0的内存地址,所以它们的is比较为True。而当你分别将3.0赋值给变量a,9/3赋值给b,就分开了内存地址,不再是同一个对象了(后面的版本中,a、b都指向同一个内存地址了,估计是不可变的类型,在内存中都是同一份了)。
三目运算符(三元表达式)
在python中的格式为:为真时的结果 if 判定条件 else 为假时的结果
例如: True if 5>3 else False
流程控制
if条件判断
if判断条件还可以简写,比如写:
if x:
print('True')
只要x是非零数值、非空字符串、非空list等,就判断为True,否则为False。
模式匹配
当我们用if ... elif ... elif ... else ...判断时,会写很长一串代码,可读性较差。
如果要针对某个变量匹配若干种情况,可以使用match语句。
match语句除了可以匹配简单的单个值外,还可以匹配多个值、匹配一定范围,并且把匹配后的值绑定到变量:
在上面这个示例中,第一个case x if x < 10表示当age < 10成立时匹配,且赋值给变量x,第二个case 10仅匹配单个值,第三个case 11|12|...|18能匹配多个值,用|分隔。
可见,match语句的case匹配非常灵活。
匹配列表
match语句还可以匹配列表,功能非常强大。
貌似不用在每个case中通过break进行推出??????
循环
Python的循环有两种,一种是for...in循环,依次把list或tuple中的每个元素迭代出来
如果要计算1-100的整数之和,从1写到100有点困难,幸好Python提供一个range()函数,可以生成一个整数序列,再通过list()函数可以转换为list。比如range(5)生成的序列是从0开始小于5的整数
第二种循环是while循环,只要条件满足,就不断循环,条件不满足时退出循环。
函数
为什么要使用函数呢?
第一、函数的使用可以重用代码,省去重复性代码的编写,提高代码的重复利用率。
第二、函数能封装内部实现,保护内部数据,实现对用户的透明。
第三、即使某种功能在程序中只使用一次,将其以函数的形式实现也是有必要的,因为函数使得程序模块化,从“一团散沙”变成“整齐方队”,从而有利于程序的阅读、调用、修改和完善。
基础
定义
def 函数名(参数): # 内部代码 return 表达式
参数传递
函数通常都有参数,用于将外部的实际数据传入函数内部进行处理。但是,在处理不同数据类型的参数时,会有不同的情况发生。这一切都是因为以下两点。
-
Python的函数参数传递的是实际对象的内存地址。
-
Python的数据类型分可变数据类型和不可变数据类型。
空函数
如果想定义一个什么事也不做的空函数,可以用pass语句:
def nop():
pass
pass语句什么都不做,那有什么用?实际上pass可以用来作为占位符,比如现在还没想好怎么写函数的代码,就可以先放一个pass,让代码能运行起来。
pass还可以用在其他语句里,比如:
if age >= 18:
pass
缺少了pass,代码运行就会有语法错误。
返回多个值
函数可以返回多个值吗?答案是肯定的。
但其实这只是一种假象,Python函数返回的仍然是单一值:
>>> r = move(100, 100, 60, math.pi / 6)
>>> print(r)
(151.96152422706632, 70.0)
原来返回值是一个tuple!但是,在语法上,返回一个tuple可以省略括号,而多个变量可以同时接收一个tuple,按位置赋给对应的值,所以,Python的函数返回多值其实就是返回一个tuple,但写起来更方便
高阶函数
变量可以指向函数
函数名也是变量
那么函数名是什么呢?函数名其实就是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数!
可以定义新的变量指向同一个函数
既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。
一个最简单的高阶函数:把函数作为参数传入,这样的函数称为高阶函数,函数式编程就是指这种高度抽象的编程范式。
def add(x, y, f):
return f(x) + f(y)
当我们调用add(-5, 6, abs)时,参数x,y和f分别接收-5,6和abs,根据函数定义
如map函数
接收两个参数,一个是函数,一个是Iterable,map将传入的函数依次作用到序列的每个元素,并把结果作为新的Iterator返回。
现在,我们用Python代码实现:
>>> 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]
map()传入的第一个参数是f,即函数对象本身。由于结果r是一个Iterator,Iterator是惰性序列,因此通过list()函数让它把整个序列都计算出来并返回一个list。
你可能会想,不需要map()函数,写一个循环,也可以计算出结果:
L = []
for n in [1, 2, 3, 4, 5, 6, 7, 8, 9]:
L.append(f(n))
print(L)
的确可以,但是,从上面的循环代码,能一眼看明白“把f(x)作用在list的每一个元素并把结果生成一个新的list”吗?
所以,map()作为高阶函数,事实上它把运算规则抽象了,因此,我们不但可以计算简单的f(x)=x2,还可以计算任意复杂的函数,比如,把这个list所有数字转为字符串:
>>> list(map(str, [1, 2, 3, 4, 5, 6, 7, 8, 9]))
['1', '2', '3', '4', '5', '6', '7', '8', '9']
只需要一行代码。
参数类型
位置参数
也叫必传参数,顺序参数,是最重要的,也是必须在调用函数时明确提供的参数!位置参数必须按先后顺序,一一对应,个数不多不少的传递
默认参数
在函数定义时,如果给某个参数提供一个默认值,这个参数就变成了默认参数,不再是位置参数了。在调用函数的时候,我们可以给默认参数传递一个自定义的值,也可以使用默认值。
在设置默认参数时,有几点要注意:
- 默认参数必须在位置参数后面!
- 当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。
- 使用参数名传递参数
-
通常我们在调用函数时,位置参数都是按顺序先后传入,而且必须在默认参数前面。但如果在位置参数传递时,给实参指定位置参数的参数名,那么位置参数也可以不按顺序调用
-
- 默认参数尽量指向不变的对象!
def func(a=[]):
a.append("A")
return a
print(func())
print(func())
print(func())
运行结果:
['A'] ['A', 'A'] ['A', 'A', 'A']
Why?为什么会这样?
因为Python函数体在被读入内存的时候,默认参数a指向的空列表对象就会被创建,并放在内存里了。因为默认参数a本身也是一个变量,保存了指向对象[]的地址。每次调用该函数,往a指向的列表里添加一个A。a没有变,始终保存的是指向列表的地址,变的是列表内的数据!
定义默认参数要牢记一点:默认参数必须指向不变对象!
要修改上面的例子,我们可以用None这个不变对象来实现:
def add_end(L=None):
if L is None:
L = []
L.append('A')
return L
现在,无论调用多少次,都不会有问题:
>>> add_end()
['A']
>>> add_end()
['A']
为什么要设计str、None这样的不变对象呢?因为不变对象一旦创建,对象内部的数据就不能修改,这样就减少了由于修改数据导致的错误。此外,由于对象不变,多任务环境下同时读取对象不需要加锁,同时读一点问题都没有。我们在编写程序时,如果可以设计一个不变对象,那就尽量设计成不变对象。
可变参数
如果利用可变参数,调用函数的方式可以简化成这样:
>>> calc(1, 2, 3)
14
>>> calc(1, 3, 5, 7)
84
所以,我们把函数的参数改为可变参数:
def calc(*numbers):
sum = 0
for n in numbers:
sum = sum + n * n
return sum
定义可变参数和定义一个list或tuple参数相比,仅仅在参数前面加了一个*号。在函数内部,参数numbers接收到的是一个tuple,因此,函数代码完全不变。但是,调用该函数时,可以传入任意个参数,包括0个参数:
如果已经有一个list或者tuple,要调用一个可变参数怎么办?可以这样做:
如果参数是个列表,会将整个列表当做一个参数传入
>>> nums = [1, 2, 3]
>>> calc(nums[0], nums[1], nums[2])
14
这种写法当然是可行的,问题是太繁琐,所以Python允许你在list或tuple前面加一个*号,把list或tuple的元素变成可变参数传进去:
>>> nums = [1, 2, 3]
>>> calc(*nums)
14
*nums表示把nums这个list的所有元素作为可变参数传进去。这种写法相当有用,而且很常见。
关键字参数
可变参数允许你传入0个或任意个参数,这些可变参数在函数调用时自动组装为一个tuple。而关键字参数允许你传入0个或任意个含参数名的参数,这些关键字参数在函数内部自动组装为一个dict。请看示例:
def person(name, age, **kw):
print('name:', name, 'age:', age, 'other:', kw)
函数person除了必选参数name和age外,还接受关键字参数kw。在调用该函数时,可以只传入必选参数:
>>> person('Michael', 30)
name: Michael age: 30 other: {}
也可以传入任意个数的关键字参数:
>>> person('Bob', 35, city='Beijing')
name: Bob age: 35 other: {'city': 'Beijing'}
>>> person('Adam', 45, gender='M', job='Engineer')
name: Adam age: 45 other: {'gender': 'M', 'job': 'Engineer'}
关键字参数有什么用?它可以扩展函数的功能。比如,在person函数里,我们保证能接收到name和age这两个参数,但是,如果调用者愿意提供更多的参数,我们也能收到。试想你正在做一个用户注册的功能,除了用户名和年龄是必填项外,其他都是可选项,利用关键字参数来定义这个函数就能满足注册的需求。
和可变参数类似,也可以先组装出一个dict,然后,把该dict转换为关键字参数传进去:
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, city=extra['city'], job=extra['job'])
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
当然,上面复杂的调用可以用简化的写法:
>>> extra = {'city': 'Beijing', 'job': 'Engineer'}
>>> person('Jack', 24, **extra)
name: Jack age: 24 other: {'city': 'Beijing', 'job': 'Engineer'}
**extra表示把extra这个dict的所有key-value用关键字参数传入到函数的**kw参数,kw将获得一个dict,注意kw获得的dict是extra的一份拷贝,对kw的改动不会影响到函数外的extra。
命名关键字参数
对于关键字参数,函数的调用者可以传入任意不受限制的关键字参数。至于到底传入了哪些,就需要在函数内部通过kw检查。
仍以person()函数为例,我们希望检查是否有city和job参数:
def person(name, age, **kw):
if 'city' in kw:
# 有city参数
pass
if 'job' in kw:
# 有job参数
pass
print('name:', name, 'age:', age, 'other:', kw)
但是调用者仍可以传入不受限制的关键字参数:
>>> person('Jack', 24, city='Beijing', addr='Chaoyang', zipcode=123456)
如果要限制关键字参数的名字,就可以用命名关键字参数,例如,只接收city和job作为关键字参数。这种方式定义的函数如下:
def person(name, age, *, city, job):
print(name, age, city, job)
和关键字参数**kw不同,命名关键字参数需要一个特殊分隔符*,*后面的参数被视为命名关键字参数。
调用方式如下:
>>> person('Jack', 24, city='Beijing', job='Engineer')
Jack 24 Beijing Engineer
如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:
def person(name, age, *args, city, job):
print(name, age, args, city, job)
命名关键字参数必须传入参数名,这和位置参数不同。如果没有传入参数名,调用将报错:
>>> person('Jack', 24, 'Beijing', 'Engineer')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: person() missing 2 required keyword-only arguments: 'city' and 'job'
由于调用时缺少参数名city和job,Python解释器把前两个参数视为位置参数,后两个参数传给*args,但缺少命名关键字参数导致报错。
命名关键字参数可以有缺省值,从而简化调用:
def person(name, age, *, city='Beijing', job):
print(name, age, city, job)
由于命名关键字参数city具有默认值,调用时,可不传入city参数:
>>> person('Jack', 24, job='Engineer')
Jack 24 Beijing Engineer
使用命名关键字参数时,要特别注意,如果没有可变参数,就必须加一个*作为特殊分隔符。如果缺少*,Python解释器将无法识别位置参数和命名关键字参数:
def person(name, age, city, job):
# 缺少 *,city和job被视为位置参数
pass
参数组合
在Python中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。
常用函数
range()函数
range函数是内置函数,无须特别导入,在任何地方都可以直接使用它。下面看一下具体用法:
-
.提供一个数字参数,直接遍历数字:
for i in range(10): print(i)
-
也可以指定遍历的区间:
for i in range(1, 12): print(i)
-
还可以指定步长,就像切片一样
for i in range(1, 12, 2): print(i)
-
但更多的时候是结合range和len函数,遍历一个序列的索引
a = ['Google', 'Baidu', 'Huawei', 'Taobao', 'QQ'] for i in range(len(a)): print(i, a[i])
匿名函数
当我们在创建函数时,有些时候,不需要显式地定义函数,直接传入匿名函数更方便。这省去了我们挖空心思为函数命名的麻烦,也能少写不少代码,很多编程语言都提供这一特性。
Python语言使用lambda
关键字来创建匿名函数。
所谓匿名,即不再使用def
语句这样标准的形式定义一个函数。
- lambda只是一个表达式,而不是一个代码块,函数体比def简单很多。
- 仅仅能在lambda表达式中封装有限的逻辑。
- lambda 函数拥有自己的命名空间。
其形式通常是这样的:lambda 参数: 表达式
。
匿名函数只能有一个表达式,不用也不能写return语句,表达式的结果就是其返回值。 匿名函数没有函数名字,不必担心函数名冲突,节省字义空间。此外,匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数:
>>> f = lambda x: x * x
推导式
- 列表推导式
列表推导式是一种快速生成列表的方式。其形式是用方括号括起来的一段语句,如下例子所示:
lis = [x * x for x in range(1, 10)] print(lis) ------------------------------------ 结果:[1, 4, 9, 16, 25, 36, 49, 64, 81]
列表推导式要这么理解,首先执行for循环,对于每一个x,代入x*x中进行运算,将运算结果逐一添加到一个新列表内,循环结束,得到最终列表。它相当于下面的代码:
lis = [] for i in range(1, 10): lis.append(i*i) print(lis)
列表推导式为我们提供了一种在一行内实现较为复杂逻辑的生成列表的方法。其核心语法是用中括号[]将生成逻辑封装起来。
列表推导式有多种花样用法:
- 增加条件语句
>>> [x * x for x in range(1, 11) if x % 2 == 0] [4, 16, 36, 64, 100]
通过在后面添加if子句,对x进行过滤。
- 多重循环
>>> [a + b for a in ‘123' for b in ‘abc'] ['1a', '1b', '1c', '2a', '2b', '2c', '3a', '3b', '3c']
同时循环a和b两个变量。
- 更多用法
>>> dic = {"k1":"v1","k2":"v2"} >>> a = [k+":"+v for k,v in dic.items()] >>> a ['k1:v1', 'k2:v2']
2.字典推导式
既然使用中括号[]可以编写列表推导式,那么使用大括号呢?你猜对了!使用大括号{}可以制造字典推导式!
>>> dic = {x: x**2 for x in (2, 4, 6)} >>> dic {2: 4, 4: 16, 6: 36} >>> type(dic) <class 'dict'>
注意x: x**2
的写法,中间的冒号,表示左边的是key右边的是value。
3. 集合推导式
大括号除了能用作字典推导式,还可以用作集合推导式,两者仅仅在细微处有差别。
>>> a = {x for x in 'abracadabra' if x not in 'abc'} >>> a {'d', 'r'} >>> type(a) <class 'set'>
仔细体会一下,表达式的写法差异!
4. 元组推导式?
报告老师,还有圆括号!是不是元组推导式?想法不错,但事实却没有。圆括号在Python中被用作生成器的语法了,很快我们就会讲到,没有元组推导式。
tup = (x for x in range(9)) print(tup) print(type(tup)) --------------------------- 结果: <generator object <genexpr> at 0x000000000255DA98> <class 'generator'>
要通过类似方法生成元组,需要显式调用元组的类型转换函数tuple(),如下所示:
tup = tuple(x for x in range(9)) print(tup) print(type(tup)) ------------------------ 结果: (0, 1, 2, 3, 4, 5, 6, 7, 8) <class 'tuple'>
生成器
有时候,序列或集合内的元素的个数非常巨大,如果全制造出来并放入内存,对计算机的压力是非常大的。比如,假设需要获取一个10**20次方如此巨大的数据序列,把每一个数都生成出来,并放在一个内存的列表内,这是粗暴的方式,有如此大的内存么?如果元素可以按照某种算法推算出来,需要就计算到哪个,就可以在循环的过程中不断推算出后续的元素,而不必创建完整的元素集合,从而节省大量的空间。在Python中,这种一边循环一边计算出元素的机制,称为生成器:generator。
前面我们说过,通过圆括号可以编写生成器推导式:
>>> g = (x * x for x in range(1, 4)) >>> g <generator object <genexpr> at 0x1022ef630>
可以通过next()函数获得generator的下一个返回值,这点和迭代器非常相似:
>>> next(g) 1 >>> next(g) 4 >>> next(g) 9 >>> next(g) Traceback (most recent call last): File "<pyshell#14>", line 1, in <module> next(g) StopIteration
但更多情况下,我们使用for循环。
for i in g: print(i)
除了使用生成器推导式,我们还可以使用yield
关键字。
在 Python中,使用yield返回的函数会变成一个生成器(generator)。 在调用生成器的过程中,每次遇到yield时函数会暂停并保存当前所有的运行信息,返回yield的值。并在下一次执行next()方法时从当前位置继续运行。
# 斐波那契函数 def fibonacci(n): a, b, counter = 0, 1, 0 while True: if counter > n: return yield a # yield让该函数变成一个生成器 a, b = b, a + b counter += 1 fib = fibonacci(10) # fib是一个生成器 print(type(fib)) for i in fib: print(i, end=" ")
装饰器
作为许多语言都存在的高级语法之一,装饰器是你必须掌握的知识点。
装饰器(Decorator):从字面上理解,就是装饰对象的器件。可以在不修改原有代码的情况下,为被装饰的对象增加新的功能或者附加限制条件或者帮助输出。装饰器有很多种,有函数的装饰器,也有类的装饰器。装饰器在很多语言中的名字也不尽相同,它体现的是设计模式中的装饰模式,强调的是开放封闭原则。装饰器的语法是将@装饰器名,放在被装饰对象上面。
@dec def func(): pass
在进行装饰器的介绍之前,我们必须先明确几个概念和原则:
首先,Python程序是从上往下顺序执行的,而且碰到函数的定义代码块是不会立即执行的,只有等到该函数被调用时,才会执行其内部的代码块。关于这一点,其实我们在前面的章节已经介绍过了。
def foo(): print("foo函数被运行了!") #如果就这么样,foo里的语句是不会被执行的。 #程序只是简单的将定义代码块读入内存中。 # foo() 只有调用了,才会执行
其次,由于顺序执行的原因,如果你真的对同一个函数定义了两次,那么,后面的定义会覆盖前面的定义。因此,在Python中代码的放置位置是有区别的,不能随意摆放,通常函数体要放在调用的语句之前。
def foo(): print("我是上面的函数定义!") foo() def foo(): print("我是下面的函数定义!") foo() #---------------- 执行结果: 我是上面的函数定义! 我是下面的函数定义!
然后,我们还要先搞清楚几样东西:函数名、函数体、返回值,函数的内存地址、函数名加括号、函数名被当作参数、函数名加括号被当作参数、返回函数名、返回函数名加括号。
def outer(func): def inner(): print("我是内层函数!") return inner def foo(): print("我是原始函数!") outer(foo) outer(foo())
函数名: foo
、outer
、inner
函数体:函数的整个代码结构
返回值: return后面的表达式
函数的内存地址:id(foo)
、id(outer)
等等
函数名加括号:对函数进行调用,比如foo()
、outer(foo)
函数名作为参数: outer(foo)
中的foo本身是个函数,但作为参数被传递给了outer函数
函数名加括号被当做参数:其实就是先调用函数,再将它的返回值当做别的函数的参数,例如outer(foo())
返回函数名:return inner
返回函数名加括号:return inner()
,其实就是先执行inner函数,再将其返回值作为别的函数的返回值。
如果你能理解函数也是一个对象,就能很容易地理解上面的概念。
有了这些基本的概念,我们就可以通过一个实例来讲解Python中装饰器的作用了。下例是针对函数的装饰器。
虚拟场景
有一个大公司,下属的基础平台部负责内部应用程序及API的开发。另外还有上百个业务部门负责不同的业务,这些业务部门各自调用基础平台部提供的不同函数,也就是API处理自己的业务,情况如下:
# 基础平台部门开发了上百个函数API def f1(): print("业务部门1的数据接口......") def f2(): print("业务部门2的数据接口......") def f3(): print("业务部门3的数据接口......") def f100(): print("业务部门100的数据接口......") #各部门分别调用自己需要的API f1() f2() f3() f100()
公司还在创业初期时,基础平台部就开发了这些函数。由于各种原因,比如时间紧,比如人手不足,比如架构缺陷,比如考虑不周等等,没有为函数的调用进行安全认证。现在,公司发展壮大了,不能再像初创时期的“草台班子”一样将就下去了,基础平台部主管决定弥补这个缺陷,于是(以下场景纯属虚构,调侃之言,切勿对号入座):
第一天:主管叫来了一个运维工程师,工程师跑上跑下逐个部门进行通知,让他们在代码里加上认证功能,然后,当天他被开除了。
第二天:主管又叫来了一个运维工程师,工程师用shell写了个复杂的脚本,勉强实现了功能。但他很快就回去接着做运维了,不会开发的运维不是好运维....
第三天:主管叫来了一个python自动化开发工程师。哥们是这么干的,只对基础平台的代码进行重构,让N个业务部门无需做任何修改。这哥们很快也被开了,连运维也没得做。
def f1(): #加入认证程序代码 print("业务部门1数据接口......") def f2(): # 加入认证程序代码 print("业务部门2数据接口......") def f3(): # 加入认证程序代码 print("业务部门3数据接口......") def f100(): #加入认证程序代码 print("业务部门100数据接口......") #各部门分别调用 f1() f2() f3() f100()
第四天:主管又换了个开发工程师。他是这么干的:定义个认证函数,在原来其他的函数中调用它,代码如下。
def login(): print("认证成功!") def f1(): login() print("业务部门1数据接口......") def f2(): login() print("业务部门2数据接口......") def f3(): login() print("业务部门3数据接口......") def f100(): login() print("业务部门100数据接口......") #各部门分别调用 f1() f2() f3() f100()
但是主管依然不满意,不过这一次他解释了为什么。主管说:写代码要遵循开放封闭原则,简单来说,已经实现的功能代码内部不允许被修改,但外部可以被扩展。如果将开放封闭原则应用在上面的需求中,那么就是不允许在函数f1 、f2、f3......f100的内部进行代码修改,但是可以在外部对它们进行扩展。
第五天:已经没有时间让主管找别人来干这活了,他决定亲自上阵,使用装饰器完成这一任务,并且打算在函数执行后再增加个日志功能。主管的代码如下:
def outer(func): def inner(): print("认证成功!") result = func() print("日志添加成功") return result return inner @outer def f1(): print("业务部门1数据接口......") @outer def f2(): print("业务部门2数据接口......") @outer def f3(): print("业务部门3数据接口......") @outer def f100(): print("业务部门100数据接口......") #各部门分别调用 f1() f2() f3() f100()
使用装饰器@outer,也是仅需对基础平台的代码进行拓展,就可以实现在其他部门调用函数API之前都进行认证操作,在操作结束后保存日志,并且其他业务部门无需对他们自己的代码做任何修改,调用方式也不用变。
装饰器机制分析
下面以f1函数为例,对装饰器的运行机制进行分析:
def outer(func): def inner(): print("认证成功!") result = func() print("日志添加成功") return result return inner @outer def f1(): print("业务部门1数据接口......")
-
程序开始运行,从上往下解释,读到
def outer(func):
的时候,发现这是个“一等公民”函数,于是把函数体加载到内存里,然后过。 -
读到@outer的时候,程序被@这个语法糖吸引住了,知道这是个装饰器,按规矩要立即执行的,于是程序开始运行@后面那个名字outer所定义的函数。
-
程序返回到outer函数,开始执行装饰器的语法规则。规则是:被装饰的函数的名字会被当作参数传递给装饰函数。装饰函数执行它自己内部的代码后,会将它的返回值赋值给被装饰的函数。原来的f1函数被当做参数传递给了func,而f1这个函数名之后会指向inner函数。
-
注意:@outer和@outer()有区别,没有括号时,outer函数依然会被执行,这和传统的用括号才能调用函数不同,需要特别注意!
另外,是f1这个函数名(而不是f1()这样被调用后)当做参数传递给装饰函数outer,也就是:
func = f1
,@outer
等于outer(f1)
,实际上传递了f1的函数体,而不是执行f1后的返回值。还有,outer函数return的是inner这个函数名,而不是inner()这样被调用后的返回值。
4.程序开始执行outer函数内部的内容,一开始它又碰到了一个函数inner,inner函数定义块被程序观察到后不会立刻执行,而是读入内存中(这是默认规则)。
5.再往下,碰到
return inner
,返回值是个函数名,并且这个函数名会被赋值给f1这个被装饰的函数,也就是f1 = inner
。根据前面的知识,我们知道,此时f1函数被新的函数inner覆盖了(实际上是f1这个函数名更改成指向inner这个函数名指向的函数体内存地址,f1不再指向它原来的函数体的内存地址),再往后调用f1的时候将执行inner函数内的代码,而不是先前的函数体。那么先前的函数体去哪了?还记得我们将f1当做参数传递给func这个形参么?func这个变量保存了老的函数在内存中的地址,通过它就可以执行老的函数体,你能在inner函数里看到result = func()
这句代码,它就是这么干的!6.接下来,还没有结束。当业务部门,依然通过f1()的方式调用f1函数时,执行的就不再是旧的f1函数的代码,而是inner函数的代码。在本例中,它首先会打印个“认证成功”的提示,很显然你可以换成任意的代码,这只是个示例;然后,它会执行func函数并将返回值赋值给变量result,这个func函数就是旧的f1函数;接着,它又打印了“日志保存”的提示,这也只是个示例,可以换成任何你想要的;最后返回result这个变量。我们在业务部门的代码上可以用
r = f1()
的方式接受result的值。7.以上流程走完后,你应该看出来了,在没有对业务部门的代码和接口调用方式做任何修改的同时,也没有对基础平台部原有的代码做内部修改,仅仅是添加了一个装饰函数,就实现了我们的需求,在函数调用前进行认证,调用后写入日志。这就是装饰器的最大作用。
那么为什么我们要搞一个outer函数一个inner函数这么复杂呢?一层函数不行吗?
答:请注意,@outer这句代码在程序执行到这里的时候就会自动执行outer函数内部的代码,如果不封装一下,在业务部门还未进行调用的时候,就执行了,这和初衷不符。当然,如果你对这个有需求也不是不行。请看下面的例子,它只有一层函数。
def outer(func): print("认证成功!") result = func() print("日志添加成功") return result @outer def f1(): print("业务部门1数据接口......") # 业务部门并没有调用f1函数 ------------------------------------------ 执行结果: 认证成功! 业务部门1数据接口...... 日志添加成功
看见了吗?我们只是定义好了装饰器,业务部门还没有调用f1函数呢,程序就把工作全做了。这就是为什么要封装一层函数的原因。
细心的同学可能已经发现了,上面的例子中,f1函数没有参数,在实际情况中肯定会需要参数的,函数的参数怎么传递的呢?
def outer(func): def inner(username): print("认证成功!") result = func(username) print("日志添加成功") return result return inner @outer def f1(name): print("%s 正在连接业务部门1数据接口......"%name) # 调用方法 f1("jack")
在inner函数的定义部分也加上一个参数,调用func函数的时候传递这个参数,很好理解吧?可问题又来了,那么另外一个部门调用的f2有2个参数呢?f3有3个参数呢?你怎么传递?很简单,我们有
*args
和**kwargs
嘛!号称“万能参数”!简单修改一下上面的代码:def outer(func): def inner(*args,**kwargs): print("认证成功!") result = func(*args,**kwargs) print("日志添加成功") return result return inner @outer def f1(name,age): print("%s 正在连接业务部门1数据接口......"%name) # 调用方法 f1("jack",18)
介绍到这里,装饰器的基本概念和初级使用方法,你应该有了一定的了解了。那么进一步思考一下,一个函数可以被多个函数装饰吗?可以的!看下面的例子!
def outer1(func): def inner(*args,**kwargs): print("认证成功!") result = func(*args,**kwargs) print("日志添加成功") return result return inner def outer2(func): def inner(*args,**kwargs): print("一条欢迎信息。。。") result = func(*args,**kwargs) print("一条欢送信息。。。") return result return inner @outer1 @outer2 def f1(name,age): print("%s 正在连接业务部门1数据接口......"%name) # 调用方法 f1("jack",18) #-------------------------------------------------- 执行结果: 认证成功! 一条欢迎信息。。。 jack 正在连接业务部门1数据接口...... 一条欢送信息。。。 日志添加成功
更更进一步,装饰器自己可以有参数吗?可以的!看下面的例子:
# 认证函数 def auth(request,kargs): print("认证成功!") # 日志函数 def log(request,kargs): print("日志添加成功") # 装饰器函数。接收两个参数,这两个参数应该是某个函数的名字。 def Filter(auth_func,log_func): # 第一层封装,f1函数实际上被传递给了main_fuc这个参数 def outer(main_func): # 第二层封装,auth和log函数的参数值被传递到了这里 def wrapper(request,kargs): # 下面代码的判断逻辑不重要,重要的是参数的引用和返回值 before_result = auth(request,kargs) if(before_result != None): return before_result; main_result = main_func(request,kargs) if(main_result != None): return main_result; after_result = log(request,kargs) if(after_result != None): return after_result; return wrapper return outer # 注意了,这里的装饰器函数有参数哦,它的意思是先执行filter函数 # 然后将filter函数的返回值作为装饰器函数的名字返回到这里,所以, # 其实这里,Filter(auth,log) = outer , @Filter(auth,log) = @outer @Filter(auth,log) def f1(name,age): print("%s 正在连接业务部门1数据接口......"%name) # 调用方法 f1("jack",18) #----------------------------------------------- 运行结果: 认证成功! jack 正在连接业务部门1数据接口...... 日志添加成功
装饰器的学问博大精深,需要我们不断的学习思考。官方文档和框架源码是比较好的学习对象。
内置函数
你会不会有些好奇Python为什么可以直接使用一些内建函数,而不用显式的导入它们?比如 str()、int()、dir()、id()、type(),max(),min(),len()等,许多许多非常好用,快捷方便的函数。
因为这些函数都是一个叫做builtins
模块中定义的函数,而builtins
模块默认在Python环境启动的时候就自动导入,所以你可以直接使用这些函数。
模块
在编程语言中,代码块、函数、类、模块,一直到包,逐级封装,层层调用。在Python中,一个.py
文件就是一个模块,模块是比类更高一级的封装。在其他语言,被导入的模块也通常称为库。
模块可以分为自定义模块、内置模块和第三方模块。
请注意,每一个包目录下面都会有一个__init__.py的文件,这个文件是必须存在的,否则,Python就把example
这个目录当成普通目录,而不是一个包。__init__.py可以是空文件,也可以有Python代码,因为__init__.py本身就是一个模块,而它的模块名就是目录名。
设想一下,如果我们使用from example import *
会发生什么?
Python会进入文件系统,找到这个包里面所有的子模块,一个一个的把它们都导入进来。 但是这个方法有风险,有可能导入的模块和已有的模块冲突,或者并不需要导入所有的模块。为了解决这个问题,需要提供一个精确的模块索引。这个索引要放置在__init__.py
中。
如果包定义文件__init__.py
中存在一个叫做__all__
的列表变量,那么在使用from package import *
的时候就把这个列表中的所有名字作为要导入的模块名。
例如在example/p1/__init__.py
中包含如下代码:
__all__ = ["x"]
这表示当你使用from example import *
这种用法时,你只会导入包里面的x子模块。
自己创建模块时要注意命名,不能和Python自带的模块名称冲突。例如,系统自带了sys模块,自己的模块就不可命名为sys.py,否则将无法导入系统自带的sys模块。
任何模块代码的第一个字符串都被视为模块的文档注释;
if __name__=='__main__':
test()
当我们在命令行运行hello模块文件时,Python解释器把一个特殊变量__name__置为__main__,而如果在其他地方导入该hello模块时,if判断将失败,因此,这种if测试可以让一个模块通过命令行运行时执行一些额外的代码,最常见的就是运行测试。
怎么导入
1. import xx.xx
这会将对象(这里的对象指的是包、模块、类或者函数,下同)中的所有内容导入。如果该对象是个模块,那么调用对象内的类、函数或变量时,需要以module.xxx
的方式。
2. From xx.xx import xx.xx
从某个对象内导入某个指定的部分到当前命名空间中,不会将整个对象导入。这种方式可以节省写长串导入路径的代码,但要小心名字冲突。
在Main.py
中导入Module_a
:
# Main.py from module_a import func module_a.func() # 错误的调用方式 func() # 这时需要直接调用func
3. from xx.xx import xx as rename
为了避免命名冲突,在导入的时候,可以给导入的对象重命名。
4. from xx.xx import *
将对象内的所有内容全部导入。非常容易发生命名冲突,请慎用!
# Main.py from module_a import * def func(): print("this is main module!") func() # 从module导入的func被main的func覆盖了
执行结果:this is main module!
模块搜索路径
管你在程序中执行了多少次import,一个模块只会被导入一次。这样可以防止一遍又一遍地导入模块,节省内存和计算资源。那么,当使用import语句的时候,Python解释器是怎样找到对应的文件的呢?
Python根据sys.path
的设置,按顺序搜索模块。
>>> import sys >>> sys.path ['', 'C:\\Python36\\Lib\\idlelib', 'C:\\Python36\\python36.zip', 'C:\\Python36\\DLLs', 'C:\\Python36\\lib', 'C:\\Python36', 'C:\\Python36\\lib\\site-packages']
当然,这个设置是可以修改的,就像windows系统环境变量中的path一样,可以自定义。 通过sys.path.append('路径')
的方法为sys.path
路径列表添加你想要的路径。
import sys import os new_path = os.path.abspath('../') sys.path.append(new_path)
默认情况下,模块的搜索顺序是这样的:
- 当前执行脚本所在目录
- Python的安装目录
- Python安装目录里的site-packages目录
其实就是“自定义”——>“内置”——>“第三方”模块的查找顺序。任何一步查找到了,就会忽略后面的路径,所以模块的放置位置是有区别的。
作用域:
在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用。在Python中,是通过_前缀来实现的。
类似_xxx和__xxx这样的函数或变量就是非公开的(private),不应该被直接引用,比如_abc,__abc等;
通常而言,在编程语言中,变量的作用域从代码结构形式来看,有块级、函数、类、模块、包等由小到大的级别。但是在Python中,没有块级作用域,也就是类似if语句块、for语句块、with上下文管理器等等是不存在作用域概念的,他们等同于普通的语句。
>>> if True: # if语句块没有作用域
x = 1
>>> x
1
>>> def func(): # 函数有作用域
a = 8
>>> a
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
a
NameError: name 'a' is not defined
从上面的例子中,我们可以发现,在if语句内定义的变量x,可以被外部访问,而在函数func()中定义的变量a,则无法在外部访问。
通常,函数内部的变量无法被函数外部访问,但内部可以访问;类内部的变量无法被外部访问,但类的内部可以。通俗来讲,就是内部代码可以访问外部变量,而外部代码通常无法访问内部变量。
变量的作用域决定了程序的哪一部分可以访问哪个特定的变量名称。Python的作用域一共有4层,分别是:
L (Local) 局部作用域
E (Enclosing) 闭包函数外的函数中
G (Global) 全局作用域
B (Built-in) 内建作用域
x = int(2.9) # 内建作用域,查找int函数
global_var = 0 # 全局作用域
def outer():
out_var = 1 # 闭包函数外的函数中
def inner():
inner_var = 2 # 局部作用域
前面说的都是变量可以找得到的情况,那如果出现本身作用域没有定义的变量,那该如何寻找呢?
Python以L –> E –> G –>B的规则查找变量,即:在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,最后去内建中找。如果这样还找不到,那就提示变量不存在的错误。例如下面的代码,函数func内部并没有定义变量a,可是print函数需要打印a,那怎么办?向外部寻找!按照L –> E –> G –>B的规则,层层查询,这个例子很快就从外层查找到了a,并且知道它被赋值为1,于是就打印了1。
a = 1
def func():
print(a)
全局变量和局部变量
定义在函数内部的变量拥有一个局部作用域,被叫做局部变量,定义在函数外的拥有全局作用域的变量,被称为全局变量。(类、模块等同理)
所谓的局部变量是相对的。局部变量也有可能是更小范围内的变量的外部变量。
局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。调用函数时,所有在函数内声明的变量名称都将被加入到作用域中。
a = 1 # 全局变量
def func():
b = 2 # 局部变量
print(a) # 可访问全局变量a,无法访问它内部的c
def inner():
c = 3 # 更局部的变量
print(a) # 可以访问全局变量a
print(b) # b对于inner函数来说,就是外部变量
print(c)
global和nonlocal关键字
我们先看下面的例子:
total = 0 # total是一个全局变量
def plus( arg1, arg2 ):
total = arg1 + arg2 # total在这里是局部变量.
print("函数内局部变量total= ", total)
print("函数内的total的内存地址是: ", id(total))
return total
plus(10, 20)
print("函数外部全局变量total= ", total)
print("函数外的total的内存地址是: ", id(total))
很明显,函数plus内部通过total = arg1 + arg2语句,新建了一个局部变量total,它和外面的全局变量total是两码事。而如果我们,想要在函数内部修改外面的全局变量total呢?使用global关键字!
global:指定当前变量使用外部的全局变量
total = 0 # total是一个全局变量
def plus( arg1, arg2 ):
global total # 使用global关键字申明此处的total引用外部的total
total = arg1 + arg2
print("函数内局部变量total= ", total)
print("函数内的total的内存地址是: ", id(total))
return total
plus(10, 20)
print("函数外部全局变量total= ", total)
print("函数外的total的内存地址是: ", id(total))
打印结果是:
函数内局部变量total= 30
函数内的total的内存地址是: 503494624
函数外部全局变量total= 30
函数外的total的内存地址是: 503494624
我们再来看下面的例子:
a = 1
print("函数outer调用之前全局变量a的内存地址: ", id(a))
def outer():
a = 2
print("函数outer调用之时闭包外部的变量a的内存地址: ", id(a))
def inner():
a = 3
print("函数inner调用之后闭包内部变量a的内存地址: ", id(a))
inner()
print("函数inner调用之后,闭包外部的变量a的内存地址: ", id(a))
outer()
print("函数outer执行完毕,全局变量a的内存地址: ", id(a))
如果你将前面的知识点都理解通透了,那么这里应该没什么问题,三个a各是各的a,各自有不同的内存地址,是三个不同的变量。打印结果也很好的证明了这点:
函数outer调用之前全局变量a的内存地址: 493204544
函数outer调用之时闭包外部的变量a的内存地址: 493204576
函数inner调用之后闭包内部变量a的内存地址: 493204608
函数inner调用之后,闭包外部的变量a的内存地址: 493204576
函数outer执行完毕,全局变量a的内存地址: 493204544
那么,如果,inner内部想使用outer里面的那个a,而不是全局变量的那个a,怎么办?用global关键字?先试试看吧:
a = 1
print("函数outer调用之前全局变量a的内存地址: ", id(a))
def outer():
a = 2
print("函数outer调用之时闭包外部的变量a的内存地址: ", id(a))
def inner():
global a # 注意这行
a = 3
print("函数inner调用之后闭包内部变量a的内存地址: ", id(a))
inner()
print("函数inner调用之后,闭包外部的变量a的内存地址: ", id(a))
outer()
print("函数outer执行完毕,全局变量a的内存地址: ", id(a))
运行结果如下,很明显,global使用的是全局变量a。
函数outer调用之前全局变量a的内存地址: 494384192
函数outer调用之时闭包外部的变量a的内存地址: 494384224
函数inner调用之后闭包内部变量a的内存地址: 494384256
函数inner调用之后,闭包外部的变量a的内存地址: 494384224
函数outer执行完毕,全局变量a的内存地址: 494384256
那怎么办呢?使用nonlocal关键字!它可以修改嵌套作用域(enclosing 作用域,外层非全局作用域)中的变量。将global a改成nonlocal a,代码这里我就不重复贴了,运行后查看结果,可以看到我们真的引用了outer函数的a变量。
函数outer调用之前全局变量a的内存地址: 497726528
函数outer调用之时闭包外部的变量a的内存地址: 497726560
函数inner调用之后闭包内部变量a的内存地址: 497726592
函数inner调用之后,闭包外部的变量a的内存地址: 497726592
函数outer执行完毕,全局变量a的内存地址: 497726528
面试真题:
不要上机测试,请说出下面代码的运行结果:
a = 10
def test():
a += 1
print(a)
test()
很多同学会说,这太简单了!函数内部没有定义a,那么就去外部找,找到a=10,于是加1,打印11!
我会告诉你,这段代码有语法错误吗?a += 1相当于a = a + 1,按照赋值运算符的规则是先计算右边的a+1。但是,Python的规则是,如果在函数内部要修改一个变量,那么这个变量需要是内部变量,除非你用global声明了它是外部变量。很明显,我们没有在函数内部定义变量a,所以会弹出局部变量在未定义之前就引用的错误。
更多的例子:
再来看一些例子(要注意其中的闭包,也就是函数内部封装了函数):
name = 'jack'
def outer():
name='tom'
def inner():
name ='mary'
print(name)
inner()
outer()
上面的题目很简单,因为inner函数本身有name变量,所以打印结果是mary。那么下面这个呢?
name ='jack'
def f1():
print(name)
def f2():
name = 'eric'
f1()
f2()
这题有点迷惑性,想了半天,应该是‘eric’吧,因为f2函数调用的时候,在内部又调用了f1函数,f1自己没有name变量,那么就往外找,发现f2定义了个name,于是就打印这个name。错了!!!结果是‘jack’!
Python函数的作用域取决于其函数代码块在整体代码中的位置,而不是调用时机的位置。调用f1的时候,会去f1函数的定义体查找,对于f1函数,它的外部是name ='jack',而不是name = 'eric'。
再看下面的例子,f2函数返回了f1函数:
name = 'jack'
def f2():
name = 'eric'
return f1
def f1():
print(name)
ret = f2()
ret()
仔细回想前面的例子,其实这里有异曲同工之妙,所以结果还是‘jack’。
文件操作
文件读写:
Python内置了一个open()方法,用于对文件进行读写操作。使用open()方法操作文件就像把大象塞进冰箱一样,可以分三步走,一是打开文件,二是操作文件,三是关闭文件。
open()方法的返回值是一个file对象,可以将它赋值给一个变量(文件句柄)。其基本语法格式为:
f = open(filename, mode)
注意不同read方式的区别
with关键字
with关键字用于Python的上下文管理器机制。为了防止诸如open这一类文件打开方法在操作过程出现异常或错误,或者最后忘了执行close方法,文件非正常关闭等可能导致文件泄露、破坏的问题。Python提供了with这个上下文管理器机制,保证文件会被正常关闭。在它的管理下,不需要再写close语句。注意缩进。
with open('test.txt', 'w') as f:
f.write('Hello, world!')
with支持同时打开多个文件:
with open('log1') as obj1, open('log2','w') as obj2:
s=obj1.read()
obj2.write(s)
面向对象编程
类和实例
Python使用class关键字来定义类,其基本结构如下:
class 类名(父类列表): pass
类名通常采用驼峰式命名方式,尽量让字面意思体现出类的作用。Python采用多继承机制,一个类可以同时继承多个父类(也叫基类、超类),继承的基类有先后顺序,写在类名后的圆括号里。继承的父类列表可以为空,此时的圆括号可以省略。但在Python3中,即使你采用类似class Student:pass
的方法没有显式继承任何父类的定义了一个类,它也默认继承object
类。因为,object
是Python3中所有类的基类。
下面是一个学生类:
class Student: classroom = '101' address = 'beijing' def __init__(self, name, age): self.name = name self.age = age def print_age(self): print('%s: %s' % (self.name, self.age))
可以通过调用类的实例化方法(有的语言中也叫初始化方法或构造函数)来创建一个类的实例。默认情况下,使用类似obj=Student()
的方式就可以生成一个类的实例。但是,通常每个类的实例都会有自己的实例变量,例如这里的name和age,为了在实例化的时候体现实例的不同,Python提供了一个def __init__(self):
的实例化机制。任何一个类中,名字为__init__
的方法就是类的实例化方法,具有__init__
方法的类在实例化的时候,会自动调用该方法,并传递对应的参数。比如:
li = Student("李四", 24) zhang = Student("张三", 23)
实例变量和类变量
实例变量:
在初始化方法中定义
类变量:
定义在类中,方法之外的变量,称作类变量。类变量是所有实例公有的变量,每一个实例都可以访问、修改类变量。在Student类中,classroom和address两个变量就是类变量。可以通过类名或者实例名加圆点的方式访问类变量,比如:
Student.classroom Student.address li.classroom zhang.address
在使用实例变量和类变量的时候一定要注意,使用类似zhang.name访问变量的时候,实例会先在自己的实例变量列表里查找是否有这个实例变量,如果没有,那么它就会去类变量列表里找,如果还没有,弹出异常。
Python动态语言的特点,让我们可以随时给实例添加新的实例变量,给类添加新的类变量和方法。因此,在使用li.classroom = '102'
的时候,要么是给已有的实例变量classroom重新赋值,要么就是新建一个li专属的实例变量classroom并赋值为‘102’。看下面的例子:
>>> class Student: # 类的定义体
classroom = '101' # 类变量
address = 'beijing'
def __init__(self, name, age):
self.name = name
self.age = age
def print_age(self):
print('%s: %s' % (self.name, self.age))
>>> li = Student("李四", 24) # 创建一个实例
>>> zhang = Student("张三", 23) # 创建第二个实例
>>> li.classroom # li本身没有classroom实例变量,所以去寻找类变量,它找到了!
'101'
>>> zhang.classroom # 与li同理
'101'
>>> Student.classroom # 通过类名访问类变量
'101'
>>> li.classroom = '102' # 关键的一步!实际是为li创建了独有的实例变量,只不过名字和类变量一样,都叫做classroom。
>>> li.classroom # 再次访问的时候,访问到的是li自己的实例变量classroom
'102'
>>> zhang.classroom # zhang没有实例变量classroom,依然访问类变量classroom
'101'
>>> Student.classroom # 保持不变
'101'
>>> del li.classroom # 删除了li的实例变量classroom
>>> li.classroom # 一切恢复了原样
'101'
>>> zhang.classroom
'101'
>>> Student.classroom
'101'
只能通过以下形式通过实例修改类变量
i.__class__.classroom = '102'
为了防止发生上面的混淆情况,对于类变量,请坚持使用类名.类变量
的访问方式,不要用实例去访问类变量。
类的方法:
Python的类中包含实例方法、静态方法和类方法三种方法。这些方法无论是在代码编排中还是内存中都归属于类,区别在于传入的参数和调用方式不同。
实例方法
类的实例方法由实例调用,至少包含一个self参数,且为第一个参数。执行实例方法时,会自动将调用该方法的实例赋值给self。self
代表的是类的实例,而非类本身。self
不是关键字,而是Python约定成俗的命名,你完全可以取别的名字,但不建议这么做。
静态方法
静态方法由类调用,无默认参数。将实例方法参数中的self去掉,然后在方法定义上方加上@staticmethod,就成为静态方法。它属于类,和实例无关。建议只使用类名.静态方法的调用方式。(虽然也可以使用实例名.静态方法的方式调用)
class Foo: @staticmethod def static_method(): pass
类方法
类方法由类调用,采用@classmethod装饰,至少传入一个cls(代指类本身,类似self)参数。执行类方法时,自动将调用该方法的类赋值给cls。建议只使用类名.类方法的调用方式。(虽然也可以使用实例名.类方法的方式调用)
class Foo: @classmethod def class_method(cls): pass Foo.class_method()
类方法:是绑定到类本身的方法,因为有cls参数传进来,可以访问类变量,而静态方法不能
封装、继承和多态
封装
封装是指将数据与具体操作的实现代码放在某个对象内部,使这些代码的实现细节不被外界发现,外界只能通过接口使用该对象,而不能通过任何形式修改对象内部实现,正是由于封装机制,程序在使用某一对象时不需要关心该对象的数据结构细节及实现操作的方法。使用封装能隐藏对象实现细节,使代码更易维护,同时因为不能直接调用、修改对象内部的私有信息,在一定程度上保证了系统安全性。类通过将函数和变量封装在内部,实现了比函数更高一级的封装。
继承
class Foo(superA, superB,superC....): class DerivedClassName(modname.BaseClassName): ## 当父类定义在另外的模块时
继承时,将父类的实例变量也同样继承了
Python支持多父类的继承机制,所以需要注意圆括号中基类的顺序,若是基类中有相同的方法名,并且在子类使用时未指定,Python会从左至右搜索基类中是否包含该方法。一旦查找到则直接调用,后面不再继续查找。
Python3的继承机制不同于Python2。其核心原则是下面两条,请谨记!
- 子类在调用某个方法或变量的时候,首先在自己内部查找,如果没有找到,则开始根据继承机制在父类里查找。
- 根据父类定义中的顺序,以深度优先的方式逐一查找父类(superA以及其所有父类查找,没有定义的方法就找superB)!
super()函数:
我们都知道,在子类中如果有与父类同名的成员,那就会覆盖掉父类里的成员。那如果你想强制调用父类的成员呢?使用super()函数!这是一个非常重要的函数,最常见的就是通过super调用父类的实例化方法__init__
!
语法:super(子类名, self).方法名()
,需要传入的是子类名和self,调用的是父类里的方法,按父类的方法需要传入参数。
多态
先看下面的代码:
class Animal: def kind(self): print("i am animal") class Dog(Animal): def kind(self): print("i am a dog") class Cat(Animal): def kind(self): print("i am a cat") class Pig(Animal): def kind(self): print("i am a pig") # 这个函数接收一个animal参数,并调用它的kind方法 def show_kind(animal): animal.kind() d = Dog() c = Cat() p = Pig() show_kind(d) show_kind(c) show_kind(p) ------------------ 打印结果: i am a dog i am a cat i am a pig
狗、猫、猪都继承了动物类,并各自重写了kind方法。show_kind()函数接收一个animal参数,并调用它的kind方法。可以看出,无论我们给animal传递的是狗、猫还是猪,都能正确的调用相应的方法,打印对应的信息。这就是多态。
实际上,由于Python的动态语言特性,传递给函数show_kind()的参数animal可以是 任何的类型,只要它有一个kind()的方法即可。动态语言调用实例方法时不检查类型,只要方法存在,参数正确,就可以调用。这就是动态语言的“鸭子类型”,它并不要求严格的继承体系,一个对象只要“看起来像鸭子,走起路来像鸭子”,那它就可以被看做是鸭子。
class Job: def kind(self): print("i am not animal, i am a job") j = Job() show_kind(j)
可能有人会觉得,这些内容很自然啊,没什么不好理解,不觉得多态有什么特殊,Python就是这样啊!
如果你学过JAVA这一类强类型静态语言,就不会这么觉得了,对于JAVA,必须指定函数参数的数据类型,只能传递对应参数类型或其子类型的参数,不能传递其它类型的参数,show_kind()函数只能接收animal、dog、cat和pig类型,而不能接收job类型。就算接收dog、cat和pig类型,也是通过面向对象的多态机制实现的
成员保护和访问限制
在Python中,如果要让内部成员不被外部访问,可以在成员的名字前加上两个下划线__
,这个成员就变成了一个私有成员(private)。私有成员只能在类的内部访问,外部无法访问。
class People: title = "人类" def __init__(self, name, age): self.__name = name self.__age = age def print_age(self): print('%s: %s' % (self.__name, self.__age)) obj = People("jack", 18) obj.__name ------------------------------ Traceback (most recent call last): File "F:/Python/pycharm/201705/1.py", line 68, in <module> obj.__name AttributeError: 'People' object has no attribute '__name'
那外部如果要对__name
和 __age
进行访问和修改呢?在类的内部创建外部可以访问的get和set方法!
class People: title = "人类" def __init__(self, name, age): self.__name = name self.__age = age def print_age(self): print('%s: %s' % (self.__name, self.__age)) def get_name(self): return self.__name def get_age(self): return self.__age def set_name(self, name): self.__name = name def set_age(self, age): self.__age = age obj = People("jack", 18) obj.get_name() obj.set_name("tom")
这样做,不但对数据进行了保护的同时也提供了外部访问的接口,而且在get_name
,set_name
这些方法中,可以额外添加对数据进行检测、处理、加工、包裹等等各种操作,作用巨大!
比如下面这个方法,会在设置年龄之前对参数进行检测,如果参数不是一个整数类型,则抛出异常。
def set_age(self, age): if isinstance(age, int): self.__age = age else: raise ValueError
那么,以双下划线开头的数据成员是不是一定就无法从外部访问呢?其实也不是!本质上,从内部机制原理讲,外部不能直接访问__age
是因为Python解释器对外把__age
变量改成了_People__age
,也就是_类名__age
(类名前是一个下划线)。因此,投机取巧的话,你可以通过_ People__age
在类的外部访问__age
变量:
obj = People("jack", 18) print(obj._People__name)
也就是说:Python的私有成员和访问限制机制是“假”的,没有从语法层面彻底限制对私有成员的访问。这一点和常量的尴尬地位很相似。
拓展:由于Python内部会对双下划线开头的私有成员进行名字变更,所以会出现下面的情况:
class People: title = "人类" def __init__(self, name, age): self.__name = name self.__age = age def print_age(self): print('%s: %s' % (self.__name, self.__age)) def get_name(self): return self.__name def set_name(self, name): self.__name = name obj = People("jack", 18) obj.__name = "tom" # 注意这一行 print("obj.__name: ", obj.__name) print("obj.get_name(): ", obj.get_name()) ------------------- 打印结果: obj.__name: tom obj.get_name(): jack
一定要注意,此时的obj.__name= 'tom'
,相当于给obj实例添加了一个新的实例变量__name
,而不是对原有私有成员__name
的重新赋值(避免犯错)。
此外,有些时候,你会看到以一个下划线开头的成员名,比如_name
,这样的数据成员在外部是可以访问的,但是,按照约定俗成的规定,当你看到这样的标识符时,意思就是,“虽然我可以被外部访问,但是,请把我视为私有成员,不要在外部访问我!”。
还有,在Python中,标识符类似__xxx__
的,也就是以双下划线开头,并且以双下划线结尾的,是特殊成员,特殊成员不是私有成员,可以直接访问,要注意区别对待。同时请尽量不要给自定义的成员命名__name__
或__iter__
这样的标识,它们都是Python中具有特殊意义的魔法方法名。
类的成员与下划线总结:
_name
、_name_
、_name__
:建议性的私有成员,不要在外部访问。__name
、__name_
:强制的私有成员,但是你依然可以蛮横地在外部危险访问。__name__
:特殊成员,与私有性质无关,例如__doc__
。name_
、name__
:没有任何特殊性,普通的标识符,但最好不要这么起名。
@property装饰器
Python内置的@property
装饰器可以把类的方法伪装成属性调用的方式。也就是本来是Foo.func()
的调用方法,变成Foo.func
的方式。在很多场合下,这是一种非常有用的机制。
class People: def __init__(self, name, age): self.__name = name self.__age = age @property def age(self): return self.__age @age.setter def age(self, age): if isinstance(age, int): self.__age = age else: raise ValueError @age.deleter def age(self): print("删除年龄数据!") obj = People("jack", 18) print(obj.age) obj.age = 19 print("obj.age: ", obj.age) del obj.age --------------------------- 打印结果: 18 obj.age: 19 删除年龄数据!
使用方法:
obj = People("jack", 18) a = obj.age # 获取值 obj.age = 19 # 重新赋值 del obj.age # 删除属性
将一个方法伪装成为属性后,就不再使用圆括号的调用方式了。而是类似变量的赋值、获取和删除方法了。当然,每个动作内部的代码细节还是需要你自己根据需求去实现的。
那么如何将一个普通的方法转换为一个“伪装”的属性呢?
- 首先,在普通方法的基础上添加
@property
装饰器,例如上面的age()方法。这相当于一个get方法,用于获取值,决定类似"result = obj.age"
执行什么代码。该方法仅有一个self参数。 - 写一个同名的方法,添加
@xxx.setter
装饰器(xxx表示和上面方法一样的名字),比如例子中的第二个方法。这相当于编写了一个set方法,提供赋值功能,决定类似"obj.age = ...."
的语句执行什么代码。 - 再写一个同名的方法,并添加
@xxx.delete
装饰器,比如例子中的第三个方法。用于删除功能,决定"del obj.age "
这样的语句具体执行什么代码。
简而言之,就是分别将三个方法定义为对同一个属性的获取、修改和删除。还可以定义只读属性,也就是只定义getter方法,不定义setter方法就是一个只读属性。
property()函数
除了使用装饰器的方式将一个方法伪装成属性外,Python内置的builtins模块中的property()函数,为我们提供了第二种设置类属性的手段。
class People: def __init__(self, name, age): self.__name = name self.__age = age def get_age(self): return self.__age def set_age(self, age): if isinstance(age, int): self.__age = age else: raise ValueError def del_age(self): print("删除年龄数据!") # 核心在这句 age = property(get_age, set_age, del_age, "年龄") obj = People("jack", 18) print(obj.age) obj.age = 19 print("obj.age: ", obj.age) del obj.age
通过语句age = property(get_age, set_age, del_age, "年龄")
将一个方法伪装成为属性。其效果和装饰器的方法是一样的。
property()函数的参数:
- 第一个参数是方法名,调用
实例.属性
时自动执行的方法 - 第二个参数是方法名,调用
实例.属性 = XXX
时自动执行的方法 - 第三个参数是方法名,调用
del 实例.属性
时自动执行的方法 - 第四个参数是字符串,调用
实例.属性.__doc__
时的描述信息。
特殊成员和魔法方法
Python中有大量类似__doc__
这种以双下划线开头和结尾的特殊成员及“魔法方法”,它们有着非常重要的地位和作用,也是Python语言独具特色的语法之一。使用之后,在调用的时候,python会帮忙进行特殊处理
比如:
__init__ : 构造函数,在生成对象时调用 __del__ : 析构函数,释放对象时使用 __repr__ : 打印,转换 __setitem__ : 按照索引赋值 __getitem__: 按照索引获取值 __len__: 获得长度 __cmp__: 比较运算 __call__: 调用 __add__: 加运算 __sub__: 减运算 __mul__: 乘运算 __div__: 除运算 __mod__: 求余运算 __pow__: 幂
需要注意的是,这些成员里面有些是方法,调用时要加括号,有些是属性,调用时不需要加括号(废话!)。下面将一些常用的介绍一下,:
1. __doc__
说明性文档和信息。Python自建,无需自定义。
class Foo: """ 描述类信息,可被自动收集 """ def func(self): pass # 打印类的说明文档 print(Foo.__doc__)
2. __init__()
实例化方法,通过类创建实例时,自动触发执行。
class Foo: def __init__(self, name): self.name = name self.age = 18 obj = Foo(jack') # 自动执行类中的 __init__ 方法
3. __module__
和 __class__
__module__
表示当前操作的对象在属于哪个模块。
__class__
表示当前操作的对象属于哪个类。
这两者也是Python内建,无需自定义。
class Foo: pass obj = Foo() print(obj.__module__) print(obj.__class__) ------------ 运行结果: __main__ <class '__main__.Foo'>
4. __del__()
析构方法,当对象在内存中被释放时,自动触发此方法。
注:此方法一般无须自定义,因为Python自带内存分配和释放机制,除非你需要在释放的时候指定做一些动作。析构函数的调用是由解释器在进行垃圾回收时自动触发执行的。
class Foo: def __del__(self): print("我被回收了!") obj = Foo() del obj
5. __call__()
如果为一个类编写了该方法,那么在该类的实例后面加括号,可会调用这个方法。
注:构造方法的执行是由类加括号执行的,即:对象 = 类名()
,而对于__call__()
方法,是由对象后加括号触发的,即:对象()
或者 类()()
class Foo: def __init__(self): pass def __call__(self, *args, **kwargs): print('__call__') obj = Foo() # 执行 __init__ obj() # 执行 __call__
那么,怎么判断一个对象是否可以被执行呢?能被执行的对象就是一个Callable对象,可以用Python内建的callable()函数进行测试,我们在前面的章节已经介绍过这个函数了。
>>> callable(Student()) True >>> callable(max) True >>> callable([1, 2, 3]) False >>> callable(None) False >>> callable('str') False >>> callable(int) True >>> callable(str) True
6. __dict__
列出类或对象中的所有成员!非常重要和有用的一个属性,Python自建,无需用户自己定义。
class Province: country = 'China' def __init__(self, name, count): self.name = name self.count = count def func(self, *args, **kwargs): print('func') # 获取类的成员 print(Province.__dict__) # 获取 对象obj1 的成员 obj1 = Province('HeBei',10000) print(obj1.__dict__) # 获取 对象obj2 的成员 obj2 = Province('HeNan', 3888) print(obj2.__dict__)
7. __str__()
如果一个类中定义了__str__()
方法,那么在打印对象时,默认输出该方法的返回值。这也是一个非常重要的方法,需要用户自己定义。
下面的类,没有定义__str__()
方法,打印结果是:<__main__.Foo object at 0x000000000210A358>
class Foo: pass obj = Foo() print(obj)
定义了__str__()
方法后,打印结果是:'jack'。
class Foo: def __str__(self): return 'jack' obj = Foo() print(obj)
8、__getitem__()
、__setitem__()
、__delitem__()
取值、赋值、删除这“三剑客”的套路,在Python中,我们已经见过很多次了,比如前面的@property
装饰器。
Python中,标识符后面加圆括号,通常代表执行或调用方法的意思。而在标识符后面加中括号[],通常代表取值的意思。Python设计了__getitem__()
、__setitem__()
、__delitem__()
这三个特殊成员,用于执行与中括号有关的动作。它们分别表示取值、赋值、删除数据。
也就是如下的操作:
a = 标识符[] : 执行__getitem__方法 标识符[] = a : 执行__setitem__方法 del 标识符[] : 执行__delitem__方法
如果有一个类同时定义了这三个魔法方法,那么这个类的实例的行为看起来就像一个字典一样,如下例所示:
class Foo: def __getitem__(self, key): print('__getitem__',key) def __setitem__(self, key, value): print('__setitem__',key,value) def __delitem__(self, key): print('__delitem__',key) obj = Foo() result = obj['k1'] # 自动触发执行 __getitem__ obj['k2'] = 'jack' # 自动触发执行 __setitem__ del obj['k1'] # 自动触发执行 __delitem__
9. __iter__()
这是迭代器方法!列表、字典、元组之所以可以进行for循环,是因为其内部定义了 __iter__()
这个方法。如果用户想让自定义的类的对象可以被迭代,那么就需要在类中定义这个方法,并且让该方法的返回值是一个可迭代的对象。当在代码中利用for循环遍历对象时,就会调用类的这个__iter__()
方法。
普通的类:
class Foo: pass obj = Foo() for i in obj: print(i) # 报错:TypeError: 'Foo' object is not iterable<br># 原因是Foo对象不可迭代
添加一个__iter__()
,但什么都不返回:
class Foo: def __iter__(self): pass obj = Foo() for i in obj: print(i) # 报错:TypeError: iter() returned non-iterator of type 'NoneType' #原因是 __iter__方法没有返回一个可迭代的对象
返回一个个迭代对象:
class Foo: def __init__(self, sq): self.sq = sq def __iter__(self): return iter(self.sq) obj = Foo([11,22,33,44]) for i in obj: print(i) # 这下没问题了!
最好的方法是使用生成器:
class Foo: def __init__(self): pass def __iter__(self): yield 1 yield 2 yield 3 obj = Foo() for i in obj: print(i)
10、__len__()
在Python中,如果你调用内置的len()函数试图获取一个对象的长度,在后台,其实是去调用该对象的__len__()
方法,所以,下面的代码是等价的:
>>> len('ABC') 3 >>> 'ABC'.__len__() 3
Python的list、dict、str等内置数据类型都实现了该方法,但是你自定义的类要实现len方法需要好好设计。
11. __repr__()
这个方法的作用和__str__()
很像,两者的区别是__str__()
返回用户看到的字符串,而__repr__()
返回程序开发者看到的字符串,也就是说,__repr__()
是为调试服务的。通常两者代码一样。
class Foo: def __init__(self, name): self.name = name def __str__(self): return "this is %s" % self.name __repr__ = __str__
12. __add__
: 加运算 __sub__
: 减运算 __mul__
: 乘运算 __div__
: 除运算 __mod__
: 求余运算 __pow__
: 幂运算
这些都是算术运算方法,需要你自己为类设计具体运算代码。有些Python内置数据类型,比如int就带有这些方法。Python支持运算符的重载,也就是重写。
class Vector: def __init__(self, a, b): self.a = a self.b = b def __str__(self): return 'Vector (%d, %d)' % (self.a, self.b) def __add__(self,other): return Vector(self.a + other.a, self.b + other.b) v1 = Vector(2,10) v2 = Vector(5,-2) print (v1 + v2)
13. __author__
__author__
代表作者信息!类似的特殊成员还有很多,就不罗列了。
#!/usr/bin/env python # -*- coding:utf-8 -*- """ a test module """ __author__ = "Jack" def show(): print(__author__) show()
14. __slots__
Python作为一种动态语言,可以在类定义完成和实例化后,给类或者对象继续添加随意个数或者任意类型的变量或方法,这是动态语言的特性。例如:
def print_doc(self): print("haha") class Foo: pass obj1 = Foo() obj2 = Foo() # 动态添加实例变量 obj1.name = "jack" obj2.age = 18 # 动态的给类添加实例方法 Foo.show = print_doc obj1.show() obj2.show()
但是!如果我想限制实例可以添加的变量怎么办?可以使__slots__
限制实例的变量,比如,只允许Foo的实例添加name和age属性。
def print_doc(self): print("haha") class Foo: __slots__ = ("name", "age") pass obj1 = Foo() obj2 = Foo() # 动态添加实例变量 obj1.name = "jack" obj2.age = 18 obj1.sex = "male" # 这一句会弹出错误 # 但是无法限制给类添加方法 Foo.show = print_doc obj1.show() obj2.show()
由于'sex'不在__slots__
的列表中,所以不能绑定sex属性,试图绑定sex将得到AttributeError的错误。
Traceback (most recent call last): File "F:/Python/pycharm/201705/1.py", line 14, in <module> obj1.sex = "male" AttributeError: 'Foo' object has no attribute 'sex'
需要提醒的是,__slots__
定义的属性仅对当前类的实例起作用,对继承了它的子类是不起作用的。想想也是这个道理,如果你继承一个父类,却莫名其妙发现有些变量无法定义,那不是大问题么?如果非要子类也被限制,除非在子类中也定义__slots__
,这样,子类实例允许定义的属性就是自身的__slots__
加上父类的__slots__
。
Python的特殊成员和“魔法方法”还有很多,需要大家在平时使用和学习的过程中不断积累和总结使用经验。
reflect反射
在前面的章节,我们遗留了hasattr()、getattr()、setattr()和delattr()的相关内容,它们在这里。
对编程语言比较熟悉的同学,应该听说过“反射”这个机制。Python作为一门动态语言,当然不会缺少这一重要功能。下面结合一个web路由的实例来阐述Python反射机制的使用场景和核心本质。
首先,我们要区分两个概念——“标识名”和看起来相同的“字符串”。两者字面上看起来一样,却是两种东西,比如下面的func函数和字符串“func”:
def func(): print("func是这个函数的名字!") s = "func" print("%s是个字符串" % s)
前者是函数func的函数名,后者只是一个叫“func”的字符串,两者是不同的事物。我们可以用func()的方式调用函数func,但我们不能用"func"()的方式调用函数。说白了就是,不能通过字符串来调用名字看起来相同的函数!
实例分析
考虑有这么一个场景:需要根据用户输入url的不同,调用不同的函数,实现不同的操作,也就是一个WEB框架的url路由功能。路由功能是web框架里的核心功能之一,例如Django的urls。
首先,有一个commons.py文件,它里面有几个函数,分别用于展示不同的页面。这其实就是Web服务的视图文件,用于处理实际的业务逻辑。
# commons.py def login(): print("这是一个登陆页面!") def logout(): print("这是一个退出页面!") def home(): print("这是网站主页面!")
其次,有一个visit.py文件,作为程序入口,接收用户输入,并根据输入展示相应的页面。
# visit.py import commons def run(): inp = input("请输入您想访问页面的url: ").strip() if inp == "login": commons.login() elif inp == "logout": commons.logout() elif inp == "home": commons.home() else: print("404") if __name__ == '__main__': run()
运行visit.py,输入home,页面结果如下:
请输入您想访问页面的url: home 这是网站主页面!
这就实现了一个简单的url路由功能,根据不同的url,执行不同的函数,获得不同的页面。
然而,让我们思考一个问题,如果commons文件里有成百上千个函数呢(这很常见)?难道在visit模块里写上成百上千个elif?显然这是不可能的!那么怎么办?
仔细观察visit.py中的代码,会发现用户输入的url字符串和相应调用的函数名好像!如果能用这个字符串直接调用函数就好了!但是,前面已经说了字符串是不能用来调用函数的。为了解决这个问题,Python提供了反射机制,帮助我们实现这一想法,其主要就表现在getattr()等几个内置函数上!
现在将前面的visit.py修改一下,代码如下:
# visit.py import commons def run(): inp = input("请输入您想访问页面的url: ").strip() func = getattr(commons,inp) func() if __name__ == '__main__': run()
func = getattr(commons,inp)
语句是关键,通过getattr()函数,从commons模块里,查找到和inp字符串“外形”相同的函数名,并将其返回,然后赋值给func变量。变量func此时就指向那个函数,func()就可以调用该函数。
getattr()函数的使用方法:接收2个参数,前面的是一个类或者模块,后面的是一个字符串,注意了!是个字符串!
这个过程就相当于把一个字符串变成一个函数名的过程。这是一个动态访问的过程,一切都不写死,全部根据用户输入来变化。
前面的代码还有个小瑕疵,那就是如果用户输入一个非法的url,比如jpg,由于在commons里没有同名的函数,肯定会产生运行错误,如下:
请输入您想访问页面的url: jpg Traceback (most recent call last): File "F:/Python/pycharm/s13/reflect/visit.py", line 16, in <module> run() File "F:/Python/pycharm/s13/reflect/visit.py", line 11, in run func = getattr(commons,inp) AttributeError: module 'commons' has no attribute 'jpg'
那怎么办呢?python提供了一个hasattr()的内置函数,用法和getattr()基本类似,它可以判断commons中是否具有某个成员,返回True或False。现在将代码修改一下:
# visit.py import commons def run(): inp = input("请输入您想访问页面的url: ").strip() if hasattr(commons,inp): func = getattr(commons,inp) func() else: print("404") if __name__ == '__main__': run()
这下就没有问题了!通过hasattr()的判断,可以防止非法输入导致的错误,并将其统一定位到错误页面。
Python的四个重要内置函数:getattr()、hasattr()、delattr()和setattr()较为全面的实现了基于字符串的反射机制。delattr()和setattr()就不做多解释,相信从字面意思看,你也该猜到它们的用途和用法了。它们都是对内存中的模块进行操作,并不会对源文件进行修改。
动态导入模块
前面的例子需要commons.py和visit.py模块在同一目录下,并且所有的页面处理函数都在commons模块内。如下图:
但在实际环境中,页面处理函数往往被分类放置在不同目录的不同模块中,也就是如下图:
原则上,只需要在visit.py模块中逐个导入每个视图模块即可。但是,如果这些模块很多呢?难道要在visit里写上一大堆的import语句逐个导入account、manage、commons模块吗?要是有1000个模块呢?
可以使用Python内置的__import__(字符串参数)
函数解决这个问题。通过它,可以实现类似getattr()的反射功能。__import__()
方法会根据字符串参数,动态地导入同名的模块。
再修改一下visit.py的代码。
# visit.py def run(): inp = input("请输入您想访问页面的url: ").strip() modules, func = inp.split("/") obj = __import__(modules) if hasattr(obj, func): func = getattr(obj, func) func() else: print("404") if __name__ == '__main__': run()
需要注意的是:输入的时候要同时提供模块名和函数名字,并用斜杠分隔。
运行一下试试:
请输入您想访问页面的url: commons/home 这是网站主页面! 请输入您想访问页面的url: account/find 这是一个查找功能页面!
同样的,这里也有个小瑕疵!如果我们的目录结构是这样的,visit.py和commons.py不在一个目录下,存在跨包的问题:
那么在visit的调用语句中,必须进行修改,你想当然地可能会这么做:
def run(): inp = input("请输入您想访问页面的url: ").strip() modules, func = inp.split("/") obj = __import__("lib." + modules) #注意字符串的拼接 if hasattr(obj, func): func = getattr(obj, func) func() else: print("404") if __name__ == '__main__':
看起来似乎没什么问题,和import lib.commons
的传统方法类似,但实际上运行的时候会有错误。
请输入您想访问页面的url: commons/home 404 请输入您想访问页面的url: account/find 404
为什么呢?因为对于lib.xxx.xxx.xxx
这一类的模块导入路径,__import__()
默认只会导入最开头的圆点左边的目录,也就是lib
。可以做个测试,在visit同级目录内新建一个文件,代码如下:
obj = __import__("lib.commons") print(obj)
执行结果:<module 'lib' (namespace)>
这个问题怎么解决?加上fromlist = True
参数即可!完整的代码如下:
def run(): inp = input("请输入您想访问页面的url: ").strip() modules, func = inp.split("/") obj = __import__("lib." + modules, fromlist=True) # 注意fromlist参数 if hasattr(obj, func): func = getattr(obj, func) func() else: print("404") if __name__ == '__main__': run()
异常处理
Python内置了一套try...except...finally(else)...的异常处理机制,来帮助我们进行异常处理。其基本语法是:
try: pass except Exception as ex: pass
注:在Python3中,原Python2的except Exception , ex
的别名方法已经不能使用,逗号被认为是两种异常的分隔符,而不是取别名。
异常捕获处理的方式和java相同
- 最后一个except子句可以忽略异常的名称,它将被当作通配符使用,也就是说匹配所有异常。
except: print("Unexpected error:", sys.exc_info()[0])
通用异常:Exception(和上面什么都不写相同)
在Python的异常中,有一个通用异常:Exception
,它可以捕获任意异常。
s1 = 'hello' try: int(s1) except Exception as e: print('错误')
那么既然有这个什么都能管的异常,其他诸如OSError、ValueError的异常是不是就可以不需要了?当然不是!很多时候程序只会弹出那么几个异常,没有必要针对所有的异常进行捕获,那样的效率会很低。另外,根据不同的异常种类,制定不同的处理措施,用于准确判断错误类型,存储错误日志,都是非常有必要甚至强制的。
finally和else子句
try except
语法还有一个可选的else子句,如果使用这个子句,那么必须放在所有的except子句之后。这个子句将在try子句没有发生任何异常的时候执行。
for arg in sys.argv[1:]: try: f = open(arg, 'r') except IOError: print('cannot open', arg) else: print(arg, 'has', len(f.readlines()), 'lines') f.close()
同样的,还有一个可选的finally子句。无论try执行情况和except异常触发情况,finally子句都会被执行!
try: print('try...') r = 10 / int('a') print('result:', r) except ValueError as e: print('ValueError:', e) finally: print('finally...') print('END')
那么,当else和finally同时存在时:
try: pass except: pass else: print("else") finally: print("finally")
运行结果:
else finally
如果有异常发生:
try: 1/0 except: pass else: print("else") finally: print("finally")
运行结果:
finally
主动抛出异常:raise
很多时候,我们需要主动抛出一个异常。Python内置了一个关键字raise
,可以主动触发异常。
>>> raise Traceback (most recent call last): File "<pyshell#0>", line 1, in <module> raise RuntimeError: No active exception to reraise >>> raise NameError("kkk") Traceback (most recent call last): File "<pyshell#1>", line 1, in <module> raise NameError("kkk") NameError: kkk
raise唯一的一个参数指定了要被抛出的异常的实例,如果什么参数都不给,那么会默认抛出当前异常。
自定义异常
自定义异常应该继承Exception
类,直接继承或者间接继承都可以,例如:
class MyExcept(Exception): def __init__(self, msg): self.message = msg def __str__(self): return self.message try: raise MyExcept('我的异常!') except MyExcept as ex: print(ex)
异常的名字都以Error
结尾,我们在为自定义异常命名的时候也需要遵守这一规范,就跟标准的异常命名一样。