Skip to content

6. 函数

Image

6.1. 脚本 [fonc_01]:变量作用域

脚本 [fonc_01] 展示了函数之间变量作用域的示例:

#  variable scope
def f1():
    #  avoid using global variables
    #  global variable i
    global i
    i += 1
    #  local variable j
    j = 10
    print(f"f1[i,j]=[{i},{j}]")


def f2():
    #  avoid using global variables
    #  global variable i
    global i
    i += 1
    #  local variable j
    j = 20
    print(f"f2[i,j]=[{i},{j}]")


def f3():
    #  local variable i
    i = 1
    #  local variable j
    j = 30
    print(f"f3[i,j]=[{i},{j}]")


#  main program
i = 0
j = 0
#  these two variables will only be known by a function f
#  only if it explicitly declares in the global instruction that it wants to use them
#  or that the function uses the global variable for reading only
f1()
f2()
f3()
#  j hasn't changed but i has
print(f"[i,j]=[{i},{j}]")

结果


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_01.py
f1[i,j]=[1,10]
f2[i,j]=[2,20]
f3[i,j]=[1,30]
[i,j]=[2,0]
 
Process finished with exit code 0

  • 该脚本演示了变量 i 的用法,该变量在函数 f1 和 f2 中被声明为全局变量。在此情况下,主程序与函数 f1 和 f2 共享同一个变量 i。

6.2. 脚本 [fonc_02]:变量作用域

脚本 [fonc_03] 在脚本 [fonc_02] 的基础上,演示了如何避免使用全局变量:

#  variable scope
def f1(i):
    #  local variable i
    i += 1
    #  local variable j
    j = 10
    print(f"f1[i,j]=[{i},{j}]")
    #  return the modified value
    return i


def f2(i):
    #  local variable i
    i += 1
    #  local variable j
    j = 20
    print(f"f2[i,j]=[{i},{j}]")
    #  return the modified value
    return i


def f3():
    #  local variable i
    i = 1
    #  local variable j
    j = 30
    print(f"f3[i,j]=[{i},{j}]")


#  main program
i = 0
j = 0
#  these two variables will only be known by a function f
#  only if it explicitly declares in the global instruction that it wants to use them
#  or if the function uses the global variable for reading only
i = f1(i)
i = f2(i)
f3()
#  j hasn't changed but i has
print(f"[i,j]=[{i},{j}]")

注释

  • 第 2 行和第 12 行:变量 [i] 并未被声明为全局变量,而是作为参数传递给了函数 f1 和 f2;
  • 第 9 行和第 19 行:函数 f1 和 f2 将修改后的变量 [i] 返回给主程序。主程序在第 36 行和第 37 行中获取该变量;

结果


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_02.py
f1[i,j]=[1,10]
f2[i,j]=[2,20]
f3[i,j]=[1,30]
[i,j]=[2,0]
 
Process finished with exit code 0

6.3. 脚本 [fonc_03]:变量的作用域

脚本 [fonc_03] 演示了变量在函数内部和调用代码中使用的特殊性,这取决于该变量是否仅在函数内部用于读取

def f1():
    #  here the global variable i is known
    print(f"[f1] i={i}")
    #  here the global variable j is known
    print(f"[f1] j={j}")


def f2():
    #  here the global variable i is not known
    #  because the f2 function defines a local variable with the same name
    #  it then has priority
    try:
        #  try displaying the local variable i defined below
        print(f"[f2] i={i}")
        #  the following instruction makes i a local variable of function f2
        i = 7
    except BaseException as erreur:
        print(f"[f2] erreur={erreur}")


def f3():
    #  here the global variable i is not known
    #  because the f3 function defines a local variable with the same name
    #  it then has priority

    #  the following instruction makes i a local variable
    i = 7
    #  display - here i is known
    print(f"[f3] i={i}")


#  hand -----------
#  global variables to functions
i = 10
j = 20
#  call from f1
f1()
print(f"[main] i={i}, j={j}")
#  call from f2
f2()
print(f"[main] i={i}")
#  call from f3
f3()
print(f"[main] i={i}")

注释

  • 第 34 行:主代码定义了一个变量 [i]
  • 第 1–5 行:函数 f1 同样使用了变量 [i],但未对其赋值。这是对变量 [i] 读取。在此情况下,所使用的变量 [i] 来自调用代码中的第 34 行;
  • 第 8–18 行:函数 f2 同样使用变量 [i],但在第 16 行对其赋值。在 f2 中对变量 [i] 赋值会自动使 [i] 成为函数 [f2] 的局部变量。因此,该函数将变量 [i] “隐藏” 起来,使其对调用代码不可见;
  • 第 14 行:对局部变量 [i] 的写入操作将失败,因为当执行到第 14 行时该变量尚未赋值。它将在第 16 行获得其值。此时将引发异常。因此,第 14 行被置于 try/catch 代码块中;
  • 第 21–29 行:f3 函数与 f2 函数功能相同,但更早定义了其局部变量 [i]

结果


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_03.py
[f1] i=10
[f1] j=20
[main] i=10, j=20
[f2] erreur=local variable 'i' referenced before assignment
[main] i=10
[f3] i=7
[main] i=10
 
Process finished with exit code 0

6.4. 脚本 [fonc_04]:参数传递模式

脚本如下:

#  function f1
def f1(a):
    a = 2


#  function f2
def f2(a, b):
    a = 2
    b = 3
    return a, b


#  ------------------------ hand
x = 1
f1(x)
print(f"x={x}")
(x, y) = (-1, -1)
(x, y) = f2(x, y)
print(f"x={x}, y={y}")

结果

1
2
3
4
5
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_04.py
x=1
x=2, y=3

Process finished with exit code 0

  • 在 Python 中,一切都是对象。有些对象被称为“不可变”:它们无法被修改。数字、字符串和元组就属于这种情况。当 Python 对象作为参数传递给函数时,传递的是对象的引用,除非这些对象是“不可变”的,在这种情况下,传递的是对象的值;
  • 函数 f1(第 2 行)和 f2(第 7 行)旨在演示输出参数的传递。我们希望函数的实际参数能被该函数修改;
  • 第 2–3 行:函数 f1 修改了其形式参数 a。我们想知道实际参数是否也会被修改;
  • 第 14–15 行:实际参数为 x = 1。结果的第 2 行显示实际参数未被修改。因此,实际参数 x 和形式参数 a 两个不同的对象;
  • 第 8–10 行:函数 f2 修改其形式参数 a b,并将它们作为结果返回;
  • 第 17–18 行:将实际参数 (x, y) 传递给 f2,并将 f2 的结果赋值给 (x, y)。结果的第 3 行表明实际参数 (x, y) 已被修改。

由此可得,当“不可变”对象作为输出参数时,它们必须是函数返回结果的一部分。

6.5. 脚本 [fonc_05]:脚本中函数的顺序

脚本 [fonc_05] 表明,如果函数在代码中尚未出现过,则无法被调用:

#  ------------------------ hand
print(f2(100, 200))

#  function f1
def f1(a):
    return a + 10


#  function f2
def f2(a, b):
    return f1(a + b)

注释

  • 第 2 行会引发错误,因为它使用了函数 f2,而该函数在脚本中尚未定义;

结果

1
2
3
4
5
6
7
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_05.py
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_05.py", line 2, in <module>
    print(f2(100, 200))
NameError: name 'f2' is not defined

Process finished with exit code 1

6.6. 脚本 [fonc_06]:脚本中函数的执行顺序

脚本 [fonc_06] 表明,适用于调用代码的规则并不适用于函数:

#  function f2
def f2(a, b):
    return f1(a + b)


#  function f1
def f1(a):
    return a + 10


#  ------------------------ hand
print(f2(100, 200))

注释

  • 第 3 行:函数 [f2] 使用了脚本后文定义的函数 [f1]。但这不会引发错误。因此我们可以得出结论:在 Python 脚本中,函数的定义顺序并不重要;

结果

1
2
3
4
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_06.py
310

Process finished with exit code 0

6.7. 脚本 [fonc_07]:使用模块

脚本 [fonc_07] 演示了如何将函数封装在模块中。

Image

我们将可重用的函数封装在模块中,而不是将它们从一个脚本移动到另一个脚本:

  • 而是将它们放在一个单独的文件中,并以特定方式进行声明;
  • 需要这些函数的脚本会“导入”包含这些函数的模块;

脚本 [fonctions_module_01] 如下所示:

1
2
3
4
5
6
7
#  function f2
def f2(a, b):
    return f1(a + b)

#  function f1
def f1(a):
    return a + 10

有几种方法可以确保 [fonctions_module_01] 脚本中的函数能够被其他脚本调用。这些方法因脚本是否在 [PyCharm] 中运行而有所不同。

[PyCharm] 中,导入的模块会在名为 [Root Sources] 的特定文件夹中进行搜索。有两种方法可以将文件夹设为 [Root Source]

Image

  • [4] 中,该文件夹的颜色已发生变化;

完成此操作后,[functions/modules] 文件夹将被识别为源文件夹。随后您可以在脚本中编写:

from fonctions_module_01 import f2

以此导入并使用在 [functions_module_01.py] 模块中定义的函数 f2。

另一种方法是使用项目属性:

  • 在上文中,序列 [1-6] 允许将 [shared] 文件夹用作存储待导入模块的文件夹;

目前,除了项目根目录之外,我们不会将任何文件夹声明为 [Sources Root]

Image

完成上述设置后,我们可以编写以下 [fonc-07] 脚本:

1
2
3
4
5
6
7
8
#  using modules
import sys

#  ------------------------ hand
print(f"Python path={sys.path}")
from fonctions.shared.fonctions_module_01 import f2

print(f2(100, 200))
  • 第 2 行:我们导入 [sys] 对象,以便在第 5 行使用其 [path] 属性,该属性提供所谓的 [Python Path]:一个用于搜索导入模块的目录列表;
  • 第 6 行:我们从 [fonctions_module_01] 模块导入 f2 函数。要引用该模块,我们使用从项目根目录到该模块的路径。在 PyCharm 中,当在脚本中查找导入的模块时,项目根目录始终包含在搜索的文件夹中。因此,该文件夹是项目 [Python Path] 的一部分。这正是第 5 行将帮助我们验证的内容;
  • 第 6 行:若要描述从项目根目录到 [fonctions_module_01] 文件夹的路径,我们会写成 [fonctions/shared/fonctions_module_01]。在模块路径中,斜杠 (/) 会被替换为点 (.)。因此我们写成 [fonctions.modules.fonctions_module_01]
  • 第 6 行之后定义了函数 f2。我们在第 8 行中使用了它;

结果


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_07.py
Python path=['C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions\\shared', 'C:\\myprograms\\Python38\\python38.zip', 'C:\\myprograms\\Python38\\DLLs', 'C:\\myprograms\\Python38\\lib', 'C:\\myprograms\\Python38', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv\\lib\\site-packages']
310
 
Process finished with exit code 0

上文:

  • 绿色高亮部分显示,项目根目录是 [Python Path] 的一部分;
  • 黄色高亮部分显示,包含已执行脚本的文件夹也是 [Python Path] 的一部分;
  • [Python Path] 的其他元素则直接来自 Python 安装目录;

如果我们不使用 PyCharm 来运行 [fonc-07],会发生什么?

Image

Image

  • [1] 中,我们运行 [fonc-07] 脚本。当前所在位置为 [functions] 文件夹;
  • [2] 中,我们可以看到执行目录是 [Python Path] 的一部分。这种情况总是成立的。我们还可以看到,根目录 [C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020] 并不属于 [Python Path]
  • [3] 中,Python 解释器报告无法找到 [fonctions] 模块;

为了查找被导入的模块 [fonctions.shared.fonctions_module_01],Python 解释器会在 [Python Path] 中的文件夹内搜索名为 [fonctions] 的子文件夹。但无论在何处都找不到该文件夹。 这是因为 [fonctions] 子文件夹位于 [C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020] 文件夹下而该文件夹不属于 [Python Path]

[fonc-08] 脚本为该问题提供了一种可能的解决方案。

6.8. 脚本 [fonc_08]:向 [Python Path] 添加文件夹

可以通过编程方式修改 [Python Path],具体方法如 [fonc-08] 脚本所示:

#  using modules
import os
import sys

#  script folder
script_dir = os.path.dirname(os.path.abspath(__file__))
#  Python Path before modification
print(f"Python path avant={sys.path}")
#  add the [shared] folder to the Python Path
sys.path.append(f"{script_dir}/shared")
#  Python Path after modification
print(f"Python path après={sys.path}")

#  import f2
from fonctions_module_01 import f2

#  ------------------------ hand
print(f2(100, 200))

注释

  • 第 4 行:特殊变量 [__file__] 表示正在执行的脚本名称。根据执行上下文的不同,该名称可能是绝对路径(PyCharm)或相对路径(控制台)。[os.path.abspath] 函数返回传入名称对应的文件的绝对路径。[os.path.dirname] 函数返回传入名称对应的文件所在目录的绝对路径;
  • 第 10 行:[sys.path] 是一个列表,其中包含在查找模块时需要搜索的目录名称。我们将第 4 行定义的项目根目录添加到该列表中;
  • 我们分别显示修改前(第 8 行)和修改后(第 12 行)的 [Python Path]
  • 第 15 行:我们导入 [fonctions_module_01] 模块,该模块包含 f2 函数;

在 PyCharm 中执行后得到以下结果:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_08.py
Python path avant=['C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions\\shared', 'C:\\myprograms\\Python38\\python38.zip', 'C:\\myprograms\\Python38\\DLLs', 'C:\\myprograms\\Python38\\lib', 'C:\\myprograms\\Python38', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv\\lib\\site-packages']
Python path après=['C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions\\shared', 'C:\\myprograms\\Python38\\python38.zip', 'C:\\myprograms\\Python38\\DLLs', 'C:\\myprograms\\Python38\\lib', 'C:\\myprograms\\Python38', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv\\lib\\site-packages', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions/shared']
310
 
Process finished with exit code 0
  • 第 3 行:我们可以看到 [shared] 文件夹在 [Python Path] 中出现了两次。虽然可以避免这种情况,但在此处不会造成任何问题;
  • 第 4 行:f2 函数已成功执行;

现在让我们在终端中运行 [fonc-08]


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\fonctions>python fonc_08.py
Python path avant=['C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions', 'C:\\myprograms\\Python38\\python38.zip', 'C:\\myprograms\\Python38\\DLLs', 'C:\\myprograms\\Python38\\lib', 'C:\\myprograms\\Python38', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv\\lib\\site-packages']
Python path après=['C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions', 'C:\\myprograms\\Python38\\python38.zip', 'C:\\myprograms\\Python38\\DLLs', 'C:\\myprograms\\Python38\\lib', 'C:\\myprograms\\Python38', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\venv\\lib\\site-packages', 'C:\\Data\\st-2020\\dev\\python\\cours-2020\\python3-flask-2020\\fonctions/shared']
310
  • 第 2 行:与之前一样,[shared] 文件夹不在 [Python Path] 中;
  • 第 3 行:现在已包含;
  • 第 4 行:找到了函数 f2;

6.9. 脚本 [fonc_09]:声明参数类型

[fonc_09] 脚本表明,您可以声明函数参数的类型以及返回值的类型。不过,这种声明仅用于记录函数信息。Python 解释器不会检查实际函数参数是否为预期类型。但 PyCharm 会标记实际参数与形式参数之间的类型不一致。仅凭这一点,类型声明就变得不可或缺。

脚本内容如下:

#  a function with parameter type indication
#  this is for documentation purposes only, as the python interpreter ignores it


def show(param: int) -> int:
    print(f"param={param}, type(param)={type(param)}")
    return param + 1


#  hand -------------------------
print(show(4))
show("xyz")

  • 第 5 行:我们声明形式参数 [param] 的类型为 [int],且函数的返回类型也是 [int]
  • 第 11 行:函数 [show] 的实际参数类型正确;
  • 第 12 行:函数 [show] 的实际参数类型不正确;

结果

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_09.py
param=4, type(param)=<class 'int'>
5
param=xyz, type(param)=<class 'str'>
Traceback (most recent call last):
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_09.py", line 11, in <module>
    show("xyz")
  File "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_09.py", line 6, in show
    return param + 1
TypeError: can only concatenate str (not "int") to str

Process finished with exit code 1
  • 第 10 行:参数 [param] 的类型是 [str]。当此消息出现时,我们已经进入了函数 [show] 的代码。因此,Python 解释器已接受函数 [show] 的实际参数类型为 [str]
  • 代码的第 7 行触发了结果中第 4–10 行所示的异常;

尽管如此,PyCharm 仍提示存在问题:

Image

[1] 中,PyCharm 已将错误的调用标记出来。

6.10. 脚本 [fonc_10]:命名参数

要向函数传递参数,可以使用其形式参数的名称。在这种情况下,无需遵循形式参数的顺序:

1
2
3
4
5
6
7
#  we can refer to the actual parameters by their formal names
def f(x, y):
    return x + y


#  hand
print(f(y=10, x=3))

注释

  • 第 2 行:函数 f 有两个形式参数,x 和 y;
  • 第 7 行:调用函数 f 时,可以使用形式参数的名称。这种做法至少在以下两种情况下很有用:
    • 函数参数较多,且其中大部分具有默认值。调用函数时,上述方法允许您仅对不希望使用默认值的参数进行初始化;
    • 如果形式参数具有有意义的名称,那么在函数调用中使用命名参数可以提高代码的可读性;

结果

1
2
3
4
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_10.py
13

Process finished with exit code 0

6.11. 脚本 [fonc_11]:递归函数

脚本 [fonc_11] 是一个递归函数(即调用自身的函数)的示例:

#  recursive function
def fact(i: int) -> int:
    #  factorial(1) is 1
    #  a recursive function must terminate at a certain point
    if i == 1:
        return 1
    else:
        #  factorial(i)=i*factorial(i-1)
        return i * fact(i - 1)


#  ---------- hand
print(f"fact(8)={fact(8)}")

注释

  • 第 1-9 行:阶乘函数;
  • 第 9 行:[阶乘] 函数调用自身;
  • 第5-6行:递归函数必须在满足某个条件时终止;否则,就会发生无限递归;

结果

1
2
3
4
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_11.py
fact(8)=40320

Process finished with exit code 0

6.12. 脚本 [fonc_12]:递归函数

[fonc_12] 函数进一步详细说明了递归的工作原理:

#  recursive function
#  behavior of parameter j


def fact(i: int, j: int) -> int:
    #  stop recursive function
    if i == 1:
        print(f"j={j}")
        return 1
    else:
        #  we manipulate j
        j += 1
        print(f"avant fact j={j}")
        #  recursivity
        f = fact(i - 1, j)
        print(f"après fact j={j}")
        #  result
        return i * f


#  ---------- hand
print(f"fact(8)={fact(8, 0)}")

注释

  • 第 5 行:我们仍然关注阶乘函数。我们向其中添加了参数 [j]
  • 第12行:变量j在每次阶乘计算后都会递增。我们在递归(第15行)之前(第12行)和之后(第16行)显示其值;

结果

C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/fonctions/fonc_12.py
avant fact j=1
avant fact j=2
avant fact j=3
avant fact j=4
avant fact j=5
avant fact j=6
avant fact j=7
j=7
après fact j=7
après fact j=6
après fact j=5
après fact j=4
après fact j=3
après fact j=2
après fact j=1
fact(8)=40320

Process finished with exit code 0
  • 第2–8行:我们可以看到,只要递归仍在进行,[j]的值就会增加,直到满足递归停止的条件。从那一刻起,[fact]函数的调用返回将按与调用相反的顺序进行;
  • 第 10–16 行:这些输出反映了阶乘调用中连续的返回过程。变量 [j] 恢复到其初始值 1;