Python自学指南-第五章-学习函数

5.1 【基础】普通函数创建与调用

函数是一种仅在调用时运行的代码块。您可以将数据(称为参数)传递到函数中,然后由函数可以把数据作为结果返回。

如果将函数比喻成蛋糕店的话,那么函数的参数就是生产蛋糕的原材料,而函数的返回值就是蛋糕成品。

1. 函数的创建

在 Python 中,使用 def 关键字定义函数

def 函数名(参数):
    # 内部代码
    return 表达式

举个例子,我这边手动实现一个计算两个数平均值的函数,这边这样子写

def get_average(a, b):
    '''
    计算平均值
    '''
    result = (a + b)/2
    return result

在定义函数的过程中,需要注意以下几点:

  • 函数代码块以def关键词开头,一个空格之后接函数标识符名称和圆括号(),再接个冒号。
  • 任何传入的参数必须放在圆括号中间。
  • 函数的第一行语句后可以选择性地使用文档字符串—用于存放函数说明。
  • 函数内容以冒号起始,并且缩进。
  • 使用return,返回值给调用者,并结束函数。return 关键并不是必须要加,可根据实际需要决定是否要写,若不写的话,默认返回None。
  • return语句依然在函数体内部,不能回退缩进。直到函数的所有代码写完,才回退缩进,表示函数体结束。

2. 函数的调用

函数编写出来就是给人调用的。要调用一个函数,必须使用函数名后跟圆括号的方式才能调用函数。

调用的同时要根据函数的定义体,提供相应个数和类型的参数,每个参数之间用逗号分隔。

def get_average(a, b):
    '''
    计算平均值
    '''
    result = (a + b)/2
    return result
average = get_average(2, 6)
print(average)  # output: 4

3. 函数的返回

函数的返回值,可以是多种多样的,非常灵活:

  • 可以是任意类型的对象,比如字符串,数值,列表,字典等等
def demo_func():
    return 10
  • 可以是一个表达式,函数会直接运行表达式,然后返回
def get_average(a, b):
    return (a + b)/2
  • 可以是函数本身,利用这点可以实现递归调用。
def fact(n):
    if n==1:
        return 1
    return n * fact(n - 1)
  • 另外还可以返回多个值
def demo_func():
    return 1,2.3
  • 可以是其他函数,利用这点可以实现装饰器。
def decorator(func):
    def wrapper(*args, **kw):
        return func()
    return wrapper

5.2. 【基础】11个案例讲解函数参数

1. 参数分类

函数,在定义的时候,可以有参数的,也可以没有参数。

从函数定义的角度来看,参数可以分为两种:

  1. 必选参数:调用函数时必须要指定的参数,在定义时没有等号,称位置参数也就是必选参数
  2. 可选参数:也叫默认参数,调用函数时可以指定也可以不指定,不指定就默认的参数值来。

例如下面的代码中,a 和 b 属于必选参数, c 和 d 属于可选参数也是默认参数

def func(a,b,c=0, d=1):
    pass

从函数调用的角度来看,参数可以分为两种:

  1. 关键字参数:调用时,使用 key=value 形式传参的,这样传递参数就可以不按定义顺序来。
  2. 位置参数:调用时,不使用关键字参数的 key-value 形式传参,这样传参要注意按照函数定义时参数的顺序来。
def func(a,b,c=0, d=1):
    pass

  # 关键字参数传参方法
func(a=10, c=30, b=20, d=40)

  # 位置参数传参方法
func(10, 20, 30, 40)

最后还有一种非常特殊的参数,叫做可变参数

意思是参数个数可变,可以是 0 个或者任意个,但是传参时不能指定参数名,通常使用 *args**kw 来表示:

  • *args:接收到的所有按照位置参数方式传递进来的参数,是一个元组类型,即可变位置参数
  • **kw :接收到的所有按照关键字参数方式传递进来的参数,是一个字典类型,即可变关键字参数
def func(*args, **kw):
    print(args)
    print(kw)

func(10, 20, c=20, d=40)

输出如下

(10, 20)
{'c': 20, 'd': 40}

2. 十一个案例

案例一:在下面这个函数中, a 是必选参数,是必须要指定的

def demo_func(a):
    print(a)

demo_func(10)  # 输出 10

# 报错示例
demo_func()  # TypeError: demo_func() missing 1 required positional argument: 'a'

案例二:在下面这个函数中,b 是可选参数(默认参数),可以指定也可以不指定,不指定的话,默认为10

def demo_func(b=10):
    print(b)

demo_func(20)    # 输出 20
demo_func()   # 输出 10

案例三:在下面这个函数中, name 和 age 都是必选参数,在调用指定参数时,如果不使用关键字参数方式传参,需要注意顺序

def print_profile(name, age):
    return f"我的名字叫{name},今年{age}岁了"

print(print_profile("航哥", 27))  #输出 我的名字叫航哥,今年27岁了

如果参数太多,你不想太花精力去注意顺序,可以使用关键字参数方式传参,在指定参数时附上参数名,比如这样:

def print_profile(name, age):
    return f"我的名字叫{name},今年{age}岁了。"
print(print_profile(age=27, name="航哥"))   #输出 我的名字叫航哥,今年27岁了。

案例四:在下面这个函数中,args 参数和上面的参数名不太一样,在它前面有一个 *,这就表明了它是一个可变参数,可以接收任意个数的不指定参数名的参数。

def print_args(*args):
    print(args)

print_args(1, 2, 3)
print_args('a', 'b', 'c', 'd')
#输出结果
(1, 2, 3)
('a', 'b', 'c', 'd')

案例五:在下面这个函数中,kw 参数和上面的 *args 还多了一个 * ,总共两个 ** ,这个意思是 kw 是一个可变关键字参数,可以接收任意个数的带参数名的参数。

def print_kwargs(**kwargs):
    print(kwargs)

print_kwargs(a=1, b=2, c=3)
print_kwargs(x='a', y='b', z='c')
#输出结果
{'a': 1, 'b': 2, 'c': 3}
{'x': 'a', 'y': 'b', 'z': 'c'}

请注意,当我们在函数调用中传递关键字参数时,我们不需要在参数名前加上 **。在函数内部,kw 参数将包含所有传递给函数的关键字参数的字典。

案例六:在定义时,必选参数一定要在可选参数的前面,不然运行时会报错

>>> def demo_func(a=1, b):
...     print(a, b)
...
  File "<stdin>", line 1
SyntaxError: non-default argument follows default argument
>>>
>>> def demo_func(a, b=1):
...     print(a, b)
...
>>>

案例七:在定义时,可变位置参数一定要在可变关键字参数前面,不然运行时也会报错;

因为位置参数必须出现在关键字参数之前。这是因为Python解释器无法推断出在可变关键字参数之后的参数的名称。

>>> def demo_func(**kw, *args):
  File "<stdin>", line 1
    def demo_func(**kw, *args):
                        ^
SyntaxError: invalid syntax
>>>
>>> def demo_func(*args, **kw):
...     print(args, kw)
...
>>>

正确用法

def print_args_kwargs(*args, **kwargs):
    print("位置参数:", args)
    print("关键字参数:", kwargs)


print_args_kwargs(1, 2, 3, a=4, b=5, c=6)
print_args_kwargs('a', 'b', 'c', x=1, y=2, z=3)


#输出结果
位置参数: (1, 2, 3)
关键字参数: {'a': 4, 'b': 5, 'c': 6}
位置参数: ('a', 'b', 'c')
关键字参数: {'x': 1, 'y': 2, 'z': 3}

请注意,位置参数 *args 在关键字参数 **kwargs 之前定义,并用逗号隔开。在函数调用中,我们可以传递任意数量的位置参数和关键字参数,并且它们将分别被分配给 argskwargs 参数。

案例八:在Python中,可变位置参数可以放在必选参数前面,但是在调用函数时必须要指定参数名来传入必选参数,否则会引发TypeError异常。例如,在下面的代码中,我们定义了一个带有可变位置参数和必选参数的函数demo_func,然后在调用该函数时,如果不指定b参数名来传入必选参数,则会引发TypeError异常,因为Python无法确定哪个参数是必选参数。

def demo_func(*args, b):
    print(args)
    print(b)

demo_func(1, 2, 100)  # TypeError: demo_func() missing 1 required keyword-only argument: 'b'
demo_func(1, 2, b=100)  # (1, 2) 100

因此,在定义带有可变位置参数和必选参数的函数时,建议使用参数名来传递必选参数,以避免潜在的错误。

案例九:可变关键字参数则不一样,可变关键字参数一定得放在最后,下面三个示例中,不管关键字参数后面接位置参数,还是默认参数,还是可变参数,都会报错。

>>> def demo_func(**kw, a):
  File "<stdin>", line 1
    def demo_func(**kw, a):
                        ^
SyntaxError: invalid syntax
>>>
>>> def demo_func(**kw, a=1):
  File "<stdin>", line 1
    def demo_func(**kw, a=1):
                        ^
SyntaxError: invalid syntax
>>>
>>> def demo_func(**kw, *args):
  File "<stdin>", line 1
    def demo_func(**kw, *args):
                        ^
SyntaxError: invalid syntax

正确写法

def demo_func(a, **kw):
    print(a, kw)
def demo_func(a=1, **kw):
    print(kw, a)

def demo_func(*args, **kw):
    print(args, kw)

在Python的参数列表中,有四种类型的参数:位置参数、默认参数、可变位置参数和可变关键字参数。这四种类型的参数在参数列表中的位置是有规定的:

  1. 位置参数必须在默认参数之前,因为调用函数时会按照位置依次传参,如果位置参数在默认参数之后,那么调用函数时就无法区分这些参数的位置。
  2. 可变位置参数必须在位置参数和默认参数之后,因为可变位置参数收集所有剩余的位置参数,所以如果可变位置参数之前还有其他参数,那么这些参数就无法被正确地解析。
  3. 默认参数必须在位置参数之后,因为调用函数时可以不传递默认参数,此时默认参数就会取默认值,如果默认参数在位置参数之前,那么调用函数时就无法确定哪些参数是位置参数,哪些是默认参数。
  4. 可变关键字参数必须在所有参数之后,因为可变关键字参数收集所有剩余的关键字参数,如果可变关键字参数之前还有其他参数,那么这些参数就无法被正确地解析。

所以,在Python中,参数列表的末尾指的是可变关键字参数之后的位置,而最后指的是所有参数的最后一个位置。

案例十:将上面的知识点串起来,四种参数类型可以在一个函数中出现,但一定要注意顺序

def demo_func(arg1, arg2=10, *args, **kw):
    print("arg1: ", arg1)
    print("arg2: ", arg2)
    print("args: ", args)
    print("kw: ", kw)


试着调用这个函数,输出如下:


demo_func(1, 2, 3, 4, 5, a=6, b=7)
arg1: 1
arg2: 2
args: (3, 4, 5)
kw: {'a': 6, 'b': 7}

案例十一:使用单独的 *,当你在给后面的位置参数传递时,对你传参的方式有严格要求,你在传参时必须要以关键字参数的方式传参数,要写参数名,不然会报错。

def demo_func(a, b, *, c):
    print(a)
    print(b)
    print(c)
demo_func(1, 2, 3)
#使用三个位置参数来调用 demo_func,但由于该函数只接受两个位置参数,因此 Python 引发了 TypeError 异常。
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: demo_func() takes 2 positional arguments but 3 were given


demo_func(1, 2, c=3)

3. 传参的坑

函数参数传递的是实际对象的内存地址。如果参数是引用类型的数据类型(列表、字典等),在函数内部修改后,就算没有把修改后的值返回回去,外面的值其实也已经发生了变化。

def add_item(item, source_list):
    source_list.append(item)

alist = [0, 1]
add_item(2, alist)
print(alist)
#输出结果
[0, 1, 2]

5.3 【基础】匿名函数的使用

匿名函数(英语:anonymous function)是指一类无需定义标识符(函数名)的函数。通俗来说呢,就是它可以让我们的函数,可以不需要函数名。

正常情况下,我们定义一个函数,使用的是 def 关键字,而当你学会使用匿名函数后,替代 def 的是 lambda

这边使用deflambda 分别举个例子,你很快就能理解。

def mySum(x, y):
    return x+y
mySum(2, 3)
# 5

(lambda x, y: x+y)(2, 4)
# 6

从上面的示例,我们可以看到匿名函数直接运行,省下了很多行的代码,有没有?

接下来,我们的仔细看一下它的用法

带 if/else

# 定义 lambda 函数
f = lambda x, y: x if x < y else y

# 调用 lambda 函数
result = f(1, 2)

# 输出结果  1
print(result)

嵌套函数

result = (lambda x: (lambda y: (lambda z: x + y + z)(1))(2))(3)
print(result)
#输出结果 6

递归函数

func = lambda n: 1 if n == 0 else n * func(n-1)
print(func(5))
#输出结果 120

或者

def f(func, n):
    return 1 if n == 0 else n * func(func, n - 1)

result = f(f, 4)
print(result)

#输出结果 24

从以上示例来看,lambda 表达式和常规的函数相比,写法比较怪异,可读性相对较差。除了可以直接运行之外,好像并没有其他较为突出的功能,为什么在今天我们要介绍它呢?

首先我们要知道 lambda 是一个表达式,而不是一个语句。正因为这个特点,我们可以在一些特殊的场景中去使用它。具体是什么场景呢?接下来我们会介绍到几个非常好用的内置函数。

5.4 【基础】必学高阶函数

1. map函数

map()函数是Python中常用的高阶函数之一。它接收两个参数,第一个参数是一个函数对象(也可以是lambda表达式),第二个参数是一个序列。

map()函数可以将传入的函数应用于序列中的每个元素,并将结果转换为一个新的列表返回。举个例子:

lst = [1, 2, 3, 4, 5]
result = list(map(lambda x: x*2, lst))
print(result)  # 输出结果 [2, 4, 6, 8, 10]

在这个例子中,我们使用map()函数和匿名函数lambda x: x*2对列表lst中的元素进行乘法计算,最终得到了一个新的列表[2,4,6,8,10]

2. filter函数

map()函数类似,filter()函数也是一个常用的高阶函数。它接收两个参数,第一个参数是一个函数对象(也可以是lambda表达式),第二个参数是一个序列。

filter()函数会对序列中的每个元素应用传入的函数,并将返回值为True的元素过滤出来,最终返回一个新的列表。下面是一个示例:

lst = [-2, -1, 0, 1, 2]
result = list(filter(lambda x: x < 0, lst))
print(result)  # 输出结果为 [-2, -1]

在这个例子中,我们定义了一个lambda表达式lambda x: x < 0,它接受一个参数x,并检查它是否小于0。我们将这个lambda表达式作为第一个参数传递给filter()函数,并将列表lst作为第二个参数传递给filter()函数。filter()函数会遍历序列中的每个元素,并将它们作为参数传递给lambda表达式中,当lambda表达式返回True时,对应的元素会被保留下来,最终返回一个新的列表[-2,-1]

3. reduce函数

reduce()函数也是Python中常用的高阶函数之一。它接收两个参数,第一个参数是一个函数对象(也可以是lambda表达式),第二个参数是一个序列。

reduce()函数会递归地对序列中的元素进行参与运算,最终得到一个单一的结果。具体来说,它首先将序列的前两个元素作为参数传递给函数进行计算,然后将计算结果作为新的参数传递给下一个元素,直到所有元素都参与了计算,最终得到一个单一的结果。下面是一个示例:

from functools import reduce

result = reduce(lambda x, y: x+y, [1, 2, 3, 4, 5])
print(result)  # 输出结果 15

在这个例子中,我们导入了functools模块,并使用reduce()函数和匿名函数lambda x, y: x+y对列表[1,2,3,4,5]中的元素进行加法累积运算,最终得到结果15。

4. 注意点

需要注意的是,在Python 2.x中,map()filter()函数会直接返回一个列表。而在Python 3.x中,它们返回的是一个迭代器对象,需要使用list()函数将其转换为列表。另外,在Python 3.x中,reduce()函数已被移至functools模块中,需要先导入才能使用。

from functools import reduce

5. zip函数

zip()函数也是Python中常用的高阶函数之一。它接收两个或多个序列作为参数,并将它们“压缩”成一个新的列表。

具体来说,zip()函数会从每个序列中取出对应位置的元素,将这些元素组成一个元组,并将这些元组放入一个新的列表中返回。如果传入的序列长度不同,则zip()函数只会“压缩”到最短的那个序列长度。下面是一个示例:

lst1 = [1, 2, 3]
lst2 = ['a', 'b', 'c']
result = list(zip(lst1, lst2))
print(result)  # 输出结果 [(1, 'a'), (2, 'b'), (3, 'c')]

在这个例子中,我们使用zip()函数将两个序列[1,2,3]['a','b','c']“压缩”成一个新的列表[(1,'a'),(2,'b'),(3,'c')]

6. sorted函数

sorted()函数是Python中常用的高阶函数之一。它可以对可迭代对象进行排序(默认升序),并返回一个新的列表。

sorted()函数接收一个可迭代对象作为参数,并返回一个已排序的新列表。如果需要按照特定的规则进行排序,则可以传递一个函数对象作为key参数。下面是一个示例:

lst = [3, 1, 4, 2, 5]
result = sorted(lst)
print(result)  # 输出结果 [1, 2, 3, 4, 5]

在这个例子中,我们使用sorted()函数对列表[3,1,4,2,5]进行升序排序,最终得到一个新的列表[1,2,3,4,5]

另外,如果需要按照特定的规则进行排序,则可以传递一个函数对象作为key参数。例如,下面的示例使用了一个lambda表达式作为key参数,实现了对字符串长度进行降序排序:

lst = ['apple', 'banana', 'cherry', 'date']
result = sorted(lst, key=lambda x: len(x), reverse=True)
print(result)  # 输出结果 ['banana', 'cherry', 'apple', 'date']

在这个示例中,我们使用了一个lambda表达式lambda x: len(x)作为key参数,它表示按照字符串长度来排序。由于需要降序排列,所以还将reverse参数设置为True。最终得到的输出结果为['banana','cherry','apple','date']

7. 总结

高阶函数是Python中非常重要的概念之一,掌握了高阶函数,可以让我们的代码更加简洁、高效。本文介绍了Python中常用的五个高阶函数:map()filter()reduce()zip()sorted(),包括它们的用法、示例以及注意点。希望能对大家学习Python有所帮助!

5.5 【基础】反射函数的使用

反射函数的使用

在 Python 中,反射是一种可以让程序在运行时动态地获取、检查和修改对象状态和行为的能力。Python 提供了多个反射函数,用于实现不同的功能:

  • dir(obj): 返回一个列表,列出了对象 obj 所包含的属性和方法名。
  • type(obj): 返回对象 obj 的类型。
  • id(obj): 返回对象 obj 在内存中的唯一标识符。
  • getattr(obj, name[, default]): 获取对象 obj 中名字为 name 的属性的值。如果不存在,则返回 default(默认为 None)。
  • setattr(obj, name, value): 设置对象 obj 中名字为 name 的属性的值为 value。
  • hasattr(obj, name): 检查对象 obj 中是否存在名字为 name 的属性或方法。

其中,dir() 函数可以用来查看模块、类或实例对象中包含的属性和方法,进而根据需要调用它们;hasattr() 函数可以用来检查对象是否具有某个属性或方法;getattr() 函数可以用来获取对象中的属性或方法的值,以便进行后续操作。

1. 学习 Python 模块的入口

help()

help() 函数可以用来帮助开发者了解 Python 中某个模块或对象的详细信息。在 console 模式下,输入 help() 命令即可进入帮助模式。在该模式下,通过输入需要查询的模块或函数名,可以查看相应的文档和用法说明。使用这个函数可以快速了解 Python 库和常用函数的使用方法,对于学习和开发 Python 程序非常有帮助。

dir()

dir() 函数可以用来列出一个对象中包含的属性和方法名,以便开发者更好地了解该对象的结构和属性。在 Python 中,我们可以使用 dir() 函数来查看模块、类或实例对象中包含的属性和方法,并根据需要调用它们。

2. 应用到实际开发中

type()

type() 函数有助于我们确定对象是字符串还是整数,或是其它类型的对象。它通过返回类型对象来做到这一点,可以将这个类型对象与 types 模块中定义的类型相比较:

print(type(42))
print(type([]))
#输出结果
<class 'int'>
<class 'list'>

hasattr()

使用 dir() 函数会返回一个对象的属性列表。

但是,有时我们只想测试一个或多个属性是否存在。如果对象具有我们正在考虑的属性,那么通常希望只检索该属性。这个任务可以由 hasattr() 来完成.

import json

print(hasattr(json, "dumps"))
#输出结果
True

getattr()

使用 hasattr 获知了对象拥有某个属性后,可以搭配 getattr() 函数来获取其属性值。

import json

path = getattr(json, "__path__", None)
print(path)


#输出结果
['/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/json']

使用 getattr 获取函数后,可以很方便地使用这个函数,比如下面这样,可以不再使写 json.dumps 这么字。

import json

dumps = getattr(json, "dumps")
print(dumps({"name": "MING"}))

# 当然你还有更简单的方法
mydumps = json.dumps
print(mydumps({"name": "MING"}))

id()

id() 函数返回对象的唯一标识符,标识符是一个整数。

a = "hello"
b = "world"

print(id(a))  # 输出:4463599728
print(id(b))  # 输出:4463602064
#这段代码展示了使用内置函数 id() 来获取变量在内存中的地址。

注意,由于字符串是不可变对象,所以对它们的任何修改都会创建一个新的对象。因此,在下面的代码中,ab 引用的是不同的字符串对象:

a = "hello"  # 定义字符串变量a,值为"hello"
a += " world"  # 将" world"拼接到变量a中,即变量a的值为"hello world"
b = "hello world"  # 定义字符串变量b,值为"hello world"

print(id(a))  # 输出变量a的内存地址,因为字符串是不可变类型,所以每次修改字符串会新开辟一个内存空间,输出:4463600688
print(id(b))  # 输出变量b的内存地址,因为变量b的值与a相同,所以其内存地址也与a相同,输出:4463600688

isinstance()

使用 isinstance() 函数可以确定一个对象是否是某个特定类型或定制类的实例。

# 使用 isinstance() 函数判断对象类型
print(isinstance("python", str))   # 输出 True
print(isinstance(10, int))         # 输出 True
print(isinstance(False, bool))     # 输出 True

callable()

使用 callable 可以确定一个对象是否是可调用的,比如函数,类这些对象都是可以调用的对象。

result1 = callable("hello")
result2 = callable(str)

print(result1)  # 输出:False
print(result2)  # 输出:True

3. 模块(Modules)

_ _ doc _ _

使用 __doc__ 这个魔法方法,可以查询该模块的文档,它输出的内容和 help() 一样。
在这里插入图片描述

_ _ _name ___

始终是定义时的模块名;即使你使用import … as 为它取了别名,或是赋值给了另一个变量名。

import json
print(json.__name__)  # 输出:json

import json as js
print(js.__name__)  # 输出:json

_ _ file _ _

包含了该模块的文件路径。需要注意的是内建的模块没有这个属性,访问它会抛出异常!

import json
print(json.__file__)
#输出结果
/usr/lib/python3.9/json/__init__.py

__ dict _ _

包含了模块里可用的属性名-属性的字典;也就是可以使用模块名.属性名访问的对象。

4. 类(Class)

_ _ doc _ _

文档字符串。如果类没有文档,这个值是None。

class People:
    '''
    people class
    '''

p = People()

print(p.__doc__)


#输出结果
    people class

__ name _ _

始终是定义时的类名。

class People:
    '''
    people class
    '''

p = People()
print(People.__name__)
#输出结果
People

_ _ dict _ _

包含了类里可用的属性名-属性的字典;也就是可以使用类名.属性名访问的对象。

class People:
    '''
    people class
    '''

p = People()

print(People.__dict__)
#输出结果
{'__module__': '__main__', '__doc__': '\n    people class\n    ', '__dict__': <attribute '__dict__' of 'People' objects>, '__weakref__': <attribute '__weakref__' of 'People' objects>}

_ _ module _ _

包含该类的定义的模块名;需要注意,是字符串形式的模块名而不是模块对象。

由于我是在 交互式命令行的环境下,所以模块是 __main__

>>> People.__module__
'__main__'

如果将上面的代码放入 demo.py,并且从 people 模块导入 People 类,其值就是 people 模块

在这里插入图片描述

___ bases_ _ _

直接父类对象的元组;但不包含继承树更上层的其他类,比如父类的父类。

class People: pass

class Teenager: pass

class Student(Teenager): pass

print(Student.__bases__)  # 输出:(<class '__main__.Teenager'>,)

5.6 【基础】偏函数的妙用

假如一个函数定义了多个位置参数,那你每次调用时,都需要把这些个参数一个一个地传递进去。

比如下面这个函数,是用来计算 x的n次方 的。

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

那我每次计算 x 的 n 次方,都要传递两个参数

>>> power(2, 2)
4
>>> power(3, 2)
9

后来我发现,我很多时候都是计算平方值,很多会去计算三次方,四次方。

那有什么办法可以偷个懒吗?

答案是,有。可以使用 偏函数

偏函数(Partial Function),可以将某个函数的常用参数进行固定,避免每次调用时都要指定。

使用偏函数,需要导入 functools.partial ,然后利用它创建一个新函数,新函数的 n 固定等2。

具体使用请看下面的示例

>>> from functools import partial
>>> power_2=partial(power, n=2)
>>> power_2(2)
4
>>> power_2(3)
9

5.7 【进阶】泛型函数的使用

根据传入参数类型的不同而调用不同的函数逻辑体,这种实现我们称之为泛型。在 Python 中叫做 singledispatch

singledispatch 是 PEP443 中引入的,如果你对此有兴趣,PEP443 应该是最好的学习文档:https://www.python.org/dev/peps/pep-0443/

它使用方法极其简单,只要被singledispatch 装饰的函数,就是一个single-dispatch 的泛函数(generic functions)。

  • 单分派:根据一个参数的类型,以不同方式执行相同的操作的行为。
  • 多分派:可根据多个参数的类型选择专门的函数的行为。
  • 泛函数:多个函数绑在一起组合成一个泛函数。

这边举个简单的例子。

from functools import singledispatch

@singledispatch
def age(obj):
    print('请传入合法类型的参数!')

@age.register(int)
def _(age):
    print('我已经{}岁了。'.format(age))

@age.register(str)
def _(age):
    print('I am {} years old.'.format(age))


age(23)  # int
age('twenty three')  # str
age(['23'])  # list

执行结果

我已经23岁了。
I am twenty three years old.
请传入合法类型的参数!

说起泛型,其实在 Python 本身的一些内建函数中并不少见,比如 len()iter()copy.copy()pprint()

你可能会问,它有什么用呢?实际上真没什么用,你不用它或者不认识它也完全不影响你编码。

我这里举个例子,你可以感受一下。

大家都知道,Python 中有许许多的数据类型,比如 str,list, dict, tuple 等,不同数据类型的拼接方式各不相同,所以我这里我写了一个通用的函数,可以根据对应的数据类型对选择对应的拼接方式拼接,而且不同数据类型我还应该提示无法拼接。以下是简单的实现。

def check_type(func):
    def wrapper(*args):
        arg1, arg2 = args[:2]
        if type(arg1) != type(arg2):
            return '【错误】:参数类型不同,无法拼接!!'
        return func(*args)
    return wrapper


@singledispatch
def add(obj, new_obj):
    raise TypeError

@add.register(str)
@check_type
def _(obj, new_obj):
    obj += new_obj
    return obj


@add.register(list)
@check_type
def _(obj, new_obj):
    obj.extend(new_obj)
    return obj

@add.register(dict)
@check_type
def _(obj, new_obj):
    obj.update(new_obj)
    return obj

@add.register(tuple)
@check_type
def _(obj, new_obj):
    return (*obj, *new_obj)

print(add('hello',', world'))
print(add([1,2,3], [4,5,6]))
print(add({'name': 'wangbm'}, {'age':25}))
print(add(('apple', 'huawei'), ('vivo', 'oppo')))

# list 和 字符串 无法拼接
print(add([1,2,3], '4,5,6'))

输出结果如下

hello, world
[1, 2, 3, 4, 5, 6]
{'name': 'wangbm', 'age': 25}
('apple', 'huawei', 'vivo', 'oppo')
【错误】:参数类型不同,无法拼接!!

如果不使用singledispatch 的话,你可能会写出这样的代码。

def check_type(func):
    def wrapper(*args):
        arg1, arg2 = args[:2]
        if type(arg1) != type(arg2):
            return '【错误】:参数类型不同,无法拼接!!'
        return func(*args)
    return wrapper

@check_type
def add(obj, new_obj):
    if isinstance(obj, str) :
        obj += new_obj
        return obj

    if isinstance(obj, list) :
        obj.extend(new_obj)
        return obj

    if isinstance(obj, dict) :
        obj.update(new_obj)
        return obj

    if isinstance(obj, tuple) :
        return (*obj, *new_obj)

print(add('hello',', world'))
print(add([1,2,3], [4,5,6]))
print(add({'name': 'wangbm'}, {'age':25}))
print(add(('apple', 'huawei'), ('vivo', 'oppo')))

# list 和 字符串 无法拼接
print(add([1,2,3], '4,5,6'))

输出如下

hello, world
[1, 2, 3, 4, 5, 6]
{'name': 'wangbm', 'age': 25}
('apple', 'huawei', 'vivo', 'oppo')
【错误】:参数类型不同,无法拼接!!

5.8 【基础】变量的作用域

1. 作用域

Python 中变量的作用域主要有四种:

  1. Local scope(本地作用域):变量在函数内部定义,仅在该函数内部可用。例如:

    def my_func():
        x = 1  # 局部变量 x
        print(x)
    
    my_func()  # 输出:1
    print(x)   # NameError: name 'x' is not defined
    
    
  2. Enclosing scope(嵌套作用域):变量在函数内部的函数(嵌套函数)中定义,外部函数无法访问。例如:

    def outer():
        x = 1
        
        def inner():
            y = 2
            print(x, y)  # 在 inner 函数内可以访问 x 和 y
        
        inner()
        print(x, y)  # NameError: name 'y' is not defined,无法在 outer 函数内访问 y
    
    outer()
    
    
  3. Global scope(全局作用域):变量在函数外部定义,整个程序都可以访问。例如:

    x = 1  # 全局变量 x
    
    def my_func():
        print(x)
    
    my_func()  # 输出:1
    print(x)   # 输出:1
    
    
  4. Built-in scope(内置作用域):变量是 Python 内置函数或模块中定义的,例如 printlen 函数。这些变量可以在任何地方访问。例如:

    print(len("hello"))  # 输出:5
    
    
    

Python中的变量作用域由其定义的位置和使用的位置决定。在一个函数内部,如果需要使用外部作用域的变量,可以使用global关键字声明,如果需要修改外部作用域的变量,可以使用nonlocal关键字声明。

变量/函数 的查找顺序: L –> E –> G –>B

当查找变量或函数时,Python 会首先在局部作用域内查找,如果找到则返回对应的值,如果没有找到则继续在嵌套作用域中查找,直到找到为止。如果还没有找到,则继续在全局作用域中查找,最后在内置作用域中查找,如果还没有找到,则会抛出 NameError 异常。

会影响 变量/函数 作用范围的有

  • 函数:def 或 lambda
  • 类:class
  • 关键字:global noglobal
  • 文件:*py
  • 推导式:[],{},()等,仅限Py3.x中,Py2.x会出现变量泄露。

1、赋值在前,引用在后

# ------同作用域内------
name = "MING"
print(name)

# ------不同作用域内------
name = "MING"
def main():
    print(name)

2、引用在前,赋值在后(同一作用域内)

print(name)
name = "MING"

# UnboundLocalError: local variable 'name' referenced before assignment

3、赋值在低层,引用在高层

# L -> E -> G -> B
# 从左到右,由低层到高层
def main():
    name = "MING"

print(name)
# NameError: name 'name' is not defined

2. 闭包

闭包这个概念很重要噢。你一定要掌握。

在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用。这样就构成了一个闭包。其实装饰函数,很多都是闭包。

好像并不难理解,为什么初学者会觉得闭包难以理解呢?

我解释一下,你就明白了。

一般情况下,在我们认知当中,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。

你可以看下面这段代码,就构成了闭包。在内函数里可以引用外函数的变量。

def deco():
    name = "MING"
    def wrapper():
        print(name)
    return wrapper

deco()()
# 输出:MING

3. 改变作用域

变量的作用域,与其定义(或赋值)的位置有关,但不是绝对相关。 因为我们可以在某种程度上去改变向上的作用范围。

  • 关键字:global 将 局部变量 变为全局变量
  • 关键字:nonlocal 可以在闭包函数中,引用并使用闭包外部函数的变量(非全局的噢)

global好理解,这里只讲下nonlocal。

先来看个例子

def deco():
    age = 10
    def wrapper():
        age += 1
    return wrapper

deco()()

运行一下,会报错。

# UnboundLocalError: local variable 'age' referenced before assignment

但是这样就OK

def deco():
    age = 10
    def wrapper():
        nonlocal age
        age += 1
    return wrapper

deco()()
# 输出:11

其实,你如果不使用 +=-=等一类的操作,不加nonlocal也没有关系。这就展示了闭包的特性。

def deco():
    age = 10
    def wrapper():
        print(age)
    return wrapper

deco()()
# 输出:10

4. 变量集合

在Python中,有两个内建函数,你可能用不到,但是需要掌握它们。

  • globals() :以dict的方式存储所有全局变量
  • locals():以dict的方式存储所有局部变量

globals()

def foo():
    print("I am a func")

def bar():
    foo="I am a string"
    foo_dup = globals().get("foo")
    foo_dup()

bar()
# 输出
# I am a func

locals()

other = "test"

def foobar():
    name = "MING"
    gender = "male"
    for key,value in locals().items():
        print(key, "=", value)

foobar()
# 输出
# name = MING
# gender = male

5.9 【进阶】文件操作(I/O)

我们在前面的学习中,不管是学习哪种数据类型的操作,当我们在程序测试的时候使用的数据都没有进行保存,如果我们要统计分析数据的相关性,那么我们就需要将数据保存到本地文件中,在Python中提供了访问文件、访问目录、读取文件和写入文件的操作,Python的文件对象也被称为类似文件对象或者流,本节我们先进行文件操作的相关学习。

1. python文件读写

在 Python中,如果想要操作文件,首先需要创建或者打开指定的文件,并创建一个文件对象,而这些工作可以通过内置的 open() 函数实现。

open() 函数用于创建或打开指定文件,该函数的常用语法格式如下:

file = open(file_name [, mode='r' [ , buffering=-1 [ , encoding = None ]]])

此格式中,用 [] 括起来的部分为可选参数,即可以使用也可以省略。其中,各个参数所代表的含义如下:

  • file:表示要创建的文件对象。
  • file_name:要创建或打开文件的文件名称,该名称要用引号(单引号或双引号都可以)括起来。需要注意的是,如果要打开的文件和当前执行的代码文件位于同一目录,则直接写文件名即可;否则,此参数需要指定打开文件所在的完整路径。
  • mode:可选参数,用于指定文件的打开模式。可选的打开模式如表 1 所示。如果不写,则默认以只读(r)模式打开文件。
  • buffering:可选参数,用于指定对文件做读写操作时,是否使用缓冲区(本节后续会详细介绍)。
  • encoding:手动设定打开文件时所使用的编码格式,不同平台的 ecoding 参数值也不同,以 Windows 为例,其默认为 cp936(实际上就是 GBK 编码)。

open 函数支持的文件打开模式

访问模式说明注意事项
r只读模式打开文件,读文件内容的指针会放在文件的开头。操作的文件必须存在。
rb以二进制格式、采用只读模式打开文件,读文件内容的指针位于文件的开头,一般用于非文本文件,如图片文件、音频文件等。
r+打开文件后,既可以从头读取文件内容,也可以从开头向文件中写入新的内容,写入的新内容会覆盖文件中等长度的原有内容。
rb+以二进制格式、采用读写模式打开文件,读写文件的指针会放在文件的开头,通常针对非文本文件(如音频文件)。
w以只写模式打开文件,若该文件存在,打开时会清空文件中原有的内容。若文件存在,会清空其原有内容(覆盖文件);反之,则创建新文件。
wb以二进制格式、只写模式打开文件,一般用于非文本文件(如音频文件)
w+打开文件后,会对原有内容进行清空,并对该文件有读写权限。
wb+以二进制格式、读写模式打开文件,一般用于非文本文件
a以追加模式打开一个文件,对文件只有写入权限,如果文件已经存在,文件指针将放在文件的末尾(即新写入内容会位于已有内容之后);反之,则会创建新文件。
ab以二进制格式打开文件,并采用追加模式,对文件只有写权限。如果该文件已存在,文件指针位于文件末尾(新写入文件会位于已有内容之后);反之,则创建新文件。
a+以读写模式打开文件;如果文件存在,文件指针放在文件的末尾(新写入文件会位于已有内容之后);反之,则创建新文件。
ab+以二进制模式打开文件,并采用追加模式,对文件具有读写权限,如果文件存在,则文件指针位于文件的末尾(新写入文件会位于已有内容之后);反之,则创建新文件。

1.1打开文件

f = open('学生信息.txt', 'r')

1.2读取文件内容

1.2.1读写指定字符串

使用**file.readline()**方法。

代码如下:

file = open('test.txt','w')
file.write('第一次写入的内容。')
file = open('test.txt','a+')
file.write('第二次追加写入的内容。')
print(file.read(8))
file.close()

输出结果为:

第一次写入的内容

文件中的内容为:

第一次写入的内容。第二次追加写入的内容。

需要注意的是,我们在进行读的时候,一定要保证当前文件是打开的状态,如果我们写完了信息就把文件关闭了,那么我们将读取不到信息而且会出现异常,异常如下:

Traceback (most recent call last):
  File "C:/Users/test.py", line 6, in <module>
    print(file.read(8))
ValueError: I/O operation on closed file.
1.2.2读取一行内容

使用**file.readline()**方法。

代码如下:

file = open('test.txt','w')
file.write('第一次写入的内容。')
file = open('test.txt','a+')
file.write('\n')
file.write('第二次追加写入的内容。')
print(file.readline())
file.close()

输出结果为:

第一次写入的内容。

文件中内容为:

第一次写入的内容。第二次追加写入的内容。

这种读取方式每次仅仅读取一行,对于内容过多的文件可以采用这种方式去逐行读取。

1.2.3 按行全部读取

使用**file.readlines()**方法。

代码如下:

file = open('test.txt','w')
file.write('第一次写入的内容。')
file = open('test.txt','a+')
file.write('\n')
file.write('第二次追加写入的内容。')
print(file.readlines())
file.close()
file = open('test.txt','r')
print(file.readlines())
file.close()

输出结果为:

['第一次写入的内容。\n', '第二次追加写入的内容。']

文件内容为:

第一次写入的内容。``第二次追加写入的内容。

使用这种方式的时候我们需要注意读取的时候采用的模式为r或者r+,如果是一个已经存在的文件,我们可以直接进行读取,如果是我们刚刚完成写入的一个文件,大家可以先关闭,再采用r格式进行读取。

1.2.4 总结

三种读取方式各有千秋,大家还可以通过file.seel(index)的方式访问下标去读取,还可以通过循环文件进行高效的文件读取。

1.2.5 使用迭代

open返回的文件句柄,是一个可迭代对象,因此,你可以像迭代列表一样去迭代一个文件

f = open('学生信息.txt', 'r')
for line in f:
    print(line)
1.2.6关闭文件
f = open('学生信息.txt', 'r')
for line in f:
    print(line)

f.close()

为了避免忘记close文件,还可以使用with语法,该语法可以保证在with语句块退出时可以自动关闭文件

with open('学生信息.txt', 'r') as f:
    for line in f:
        print(line)

1.3写文件

写文件,要使用w模式, 写入数据,使用write方法,需要注意的是,write并不自动写入换行,因此需要你自己添加换行

with open('test', 'w') as f:
    for i in range(10):
        f.write(str(i) + "\n")

2.读写csv文件

在这里插入图片描述

csv是一种文本文件,因此python可以像读取文本文件一样读取csv文件,但通常我们使用专门的csv模块来读取csv文件,操作起来会更加的便利。

2.1读取csv文件

准备数据data.csv

name,age
小明,14
小刚,15

通常,我们用下面的代码读取csv

import csv

with open(r'C:\Users\zhangdongsheng\Desktop\data.csv', encoding='utf-8')as f:
    reader = csv.reader(f)
    headers = next(reader)
    print(headers)
    for row in reader:
        print(row)

程序输出结果

['name', 'age']
['小明', '14']
['小刚', '15']

我们完全可以像读取txt文件那样去读取csv文件,但那样读取到的数据一行就是一个字符串,还需你自己进行一次split操作才能将数据分隔开,因此我建议你使用csv模块读取csv文件。

上面的读取方法,有一个让人感到难受的地方,每一行数据虽然都是以列表的形式返回,可如果你想获取一行中的某一列数据,就只能通过列表的索引,这样并不方便。

针对这个需求,可以使用namedtuple

import csv
from collections import namedtuple

with open(r'C:\Users\zhangdongsheng\Desktop\data.csv', encoding='utf-8')as f:
    reader = csv.reader(f)
    headers = next(reader)
    Row = namedtuple('Row', headers)
    for row in reader:
        row = Row(*row)
        print(row.name, row.age)

这样可以非常方便的获取一行数据中的某一列数据。

2.2写csv文件

2.2.1普通方法写csv文件

用csv模块写csv文件,主要用到writerow和writerows这两个方法,前者是写入一行,后者是写入多行。

import csv

headers = ['name', 'age']
row_1 = ['小明', '14']
row_2 = ['小刚', '15']

with open("data.csv", "w", encoding='utf-8', newline='') as csvfile:
    writer = csv.writer(csvfile)
    writer.writerow(headers)
    writer.writerows([row_1, row_2])

在打开文件时,一定要设置encoding=‘utf-8’ 否则中文无法正常显示,另外要设置newline=‘’,否则就会在两行数据之间间隔一个空行。

csv文件是文本文件,因此你完全可以抛开csv模块,单纯的用写文本文件的方法来写csv文件

headers = ['name', 'age']
row_1 = ['小明', '14']
row_2 = ['小刚', '15']

with open("data.csv", "w", encoding='utf-8') as csvfile:
    csvfile.write(','.join(headers) + "\n")
    csvfile.write(','.join(row_1) + "\n")
    csvfile.write(','.join(row_2))
2.2.2用pandas写csv文件

使用pandas写csv文件需要先创建dataframe对象

import pandas as pd

headers = ['name', 'age']
row_1 = ['小明', '14']
row_2 = ['小刚', '15']

df = pd.DataFrame([row_1, row_2], columns=headers)
df.to_csv("data.csv", index=False, sep=',')

效果与csv模块写文件是一样的

3.文件的绝对路径与相对路径

当我们在Python中打开文件时,需要指定文件的路径。Python中文件路径共分为两种:绝对路径和相对路径。

绝对路径就是文件的真正存在的路径,是指从硬盘的根目录 (盘符)开始,进行一级级目录指向文件。

相对路径是以当前文件为基准进行一级级目录指向被引用的资源文件。

3.1以Pycharm为例:绝对路径和相对路径的调用及使用

在这里插入图片描述

在这里插入图片描述

如上图来自内容根的路径就是相对路径,绝对路径亦然,此外还有快捷键ctrl+shift+c。

假设存在如下图的文件架构,需要导入在h2文件夹的某个文件,其用法如下:

在这里插入图片描述

目标:希望在Main.py中导入h2文件夹内的Param_Pred.npy文件,具体代码示例如下:

import numpy as np

np.load(r"h1/h2/Param_Pred.npy")

#如果是绝对路径,应如下:
np.load(r"C:\Users\XXX\PycharmProjects\ZS_DeepLearning\h1\h2\Param_Pred.npy")

5.10 【进阶】上下文管理器

当你准备从一个文件中读取内容时,通常来说,都是这么写的。

>>> file=open('test.txt')
>>>
>>> print(file.readlines())  # 读取并打印
['hello,python\n']
>>>
>>> file.close()  # 关闭文件句柄

上面这种方法,需要你手动关闭文件句柄,但是很多时候,程序员是会忘记这一操作的。

因为推荐你使用下面这种方法,使用 with 这个关键字,可以在文件读取结束后,自动关闭文件句柄。

with open('test.txt') as file:
    print(file.readlines())

使用 Python 的专业术语来说,with 的这个用法叫做 上下文管理器

1. 什么是上下文管理器?

基本语法

with EXPR as VAR:
    代码块

从上面这个语法中,先理清几个概念:

  1. 上下文表达式:with open('test.txt') as file:
  2. 上下文管理器:open('test.txt')
  3. file 不是上下文管理器,应该是资源对象。

2. 如何写上下文管理器?

要手动实现一个上下文管理器,需要你有对类有一些了解,至少需要知道什么是类,怎么定义类。对于类的知识

学习了类的基本知识,想要自己实现这样一个上下文管理,就简单了。

你只要在一个类里实现上下文管理协议,简单点说,就是在一个类里,定义了__enter____exit__的方法,这个类的实例就是一个上下文管理器。

例如这个示例:

class Resource():
    def __enter__(self):
        print('===connect to resource===')
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('===close resource connection===')

    def operate(self):
        print('===in operation===')

with Resource() as res:
    res.operate()

我们执行一下,通过日志的打印顺序。可以知道其执行过程。

===connect to resource===
===in operation===
===close resource connection===

从这个示例可以很明显的看出,在编写代码时,可以将资源的连接或者获取放在__enter__中,而将资源的关闭写在__exit__ 中。

3. 为什么需要上下文管理器?

学习时多问自己几个为什么,养成对一些细节的思考,有助于加深对知识点的理解。

为什么要使用上下文管理器?

在我看来,这和 Python 崇尚的优雅风格有关。

  1. 可以以一种更加优雅的方式,操作(创建/获取/释放)资源,如文件操作、数据库连接;
  2. 可以以一种更加优雅的方式,处理异常;

第一种,我们上面已经以资源的连接为例讲过了。

而第二种,会被大多数人所忽略。这里会重点讲一下。

大家都知道,处理异常,通常都是使用 try...execept.. 来捕获处理的。这样做一个不好的地方是,在代码的主逻辑里,会有大量的异常处理代理,这会很大的影响我们的可读性。

好一点的做法呢,可以使用 with 将异常的处理隐藏起来。

仍然是以上面的代码为例,我们将1/0 这个一定会抛出异常的代码写在 operate

class Resource():
    def __enter__(self):
        print('===connect to resource===')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('===close resource connection===')
        return True

    def operate(self):
        1/0

with Resource() as res:
    res.operate()

运行一下,惊奇地发现,居然不会报错。

这就是上下文管理协议的一个强大之处,异常可以在__exit__ 进行捕获并由你自己决定如何处理,是抛出呢还是在这里就解决了。在__exit__ 里返回 True(没有return 就默认为 return False),就相当于告诉 Python解释器,这个异常我们已经捕获了,不需要再往外抛了。

在 写__exit__ 函数时,需要注意的事,它必须要有这三个参数:

  • exc_type:异常类型
  • exc_val:异常值
  • exc_tb:异常的错误栈信息

当主逻辑代码没有报异常时,这三个参数将都为None。

4. 学会使用 contextlib

在上面的例子中,我们只是为了构建一个上下文管理器,却写了一个类。如果只是要实现一个简单的功能,写一个类未免有点过于繁杂。这时候,我们就想,如果只写一个函数就可以实现上下文管理器就好了。

这个点Python早就想到了。它给我们提供了一个装饰器,你只要按照它的代码协议来实现函数内容,就可以将这个函数对象变成一个上下文管理器。

我们按照 contextlib 的协议来自己实现一个打开文件(with open)的上下文管理器。

import contextlib

@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')

    # 【重点】:yield
    yield file_handler

    # __exit__方法
    print('close file:', file_name, 'in __exit__')
    file_handler.close()
    return

with open_func('/Users/MING/mytest.txt') as file_in:
    for line in file_in:
        print(line)

在被装饰函数里,必须是一个生成器(带有yield),而yield之前的代码,就相当于__enter__里的内容。yield 之后的代码,就相当于__exit__ 里的内容。

上面这段代码只能实现上下文管理器的第一个目的(管理资源),并不能实现第二个目的(处理异常)。

如果要处理异常,可以改成下面这个样子。

import contextlib

@contextlib.contextmanager
def open_func(file_name):
    # __enter__方法
    print('open file:', file_name, 'in __enter__')
    file_handler = open(file_name, 'r')

    try:
        yield file_handler
    except Exception as exc:
        # deal with exception
        print('the exception was thrown')
    finally:
        print('close file:', file_name, 'in __exit__')
        file_handler.close()

        return

with open_func('/Users/MING/mytest.txt') as file_in:
    for line in file_in:
        1/0
        print(line)

好像只要讲到上下文管理器,大多数人都会谈到打开文件这个经典的例子。

但是在实际开发中,可以使用到上下文管理器的例子也不少。我这边举个我自己的例子。

在OpenStack中,给一个虚拟机创建快照时,需要先创建一个临时文件夹,来存放这个本地快照镜像,等到本地快照镜像创建完成后,再将这个镜像上传到Glance。然后删除这个临时目录。

这段代码的主逻辑是创建快照,而创建临时目录,属于前置条件,删除临时目录,是收尾工作。

虽然代码量很少,逻辑也不复杂,但是“创建临时目录,使用完后再删除临时目录”这个功能,在一个项目中很多地方都需要用到,如果可以将这段逻辑处理写成一个工具函数作为一个上下文管理器,那代码的复用率也大大提高。

代码是这样的

在这里插入图片描述

5. 总结起来

使用上下文管理器有三个好处:

  1. 提高代码的复用率;
  2. 提高代码的优雅度;
  3. 提高代码的可读性;

5.11 【进阶】装饰器的六种写法

Hello,装饰器

装饰器本质上是一个Python函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。

它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。

装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

装饰器的使用方法很固定

  • 先定义一个装饰器(帽子)
  • 再定义你的业务函数或者类(人)
  • 最后把这装饰器(帽子)扣在这个函数(人)头上

就像下面这样子

# 定义装饰器
def decorator(func):
    def wrapper(*args, **kw):
        return func()
    return wrapper

# 定义业务函数并进行装饰
@decorator
def function():
    print("hello, decorator")

实际上,装饰器并不是编码必须性,意思就是说,你不使用装饰器完全可以,它的出现,应该是使我们的代码

  • 更加优雅,代码结构更加清晰
  • 将实现特定的功能代码封装成装饰器,提高代码复用率,增强代码可读性

接下来,我将以实例讲解,如何编写出各种简单及复杂的装饰器。

第一种:普通装饰器

首先咱来写一个最普通的装饰器,它实现的功能是:

  • 在函数执行前,先记录一行日志
  • 在函数执行完,再记录一行日志
# 这是装饰器函数,参数 func 是被装饰的函数
def logger(func):
    def wrapper(*args, **kw):
        print('我准备开始执行:{} 函数了:'.format(func.__name__))

        # 真正执行的是这行。
        func(*args, **kw)

        print('主人,我执行完啦。')
    return wrapper

假如,我的业务函数是,计算两个数之和。写好后,直接给它带上帽子。

@logger
def add(x, y):
    print('{} + {} = {}'.format(x, y, x+y))

然后执行一下 add 函数。

add(200, 50)

来看看输出了什么?

我准备开始执行:add 函数了:
200 + 50 = 250
我执行完啦。

第二种:带参数的函数装饰器

通过上面两个简单的入门示例,你应该能体会到装饰器的工作原理了。

不过,装饰器的用法还远不止如此,深究下去,还大有文章。今天就一起来把这个知识点学透。

回过头去看看上面的例子,装饰器是不能接收参数的。其用法,只能适用于一些简单的场景。不传参的装饰器,只能对被装饰函数,执行固定逻辑。

装饰器本身是一个函数,做为一个函数,如果不能传参,那这个函数的功能就会很受限,只能执行固定的逻辑。这意味着,如果装饰器的逻辑代码的执行需要根据不同场景进行调整,若不能传参的话,我们就要写两个装饰器,这显然是不合理的。

比如我们要实现一个可以定时发送邮件的任务(一分钟发送一封),定时进行时间同步的任务(一天同步一次),就可以自己实现一个 periodic_task (定时任务)的装饰器,这个装饰器可以接收一个时间间隔的参数,间隔多长时间执行一次任务。

可以这样像下面这样写,由于这个功能代码比较复杂,不利于学习,这里就不贴了。

@periodic_task(spacing=60)
def send_mail():
     pass

@periodic_task(spacing=86400)
def ntp()
    pass

那我们来自己创造一个伪场景,可以在装饰器里传入一个参数,指明国籍,并在函数执行前,用自己国家的母语打一个招呼。

# 小明,中国人
@say_hello("china")
def xiaoming():
    pass

# jack,美国人
@say_hello("america")
def jack():
    pass

那我们如果实现这个装饰器,让其可以实现 传参 呢?

会比较复杂,需要两层嵌套。

def say_hello(contry):
    def wrapper(func):
        def deco(*args, **kwargs):
            if contry == "china":
                print("你好!")
            elif contry == "america":
                print('hello.')
            else:
                return

            # 真正执行函数的地方
            func(*args, **kwargs)
        return deco
    return wrapper

来执行一下

xiaoming()
print("------------")
jack()

看看输出结果。

你好!
------------
hello.

第三种:不带参数的类装饰器

以上都是基于函数实现的装饰器,在阅读别人代码时,还可以时常发现还有基于类实现的装饰器。

基于类装饰器的实现,必须实现 __call____init__两个内置函数。 __init__ :接收被装饰函数 __call__ :实现装饰逻辑。

还是以日志打印这个简单的例子为例

class logger(object):
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("[INFO]: the function {func}() is running..."\
            .format(func=self.func.__name__))
        return self.func(*args, **kwargs)

@logger
def say(something):
    print("say {}!".format(something))

say("hello")

执行一下,看看输出

[INFO]: the function say() is running...
say hello!

第四种:带参数的类装饰器

上面不带参数的例子,你发现没有,只能打印INFO级别的日志,正常情况下,我们还需要打印DEBUG WARNING等级别的日志。 这就需要给类装饰器传入参数,给这个函数指定级别了。

带参数和不带参数的类装饰器有很大的不同。

__init__ :不再接收被装饰函数,而是接收传入参数。 __call__ :接收被装饰函数,实现装饰逻辑。

class logger(object):
    def __init__(self, level='INFO'):
        self.level = level

    def __call__(self, func): # 接受函数
        def wrapper(*args, **kwargs):
            print("[{level}]: the function {func}() is running..."\
                .format(level=self.level, func=func.__name__))
            func(*args, **kwargs)
        return wrapper  #返回函数

@logger(level='WARNING')
def say(something):
    print("say {}!".format(something))

say("hello")

我们指定WARNING级别,运行一下,来看看输出。

[WARNING]: the function say() is running...
say hello!

第五种:使用偏函数与类实现装饰器

绝大多数装饰器都是基于函数和闭包实现的,但这并非制造装饰器的唯一方式。

事实上,Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象

对于这个 callable 对象,我们最熟悉的就是函数了。

除函数之外,类也可以是 callable 对象,只要实现了__call__ 函数(上面几个例子已经接触过了)。

还有容易被人忽略的偏函数其实也是 callable 对象。

接下来就来说说,如何使用 类和偏函数结合实现一个与众不同的装饰器。

如下所示,DelayFunc 是一个实现了 __call__ 的类,delay 返回一个偏函数,在这里 delay 就可以做为一个装饰器。(以下代码摘自 Python工匠:使用装饰器的小技巧)

import time
import functools

class DelayFunc:
    def __init__(self,  duration, func):
        self.duration = duration
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f'Wait for {self.duration} seconds...')
        time.sleep(self.duration)
        return self.func(*args, **kwargs)

    def eager_call(self, *args, **kwargs):
        print('Call without delay')
        return self.func(*args, **kwargs)

def delay(duration):
    """
    装饰器:推迟某个函数的执行。
    同时提供 .eager_call 方法立即执行
    """
    # 此处为了避免定义额外函数,
    # 直接使用 functools.partial 帮助构造 DelayFunc 实例
    return functools.partial(DelayFunc, duration)

我们的业务函数很简单,就是相加

@delay(duration=2)
def add(a, b):
    return a+b

来看一下执行过程

>>> add    # 可见 add 变成了 Delay 的实例
<__main__.DelayFunc object at 0x107bd0be0>
>>>
>>> add(3,5)  # 直接调用实例,进入 __call__
Wait for 2 seconds...
8
>>>
>>> add.func # 实现实例方法
<function add at 0x107bef1e0>

第六种:能装饰类的装饰器

用 Python 写单例模式的时候,常用的有三种写法。其中一种,是用装饰器来实现的。

以下便是我自己写的装饰器版的单例写法。

instances = {}

def singleton(cls):
    def get_instance(*args, **kw):
        cls_name = cls.__name__
        print('===== 1 ====')
        if not cls_name in instances:
            print('===== 2 ====')
            instance = cls(*args, **kw)
            instances[cls_name] = instance
        return instances[cls_name]
    return get_instance

@singleton
class User:
    _instance = None

    def __init__(self, name):
        print('===== 3 ====')
        self.name = name

可以看到我们用singleton 这个装饰函数来装饰 User 这个类。装饰器用在类上,并不是很常见,但只要熟悉装饰器的实现过程,就不难以实现对类的装饰。在上面这个例子中,装饰器就只是实现对类实例的生成的控制而已。

其实例化的过程,你可以参考我这里的调试过程,加以理解。

[WARNING]: the function say() is running...
say hello!

第五种:使用偏函数与类实现装饰器

绝大多数装饰器都是基于函数和闭包实现的,但这并非制造装饰器的唯一方式。

事实上,Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象

对于这个 callable 对象,我们最熟悉的就是函数了。

除函数之外,类也可以是 callable 对象,只要实现了__call__ 函数(上面几个例子已经接触过了)。

还有容易被人忽略的偏函数其实也是 callable 对象。

接下来就来说说,如何使用 类和偏函数结合实现一个与众不同的装饰器。

如下所示,DelayFunc 是一个实现了 __call__ 的类,delay 返回一个偏函数,在这里 delay 就可以做为一个装饰器。(以下代码摘自 Python工匠:使用装饰器的小技巧)

import time
import functools

class DelayFunc:
    def __init__(self,  duration, func):
        self.duration = duration
        self.func = func

    def __call__(self, *args, **kwargs):
        print(f'Wait for {self.duration} seconds...')
        time.sleep(self.duration)
        return self.func(*args, **kwargs)

    def eager_call(self, *args, **kwargs):
        print('Call without delay')
        return self.func(*args, **kwargs)

def delay(duration):
    """
    装饰器:推迟某个函数的执行。
    同时提供 .eager_call 方法立即执行
    """
    # 此处为了避免定义额外函数,
    # 直接使用 functools.partial 帮助构造 DelayFunc 实例
    return functools.partial(DelayFunc, duration)

我们的业务函数很简单,就是相加

@delay(duration=2)
def add(a, b):
    return a+b

来看一下执行过程

>>> add    # 可见 add 变成了 Delay 的实例
<__main__.DelayFunc object at 0x107bd0be0>
>>>
>>> add(3,5)  # 直接调用实例,进入 __call__
Wait for 2 seconds...
8
>>>
>>> add.func # 实现实例方法
<function add at 0x107bef1e0>

第六种:能装饰类的装饰器

用 Python 写单例模式的时候,常用的有三种写法。其中一种,是用装饰器来实现的。

以下便是我自己写的装饰器版的单例写法。

instances = {}

def singleton(cls):
    def get_instance(*args, **kw):
        cls_name = cls.__name__
        print('===== 1 ====')
        if not cls_name in instances:
            print('===== 2 ====')
            instance = cls(*args, **kw)
            instances[cls_name] = instance
        return instances[cls_name]
    return get_instance

@singleton
class User:
    _instance = None

    def __init__(self, name):
        print('===== 3 ====')
        self.name = name

可以看到我们用singleton 这个装饰函数来装饰 User 这个类。装饰器用在类上,并不是很常见,但只要熟悉装饰器的实现过程,就不难以实现对类的装饰。在上面这个例子中,装饰器就只是实现对类实例的生成的控制而已。

其实例化的过程,你可以参考我这里的调试过程,加以理解。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李乾星

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值