Python函数和模块
纸上得来终觉浅,绝知此事要躬行。
程序写着写着就会变得越来越大,怎么能够更好的组织代码,就是我们应该关注的重点。而正确的做法,就是将程序分解成多个较小的方法,一般分为函数、对象、模块三种。
1. 函数
函数是带名称的代码块,可以将逻辑封装起来,多次使用。
- 函数格式
def <name>(arg1, arg2, ..., argN): <statements> return <value>
- 函数定义
# 没有参数的函数定义 In [1]: def hello(): ...: print('Hello World!') ...: return True ...: In [2]: hello() Hello World! Out[2]: True
# 有参数的函数定义 In [3]: def hello(name='World!'): ...: print(f'Hello, {name}') ...: In [4]: hello() Hello, World! In [5]: hello('Escape') Hello, Escape
1.1 函数中的参数
- 函数参数
- 形参:形参是指函数定义中在内部使用的参数,这个函数完成其工作所需要的信息内容,在没有实际调用的时候函数用形参来指代
- 实参:实参是指调用函数时由调用者传入的参数,这个时候形参纸袋的内容就是实参内容
- 实参类型
- 位置参数(positional argument)
- 关键字参数(keyword argument)
# 位置参数 # [方式一] 以值的形式传递 In [8]: def hello(name): ...: print(f'Hello, {name}!') ...: In [9]: hello('Escape') Hello, Escape # [方式二] 以*开头的元组传递,即变长位置参数 In [10]: def hello(*names): ...: print(names) ...: In [11]: hello('Escape') ('Escape',)
# 关键字参数 # [方式一] 以名称等于值的形式传递 In [16]: def hello(name='World'): ...: print(f'Hello, {name}!') ...: In [17]: hello() Hello, World! In [18]: hello('Escape') Hello, Escape! # [方式二] 以两个*开头的字典传递,即变长关键字参数 In [21]: def hello(prefix, name='World', **kwargs): ...: print(f'{prefix} {name}! {kwargs}') ...: In [22]: hello('Hello') Hello World! {} In [23]: hello('Hello', name='Escape') Hello Escape! {} In [24]: hello('Hello', name='Escape', say='hahaha...') Hello Escape! {'say': 'hahaha...'}
# 强制关键字参数 # 这是在Python3.6当中新引入的,强制让*之后使用关键字参数传递 In [13]: def powerkw(maxsize, *, block): ...: pass ...: In [14]: powerkw(1000, block=True) In [15]: powerkw(1000, True) --------------------------- TypeError Traceback (most recent call last) <ipython-input-15-ef44e44233b2> in <module>() ----> 1 powerkw(1000, True) TypeError: powerkw() takes 1 positional argument but 2 were given
- 混合使用
- 顺序:常规参数、默认参数、变长元组参数、变长关键字参数
- 注意:位置参数必须在关键字参数之前出现
In [25]: def func(a, b=0, *args, **kwargs): ...: print('a =', a, 'b =', b, 'args =', args, 'kwargs =', kwargs) ...: In [26]: func(1, 2) a = 1 b = 2 args = () kwargs = {} In [27]: func(1, 2, d=4) a = 1 b = 2 args = () kwargs = {'d': 4} In [28]: func(1, 2, 3) a = 1 b = 2 args = (3,) kwargs = {} In [29]: func(1, 2, 3, d=4) a = 1 b = 2 args = (3,) kwargs = {'d': 4}
1.2 函数中的返回值
In [30]: def add(a, b): ...: return a + b ...: In [31]: add(1, 2) Out[31]: 3 In [32]: def partition(string, sep): ...: return string.partition(sep) ...: In [33]: partition('/home/escape/bran', '/') Out[33]: ('', '/', 'home/escape/bran')
1.3 函数中的作用域
变量作用域(BGEL):从上往下级别依次递增
- (B) 系统变量
- (G) 全局变量
- (E) 嵌套作用域
- (L) 本地作用域
- 系统变量
- 解释器自行定义的系统变量集合
# 在Python3中引入builtins不需要使用双下划线 In [44]: import builtins In [45]: ', '.join((i for i in dir(builtins) if i.islower() and '_' not in i)) Out[45]: 'abs, all, any, ascii, bin, bool, bytearray, bytes, callable, chr, classmethod, compile, complex, copyright, credits, delattr, dict, dir, display, divmod, enumerate, eval, exec, filter, float, format, frozenset, getattr, globals, hasattr, hash, help, hex, id, input, int, isinstance, issubclass, iter, len, license, list, locals, map, max, memoryview, min, next, object, oct, open, ord, pow, print, property, range, repr, reversed, round, set, setattr, slice, sorted, staticmethod, str, sum, super, tuple, type, vars, zip' # 在Python2需要使用双下划线,但Python3不需要导入就能使用 >>> import __builtin__ >>>> dir(__builtin__) ...
- 全局变量
- 如果全局有定义,而在局部没有定义,就会使用全局变量
- 如果局部要定义,定义前不要使用这个变量,否则要引入global或nonlocal关键字声明
# 全局变量 In [38]: g = 0 In [39]: def run(): ...: print(g) ...: In [40]: run() 0 In [41]: In [41]: def run(): ...: g = 2 ...: In [42]: run() In [43]: g Out[43]: 0
# [全局变量的常见错误一] # 报错信息提示本地变量g没有被定义就使用了,这是因为在函数体内定义了和全局变量同名的局部变量, # 不管全局内是否定义了此变量的值,函数体内只是用局部变量的值 In [56]: g = 0 In [57]: def run(): ...: print(g) ...: g = 2 ...: print(g) ...: In [58]: run() --------------------------- UnboundLocalError Traceback (most recent call last) <ipython-input-58-ec9775ede022> in <module>() ----> 1 run() <ipython-input-57-70ad5c1b5edb> in run() 1 def run(): ----> 2 print(g) 3 g = 2 4 print(g) 5 UnboundLocalError: local variable 'g' referenced before assignment # [全局变量的常见错误二] # 报错信息提示本地变量g没有被定义就使用了,这是因为在函数体内定义了和全局变量同名的局部变量, # 不管全局内是否定义了此变量的值,函数体内只是用局部变量的值 In [59]: g = 0 In [60]: def run(): ...: g += 2 ...: print(g) ...: In [61]: run() --------------------------- UnboundLocalError Traceback (most recent call last) <ipython-input-61-ec9775ede022> in <module>() ----> 1 run() <ipython-input-60-2eed38be07b2> in run() 1 def run(): ----> 2 g += 2 3 print(g) 4 UnboundLocalError: local variable 'g' referenced before assignment # [解决方式] global关键字 # 不推荐使用,如果没有使用的话很可能是因为程序设计不合理导致的 In [62]: def run(): ...: global g ...: g += 2 ...: print(g) ...: In [63]: run() 2
- 嵌套作用域
- 嵌套作用域和本地作用域是相对的,嵌套作用域的变量相对于上层来说也是本地变量
# run2就是闭包了 In [48]: g = 0 In [49]: def run(): ...: g = 2 ...: def run2(): ...: print(g) ...: return run2 ...: In [50]: run()() 2
- 本地变量
- 函数体内定义的变量
# 本地变量 In [36]: def run(name): ...: s = f'{name}' ...: for x in range(5): ...: if x == 3: ...: return ...: print(s) ...: In [37]: run('Escape')
1.4 函数中的闭包
闭包:指延伸了作用域的函数,其中包含函数定义体中的引用,但是不在定义体中定义的非全局变量,它能访问定义体之外定义的非全局变量
- 闭包:嵌套作用域
# maker就是工厂函数,action就是闭包 In [51]: def maker(n): ...: def action(m): ...: return n * m ...: return action ...: In [52]: f = maker(3) In [53]: f(2) Out[53]: 6 In [54]: g = maker(10) In [55]: g(2) Out[55]: 20
- 修改嵌套作用域变量:nonlocal
- 赋值的变量名,如不使用global或nonlocal声明为全局变量或非本地变量,均为本地变量
- 需要注意的是,这里的nonlocal声明只改变定义体上一层的变量的值
In [63]: def run(): ...: g = 2 ...: def run2(): ...: g = 4 ...: print(f'inner: {g}') ...: run2() ...: print(f'outer: {g}') ...: In [64]: run() inner: 4 outer: 2
In [65]: def run(): ...: g = 2 ...: def run2(): ...: nonlocal g ...: g = 4 ...: print(f'inner: {g}') ...: run2() ...: print(f'outer: {g}') ...: In [66]: run() inner: 4 outer: 4
1.5 函数中的高阶函数
- 参数为函数
- 函数中的参数为另一个函数的方法,可以成为高阶函数
In [33]: def hello(name): ...: print(f'Hello {name}!') ...: In [34]: def test(func, name='World'): ...: func(name) ...: In [35]: test(hello, 'Escape') Hello Escape!
- 匿名函数
In [67]: f = lambda n: n * 2 In [68]: f(10) Out[68]: 20
In :def double(n): ...: return n * 2...: In : list(map(double, l1)) Out: [2, 6, 8] In : list(map(lambda x: x * 2, l1)) Out: [2, 6, 8] In : l = [[2, 4], [1, 1], [9, 3]] In : sorted(l) Out: [[1, 1], [2, 4], [9, 3]] # 匿名函数中使用对象第二项进行排序 In : sorted(l, key=lambda x: x[1]) Out: [[1, 1], [9, 3], [2, 4]] # 匿名函数中使用对象的属性 In : l3 = ['/boot/grub', '/usr/local', '/home/escape'] In : sorted(l3, key=lambda x: x.rsplit('/')[2]) Out: ['/home/escape', '/boot/grub', '/usr/local']
- 高阶函数:map
- 对可迭代对象的每一个元素进行计算
In : rs = map(double, l1) In : rs Out: <map at0x105986748> In : list(rs) Out: [2, 6, 8]
- 高阶函数:filter
- 筛选出值为True的值,其余则将筛除
In : def is_odd(x): ...: return x % 2 == 1 ...: In : rs = filter(is_odd, l1) In : rsOut: <filter at0x105986d68> In : list(rs) Out: [1, 3] In : list(filter(None, [1, '', {}, (), False, None, set()])) Out: [1]
- 高阶函数:reduce
- 对可迭代对象的值俩俩重复进行计算
# 函数必须接收两个参数 In : def add(a, b): ...: return a + b ...: # 在Python3中需要额外导入 In : from functools import reduce In : reduce(add, [1, 2, 3]) Out: 6 # 第三个参数表示计算的初始值 In : reduce(add, [1, 2, 3], 10) Out: 16
1.6 函数中的常见函数
- 常见函数:zip
In [69]: a = list(range(1, 4)) In [70]: b = list(range(4, 7)) In [71]: c = list(range(7, 11)) In [72]: zip(a, b) Out[72]: <zip at 0x10d0beb08> In [73]: list(zip(a, b)) Out[73]: [(1, 4), (2, 5), (3, 6)] In [74]: list(zip(a, c)) Out[74]: [(1, 7), (2, 8), (3, 9)] In [75]: list(zip(*zip(a, b))) Out[75]: [(1, 2, 3), (4, 5, 6)]
- 常见函数:sum
# 求和 In [76]: sum([1, 2, 3]) Out[76]: 6 # 设定初始值 In [77]: sum([1, 2, 3], 10) Out[77]: 16 # 可以把嵌套的数据扁平化 In [78]: sum([[1, 2], [3, 4]], []) Out[78]: [1, 2, 3, 4]
1.7 函数中的开发陷阱
- 开发陷阱:可变默认参数
- 当默认参数时可变对象的时候,其实每次更改的都是同一个变量
# [开发陷阱] In [79]: def append_list(ele, to=[]): ...: to.append(ele) ...: return to ...: In [80]: append_list(1) Out[80]: [1] In [81]: append_list(2) Out[81]: [1, 2]
# [解决方法] 初始化校验 In [82]: def append_list(ele, to=None): ...: if to is None: ...: to = [] ...: to.append(ele) ...: return to ...: In [83]: append_list(1) Out[83]: [1] In [84]: append_list(2) Out[84]: [2]
- 开发陷阱:延迟绑定特性
- 因为闭包中用到的值是在用到的时候调用查询得到的,也就是延迟绑定,所以i在range(5)的时候被绑定为4,所以之后的值都是在4基础上计算而来的
# [开发陷阱] In [85]: def create_mul(): ...: return [lambda x: x * i for i in range(5)] ...: In [86]: for mul in create_mul(): ...: print(mul(2)) ...: 8 8 8 8 8
# [解决方法] 函数默认值 In [87]: def create_mul(): ...: return [lambda x, i=i: x * i for i in range(5)] ...: # [解决方法] 偏函数绑定 In [88]: from functools import partial In [89]: from operator import mul In [90]: def create_mul(): ...: return [partial(mul, i) for i in range(5)]
2. 模块
模块是一个一个的Python文件,它将程序代码和数据封装起来,可以让项目组织清晰明了,更好维护。
2.1 模块中的导入方式
- 导入模块:import …
In : import my_module In : my_module.A Out: 100 In : my_module.add(1, 2) Out: 3
- 导入模块:from … import …
# 不建议使用[from ... import *]的用法 # 1. 不好跟踪定位问题 # 2. 导入的变量没有被用到 # 3. 污染命名空间
In : from my_module import A, add In : A Out: 100 In : add(1, 2) Out: 3
2.2 模块中的导入原理
- 第一次导入模块步骤:之后直接从内存加载
- [1] 搜集并找到对应模块文件
- [2] 在必要时把模块文件编译成字节码
- [3] 执行模块的代码来创建所定义的对象
- 搜索路径顺序
- [1] 程序的主目录
- [2] PYTHONPATH系统变量
- [3] 标准库目录
- [4] .pth文件
- 搜索文件类型
- [1] a.py代码源文件
- [2] a.pyc字节码文件
- [3] 目录a作为包导入
- [4] s.so、a.dll、a.pyd编译扩展文件
- [5] 用c编译好的内置模块
- [6] zip文件包
# 搜索路径都汇总在sys.path变量里面 # 可以通过对sys.path的操作达到我们需要的效果 In [1]: import sys In [2]: sys.path Out[2]: ['', '/usr/local/bin', '/usr/local/Cellar/python/3.6.4_4/Frameworks/Python.framework/Versions/3.6/lib/python36.zip', '/usr/local/Cellar/python/3.6.4_4/Frameworks/Python.framework/Versions/3.6/lib/python3.6', '/usr/local/Cellar/python/3.6.4_4/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload', '/usr/local/lib/python3.6/site-packages', '/usr/local/lib/python3.6/site-packages/IPython/extensions', '/Users/escape/.ipython']
- 生成的字节码
# Python3中的字节码会保存在__pycache__的目录下面 In : !tree . ├── __pycache__ │ └── my_module.cpython-36.pyc └── my_module.py 1 directory, 2 files
- __name__和__main__
def run(): print('Run') if __name__ == '__main__': run()
2.3 模块中的模块包
# 注意模块包导入的时候需要在目录下面添加__init__.py文件 In : !tree . ├── dir1 │ ├── __init__.py │ ├──a.py │ └── dir2 │ ├── __init__.py │ └──b.py
In : import dir1 In : dir1 Out: <module'dir1'from'dir1/__init__.py'> In : import dir1.dir2 In : from dir1 import a In : from dir1.dir2 import b In : dir1.a Out: <module'dir1.a'from'dir1/a.py'> In : dir1.dir2.b Out: <module'dir1.dir2.b'from'dir1/dir2/b.py'>
2.4 模块导入异常问题
- 今天写代码的时候,发现怎么执行 config.py 这个模块都会显示无法找到 'tool' 这个模块,调试了好久,才发现还是自己没有搞明白 Python 的导入机制,导致了这个问题的出现,在此记录一下。
# 目录结构 ├── main.py └── tool ├── __init__.py ├── config.py └── test.py
# main.py import tool.test # config.py debug = True # test.py print(__name__) try: from . import config print(config.debug) del config except ImportError: print('Relative import failed') try: import config print(config.debug) except ModuleNotFoundError: print('Absolute import failed')
- 目录结构以及对于代码内容,如上所示。我们分别通过两种形式进行执行和调用,来观察其对应的输出。
# Let's run test.py $ python tool/test.py __main__ Relative import failed True
# Let's run main.py $ python main.py tool.test True Absolute import failed
- 通过上面的输出,我们可以很清楚的得知。
- test.py:直接运行 test.py 文件后,__name__ 变量对于的输出是 __main__,得知 "test.py" 文件之后其不知道其所属于那个包,所以在使用相对引用的时候就无法引入。然而使用 import config 是可以,这是因为在执行的时候 tool 目录被加到了 sys.path 环境变量中了,所以可以直接导入。
- main.py:直接运行 main.py 文件后,__name__ 变量对于的输出是 tool.test,得知 tool 目录在环境变量中了,所以可以直接导入。但是使用 import config 绝对导入就失败了,这是因为在 Python3 中不允许隐式的相对导入。
- 如果,我们直接使用 Python3 的话,则可以不需要 __init__.py 文件了。