Python 入门指南(二)(2)

简介: Python 入门指南(二)

Python 入门指南(二)(1)https://developer.aliyun.com/article/1507366

在最短输入序列上终止的迭代器

这个类别非常有趣。它允许您基于多个迭代器创建一个迭代器,并根据某种逻辑组合它们的值。这里的关键是,在这些迭代器中,如果有任何一个比其他迭代器短,那么生成的迭代器不会中断,它将在最短的迭代器耗尽时停止。我知道这很理论化,所以让我用compress给您举个例子。这个迭代器根据选择器中的相应项目是True还是False,将数据返回给您:

compress('ABC', (1, 0, 1))会返回'A''C',因为它们对应于1。让我们看一个简单的例子:

# compress.py
from itertools import compress
data = range(10)
even_selector = [1, 0] * 10
odd_selector = [0, 1] * 10
even_numbers = list(compress(data, even_selector))
odd_numbers = list(compress(data, odd_selector))
print(odd_selector)
print(list(data))
print(even_numbers)
print(odd_numbers)

请注意,odd_selectoreven_selector 长度为 20,而 data 只有 10 个元素。compress 会在 data 产生最后一个元素时停止。运行此代码会产生以下结果:

$ python compress.py
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 2, 4, 6, 8]
[1, 3, 5, 7, 9]

这是一种非常快速和方便的方法,可以从可迭代对象中选择元素。代码非常简单,只需注意,我们使用 list() 而不是 for 循环来迭代压缩调用返回的每个值,它们的作用是相同的,但是 list() 不执行一系列指令,而是将所有值放入列表并返回。

组合生成器

最后但并非最不重要的,组合生成器。如果你喜欢这种东西,这些真的很有趣。让我们来看一个关于排列的简单例子。

根据 Wolfram Mathworld:

排列,也称为“排列数”或“顺序”,是将有序列表 S 的元素重新排列,使其与 S 本身形成一一对应的重新排列。

例如,ABC 有六种排列:ABC、ACB、BAC、BCA、CAB 和 CBA。

如果一个集合有 N 个元素,那么它们的排列数是 N! (N 阶乘)。对于 ABC 字符串,排列数为 3! = 3 * 2 * 1 = 6。让我们用 Python 来做一下:

# permutations.py
from itertools import permutations 
print(list(permutations('ABC'))) 

这段非常简短的代码片段产生了以下结果:

$ python permutations.py
[('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')]

当你玩排列时要非常小心。它们的数量增长速度与你要排列的元素的阶乘成比例,而这个数字可能会变得非常大,非常快。

总结

在本章中,我们迈出了扩展我们编码词汇的又一步。我们看到了如何通过评估条件来驱动代码的执行,以及如何循环和迭代序列和对象集合。这赋予了我们控制代码运行时发生的事情的能力,这意味着我们正在了解如何塑造它,使其做我们想要的事情,并对动态变化的数据做出反应。

我们还看到了如何在几个简单的例子中将所有东西组合在一起,最后,我们简要地看了一下 itertools 模块,其中充满了可以进一步丰富我们使用 Python 的有趣迭代器。

现在是时候换个方式,向前迈进一步,谈谈函数。下一章将全面讨论它们,因为它们非常重要。确保你对到目前为止所涵盖的内容感到舒适。我想给你提供一些有趣的例子,所以我会快一点。准备好了吗?翻页吧。

第四章:函数,代码的构建块

创建架构就是把东西放在一起。把什么放在一起?函数和对象。"– 勒·柯布西耶

在前几章中,我们已经看到 Python 中的一切都是对象,函数也不例外。但是,函数究竟是什么?函数是一系列执行任务的指令,打包成一个单元。然后可以在需要的地方导入并使用这个单元。使用函数在代码中有许多优点,我们很快就会看到。

在本章中,我们将涵盖以下内容:

  • 函数——它们是什么,为什么我们应该使用它们
  • 作用域和名称解析
  • 函数签名——输入参数和返回值
  • 递归和匿名函数
  • 导入对象以重用代码

我相信这句话,一张图胜过千言万语,在向一个对这个概念新手解释函数时尤其正确,所以请看一下下面的图表:


如您所见,函数是一组指令,作为一个整体打包,就像一个盒子。函数可以接受输入参数并产生输出值。这两者都是可选的,我们将在本章的示例中看到。

在 Python 中,使用def关键字定义函数,随后是函数名称,后面跟着一对括号(可能包含或不包含输入参数),冒号(:)表示函数定义行的结束。紧接着,缩进四个空格,我们找到函数的主体,这是函数在调用时将执行的一组指令。

请注意,缩进四个空格不是强制性的,但这是PEP 8建议的空格数量,在实践中是最广泛使用的间距度量。

函数可能会返回输出,也可能不会。如果函数想要返回输出,它会使用return关键字,后面跟着期望的输出。如果您有鹰眼,您可能已经注意到在前面图表的输出部分的Optional后面有一个小*****。这是因为在 Python 中,函数总是返回一些东西,即使您没有明确使用return子句。如果函数在其主体中没有return语句,或者return语句本身没有给出值,函数将返回None。这种设计选择背后的原因超出了介绍性章节的范围,所以您需要知道的是,这种行为会让您的生活更轻松。一如既往,感谢 Python。

为什么要使用函数?

函数是任何语言中最重要的概念和构造之一,所以让我给你几个需要它们的原因:

  • 它们减少了程序中的代码重复。通过将特定任务由一个良好的打包代码块处理,我们可以在需要时导入并调用它,无需重复其实现。
  • 它们有助于将复杂的任务或过程分解为更小的块,每个块都成为一个函数。
  • 它们隐藏了实现细节,使其用户不可见。
  • 它们提高了可追溯性。
  • 它们提高可读性。

让我们看一些示例,以更好地理解每一点。

减少代码重复

想象一下,您正在编写一段科学软件,需要计算素数直到一个限制,就像我们在上一章中所做的那样。您有一个很好的算法来计算它们,所以您将它复制并粘贴到需要的任何地方。然而,有一天,您的朋友B.黎曼给了您一个更好的算法来计算素数,这将为您节省大量时间。在这一点上,您需要检查整个代码库,并用新的代码替换旧的代码。

这实际上是一个不好的做法。这容易出错,你永远不知道你是不是误删或者误留了哪些代码行,当你把代码剪切粘贴到其他代码中时,你也可能会错过其中一个计算质数的地方,导致你的软件处于不一致的状态,同样的操作在不同的地方以不同的方式执行。如果你需要修复一个 bug 而不是用更好的版本替换代码,而你错过了其中一个地方呢?那将更糟糕。

那么,你应该怎么做?简单!你写一个函数get_prime_numbers(upto),在任何需要质数列表的地方使用它。当B. Riemann给你新代码时,你只需要用新实现替换该函数的主体,就完成了!其余的软件将自动适应,因为它只是调用函数。

你的代码会更短,不会受到旧方法和新方法执行任务的不一致性的影响,也不会因为复制粘贴失败或疏忽而导致未检测到的 bug。使用函数,你只会从中获益,我保证。

拆分复杂任务

函数还非常有用,可以将长或复杂的任务分解为较小的任务。最终结果是,代码从中受益,例如可读性、可测试性和重用性。举个简单的例子,想象一下你正在准备一份报告。你的代码需要从数据源获取数据,解析数据,过滤数据,整理数据,然后对其运行一系列算法,以产生将供Report类使用的结果。阅读这样的程序并不罕见,它们只是一个大大的do_report(data_source)函数。有数十行或数百行代码以return report结束。

这些情况在科学代码中更常见,这些代码在算法上可能很出色,但有时在编写风格上缺乏经验丰富的程序员的触觉。现在,想象一下几百行代码。很难跟进,找到事情改变上下文的地方(比如完成一个任务并开始下一个任务)。你有这个画面了吗?好了。不要这样做!相反,看看这段代码:

# data.science.example.py
def do_report(data_source):
    # fetch and prepare data
    data = fetch_data(data_source)
    parsed_data = parse_data(data)
    filtered_data = filter_data(parsed_data)
    polished_data = polish_data(filtered_data)
    # run algorithms on data
    final_data = analyse(polished_data)
    # create and return report
    report = Report(final_data)
    return report

前面的例子当然是虚构的,但你能看出来如果需要检查代码会有多容易吗?如果最终结果看起来不对,逐个调试do_report函数中的单个数据输出将会非常容易。此外,暂时从整个过程中排除部分过程也更容易(你只需要注释掉需要暂停的部分)。这样的代码更容易处理。

隐藏实现细节

让我们继续使用前面的例子来谈谈这一点。你可以看到,通过阅读do_report函数的代码,你可以在不阅读一行实现的情况下获得相当好的理解。这是因为函数隐藏了实现细节。这意味着,如果你不需要深入了解细节,你就不必强制自己去了解,就像如果do_report只是一个庞大的函数一样。为了理解发生了什么,你必须阅读每一行代码。而使用函数,你就不需要这样做。这减少了你阅读代码的时间,而在专业环境中,阅读代码所花费的时间比实际编写代码的时间要多得多,因此尽可能减少这部分时间非常重要。

提高可读性

程序员有时候看不出来为什么要写一个只有一两行代码的函数,所以让我们看一个例子,告诉你为什么你应该这样做。

想象一下,你需要计算两个矩阵的乘积:


你更喜欢阅读这段代码吗:

# matrix.multiplication.nofunc.py
a = [[1, 2], [3, 4]]
b = [[5, 1], [2, 1]]
c = [[sum(i * j for i, j in zip(r, c)) for c in zip(*b)]
     for r in a]

或者你更喜欢这个:

# matrix.multiplication.func.py
# this function could also be defined in another module
def matrix_mul(a, b):
    return [[sum(i * j for i, j in zip(r, c)) for c in zip(*b)]
            for r in a]
a = [[1, 2], [3, 4]]
b = [[5, 1], [2, 1]]
c = matrix_mul(a, b)

在第二个例子中,更容易理解cab之间的乘法结果。通过代码更容易阅读,如果您不需要修改该乘法逻辑,甚至不需要深入了解实现细节。因此,在这里提高了可读性,而在第一个片段中,您将不得不花时间尝试理解那个复杂的列表推导在做什么。

提高可追溯性

想象一下,您已经编写了一个电子商务网站。您在页面上展示了产品价格。假设您的数据库中的价格是不含增值税(销售税)的,但是您希望在网站上以 20%的增值税显示它们。以下是从不含增值税价格计算含增值税价格的几种方法:

# vat.py
price = 100  # GBP, no VAT
final_price1 = price * 1.2
final_price2 = price + price / 5.0
final_price3 = price * (100 + 20) / 100.0
final_price4 = price + price * 0.2

这四种不同的计算含增值税价格的方式都是完全可以接受的,我向您保证,这些年来我在同事的代码中找到了它们。现在,想象一下,您已经开始在不同的国家销售您的产品,其中一些国家有不同的增值税率,因此您需要重构您的代码(整个网站)以使增值税计算动态化。

您如何追踪所有进行增值税计算的地方?今天的编码是一个协作任务,您无法确定增值税是否仅使用了这些形式中的一种。相信我,这将是一场噩梦。

因此,让我们编写一个函数,该函数接受输入值vatprice(不含增值税),并返回含增值税的价格:

# vat.function.py
def calculate_price_with_vat(price, vat):
    return price * (100 + vat) / 100

现在您可以导入该函数,并在您的网站的任何地方使用它,需要计算含增值税的价格,并且当您需要跟踪这些调用时,您可以搜索calculate_price_with_vat

请注意,在前面的例子中,假定price是不含增值税的,vat是一个百分比值(例如 19、20 或 23)。

作用域和名称解析

您还记得我们在第一章中谈到的作用域和命名空间吗,Python 的初级介绍?我们现在将扩展这个概念。最后,我们可以谈论函数,这将使一切更容易理解。让我们从一个非常简单的例子开始:

# scoping.level.1.py
def my_function():
    test = 1  # this is defined in the local scope of the function
    print('my_function:', test)
test = 0  # this is defined in the global scope
my_function()
print('global:', test)

在前面的例子中,我在两个不同的地方定义了test名称。实际上它在两个不同的作用域中。一个是全局作用域(test = 0),另一个是my_function函数的局部作用域(test = 1)。如果您执行该代码,您会看到这个:

$ python scoping.level.1.py
my_function: 1
global: 0

很明显,test = 1覆盖了my_function中的test = 0赋值。在全局上下文中,test仍然是0,正如您从程序的输出中所看到的,但是我们在函数体中再次定义了test名称,并将其指向值为1的整数。因此,这两个test名称都存在,一个在全局作用域中,指向值为0int对象,另一个在my_function作用域中,指向值为1int对象。让我们注释掉test = 1的那一行。Python 会在下一个封闭的命名空间中搜索test名称(回想一下LEGB规则:localenclosingglobalbuilt-in,在第一章中描述,Python 的初级介绍),在这种情况下,我们将看到值0被打印两次。在您的代码中尝试一下。

现在,让我们提高一下难度:

# scoping.level.2.py
def outer():
    test = 1  # outer scope
    def inner():
        test = 2  # inner scope
        print('inner:', test)
    inner()
    print('outer:', test)
test = 0  # global scope
outer()
print('global:', test)

在前面的代码中,我们有两个级别的遮蔽。一个级别在函数outer中,另一个级别在函数inner中。这并不是什么难事,但可能会有些棘手。如果我们运行代码,我们会得到:

$ python scoping.level.2.py
inner: 2
outer: 1
global: 0

尝试注释掉test = 1行。您能猜到结果会是什么吗?嗯,当达到print('outer:', test)行时,Python 将不得不在下一个封闭范围中查找test,因此它将找到并打印0,而不是1。确保您也注释掉test = 2,以查看您是否理解发生了什么,以及 LEGB 规则是否清晰,然后再继续。

另一个需要注意的事情是,Python 允许您在另一个函数中定义一个函数。内部函数的名称在外部函数的命名空间中定义,就像任何其他名称一样。

global 和 nonlocal 语句

回到前面的例子,我们可以通过使用这两个特殊语句之一来更改对test名称的遮蔽:globalnonlocal。正如您从前面的例子中看到的,当我们在inner函数中定义test = 2时,我们既不会覆盖outer函数中的test,也不会覆盖全局范围中的test。如果我们在不定义它们的嵌套范围中使用它们,我们可以读取这些名称,但是我们不能修改它们,因为当我们编写赋值指令时,实际上是在当前范围中定义一个新名称。

我们如何改变这种行为呢?嗯,我们可以使用nonlocal语句。根据官方文档:

“nonlocal 语句使列出的标识符引用最近的封闭范围中先前绑定的变量,不包括全局变量。”

让我们在inner函数中引入它,看看会发生什么:

# scoping.level.2.nonlocal.py
def outer():
    test = 1  # outer scope
    def inner():
        nonlocal test
        test = 2  # nearest enclosing scope (which is 'outer')
        print('inner:', test)
    inner()
    print('outer:', test)
test = 0  # global scope
outer()
print('global:', test)

请注意,在inner函数的主体中,我已经声明了test名称为nonlocal。运行此代码会产生以下结果:

$ python scoping.level.2.nonlocal.py
inner: 2
outer: 2
global: 0

哇,看看那个结果!这意味着,通过在inner函数中声明testnonlocal,我们实际上得到了将test名称绑定到在outer函数中声明的名称。如果我们从inner函数中删除nonlocal test行并尝试在outer函数中尝试相同的技巧,我们将得到一个SyntaxError,因为nonlocal语句在封闭范围上运行,不包括全局范围。

那么有没有办法到达全局命名空间中的test = 0呢?当然,我们只需要使用global语句:

# scoping.level.2.global.py
def outer():
    test = 1  # outer scope
    def inner():
        global test
        test = 2  # global scope
        print('inner:', test)
    inner()
    print('outer:', test)
test = 0  # global scope
outer()
print('global:', test)

请注意,我们现在已经声明了test名称为global,这基本上将其绑定到我们在全局命名空间中定义的名称(test = 0)。运行代码,您应该会得到以下结果:

$ python scoping.level.2.global.py
inner: 2
outer: 1
global: 2

这表明受test = 2赋值影响的名称现在是global。这个技巧在outer函数中也会起作用,因为在这种情况下,我们是在引用全局范围。自己尝试一下,看看有什么变化,熟悉作用域和名称解析,这非常重要。此外,您能告诉在前面的例子中如果在outer之外定义inner会发生什么吗?

输入参数

在本章的开始,我们看到函数可以接受输入参数。在我们深入讨论所有可能类型的参数之前,让我们确保您清楚地理解了向函数传递参数的含义。有三个关键点需要记住:

  • 参数传递只不过是将对象分配给本地变量名称
  • 在函数内部将对象分配给参数名称不会影响调用者
  • 更改函数中的可变对象参数会影响调用者

让我们分别看一下这些要点的例子。

参数传递

看一下以下代码。我们在全局范围内声明一个名称x,然后我们声明一个函数func(y),最后我们调用它,传递x

# key.points.argument.passing.py
x = 3
def func(y):
    print(y)
func(x)  # prints: 3

当使用x调用func时,在其局部范围内,创建了一个名为y的名称,并且它指向与x指向的相同对象。这可以通过以下图表更清楚地解释(不用担心 Python 3.3,这是一个未更改的功能):


前面图的右侧描述了程序在执行到结束后的状态,即func返回(None)后的状态。看一下 Frames 列,注意我们在全局命名空间(全局帧)中有两个名称,xfunc,分别指向一个int(值为3)和一个function对象。在下面的名为func的矩形中,我们可以看到函数的局部命名空间,其中只定义了一个名称:y。因为我们用x调用了func(图的左侧第 5 行),y指向与x指向的相同的对象。这就是在将参数传递给函数时发生的情况。如果我们在函数定义中使用名称x而不是y,情况将完全相同(可能一开始有点混乱),函数中会有一个局部的x,而外部会有一个全局的x,就像我们在本章前面的作用域和名称解析部分中看到的那样。

总之,实际发生的是函数在其局部范围内创建了作为参数定义的名称,当我们调用它时,我们基本上告诉 Python 这些名称必须指向哪些对象。

分配给参数名称不会影响调用者

这一点一开始可能有点难以理解,所以让我们看一个例子:

# key.points.assignment.py
x = 3
def func(x):
    x = 7  # defining a local x, not changing the global one
func(x)
print(x)  # prints: 3

在前面的代码中,当执行x = 7行时,在func函数的局部范围内,名称x指向一个值为7的整数,而全局的x保持不变。

更改可变对象会影响调用者

这是最后一点,非常重要,因为 Python 在处理可变对象时表现得似乎有所不同(尽管只是表面上)。让我们看一个例子:

# key.points.mutable.py
x = [1, 2, 3]
def func(x):
    x[1] = 42  # this affects the caller!
func(x)
print(x)  # prints: [1, 42, 3]

哇,我们实际上改变了原始对象!如果你仔细想想,这种行为并不奇怪。函数中的x名称被设置为通过函数调用指向调用者对象,在函数体内,我们没有改变x,也就是说,我们没有改变它的引用,换句话说,我们没有改变x指向的对象。我们访问该对象在位置 1 的元素,并更改其值。

记住输入参数部分下的第 2 点:在函数内将对象分配给参数名称不会影响调用者。如果这对你来说很清楚,下面的代码就不会让你感到惊讶:

# key.points.mutable.assignment.py
x = [1, 2, 3]
def func(x):
    x[1] = 42  # this changes the caller!
    x = 'something else'  # this points x to a new string object
func(x)
print(x)  # still prints: [1, 42, 3]

看一下我标记的两行。一开始,就像以前一样,我们再次访问调用者对象,位于位置 1,并将其值更改为数字42。然后,我们重新分配x指向'something else'字符串。这样留下了调用者不变,实际上,输出与前面片段的输出相同。

花点时间来玩弄这个概念,并尝试使用打印和调用id函数,直到你的思路清晰。这是 Python 的一个关键方面,必须非常清楚,否则你可能会在代码中引入微妙的错误。再次强调,Python Tutor 网站(www.pythontutor.com/)将通过可视化这些概念来帮助你很多。

现在我们对输入参数及其行为有了很好的理解,让我们看看如何指定它们。

如何指定输入参数

指定输入参数的五种不同方式:

  • 位置参数
  • 关键字参数
  • 变量位置参数
  • 可变关键字参数
  • 仅关键字参数

让我们逐一来看看它们。

位置参数

位置参数从左到右读取,它们是最常见的参数类型:

# arguments.positional.py
def func(a, b, c):
    print(a, b, c)
func(1, 2, 3)  # prints: 1 2 3

没有什么别的要说的。它们可以是任意多个,按位置分配。在函数调用中,1排在前面,2排在第二,3排在第三,因此它们分别分配给abc

关键字参数和默认值

关键字参数通过使用name=value语法进行分配:

# arguments.keyword.py
def func(a, b, c):
    print(a, b, c)
func(a=1, c=2, b=3)  # prints: 1 3 2

关键字参数是按名称匹配的,即使它们不遵守定义的原始位置(当我们混合和匹配不同类型的参数时,稍后我们将看到这种行为的限制)。

关键字参数的对应物,在定义方面,是默认值。 语法是相同的,name=value,并且允许我们不必提供参数,如果我们对给定的默认值感到满意:

# arguments.default.py
def func(a, b=4, c=88):
    print(a, b, c)
func(1)  # prints: 1 4 88
func(b=5, a=7, c=9)  # prints: 7 5 9
func(42, c=9)  # prints: 42 4 9
func(42, 43, 44)  # prints: 42, 43, 44

有两件非常重要的事情需要注意。 首先,你不能在位置参数的左边指定默认参数。 其次,在这些例子中,注意当参数在列表中没有使用argument_name=value语法时,它必须是列表中的第一个参数,并且总是分配给a。 还要注意,以位置方式传递值仍然有效,并且遵循函数签名顺序(示例的最后一行)。

尝试混淆这些参数,看看会发生什么。 Python 错误消息非常擅长告诉你出了什么问题。 例如,如果你尝试了这样的东西:

# arguments.default.error.py
def func(a, b=4, c=88):
    print(a, b, c)
func(b=1, c=2, 42)  # positional argument after keyword one

你会得到以下错误:

$ python arguments.default.error.py
 File "arguments.default.error.py", line 4
 func(b=1, c=2, 42) # positional argument after keyword one
 ^
SyntaxError: positional argument follows keyword argument

这会告诉你你错误地调用了函数。

可变位置参数

有时候你可能想要向函数传递可变数量的位置参数,Python 为你提供了这样的能力。 让我们看一个非常常见的用例,minimum函数。 这是一个计算其输入值的最小值的函数:

# arguments.variable.positional.py
def minimum(*n):
    # print(type(n))  # n is a tuple
    if n:  # explained after the code
        mn = n[0]
        for value in n[1:]:
            if value < mn:
                mn = value
        print(mn)
minimum(1, 3, -7, 9)  # n = (1, 3, -7, 9) - prints: -7
minimum()             # n = () - prints: nothing

正如你所看到的,当我们在参数名前面加上*时,我们告诉 Python 该参数将根据函数的调用方式收集可变数量的位置参数。 在函数内部,n是一个元组。 取消注释print(type(n)),自己看看并玩一会儿。

你是否注意到我们如何用简单的if n:检查n是否为空? 这是因为在 Python 中,集合对象在非空时求值为True,否则为False。 这对于元组,集合,列表,字典等都是成立的。

还有一件事要注意的是,当我们在没有参数的情况下调用函数时,我们可能希望抛出错误,而不是默默地什么都不做。 在这种情况下,我们不关心使这个函数健壮,而是理解可变位置参数。

让我们举个例子,展示两件事,根据我的经验,对于新手来说是令人困惑的:

# arguments.variable.positional.unpacking.py
def func(*args):
    print(args)
values = (1, 3, -7, 9)
func(values)   # equivalent to: func((1, 3, -7, 9))
func(*values)  # equivalent to: func(1, 3, -7, 9)

好好看看前面例子的最后两行。 在第一个例子中,我们用一个参数调用func,一个四元组。 在第二个例子中,通过使用*语法,我们正在做一种叫做解包的事情,这意味着四元组被解包,函数被调用时有四个参数:1, 3, -7, 9

这种行为是 Python 为你做的魔术的一部分,允许你在动态调用函数时做一些惊人的事情。

变量关键字参数

可变关键字参数与可变位置参数非常相似。 唯一的区别是语法(**而不是*),以及它们被收集在一个字典中。 集合和解包的工作方式相同,所以让我们看一个例子:

# arguments.variable.keyword.py
def func(**kwargs):
    print(kwargs)
# All calls equivalent. They print: {'a': 1, 'b': 42}
func(a=1, b=42)
func(**{'a': 1, 'b': 42})
func(**dict(a=1, b=42))

在前面的例子中,所有的调用都是等价的。 你可以看到,在函数定义中在参数名前面添加**告诉 Python 使用该名称来收集可变数量的关键字参数。 另一方面,当我们调用函数时,我们可以显式地传递name=value参数,或者使用相同的**语法解包字典。

能够传递可变数量的关键字参数之所以如此重要的原因可能目前还不明显,那么,来一个更现实的例子怎么样?让我们定义一个连接到数据库的函数。我们希望通过简单调用这个函数而连接到默认数据库。我们还希望通过传递适当的参数来连接到任何其他数据库。在继续阅读之前,试着花几分钟时间自己想出一个解决方案:

# arguments.variable.db.py
def connect(**options):
    conn_params = {
        'host': options.get('host', '127.0.0.1'),
        'port': options.get('port', 5432),
        'user': options.get('user', ''),
        'pwd': options.get('pwd', ''),
    }
    print(conn_params)
    # we then connect to the db (commented out)
    # db.connect(**conn_params)
connect()
connect(host='127.0.0.42', port=5433)
connect(port=5431, user='fab', pwd='gandalf')

注意在函数中,我们可以准备一个连接参数的字典(conn_params),使用默认值作为回退,允许在函数调用中提供时进行覆盖。有更好的方法可以用更少的代码行来实现,但我们现在不关心这个。运行上述代码会产生以下结果:

$ python arguments.variable.db.py
{'host': '127.0.0.1', 'port': 5432, 'user': '', 'pwd': ''}
{'host': '127.0.0.42', 'port': 5433, 'user': '', 'pwd': ''}
{'host': '127.0.0.1', 'port': 5431, 'user': 'fab', 'pwd': 'gandalf'}

注意函数调用和输出之间的对应关系。注意默认值是如何根据传递给函数的参数进行覆盖的。

仅限关键字参数

Python 3 允许一种新类型的参数:仅限关键字参数。我们只会简要地研究它们,因为它们的使用情况并不那么频繁。有两种指定它们的方式,要么在可变位置参数之后,要么在单独的*之后。让我们看一个例子:

# arguments.keyword.only.py
def kwo(*a, c):
    print(a, c)
kwo(1, 2, 3, c=7)  # prints: (1, 2, 3) 7
kwo(c=4)  # prints: () 4
# kwo(1, 2)  # breaks, invalid syntax, with the following error
# TypeError: kwo() missing 1 required keyword-only argument: 'c'
def kwo2(a, b=42, *, c):
    print(a, b, c)
kwo2(3, b=7, c=99)  # prints: 3 7 99
kwo2(3, c=13)  # prints: 3 42 13
# kwo2(3, 23)  # breaks, invalid syntax, with the following error
# TypeError: kwo2() missing 1 required keyword-only argument: 'c'

如预期的那样,函数kwo接受可变数量的位置参数(a)和一个仅限关键字参数c。调用的结果很直接,您可以取消注释第三个调用以查看 Python 返回的错误。

相同的规则适用于函数kwo2,它与kwo不同之处在于它接受一个位置参数a,一个关键字参数b,然后是一个仅限关键字参数c。您可以取消注释第三个调用以查看错误。

现在你知道如何指定不同类型的输入参数了,让我们看看如何在函数定义中结合它们。

结合输入参数

只要遵循以下顺序规则,就可以结合输入参数:

  • 在定义函数时,普通的位置参数先出现(name),然后是任何默认参数(name=value),然后是可变位置参数(*name或简单的*),然后是任何关键字参数(namename=value形式都可以),最后是任何可变关键字参数(**name)。
  • 另一方面,在调用函数时,参数必须按照以下顺序给出:先是位置参数(value),然后是任意组合的关键字参数(name=value),可变位置参数(*name),然后是可变关键字参数(**name)。

由于这在理论世界中留下来可能有点棘手,让我们看几个快速的例子:

# arguments.all.py
def func(a, b, c=7, *args, **kwargs):
    print('a, b, c:', a, b, c)
    print('args:', args)
    print('kwargs:', kwargs)
func(1, 2, 3, *(5, 7, 9), **{'A': 'a', 'B': 'b'})
func(1, 2, 3, 5, 7, 9, A='a', B='b')  # same as previous one

注意函数定义中参数的顺序,两个调用是等价的。在第一个调用中,我们使用了可迭代对象和字典的解包操作符,而在第二个调用中,我们使用了更明确的语法。执行这个函数会产生以下结果(我只打印了一个调用的结果,另一个结果相同):

$ python arguments.all.py
a, b, c: 1 2 3
args: (5, 7, 9)
kwargs: {'A': 'a', 'B': 'b'}

现在让我们看一个关键字参数的例子:

# arguments.all.kwonly.py
def func_with_kwonly(a, b=42, *args, c, d=256, **kwargs):
    print('a, b:', a, b)
    print('c, d:', c, d)
    print('args:', args)
    print('kwargs:', kwargs)
# both calls equivalent
func_with_kwonly(3, 42, c=0, d=1, *(7, 9, 11), e='E', f='F')
func_with_kwonly(3, 42, *(7, 9, 11), c=0, d=1, e='E', f='F')

请注意我在函数声明中突出显示了仅限关键字参数。它们出现在*args可变位置参数之后,如果它们直接出现在单个*之后,情况也是一样的(在这种情况下就没有可变位置参数了)。

执行这个函数会产生以下结果(我只打印了一个调用的结果):

$ python arguments.all.kwonly.py
a, b: 3 42
c, d: 0 1
args: (7, 9, 11)
kwargs: {'e': 'E', 'f': 'F'}

还要注意的一件事是我给可变位置和关键字参数起的名字。你可以选择不同的名字,但要注意argskwargs是这些参数的通用约定名称,至少是通用的。

Python 入门指南(二)(3)https://developer.aliyun.com/article/1507370

相关文章
|
3天前
|
Linux 开发工具 Python
初学者从无到有的Python语言如何入门,这份Python学习路线赶紧带走_python 从无到(1)
初学者从无到有的Python语言如何入门,这份Python学习路线赶紧带走_python 从无到(1)
初学者从无到有的Python语言如何入门,这份Python学习路线赶紧带走_python 从无到(1)
|
3天前
|
数据采集 算法 Python
2024年Python最全python基础入门:高阶函数,小米面试编程题
2024年Python最全python基础入门:高阶函数,小米面试编程题
|
3天前
|
存储 数据采集 数据挖掘
真正零基础Python入门:手把手教你从变量和赋值语句学起
真正零基础Python入门:手把手教你从变量和赋值语句学起
|
4天前
|
数据挖掘 数据处理 Python
【Python DataFrame 专栏】Python DataFrame 入门指南:从零开始构建数据表格
【5月更文挑战第19天】本文介绍了Python数据分析中的核心概念——DataFrame,通过导入`pandas`库创建并操作DataFrame。示例展示了如何构建数据字典并转换为DataFrame,以及进行数据选择、添加修改列、计算统计量、筛选和排序等操作。DataFrame适用于处理各种规模的表格数据,是数据分析的得力工具。掌握其基础和应用是数据分析之旅的重要起点。
【Python DataFrame 专栏】Python DataFrame 入门指南:从零开始构建数据表格
|
5天前
|
网络协议 网络架构 Python
Python 网络编程基础:套接字(Sockets)入门与实践
【5月更文挑战第18天】Python网络编程中的套接字是程序间通信的基础,分为TCP和UDP。TCP套接字涉及创建服务器套接字、绑定地址和端口、监听、接受连接及数据交换。UDP套接字则无连接状态。示例展示了TCP服务器和客户端如何使用套接字通信。注意选择唯一地址和端口,处理异常以确保健壮性。学习套接字可为构建网络应用打下基础。
20 7
|
6天前
|
Python
10个python入门小游戏,零基础打通关,就能掌握编程基础_python编写的入门简单小游戏
10个python入门小游戏,零基础打通关,就能掌握编程基础_python编写的入门简单小游戏
|
8天前
|
Python 索引 C语言
Python3从零基础到入门(2)—— 运算符-3
Python3从零基础到入门(2)—— 运算符
|
8天前
|
Python
Python3从零基础到入门(2)—— 运算符-2
Python3从零基础到入门(2)—— 运算符
Python3从零基础到入门(2)—— 运算符-2
|
8天前
|
Python C语言 存储
Python3从零基础到入门(2)—— 运算符-1
Python3从零基础到入门(2)—— 运算符
Python3从零基础到入门(2)—— 运算符-1
|
8天前
|
存储 C语言 Python