Python 中的鸭子类型、作用域和调查函数

Python 是一种鸭子类型语言。这意味着变量的数据类型可以改变,只要语法兼容即可。Python 也是一种动态编程语言。这意味着我们可以在程序运行时更改它,包括定义新函数和名称解析的作用域。这些不仅为我们编写 Python 代码提供了一种新的范式,也为调试提供了一套新工具。接下来,我们将看到在 Python 中可以做到而许多其他语言无法做到的事情。

完成本教程后,您将了解

  • Python 如何管理你定义的变量
  • Python 代码如何使用变量,以及为什么我们不需要像 C 或 Java 那样定义其类型

使用我的新书《Python for Machine Learning》启动你的项目,其中包含分步教程和所有示例的Python源代码文件。

让我们开始吧。

Python 中的鸭子类型、作用域和检查函数。照片由 Julissa Helmuth 提供。部分权利保留

概述

本教程分为三个部分;它们是

  • 编程语言中的鸭子类型
  • Python 中的作用域和命名空间
  • 检查类型和作用域

编程语言中的鸭子类型

鸭子类型是某些现代编程语言的一项特性,允许数据类型动态化。

一种编程风格,它不查看对象的类型来确定它是否具有正确的接口;相反,只是调用或使用方法或属性(“如果它看起来像鸭子,并且像鸭子一样叫,那它一定是一只鸭子。”)通过强调接口而不是特定类型,精心设计的代码通过允许多态替换来提高其灵活性。

Python 术语表

简单来说,程序应该允许你交换数据结构,只要相同的语法仍然有意义。例如,在 C 语言中,你必须定义如下函数:

虽然操作 `x * x` 对于整数和浮点数是相同的,但接受整数参数的函数和接受浮点数参数的函数并不相同。因为 C 语言中的类型是静态的,所以我们必须定义两个函数,即使它们执行相同的逻辑。在 Python 中,类型是动态的;因此,我们可以将相应的函数定义为:

这一特性确实给了我们巨大的能力和便利。例如,在 scikit-learn 中,我们有一个函数用于交叉验证:

但在上面,`model` 是一个 scikit-learn 模型对象的变量。它是什么模型并不重要,无论是上面提到的感知器模型、决策树,还是支持向量机模型。重要的是,在 `cross_val_score()` 函数内部,数据将通过其 `fit()` 函数传递给模型。因此,模型必须实现 `fit()` 成员函数,并且 `fit()` 函数的行为必须相同。结果是 `cross_val_score()` 函数不期望任何特定的模型类型,只要它看起来像一个模型即可。如果我们使用 Keras 来构建神经网络模型,我们可以通过包装器使 Keras 模型看起来像一个 scikit-learn 模型。

在上面,我们使用了 Keras 的包装器。还存在其他包装器,例如 scikeras。它所做的就是确保 Keras 模型的接口看起来像一个 scikit-learn 分类器,这样您就可以使用 `cross_val_score()` 函数。如果我们用以下方式替换上面的 `model`:

那么 scikit-learn 函数将报错,因为它找不到 `model.score()` 函数。

同样,由于鸭子类型,我们可以重用一个期望列表的函数,但提供 NumPy 数组或 pandas Series,因为它们都支持相同的索引和切片操作。例如,我们使用 ARIMA 对时间序列进行拟合,如下所示:

上面的代码应该为每次拟合产生相同的 AIC 分数。

Python 中的作用域和命名空间

在大多数语言中,变量定义在有限的作用域内。例如,在函数内定义的变量只能在该函数内访问。

局部变量 `discrim` 在 `quadratic()` 函数之外的任何地方都无法访问。此外,这可能会让某些人感到惊讶:

我们在函数 `f` 外部定义了变量 `a`,但在 `f` 内部,变量 `a` 被赋值为 `2 * x`。但是,函数内部的 `a` 和外部的 `a` 除了名称之外是无关的。因此,当我们退出函数时,`a` 的值不会改变。为了使其在函数 `f` 内部可修改,我们需要将名称 `a` 声明为 `global`,这样就可以清楚地表明该名称应该来自全局作用域,而不是局部作用域

然而,当我们引入函数中的嵌套作用域时,我们可能会进一步使问题复杂化。考虑以下示例:

函数 `f` 中的变量 `a` 与全局变量不同。但是,在 `g` 中,由于没有向 `a` 写入任何内容,只是从它读取,Python 会看到来自最近作用域的同一个 `a`,即函数 `f` 中的 `a`。然而,变量 `x` 是作为函数 `g` 的参数定义的,当我们调用 `g(3)` 时,它取值为 `3`,而不是假定函数 `f` 中的 `x` 的值。

注意:如果一个变量在函数中任何地方被赋值,它就被定义在局部作用域中。如果该变量在赋值之前就尝试从它读取值,则会引发错误,而不是使用来自外部或全局作用域的同名变量的值。

这个属性有很多用途。Python 中许多记忆化装饰器的实现巧妙地利用了函数作用域。另一个例子是:

这是一个生成器函数,它从输入的 NumPy 数组 `X` 和 `y` 创建样本批次。Keras 模型可以在其训练中使用这样的生成器。但是,出于交叉验证等原因,我们不想从整个输入数组 `X` 和 `y` 中采样,而是从它们中采样固定的行子集。我们的做法是在 `datagen()` 函数的开头随机选择一部分行,并将它们保留在 `Xsam`、`ysam` 中。然后在内部函数 `_gen()` 中,从 `Xsam` 和 `ysam` 中采样行,直到创建批次。虽然列表 `Xbatch` 和 `ybatch` 在 `_gen()` 函数内部定义和创建,但数组 `Xsam` 和 `ysam` 并不是 `_gen()` 的局部变量。更有趣的是,当生成器创建时:

函数 `datagen()` 被调用了两次,因此创建了两组不同的 `Xsam`、`ysam`。但是,由于内部函数 `_gen()` 依赖于它们,这两组 `Xsam`、`ysam` 会同时驻留在内存中。技术上讲,当我们调用 `datagen()` 时,会创建一个闭包,其中包含定义的特定 `Xsam`、`ysam`,而对 `_gen()` 的调用会访问该闭包。换句话说,`datagen()` 调用的两个实例的作用域是共存的。

总之,当一行代码引用一个名称(无论是变量、函数还是模块)时,该名称将按照 LEGB 规则的顺序进行解析。

  1. 首先是局部作用域,即在同一个函数中定义的那些名称。
  2. 闭包或“ nonlocal ”作用域。如果我们位于嵌套函数内部,那就是上一级函数。
  3. 全局作用域,即在同一脚本的顶层定义的那些名称(但不跨不同的程序文件)。
  4. 内置作用域,即 Python 自动创建的那些名称,例如变量 __name__ 或函数 list()

想开始学习机器学习 Python 吗?

立即参加我为期7天的免费电子邮件速成课程(附示例代码)。

点击注册,同时获得该课程的免费PDF电子书版本。

检查类型和作用域

由于 Python 中的类型不是静态的,有时我们想知道我们正在处理什么,但从代码中很难判断。一种方法是使用 type()isinstance() 函数。例如:

type() 函数返回一个类型对象。isinstance() 函数返回一个布尔值,允许我们检查某物是否匹配特定类型。如果我们想知道变量的类型,这些函数很有用。如果我们正在调试代码,这会很有帮助。例如,如果我们把一个 pandas DataFrame 传递给上面定义的 datagen() 函数:

在 Python 的调试器 pdb 下运行上述代码将得到以下结果:

从堆栈跟踪中可以看出,出现了问题,因为我们无法获取 ysam[i]。我们可以使用以下方法来验证 ysam 确实是一个 Pandas DataFrame 而不是 NumPy 数组。

因此,我们不能使用 ysam[i]ysam 中选择第 i 行。在调试器中,我们可以做什么来验证如何修改我们的代码?有几个有用的函数可以用来检查变量和作用域。

  • dir() 用于查看作用域中定义的名称或对象中定义的属性。
  • locals()globals() 分别用于查看局部和全局定义的名称和值。

例如,我们可以使用 dir(ysam) 来查看 ysam 中定义了哪些属性或函数。

其中一些是属性,例如 shape,一些是函数,例如 describe()。您可以在 pdb 中读取属性或调用函数。通过仔细阅读此输出,我们回想起从 DataFrame 中读取第 i 行的方法是通过 iloc,因此我们可以通过以下方式验证语法:

如果调用不带参数的 dir(),它会显示当前作用域中定义的所有名称,例如:

作用域会随着您在调用堆栈中的移动而改变。与不带参数的 dir() 类似,我们可以调用 locals() 来显示所有局部定义的变量,例如:

确实,locals() 返回一个 dict,允许您查看所有名称和值。因此,如果我们想读取变量 Xbatch,我们可以通过 locals()["Xbatch"] 来获取。类似地,我们可以使用 globals() 来获取全局作用域中定义的名称的字典。

这项技术有时很有益。例如,我们可以通过使用 dir(model) 来检查 Keras 模型是否“已编译”。在 Keras 中,编译模型是为训练设置损失函数并构建前向和后向传播的流程。因此,已编译的模型将具有一个额外的属性 loss

这使我们能够在代码中添加一个额外的检查,以避免出现错误。

延伸阅读

如果您想深入了解,本节提供了更多关于该主题的资源。

文章

书籍

总结

在本教程中,您了解了 Python 如何组织命名作用域以及变量如何与代码交互。具体来说,您学到了:

  • Python 代码通过变量的接口来使用它们;因此,变量的数据类型通常不重要。
  • Python 变量在其命名作用域或闭包中定义,同名变量可以在不同作用域中共存,因此它们不会相互干扰。
  • 我们有一些 Python 内置函数,允许我们检查当前作用域中定义的名称或变量的数据类型。

掌握机器学习 Python!

Python For Machine Learning

更自信地用 Python 编写代码

...从学习实用的 Python 技巧开始

在我的新电子书中探索如何实现
用于机器学习的 Python

它提供自学教程数百个可运行的代码,为您提供包括以下技能:
调试性能分析鸭子类型装饰器部署等等...

向您展示高级 Python 工具箱,用于
您的项目


查看内容

对“鸭子类型、作用域和调查函数在 Python 中”的 2 条回复

  1. Chandra 2022 年 2 月 17 日下午 6:50 #

    很棒的观点!Jason 博士。感谢分享。我只是快速浏览了一下,希望能再读一遍以深入理解。祝好!

    • James Carmichael 2022 年 2 月 18 日下午 12:52 #

      不客气,Chandra!

留下回复

Machine Learning Mastery 是 Guiding Tech Media 的一部分,Guiding Tech Media 是一家领先的数字媒体出版商,专注于帮助人们了解技术。访问我们的公司网站以了解更多关于我们的使命和团队的信息。