MicroPython-物联网教程-二-

114 阅读1小时+

MicroPython 物联网教程(二)

原文:MicroPython for the Internet of Things

协议:CC BY-NC-SA 4.0

四、如何在 MicroPython 中编程

现在我们已经对各种 MicroPython 板有了基本的了解,我们可以学习更多关于 MicroPython 编程的知识——这是一种非常健壮和强大的语言,可以用来编写非常强大的应用。掌握 MicroPython 非常容易,有些人可能认为使用它不需要任何正式的培训。这在很大程度上是正确的,因此您应该只需要一点点语言知识就能够编写 MicroPython 脚本。

鉴于 MicroPython 是 Python,我们可以先通过我们 PC 上的例子来学习 Python 语言的基础。因此,本章介绍了 Python 编程基础的速成课程,包括对一些最常用语言特性的解释。因此,本章将为您提供理解互联网上的 Python IOT 项目示例所需的技能。本章还通过可以在 PC 或 MicroPython 板上运行的例子演示了如何用 Python 编程。所以,让我们开始吧!

Note

在本章中,我使用术语 Python 来描述同时适用于 MicroPython 和 Python 的编程概念。MicroPython 特有的概念使用术语 MicroPython。

现在让我们学习一些 Python 编程的基本概念。我们将从语言的构件开始,比如变量、模块和基本语句,然后进入更复杂的流控制和数据结构的概念。虽然这些材料看起来很仓促,但是本 Python 教程只涵盖了该语言最基本的知识,以及如何在 PC 和 MicroPython 板上使用它。它旨在帮助您开始编写 Python IOT 应用。

如果你知道 Python 编程的基础,请随意浏览这一章。但是,我建议您完成本章末尾的示例项目,尤其是如果您没有编写过很多 Python 应用的话。

下面几节介绍了 Python 编程的许多基本特性,您需要了解这些特性才能理解本书中的示例项目。

基本概念

Python 是一种高级的、解释性的、面向对象的脚本语言。Python 的最大目标之一是拥有一个清晰、易于理解的语法,读起来尽可能接近英语。也就是说,即使你没有学过 Python 语言,你也应该能够阅读和理解 Python 脚本。Python 也比其他语言有更少的标点符号(特殊符号)和更少的语法机制。下面列出了 Python 的一些关键特性。

  • 解释器在运行时处理 Python。不使用外部(单独的)编译器。
  • Python 通过类的方式支持面向对象的编程结构。
  • 对于初级程序员来说,Python 是一种很好的语言,并且支持各种应用的开发。
  • Python 是一种脚本语言,但可用于广泛的应用。
  • Python 在全世界范围内都非常流行和使用,这给了它一个巨大的支持基础。
  • Python 关键字少,结构简单,语法定义清晰。这使得学生能够很快学会这门语言。
  • Python 代码定义更清晰,肉眼可见。

Python 可以在你可能遇到或使用的几乎所有平台上下载(python.org/downloads)——甚至是 Windows!Python 是一种非常容易学习的语言,它的结构非常少,甚至有点难学。与其抛出一个示例应用,不如让我们以类似 Python 的方式来学习 Python 的基础知识:一步一步来。

Note

如果你没有 MicroPython 板,也没有在你的 PC 上安装 Python,你应该现在就安装,这样你就可以运行本章中的例子。

代码块

你应该学习的第一件事是 Python 不像其他语言那样使用用符号划分的代码块。更具体地说,对于诸如函数、条件或循环之类的构造来说,局部的代码是使用缩进来指定的。因此,下面的行是缩进的(通过空格或制表符),以便起始字符与结构的代码体对齐。

Tip

下面展示了这个概念的实际应用。如果缩进不一致,Python 解释器会抱怨并产生奇怪的结果。

if (expr1):
    print("inside expr1")
    print("still inside expr1")
else:
    print("inside else")
    print("still inside else")
print("in outer level")

这里我们看到一个条件或 if 语句。注意函数调用print()是缩进的。这向解释器发出信号,表明这些行属于它上面的结构。例如,提到 expr1 的两个 print 语句构成了 if 条件的代码块(当表达式的计算结果为 true 时执行)。类似地,接下来的两个 print 语句构成了 else 条件的代码块。最后,非缩进的行不是条件行的一部分,因此在 if 或 else 之后执行,这取决于表达式的计算。

如您所见,缩进是编写 Python 时需要学习的一个关键概念。尽管这很简单,但是在缩进中出错可能会导致代码意外执行,或者解释器出现更糟糕的错误。

Note

在讨论 Python 时,我将“程序”和“应用”与“脚本”互换使用。虽然从技术上讲,保存在文件中的 Python 代码是一个脚本,但我们通常在“程序”或“应用”更合适的上下文中使用它。

有一个你会经常遇到的特殊符号。注意上面代码中冒号(:)的使用。这个符号用来终止一个构造,并向解释器发出信号,说明声明已经完成,代码块的主体跟在后面。我们把它用于条件、循环、类和函数。

评论

任何编程语言中最基本的概念之一是用不可执行的文本注释源代码的能力,这不仅允许您在代码行之间做笔记,还形成了一种记录源代码的方法。

要向源代码添加注释,请使用井号(#)。在行首放置至少一个符号,为该行创建注释,对后续的每一行重复使用#符号。这将创建所谓的块注释,如图所示。注意,我使用了不带任何文本的注释来创建空白。这有助于提高可读性,并且是块注释的常见做法。

#
# MicroPython for the IOT
#
# Example Python application.
#
# Created by Dr. Charles Bell
#

您也可以将注释放在源代码所在的同一行。编译器将忽略从井号到行尾的所有内容。例如,下面显示了记录变量的常见样式。

zip = 35012                # Zip or postal code
address1= "123 Main St."  # Store the street address

算术

您可以在 Python 中执行许多数学运算,包括常见的原语以及逻辑运算和用于比较值的运算。与其详细讨论这些,我在表 4-1 中提供了一个快速参考,显示了操作和如何使用操作的例子。

表 4-1。

Arithmetic, Logical, and Comparison Operators in Python

| 类型 | 操作员 | 描述 | 例子 | | --- | --- | --- | --- | | 算术 | + | 添加 | `int_var + 1` | |   | - | 减法 | `int_var - 1` | |   | * | 增加 | `int_var * 2` | |   | / | 分开 | `int_var / 3` | |   | % | 系数 | `int_var % 4` | |   | - | 一元减法 | `-int_var` | |   | + | 一元加法 | `+int_var` | | 逻辑学的 | & | 按位 and | `var1&var2` | |   | | | 按位或 | `var1|var2` | |   | ^ | 按位异或 | `var1^var2` | |   | 你是谁 | 逐位补码 | `∼var1` | |   | 和 | 逻辑与 | `var1and var2` | |   | 或者 | 逻辑或 | `var1or var2` | | 比较 | == | 平等的 | `expr1==expr2` | |   | != | 不相等 | `expr1!=expr2` | |   | < | 不到 | `expr1 | 大于 | `expr1>expr2` | |   | <= | 小于或等于 | `expr1<=expr2` | |   | >= | 大于或等于 | `expr1>=expr2` |

按位运算产生对每个位执行的值的结果。逻辑运算符(and、or)产生一个值,该值可为真或为假,通常与表达式或条件一起使用。

输出到屏幕

我们已经看到了一些如何将消息打印到屏幕上的例子,但是没有对所显示的语句进行任何解释。虽然不太可能为您部署的项目打印 MicroPython 板的输出,但是当您可以在屏幕上显示消息时,学习 Python 会容易得多。

正如我们在前面的例子中看到的,你可能想要打印的一些东西是为了传达你的程序内部正在发生的事情。这可以包括简单的消息(字符串),但也可以包括变量、表达式等的值。

正如我们所见,内置的print()函数是显示包含在单引号或双引号中的输出文本的最常见方式。我们还看到了一些使用另一个名为format()的函数的有趣例子。format()函数为每个传递的参数生成一个字符串。这些参数可以是其他字符串、表达式、变量等。该函数与一个特殊字符串一起使用,该字符串包含由花括号{ } (called string interpolation 1 )分隔的替换键。每个替换键包含一个索引(从 0 开始)或一个命名关键字。这个特殊字符串称为格式字符串。让我们看几个例子来说明这个概念。您可以在您的 PC 或 MicroPython 板上运行这些程序。我包括了输出,这样您可以看到每个语句做了什么。

>>> a = 42
>>> b = 1.5
>>> c = "seventy"
>>> print("{0} {1} {2} {3}".format(a,b,c,(2+3)))
42 1.5 seventy 5
>>> print("{a_var} {b_var} {c_var} {0}".format((3*3),c_var=c,b_var=b,a_var=a))
42 1.5 seventy 9

请注意,我创建了三个变量(我们将在下一节讨论变量),用等号(=)给它们分配不同的值。然后,我使用带有四个替换键的格式字符串打印了一条消息,这四个替换键使用索引进行标记。请注意打印语句的输出。请注意,我在结尾处包含了一个表达式,以展示format()函数如何计算表达式。

最后一行更有趣。这里,我使用了三个命名参数(a_varb_varc_var),并在format()函数中使用了一个特殊的参数选项,在这里我给参数赋值。请注意,我以不同的顺序列出了它们。这是使用命名参数的最大优点;它们可以以任何顺序出现,但被放在格式字符串中指定的位置。

如您所见,这只是用 format()函数中的键替换{ }键的一个例子,format()函数将参数转换为字符串。我们在任何需要包含从多个区域或类型收集的数据的字符串的地方使用这种技术。我们可以在上面的例子中看到这一点。

Tip

有关格式字符串的更多信息,请参见 https://docs.python.org/3/library/string.html#formatstrings

现在让我们看看如何在我们的程序(脚本)中使用变量。

Tip

对于那些已经学会用另一种语言如 C 或 C++编程的人来说,Python 允许你用分号(;)终止一个语句;然而,包含它是不必要的,并且被认为是不好的形式。

变量

Python 是一种动态类型语言,这意味着变量的类型(它可以存储的数据类型)是由遇到或使用的上下文决定的。这与 C 和 C++等其他语言形成对比,在这些语言中,必须在使用变量之前声明类型。

Python 中的变量只是命名的内存位置,可以用来在执行过程中存储值。我们通过使用等号赋值来存储值。Python 变量名可以是您想要的任何名称,但是大多数 Python 开发人员都遵循一些规则和惯例。Python 编码标准中列出了这些规则。 2

然而,一般的、首要的规则要求变量名是描述性的、在上下文中有意义的并且容易阅读。也就是说,您应该避免使用带有随机字符、强制缩写、首字母缩略词以及类似的晦涩难懂的名称。按照惯例,变量名应该长于一个字符(除了一些可接受的循环计数变量),并且足够短以避免过长的代码行。

What is a Long Code Line?

大多数人会说一个代码行不应超过 80 个字符,但这是从编程的黑暗时代听来的,那时我们使用穿孔卡,每张卡最多允许 80 个字符,后来的显示设备也有同样的限制。对于现代的宽屏显示器来说,这没什么大不了的,但我仍然建议保持短行以确保更好的可读性。没有人喜欢向下(或向右)滚动阅读!

因此,给变量命名有很大的灵活性。在 PEP8 标准中有额外的规则和指南,如果您希望使您的项目源代码与这些标准保持一致,您应该查看 PEP8 函数、类等的命名标准。有关规则和标准的完整列表,请参见 https://www.python.org/dev/peps/pep-0008 的 pep 8 Python 编码指南。

下面显示了一些简单变量及其动态确定类型的示例。

# floating point number
length = 10.0
# integer
width = 4
# string
box_label = "Tools"
# list
car_makers = ['Ford', 'Chevrolet', 'Dodge']
# tuple
porsche_cars = ('911', 'Cayman', 'Boxster')
# dictionary
address = {"name": "Joe Smith", "Street": "123 Main", "City": "Anytown", "State": "New Happyville"}

那么,我们怎么知道变量width是一个整数呢?仅仅因为数字 4 是一个整数。同样,Python 将“工具”解释为字符串。我们将在下一节看到更多关于最后三种类型和 Python 支持的其他类型的内容。

Tip

有关 Python 编码标准(PEP8)管理的命名约定的更多信息,请参见 https://www.python.org/dev/peps/pep-0008/#naming-conventions

类型

如前所述,Python 不像其他语言那样有正式的类型规范机制。但是,您仍然可以定义变量来存储您想要的任何内容。事实上,Python 允许您基于上下文创建和使用变量,并且您可以使用初始化来“设置”变量的数据类型。下面给出了几个例子。

# Numbers
float_value = 9.75
integer_value = 5

# Strings
my_string = "He says, he's already got one."

print("Floating number: {0}".format(float_value))
print("Integer number: {0}".format(integer_value))
print(my_string)

对于需要转换类型或希望确保值以某种方式键入的情况,有许多用于转换数据的函数。表 4-2 显示了一些更常用的类型转换函数。我将在后面的章节中讨论一些数据结构。

表 4-2。

Type Conversion in Python

| 功能 | 描述 | | --- | --- | | `int(x [,base])` | 将 x 转换为整数。基数是可选的(例如,十六进制为 16)。 | | `long(x [,base])` | 将 x 转换为长整数。 | | `float(x)` | 将 x 转换为浮点。 | | `str(x)` | 将对象 x 转换为字符串。 | | `tuple(t)` | 将 t 转换为元组。 | | `list(l)` | 将 l 转换为列表。 | | `set(s)` | 将转换为集合。 | | `dict(d)` | 创建字典。 | | `chr(x)` | 将整数转换为字符。 | | `hex(x)` | 将整数转换为十六进制字符串。 | | `oct(x)` | 将整数转换为八进制字符串。 |

但是,您应该小心使用这些转换函数,以避免数据丢失或舍入。例如,将浮点数转换为整数可能会导致截断。同样,打印浮点数会导致舍入。

现在让我们看看一些常用的数据结构,包括这个叫做字典的奇怪的东西。

基本数据结构

到目前为止,你所学到的关于 Python 的知识足以编写最基本的程序,而且对于处理本章后面的示例项目来说也绰绰有余。然而,当您开始需要对数据进行操作时——无论是来自用户还是来自传感器和类似来源——您将需要一种方法来组织和存储数据,以及对内存中的数据执行操作。下面按复杂程度介绍三种数据结构:列表、元组和字典。

列表

列表是 Python 中组织数据的一种方式。这是一种构建集合的自由形式的方法。也就是说,项目(或元素)不必是相同的数据类型。列表还允许你做一些有趣的操作,比如在末尾、开头或特殊索引处添加内容。下面演示了如何创建列表。

# List
my_list = ["abacab", 575, "rex, the wonder dog", 24, 5, 6]
my_list.append("end")
my_list.insert(0,"begin")
for item in my_list:
  print("{0}".format(item))

这里我们看到我使用方括号([])创建了列表。列表定义中的项目用逗号分隔。注意,您可以简单地通过设置一个等于[]的变量来创建一个空列表。因为列表和其他数据结构一样,都是对象,所以有几种操作可用于列表,如下所示。

  • append(x):在列表末尾添加 x
  • extend(l):将所有项目添加到列表末尾
  • insert(pos,item):在 pos 位置插入项目
  • remove(value):删除第一个匹配(==)值的项目
  • pop([i]):移除并返回位置 I 或列表末尾的项目
  • index(value):返回第一个匹配项的索引
  • count(value):统计值的出现次数
  • sort():列表排序(升序)
  • reverse():反向排序列表

列表就像其他语言中的数组一样,对于构建动态数据集合非常有用。

元组

另一方面,元组是一种限制性更强的集合类型。也就是说,它们是由一组特定的数据构建的,不允许像列表一样进行操作。事实上,您不能更改元组中的元素。因此,我们可以对不应该改变的数据使用元组。下面显示了一个元组的示例以及如何使用它。

# Tuple
my_tuple = (0,1,2,3,4,5,6,7,8,"nine")
for item in my_tuple:
  print("{0}".format(item))
if 7 in my_tuple:
  print("7 is in the list")

这里我们看到我使用括号()创建了元组。元组定义中的各项用逗号分隔。注意,只需将变量设置为()就可以创建一个空元组。因为元组像其他数据结构一样是对象,所以有如下几种可用的操作,包括对诸如包含、定位等序列的操作。

  • x in t:确定 t 是否包含 x
  • x not in t:确定 t 是否不包含 x
  • s + t:连接元组
  • s[i]:获取元素 I
  • len(t):t 的长度(元素数)
  • min(t):最小(最小值)
  • max(t):最大值(最大值)

如果你想在内存中存储更多的数据,你可以使用一个叫做字典的特殊结构(对象)。

字典

字典是一种数据结构,允许您存储键、值对,通过键来评估数据。字典是一种非常结构化的数据处理方式,也是我们在收集复杂数据时想要使用的最符合逻辑的形式。下面是一个字典的例子。

# Dictionary
my_dictionary = {
  'first_name': "Chuck",
  'last_name': "Bell",
  'age': 36,
  'my_ip': (192,168,1,225),
  42: “What is the meaning of life?”,
}
# Access the keys:
print(my_dictionary.keys())
# Access the items (key, value) pairs
print(my_dictionary.items())
# Access the values
print(my_dictionary.values())
# Create a list of dictionaries
my_addresses = [my_dictionary]

这里发生了很多事情!我们看到一个使用花括号创建字典的基本字典声明。在里面,我们可以创建尽可能多的键、值对,用逗号分隔。使用字符串(我习惯使用单引号,但双引号也可以)或整数定义键,值可以是我们想要的任何数据类型。对于 my_ip 属性,我们还存储了一个元组。

按照字典,我们看到在字典上执行的几个操作,包括打印键、打印所有值和只打印值。下面显示了从 Python 解释器执行该代码片段的输出。

[42, 'first_name', 'last_name', 'age', 'my_ip']
[(42, 'what is the meaning of life?'), ('first_name', 'Chuck'), ('last_name', 'Bell'), ('age', 36), ('my_ip', (192, 168, 1, 225))]
['what is the meaning of life?', 'Chuck', 'Bell', 36, (192, 168, 1, 225)]
'42': what is the meaning of life?
'first_name': Chuck
'last_name': Bell
'age': 36
'my_ip': (192, 168, 1, 225)

正如我们在这个例子中看到的,有几个操作(函数或方法)可用于字典,包括如下。这些操作使得字典成为一个非常强大的编程工具。

  • len(d):d 中的项目数
  • d[k]:带 k 键的 d 项
  • d[k] = x:给键 k 赋值 x
  • del d[k]:用 k 键删除项目
  • k in d:确定 d 是否有一个带有关键字 k 的项目
  • d.items():返回 d 中(键,值)对的列表(视图)
  • d.keys():返回 d 中键的列表(视图)
  • d.values():返回 d 中值的列表(视图)

最重要的是,对象可以放在其他对象中。例如,你可以像我上面做的那样创建一个字典列表,一个包含列表和元组的字典,以及你需要的任何组合。因此,列表、元组和字典是管理程序数据的强大方法。

在下一节中,我们将学习如何控制程序的流程。

声明

现在我们对 Python 的基础有了更多的了解,我们可以发现一些完成项目所需的更复杂的代码概念,比如条件语句和循环。

条件语句

我们还看到了一些简单的条件语句:根据一个或多个表达式的计算来改变执行流程的语句。条件语句允许我们根据一个或多个表达式的计算,将程序的执行指向代码段(块)。Python 中的条件语句是 if 语句。

在我们的示例代码中,我们已经看到了 if 语句的作用。注意,在示例中,我们可以有一个或多个(可选的)else 短语,一旦 if 条件的表达式计算为 false,我们就执行这些短语。我们可以链接 if / else 语句来包含多个条件,其中执行的代码取决于几个条件的评估。下面显示了 if 语句的一般结构。注意在注释中我是如何解释执行是如何到达每个条件的主体的。

if (expr1):
    # execute only if expr1 is true
elif ((expr2) or (expr3)):
    # execute only if expr1 is false *and* either expr2 or expr3 is true
else:
    # execute if both sets of if conditions evaluate to false

虽然您可以尽可能多地链接语句,但在这里要小心,因为您拥有的elif部分越多,就越难理解、维护和避免表达式中的逻辑错误。

还有另一种形式的条件语句,称为三元运算符。在 Python 中,三元运算符通常被称为条件表达式。这些操作符基于条件的真或假来评估某些东西。在 2.4 版本中,它们成为 Python 的一部分。条件表达式是在赋值语句中使用的 if-then-else 结构的简写符号,如下所示。

variable = value_if_true if condition else value_if_false

这里我们看到如果条件被评估为真,则使用 if 前面的值,但是如果条件被评估为假,则使用 else 后面的值。下面是一个简短的例子。

>>> numbers = [1,2,3,4]
>>> for n in numbers:
...   x = 'odd' if n % 2 else 'even'
...   print("{0} is {1}.".format(n, x))
...
1 is odd.
2 is even.
3 is odd.
4 is even.
>>>

条件表达式允许您快速测试条件,而不是使用多行条件语句,这有助于使您的代码更容易阅读(也更短)。

循环用于控制代码块的重复执行。有三种形式的循环,它们的行为略有不同。所有循环都使用条件语句来决定是否重复执行。也就是说,只要条件为真,它们就会重复。两种类型的循环是 while 和 for。我用一个例子来解释每一个。

while 循环的条件位于代码块的“顶部”或开始处。因此,while 循环仅当且仅当条件在第一次通过时评估为 true 时才执行主体。下面说明了 while 循环的语法。只有当某些表达式的计算结果为 true 时,才需要执行代码,此时最好使用这种形式的循环。例如,遍历一个元素个数未知的事物集合(循环,直到集合中的事物用完)。

while (expression):
   # do something here

For 循环由于其独特的形式,有时也被称为计数循环。For 循环允许您定义一个计数变量和一个要迭代的范围或列表。下面说明了 for 循环的结构。这种形式的循环最适合用于在集合中执行操作。在这种情况下,Python 会在每次循环中自动将集合中的每一项放入变量中,直到没有更多项可用为止。

for variable_name in list:
  # do something here

你也可以做范围循环或计数循环。这使用了一个名为range()的特殊函数,它最多接受三个参数,range([start],stop[,step]),其中 start 是起始数字(一个整数),stop 是序列中的最后一个数字,step 是增量。所以,你可以按 1,2,3 等来数。,通过一系列的数字。下面是一个简单的例子。

for i in range(2,9):
   # do something here

你可能会遇到range()的其他用法。更多信息请参见 https://docs.python.org/3/library/functions.html 中关于此功能和其他内置功能的文档。

Python 还提供了一种使用一些特殊关键字来控制循环流(例如,持续时间或终止)的机制,如下所示。

  • break:立即退出循环体
  • continue:跳到循环的下一次迭代
  • else:循环结束时执行代码(如果用 break 语句停止循环,则不执行)

这些关键字有一些用途,尤其是 break,但它不是终止和控制循环的首选方法。也就是说,专业人士认为条件表达式或错误处理代码应该表现得足够好,不需要这些选项。

模块化;模块、函数和类

最后几组主题是最高级的,包括模块化(代码组织)。正如我们将看到的,我们可以使用函数对代码进行分组,消除重复,并将功能封装到对象中。

包括模块

Python 应用可以从 Python 环境提供的可重用库构建。它们也可以从您自己创建或从第三方下载的自定义模块或库构建。这些文件通常作为一组 Python 代码文件分发(例如,文件扩展名为.py的文件)。当我们想使用一个库(函数、类等)时。)包含在一个模块中,我们使用import关键字并列出模块的名称。下面是一些例子。

import os
import sys

前两行演示了如何导入 Python 提供的基本或公共模块。在这种情况下,我们为 os 和 sys 模块(操作系统和 Python 系统函数)使用或导入模块。

Tip

习惯上(但不要求)按字母顺序列出您的导入,首先是内置模块,然后是第三方模块,最后是您自己的模块。

功能

Python 允许在代码中使用模块化。虽然它通过类的方式支持面向对象编程(对于大多数 Python GPIO 示例来说,这是一个不太可能遇到的更高级的特性),但在更基本的层面上,您可以使用函数将代码分成更小的块。

函数使用特殊的关键字构造(Python 中很少见)来定义函数。我们简单地使用def,后跟一个函数名和一个用逗号分隔的参数列表(在括号中)。冒号用于终止声明。下面显示了一个示例。

def print_dictionary(the_dictionary):
    for key, value in the_dictionary.items():
      print("'{0}': {1}".format(key, value))

# define some data
my_dictionary = {
  'name': "Chuck",
  ‘age’: 37,
}

您可能想知道这个奇怪的代码是做什么的。注意,该循环从items()函数的结果中分配了两个值。这是 dictionary 对象提供的一个特殊函数。3items()函数返回键、值对:因此得名变量。

下一行打印出这些值。对于 Python 3 应用来说,使用格式化字符串(其中花括号定义了从 0 开始的参数编号)是很常见的。有关格式化字符串( https://docs.python.org/3/library/string.html#format-string-syntax )的更多信息,请参见 Python 文档。

函数体是缩进的。该函数声明下缩进的所有语句都属于该函数,并在调用该函数时执行。我们可以通过名字调用函数,提供如下参数。注意我是如何使用键名引用字典中的值的。

print_dictionary(my_dictionary)
print(my_dictionary['age'])
print(my_dictionary['name'])

这个示例和上面的代码一起执行时,会生成以下内容。

$ python3
Python 3.6.0 (v3.6.0:41df79263a11, Dec 22 2016, 17:23:13)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def print_dictionary(the_dictionary):
...     for key, value in the_dictionary.items():
...       print("'{0}': {1}".format(key, value))
...
>>> # define some data
... my_dictionary = {
...     'name': "Chuck",
...     'age': 41,
... }
>>> print_dictionary(my_dictionary)
'name': Chuck
'age': 41
>>> print(my_dictionary['age'])
41
>>> print(my_dictionary['name'])
Chuck

现在让我们看看 Python 中最复杂的概念——面向对象编程。

类和对象

你可能听说过 Python 是一种面向对象的编程语言。但这意味着什么呢?简单地说,Python 是一种编程语言,它提供了描述对象(事物)以及可以用对象做什么(操作)的工具。对象是数据抽象的一种高级形式,其中数据对调用者是隐藏的,只能由对象提供的操作(方法)来操作。

我们在 Python 中使用的语法是class语句,您可以使用它来帮助您的项目模块化。所谓模块化,我们的意思是源代码被安排得更容易开发和维护。通常,我们将类放在单独的模块(代码文件)中,这有助于更好地组织代码。虽然这不是必需的,但我建议使用这种将类放在它自己的源文件中的技术。这使得修改类或修复问题(错误)更加容易。

那么,什么是 Python 类呢?让我们从将构造视为一种组织技术开始。我们可以使用类将数据和方法分组在一起。类名紧跟在关键字 class 后面,后面跟一个冒号。像其他方法一样声明其他类方法,除了第一个参数必须是self,它在执行时将方法绑定到类实例。

Note

我更喜欢使用语言设计者或开发人员社区已经采用的术语。例如,有些使用“函数”,但有些可能使用“方法”。还有一些可能使用子程序、例行程序、过程等。你使用哪个术语并不重要,但是你应该努力使用一致的术语。一个可能让一些人感到困惑的例子是,我在讨论面向对象的例子时使用了术语方法。也就是说,一个类有方法而没有函数。然而,你可以用函数代替方法,你仍然是正确的(大多数情况下)。

通过使用类(创建实例)和使用点标记来引用数据成员或函数,使用一种或多种方法来访问数据。让我们看一个例子。清单 4-1 展示了一个完整的类,它描述(模拟)了用于运输的车辆的最基本特征。我创建了一个名为vehicle.py的文件来包含这段代码。

#
# MicroPython for the IOT
#
# Class Example: A generic vehicle
#
# Dr. Charles Bell
#
class Vehicle:
    """Base class for defining vehicles"""
    axles = 0
    doors = 0
    occupants = 0

    def __init__(self, num_axles, num_doors):
        self.axles = num_axles
        self.doors = num_doors

    def get_axles(self):
        return self.axles

    def get_doors(self):
        return self.doors

    def add_occupant(self):
        self.occupants += 1

    def num_occupants(self):
        return self.occupants

Listing 4-1.
Vehicle class

注意这里的一些事情。首先,有一个名为 init()的方法。这是构造函数,在创建类实例时调用。您将所有初始化代码像设置变量一样放在这个方法中。我们也有返回轴、门和居住者数量的方法。我们在这个类中有一个方法:添加居住者。

还要注意,我们使用self.<name>来处理每个类属性(数据)。这就是我们如何确保我们总是访问与创建的实例相关联的数据,而不是全局变量或其他局部变量。

让我们看看这个类如何被用来定义一个家庭轿车。清单 4-2 展示了使用这个类的代码。我们可以将这段代码放在一个名为sedan.py的文件中。

#
# MicroPython for the IOT
#
# Class Example: Using the generic Vehicle class
#
# Dr. Charles Bell
#
from vehicle import Vehicle

sedan = Vehicle(2, 4)
sedan.add_occupant()
sedan.add_occupant()
sedan.add_occupant()
print("The car has {0} occupants.".format(sedan.num_occupants()))

Listing 4-2.Using the Vehicle class

注意,第一行从 vehicle 模块导入了 Vehicle 类。注意,我大写了类名,而不是文件名。这是一种非常常见的命名方案。接下来,在代码中,我们创建类的一个实例。注意我把 2,4 传递给了类名。这将导致在实例化类时调用__init__()方法。变量sedan变成了我们可以操作的类实例变量(对象),我通过添加三个居住者然后使用Vehicle类中的方法打印出居住者的数量来实现。

我们可以使用下面的命令在 PC 上运行代码。正如我们所看到的,当代码运行时,它告诉我们车上有三个人。不错。

$ python ./sedan.py
The car has 3 occupants.

Object-Oriented Programming (OOP) Terminology

像任何技术或概念一样,有一定数量的术语,您必须学会这些术语,才能理解技术并与他人交流。下面简要描述了一些您需要了解的术语,以便更好地了解面向对象编程。

属性:类中的数据元素。

类:用于以属性(数据)和对数据进行操作的方法(函数)的形式定义对象的代码构造。Python 中的方法和属性可以使用点符号来访问。

类实例变量:用于存储对象实例的变量。它们像其他变量一样使用,与点符号结合,允许我们操作对象。

实例:类的可执行形式,通过将类赋给变量来创建,将代码初始化为对象。

继承:将一个类的属性和方法包含在另一个类中。

实例化:创建一个类的实例。

方法重载:创建两个或多个同名但参数不同的方法。这允许我们创建具有相同名称的方法,但是根据所传递的参数,操作可能不同。

多态性:从基类继承属性和方法,添加额外的方法或覆盖(改变)方法。

还有很多 OOP 术语,但这些是你最常遇到的。

现在,让我们看看如何使用 vehicle 类来演示继承。在这种情况下,我们将创建一个名为PickupTruck的新类,它使用了 vehicle 类,但是向结果类添加了专门化。清单 4-3 显示了新的类。我将这段代码放在一个名为pickup_truck.py的文件中。如你所见,皮卡是一种交通工具。

#
# MicroPython for the IOT
#
# Class Example: Inheriting the Vehicle class to form a
# model of a pickup truck with maximum occupants and maximum
# payload.
#
# Dr. Charles Bell
#
from vehicle import Vehicle

class PickupTruck(Vehicle):
    """This is a pickup truck that has:
    axles = 2,
    doors = 2,
    __max occupants = 3
    The maximum payload is set on instantiation.
    """
    occupants = 0
    payload = 0
    max_payload = 0

    def __init__(self, max_weight):
        super().__init__(2,2)
        self.max_payload = max_weight
        self.__max_occupants = 3

    def add_occupant(self):
        if (self.occupants < self.__max_occupants):
            super().add_occupant()
        else:
            print("Sorry, only 3 occupants are permitted in the truck.")

    def add_payload(self, num_pounds):
        if ((self.payload + num_pounds) < self.max_payload):
            self.payload += num_pounds
        else:
            print("Overloaded!")

    def remove_payload(self, num_pounds):
        if ((self.payload - num_pounds) >= 0):
            self.payload -= num_pounds
        else:
            print("Nothing in the truck.")

    def get_payload(self):
        return self.payload

Listing 4-3.
Pickup Truck class

注意这里的一些事情。首先注意类语句:class PickupTruck(Vehicle) :。当我们想从另一个类继承时,我们用基类的名字加上括号。这确保 Python 将使用基类,允许派生类使用其所有可访问的数据和内存。如果你想从一个以上的类继承,你可以(称为多重继承),只要用逗号分隔的列表列出基(父)类。

接下来,注意__max_occupants变量。按照惯例,在一个类中为一个属性或方法使用两个下划线会使该项成为该类的私有项。 4 也就是说,它应该只能从类内部访问。该类的调用方(通过类变量/实例)不能访问私有项,从该类派生的任何类也不能访问私有项。隐藏属性(数据)总是一个好的做法。

您可能想知道 occupant 方法发生了什么变化。他们为什么不在新班级?它们不在那里,因为我们的新类继承了基类的所有行为。不仅如此,还修改了代码,将乘员限制为三人。

我还想指出我添加到该类中的文档。我们使用文档字符串(前后使用三个双引号的字符串)来记录类。您可以将文档放在这里解释该类及其方法。稍后我们会看到它的一个很好的用途。

最后,请注意构造函数中的代码。这演示了如何调用基类方法,我这样做是为了设置轴和门的数量。如果我们想调用基类方法的版本,我们可以在其他方法中做同样的事情。

现在,让我们写一些代码来使用这个类。清单 4-4 显示了我们用来测试这个类的代码。在这里,我们创建了一个名为 pickup.py 的文件,该文件创建了一个皮卡实例,添加了乘员和有效载荷,然后打印出卡车的内容。

#
# MicroPython for the IOT
#
# Class Example: Exercising the PickupTruck class.
#
# Dr. Charles Bell
#
from pickup_truck import PickupTruck

pickup = PickupTruck(500)
pickup.add_occupant()
pickup.add_occupant()
pickup.add_occupant()
pickup.add_occupant()
pickup.add_payload(100)
pickup.add_payload(300)
print("Number of occupants in truck = {0}.".format(pickup.num_occupants()))
print("Weight in truck = {0}.".format(pickup.get_payload()))
pickup.add_payload(200)
pickup.remove_payload(400)
pickup.remove_payload(10)

Listing 4-4.Using the PickupTruck class

注意,我添加了几个对add_occupant()方法的调用,新类继承并覆盖了它。我还添加了一些调用,这样我们就可以在检查过度占用和最大有效负载能力的方法中测试代码。当我们运行这段代码时,我们将看到如下所示的结果。

$ python ./pickup.py
Sorry, only 3 occupants are permitted in the truck.
Number of occupants in truck = 3.
Weight in truck = 400.
Overloaded!
Nothing in the truck.

我再次在我的 PC 上运行这段代码,但是我可以在 MicroPython 板上运行所有这些代码,并且会看到相同的结果。

关于类,我们还应该了解一件事:内置属性。回忆一下__init__()方法。Python 自动提供了几个内置属性,每个属性都以 __ 开头,您可以使用它们来了解更多关于对象的信息。下面列出了几个可用于类的运算符。

  • __dict__:包含类名称空间的字典
  • __doc__:类文档字符串
  • __name__:类名
  • __module__:定义类的模块名
  • __bases__:继承顺序中的基类

下面显示了每个属性为上面的 PickupTruck 类返回的内容。我将这段代码添加到 pickup.py 文件中。

print("PickupTruck.__doc__:", PickupTruck.__doc__)
print("PickupTruck.__name__:", PickupTruck.__name__)
print("PickupTruck.__module__:", PickupTruck.__module__)
print("PickupTruck.__bases__:", PickupTruck.__bases__)
print("PickupTruck.__dict__:", PickupTruck.__dict__)

运行这段代码时,我们会看到以下输出。

PickupTruck.__doc__: This is a pickup truck that has:
    axles = 2,
    doors = 2,
    max occupants = 3
    The maximum payload is set on instantiation.

PickupTruck.__name__: PickupTruck
PickupTruck.__module__: pickup_truck
PickupTruck.__bases__: (<class 'vehicle.Vehicle'>,)
PickupTruck.__dict__: {'__module__': 'pickup_truck', '__doc__': 'This is a pickup truck that has:\n    axles = 2,\n    doors = 2,\n    max occupants = 3\n    The maximum payload is set on instantiation.\n    ', 'occupants': 0, 'payload': 0, 'max_payload': 0, ' _PickupTruck__max_occupants': 3, '__init__': <function PickupTruck.__init__ at 0x1018a1488>, 'add_occupant': <function PickupTruck.add_occupant at 0x1018a17b8>, 'add_payload': <function PickupTruck.add_payload at 0x1018a1840>, 'remove_payload': <function PickupTruck.remove_payload at 0x1018a18c8>, 'get_payload': <function PickupTruck.get_payload at 0x1018a1950>}

当您需要关于一个类的更多信息时,您可以使用内置属性。注意字典中的_PickupTruck__max_occupants条目。回想一下,我们制作了一个伪私有变量__max_occupants。在这里,我们看到 Python 是如何通过在变量前添加类名来引用变量的。请记住,以两个下划线(而不是一个)开头的变量表示它应该被认为是该类的私有变量,并且只能在该类内部使用。

Tip

有关 Python 中类的更多信息,请参见 https://docs.python.org/3/tutorial/classes.html

现在,让我们看几个可以用来练习的 Python 程序的例子。与前面的例子一样,您可以在 PC 或 MicroPython 板上编写和执行这些代码。

通过示例学习 Python

学习如何用任何语言编程的最好方法是用例子练习。在这一节中,我将展示几个例子,您可以用它们来练习 Python 编码。您可以使用 MicroPython 板或 PC 来运行这些示例。我通过 Python 控制台在我的 PC 上展示了前两个例子,通过 REPL 控制台使用 MicroPython 板展示了后两个例子。

我详细解释了每个示例的代码,并展示了执行代码时的示例输出,以及让您自己尝试对每个示例进行一两次修改的挑战。我鼓励你实现这些例子,并在本书后面的项目实践中自己找出挑战。

示例 1:使用循环

这个例子演示了如何使用 for 循环在 Python 中编写循环。我们试图解决的问题是将整数从十进制转换成二进制、十六进制和八进制。通常在 IOT 项目中,我们需要看到一种或多种格式的值,在某些情况下,我们使用的传感器(和相关文档)使用十六进制而不是十进制。因此,这个例子不仅对如何使用 for 循环,而且对如何将整数转换成不同的格式都有帮助。

写代码

该示例从要转换的整数元组开始。可以使用 for 循环遍历元组和列表(按顺序读取值)。回想一下,一个元组是只读的,所以在这种情况下,因为它是输入的,所以它是好的,但在其他情况下,您可能需要更改值,您将需要使用列表。回想一下,元组和列表之间的语法差异是元组使用括号,而列表使用方括号。

这里演示的 for 循环称为“for each”循环。注意,我使用了语法“for value in values,”,它告诉 Python 遍历名为values的元组,每次遍历元组时,将每个条目提取(存储)到value变量中。

最后,我使用print()format()函数替换两个占位符{0}{1},使用方法bin()为二进制、oct()为八进制、hex()为十六进制打印出不同格式的整数。清单 4-5 展示了将整数转换成不同形式的例子。

#
# MicroPython for the IOT
#
# Example: Convert integer to binary, hex, and octal
#
# Dr. Charles Bell
#

# Create a tuple of integer values
values = (12, 450, 1, 89, 2017, 90125)

# Loop through the values and convert each to binary, hex, and octal
for value in values:
    print("{0} in binary is {1}".format(value, bin(value)))
    print("{0} in octal is {1}".format(value, oct(value)))
    print("{0} in hexadecimal is {1}".format(value, hex(value)))

Listing 4-5.Converting Integers

执行代码

您可以将这段代码保存在 PC 上一个名为conversions.py的文件中,然后打开一个终端(控制台窗口)并使用命令python ./conversions.py运行这段代码(如果您安装了多个版本的 python,则使用 python3)。清单 4-6 显示了输出。

$ python3 ./conversions.py
12 in binary is 0b1100
12 in octal is 0o14
12 in hexadecimal is 0xc
450 in binary is 0b111000010
450 in octal is 0o702
450 in hexadecimal is 0x1c2
1 in binary is 0b1
1 in octal is 0o1
1 in hexadecimal is 0x1
89 in binary is 0b1011001
89 in octal is 0o131
89 in hexadecimal is 0x59
2017 in binary is 0b11111100001
2017 in octal is 0o3741
2017 in hexadecimal is 0x7e1
90125 in binary is 0b10110000000001101
90125 in octal is 0o260015
90125 in hexadecimal is 0x1600d
Listing 4-6.Conversions Example Output

注意元组中所有被转换的值。

你的挑战

为了使这个示例更好,不使用静态元组来包含硬编码的整数,而是重写这个示例,以便从命令行上的参数中读取整数以及格式。例如,代码将按如下方式执行。

$ python3 ./conversions.py 123 hex
123 in hexadecimal is 0x7b

要从命令行读取参数,请使用参数解析器,argparse ( https://docs.python.org/3/howto/argparse.html )。如果你想从命令行读取整数,你可以使用argparse模块来添加一个参数,如下所示。

import argparse

# Setup the argument parser
parser = argparse.ArgumentParser()

# We need two arguments: integer, and conversion
parser.add_argument("original_val")
parser.add_argument("conversion")

# Get the arguments
args = parser.parse_args()

当您使用参数解析器(argparse)模块时,参数的值都是字符串,因此您需要在使用bin()hex()oct()方法之前将值转换为整数。

您还需要确定所请求的转换。我建议只使用十六进制、二进制和十进制进行转换,并使用一组条件来检查所请求的转换。类似于下面的内容会起作用。

if args.conversion == 'bin':
    # do conversion to binary
elif args.conversion == 'oct':
    # do conversion to octal
elif args.conversion == 'hex':
    # do conversion to hexadecimal
else:
    print("Sorry, I don't understand, {0}.".format(args.conversion))

请注意,最后一个 else 通知参数未被识别。这有助于管理用户错误。

关于参数解析器,还有一点你应该知道。添加参数时,可以传入一个帮助字符串。参数解析器还免费为您提供帮助参数(-h)。请注意以下事项。注意,我使用 help=参数添加了几个字符串。

# We need two arguments: integer, and conversion
parser.add_argument("original_val", help="Value to convert.")
parser.add_argument("conversion", help="Conversion options: hex, bin, or oct.")

现在,当我们完成代码并使用-h选项运行它时,我们会得到下面的输出。酷吧。

$ python3 ./conversions.py -h
usage: conversions.py [-h] original_val conversion

positional arguments:
  original_val  Value to convert.
  conversion    Conversion options: hex, bin, or oct.

optional arguments:
  -h, --help    show this help message

  and exit

示例 2:使用复杂的数据和文件

这个例子演示了如何在 Python 中使用 JavaScript 对象符号 5 (JSON)。简而言之,JSON 是一种用于交换数据的标记语言。它不仅可读,还可以直接在应用中使用,在其他应用、服务器甚至 MySQL 之间存储和检索数据。事实上,程序员对 JSON 很熟悉,因为它类似于其他标记方案。JSON 也非常简单,因为它只支持两种类型的结构:1)包含(名称,值)对的集合,2)有序列表(或数组)。当然,您也可以混合和匹配一个对象中的结构。当我们创建一个 JSON 对象时,我们称之为 JSON 文档。

我们试图解决的问题是向/从文件中写入和读取数据。在这种情况下,我们将使用一个名为json的特殊 JSON 编码器和解码器模块,它允许我们轻松地将文件(或其他流)中的数据与 JSON 相互转换。正如您将看到的,访问 JSON 数据很容易,只需使用键名(有时称为字段)来访问数据。因此,这个例子不仅对将来如何使用读写文件有帮助,而且对如何使用 JSON 文档也有帮助。

写代码

此示例存储和检索文件中的数据。这些数据是关于宠物的基本信息,包括名字、年龄、品种和类型。该类型用于确定广泛的类别,如鱼、狗或猫。

我们从导入 JSON 模块(名为json)开始,该模块内置于 MicroPython 平台中。接下来,我们通过构建 JSON 文档并将其存储在 Python 列表中来准备一些初始数据。我们使用json.loads()方法传入一个 JSON 格式的字符串。结果是一个 JSON 文档,我们可以将它添加到我们的列表中。这些例子使用了一种非常简单的 JSON 文档——一组(名称,值)对。下面显示了一个使用的 JSON 格式字符串的例子。

{"name":"Violet", "age": 6, "breed":"dachshund", "type":"dog"}

注意,我们用花括号将字符串括起来,并使用一系列键名、一个冒号和一个用逗号分隔的值。如果这看起来很熟悉,那是因为与 Python 字典的格式相同。这证明了我的评论,即程序员对 JSON 语法很熟悉。

JSON 方法json.loads()获取 JSON 格式的字符串,然后分析字符串的有效性并返回一个 JSON 文档。然后,我们将该文档存储在一个变量中,并将其添加到列表中,如下所示。

parsed_json = json.loads('{"name":"Violet", "age": 6, "breed":"dachshund", "type":"dog"}')
pets.append(parsed_json)

一旦数据被添加到列表中,我们就把数据写到一个名为my_data.json的文件中。为了处理文件,我们首先用open()函数打开文件,它接受一个文件名(如果您想把文件放在一个目录中,还包括一个路径)和一个访问模式。我们用“r”读,用“w”写。如果你想打开一个文件并添加到末尾,你也可以使用“a”进行追加。请注意,当您写入文件时,“w”访问将会覆盖该文件。如果open()函数成功,您将获得一个 file 对象,它允许您调用额外的函数来读取或写入数据。如果文件不存在(并且您已经请求了读取权限)或者您没有权限写入文件,那么open()将会失败。

如果你想知道还有哪些访问模式,表 4-3 显示了open()功能可用的模式列表。

表 4-3。

Python File Access Modes

| 方式 | 描述 | | --- | --- | | `r` | 以只读方式打开文件。文件指针放在文件的开头。这是默认模式。 | | `rb` | 以二进制格式打开只读文件。文件指针放在文件的开头。这是默认模式。 | | `r+` | 打开文件进行读写。文件指针放在文件的开头。 | | `rb+` | 以二进制格式打开文件进行读写。文件指针放在文件的开头。 | | `w` | 打开一个只写的文件。如果文件存在,则覆盖文件。如果该文件不存在,它将创建一个新文件进行写入。 | | `wb` | 以二进制格式打开一个只写的文件。如果文件存在,则覆盖文件。如果该文件不存在,它将创建一个新文件进行写入。 | | `w+` | 打开文件进行读写。如果文件存在,则覆盖现有文件。如果文件不存在,它会创建一个新文件进行读写。 | | `wb+` | 以二进制格式打开文件进行读写。如果文件存在,则覆盖现有文件。如果文件不存在,它会创建一个新文件进行读写。 | | `a` | 打开要追加的文件。如果文件存在,文件指针在文件的末尾。也就是说,文件处于追加模式。如果该文件不存在,它将创建一个新文件进行写入。 | | `ab` | 以二进制格式打开要追加的文件。如果文件存在,文件指针在文件的末尾。也就是说,文件处于追加模式。如果该文件不存在,它将创建一个新文件进行写入。 | | `a+` | 打开文件进行追加和读取。如果文件存在,文件指针在文件的末尾。文件以追加模式打开。如果文件不存在,它会创建一个新文件进行读写。 | | `ab+` | 以二进制格式打开文件进行追加和读取。如果文件存在,文件指针在文件的末尾。文件以追加模式打开。如果文件不存在,它会创建一个新文件进行读写。 |

文件打开后,我们可以通过遍历列表将 JSON 文档写入文件。迭代意味着从第一个元素开始,按顺序(它们在列表中出现的顺序)一次访问列表中的一个元素。回想一下,Python 中的迭代非常容易。我们简单地说,“对于列表中的每一项”,for 循环如下。

for pet in pets:
  // do something with the pet data

为了将 JSON 文档写入文件,我们使用了json.dumps()方法,这将产生一个 JSON 格式的字符串,使用 file 变量和write()方法将该字符串写入文件。因此,我们现在看到如何从字符串构建 JSON 文档,然后将它们解码(转储)成一个字符串。

一旦我们将数据写入文件,我们就用close()函数关闭文件,然后重新打开它并从文件中读取数据。在这种情况下,我们使用 for 循环的另一种特殊实现。我们使用 file 变量通过readlines()方法读取文件中的所有行,然后用下面的代码遍历它们。

json_file = open("my_data.json", "r")
for pet in json_file.readlines():
  // do something with the pet string

我们再次使用json.loads()方法读取从文件中读取的 JSON 格式的字符串,将其转换为 JSON 文档,并将其添加到另一个列表中。然后我们关闭文件。现在数据已经读回到我们的程序中,我们可以使用它了。最后,我们遍历新列表,并使用键名从 JSON 文档中打印出数据,以检索我们想要的数据。清单 4-7 显示了这个例子的完整代码。

#
# MicroPython for the IOT
#
# Example: Storing and retrieving JSON objects in files
#
# Dr. Charles Bell
#

import json

# Prepare a list of JSON documents for pets by converting JSON to a dictionary
pets = []
parsed_json = json.loads('{"name":"Violet", "age": 6, "breed":"dachshund", "type":"dog"}')
pets.append(parsed_json)
parsed_json = json.loads('{"name": "JonJon", "age": 15, "breed":"poodle", "type":"dog"}')
pets.append(parsed_json)
parsed_json = json.loads('{"name": "Mister", "age": 4, "breed":"siberian khatru", "type":"cat"}')
pets.append(parsed_json)
parsed_json = json.loads('{"name": "Spot", "age": 7, "breed":"koi", "type":"fish"}')
pets.append(parsed_json)
parsed_json = json.loads('{"name": "Charlie", "age": 6, "breed":"dachshund", "type":"dog"}')
pets.append(parsed_json)

# Now, write these entries to a file. Note: overwrites the file
json_file = open("my_data.json", "w")
for pet in pets:
    json_file.write(json.dumps(pet))
    json_file.write("\n")
json_file.close()

# Now, let's read the JSON documents

then print the name and age for all of the dogs in the list
my_pets = []
json_file = open("my_data.json", "r")
for pet in json_file.readlines():
    parsed_json = json.loads(pet)
    my_pets.append(parsed_json)
json_file.close()

print("Name, Age")
for pet in my_pets:
    if pet['type'] == 'dog':
        print("{0}, {1}".format(pet['name'], pet['age']))

Listing 4-7.Writing and Reading JSON Objects to/from Files

注意写数据的循环。我们添加了第二个write()方法,传入一个奇怪的字符串(它实际上是一个转义字符)。\n是一个叫做换行符的特殊字符。这迫使 JSON 格式的字符串在文件中位于单独的行上,有助于提高可读性。

Tip

要更深入地了解如何在 Python 中处理文件,请参见 https://docs.python.org/3/tutorial/inputoutput.html#reading-and-writing-files

那么,文件看起来像什么?下面是使用 more 实用程序的文件转储,它显示了文件的内容。注意,该文件包含 JSON 格式的字符串,就像我们在代码中一样。

$ more my_data.json
{"age": 6, "breed": "dachshund", "type": "dog", "name": "Violet"}
{"age": 15, "breed": "poodle", "type": "dog", "name": "JonJon"}
{"age": 4, "breed": "siberian khatru", "type": "cat", "name": "Mister"}
{"age": 7, "breed": "koi", "type": "fish", "name": "Spot"}
{"age": 6, "breed": "dachshund", "type": "dog", "name": "Charlie"}

现在,让我们看看运行这个脚本会发生什么。

执行代码

您可以将此代码保存在 PC 上名为rw_json.py的文件中,然后打开一个终端(控制台窗口)并使用命令python ./rw_json.py运行代码(如果您安装了多个版本的 python,则使用 python3)。下面显示了输出。

$ python ./rw_json.py
Name, Age
Violet, 6
JonJon, 15
Charlie, 6

虽然输出可能不是很令人印象深刻,但是通过完成这个示例,您已经学到了很多关于使用 JSON 文档处理文件和结构化数据的知识。

你的挑战

为了让这个例子更有挑战性,您可以修改它,以包含更多关于您的宠物的信息。我建议你从一个简单的文本文件开始,为你的宠物输入 JSON 格式的字符串。要增加复杂性,请尝试添加与宠物类型相关的信息。例如,您可以为一个或多个宠物添加一些关键点,为其他宠物添加其他关键点,等等。这样做将显示 JSON 文档的一个强大之处;JSON 文档的集合不必具有相同的格式。

有了这个文件后,修改代码,从文件中读取并打印出每只宠物的所有信息,方法是打印键名和值。提示:您将需要使用特殊的代码来打印出键名和值,称为“漂亮打印”例如,下面的代码将以易读的格式打印出 JSON 文档。注意,我们使用sort_keys选项来打印键(字段),我们可以控制缩进的空格数。

for pet in my_pets:
    print(json.dumps(pet, sort_keys=True, indent=4))

运行时,输出将如下所示。

{
    "age": 6,
    "breed": "dachshund",
    "name": "Violet",
    "type": "dog"
}
{
    "age": 15,
    "breed": "poodle",
    "name": "JonJon",
    "type": "dog"
}

示例 3:使用函数

这个例子演示了如何创建和使用函数。回调函数用于帮助我们的代码更加模块化。函数也是避免代码重复的重要工具。也就是说,我们可以通过将部分代码放在函数中来重复使用它们。函数也用于帮助隔离特殊操作的代码,例如数学公式。

我们在这个例子中探索的问题是如何创建函数来执行计算。我们还将探索一种常见的计算机科学技术,称为递归 6 ,在这种技术中,函数会重复调用自身。我还将向您展示以迭代方式实现的相同功能(通常使用循环)。虽然有些人建议避免递归,但是递归函数写起来更短,但是如果出错的话,调试起来会更困难。我能提供的最好的建议是,几乎每个递归函数都可以写成迭代函数,新手程序员应该坚持迭代解决方案,直到他们对使用函数有了信心。

写代码

这个例子是用来计算斐波那契数列的。 7 斐波纳契数列是以数列中前两个值的和来计算的。该序列从 1 开始,后跟 1(无加 1),然后是 1 + 1 = 2,依此类推。对于这个例子,我们将要求用户输入一个整数,然后计算斐波那契数列中值的个数。如果输入为 5,则序列为 1、1、2、3、5。

我们将创建两个函数:一个使用迭代计算数列的代码计算斐波那契数列,另一个使用递归函数计算第 n 个斐波那契数列。我们先来看迭代函数。

为了定义一个函数,我们使用语法def func_name(<parameters>):,其中我们提供一个函数名和一个零个或多个参数的列表,后跟一个冒号。这些参数可以在函数内部使用。我们使用参数将数据传递给函数。下面显示了斐波那契数列代码的迭代版本。我们把这个函数命名为fibonacci_iterative

def fibonacci_iterative(count):
    i = 1
    if count == 0:
        fib = []
    elif count == 1:
        fib = [1]
    elif count == 2:
        fib = [1,1]
    elif count > 2:
        fib = [1,1]
        while i < (count - 1):
            fib.append(fib[i] + fib[i-1])
            i += 1
    return fib

这段代码简单地计算序列中的前 N 个值,并在一个列表中返回它们。参数 count 是序列中值的数量。该函数首先检查是否请求了小值:值已知的 0、1 或 2。如果计数值大于 2,我们从已知的数列[1,1]开始,然后使用一个循环,通过将前两个值相加来计算下一个值。花点时间注意我是如何使用列表索引来获取列表中的前两个值的(ii-1)。我们将使用这个函数和代码中直接返回的列表来查找序列中的特定值并打印出来。

现在让我们看看函数的递归版本。下面显示了代码。我们将这个函数命名为fibonacci_recursive

def fibonacci_recursive(number):
    if number == 0:
        return 0
    elif number == 1:
        return 1
    else:
        # Call our self counting down.
        value = fibonacci_recursive(number-1) + fibonacci_recursive(number-2)
        return value

在这种情况下,我们不返回整个系列;相反,我们返回序列中的特定值——第 n 个值。与迭代示例一样,我们对返回所请求数字的平凡值做同样的事情。否则,我们对每个数字再次调用相同的函数。你可能需要一些时间来理解它是如何工作的,但是它确实计算了第 n 个值。

现在,您可能想知道将函数放在代码中的什么位置。我们需要将它们放在代码的顶部。Python 将解析函数,并继续执行定义之后的语句。因此,我们将“主”代码放在函数之后。

本例的主要代码从请求斐波那契数列的第 n 个值开始,然后首先使用递归函数来计算该值。然后,我们询问用户是否想要查看整个系列,如果是,我们使用函数的迭代版本来获取列表并将其打印出来。我们打印出第 n 个值,并再次给出查看整个系列的选项,以显示使用两个函数的结果是相同的。清单 4-8 展示了这个例子的完整代码。我们将这个代码命名为fibonacci.py

#
# MicroPython for the IOT
#
# Example: Fibonacci series using recursion
#
# Calculate the Fibonacci series based on user input
#
# Dr. Charles Bell
#

# Create a function to calculate Fibonacci series (iterative)
# Returns a list.
def fibonacci_iterative(count):
    i = 1
    if count == 0:
        fib = []
    elif count == 1:
        fib = [1]
    elif count == 2:
        fib = [1,1]
    elif count > 2:
        fib = [1,1]
        while i < (count - 1):
            fib.append(fib[i] + fib[i-1])
            i += 1
    return fib

# Create a function to calculate the nth Fibonacci number

(recursive)
# Returns an integer.
def fibonacci_recursive(number):
    if number == 0:
        return 0
    elif number == 1:
        return 1
    else:
        # Call our self counting down.
        value = fibonacci_recursive(number-1) + fibonacci_recursive(number-2)
        return value

# Main code
print("Welcome to my Fibonacci calculator!")
index = int(input("Please enter the number of integers in the series: "))

# Recursive example
print("We calculate the value using a recursive algoritm.")
nth_fibonacci = fibonacci_recursive(index)
print("The {0}{1} fibonacci number is {2}."
      "".format(index, "th" if index > 1 else "st", nth_fibonacci))
see_series = str(input("Do you want to see all of the values in the series? "))
if see_series in ["Y","y"]:
    series = []
    for i in range(1,index+1):
        series.append(fibonacci_recursive(i))
    print("Series: {0}: ".format(series))

# Iterative example
print("We calculate the value using an iterative algoritm.")
series = fibonacci_iterative(index)
print("The {0}{1} fibonacci number is {2}."
      "".format(index, "th" if index > 1 else "st", series[index-1]))
see_series = str(input("Do you want to see all of the values in the series? "))
if see_series in ["Y","y"]:
    print("Series: {0}: ".format(series))

print("bye!")

Listing 4-8.Calculating Fibonacci Series

花一些时间通读代码。虽然要解决的问题比前一个例子简单一点,但是需要阅读更多的代码。准备就绪后,连接 MicroPython 板并创建文件。您在 PC 上为这个例子创建了一个文件,并将其命名为fibonacci.py。在下一节中,我们将把它复制到我们的 MicroPython 板上。

Tip

要更深入地了解如何在 Python 中创建和使用自己的函数(方法),请参见 https://docs.python.org/3/tutorial/controlflow.html#defining-functions

现在,让我们看看运行这个脚本会发生什么。回想一下,我们将在我们的 MicroPython 板上运行这段代码,因此如果您正在跟进,请确保设置好您的板并将其连接到您的 PC。

执行代码

回想一下第三章,当我们想要将代码移动到我们的 MicroPython 板上时,我们需要创建文件,然后将它复制到 MicroPython 板上,然后执行它。在这一节中,我将向您展示如何在 macOS 上做到这一点。在 Windows 10 上如何做到这一点的示例如示例 4 所示。

当您将 MicroPython 板插入 PC 上的 USB 端口时,闪存盘将出现在您的 Finder 和桌面上。图 4-1 显示了 macOS 上的一个例子。

A447395_1_En_4_Fig1_HTML.jpg

图 4-1。

MicroPython board Flash Drive Mounted

我们现在需要将fibonacci.py文件复制到闪存驱动器。在 macOS 上,打开闪存驱动器,然后将文件拖到驱动器上。当你复制完文件后,你应该会看到类似图 4-2 的东西。

A447395_1_En_4_Fig2_HTML.jpg

图 4-2。

Files Copied to the MicroPython board Flash Drive

请注意,我对图像进行了注释以显示文件。注意,我们还看到了 MicroPython 文件boot.pymain.py。我们不会修改main.py文件来运行我们的例子,因为我们不希望程序在每次启动设备时都运行。相反,我们将从 REPL 控制台运行代码。

使用终端窗口打开 REPL 控制台。您应该会看到来自 MicroPython 的欢迎消息,后面是 RELP 提示(>>>)。在 REPL 提示符下,发出以下代码语句。

import fibonnaci

这段代码导入我们刚刚复制的fibonnaci.py文件,当它导入时,它执行这段代码。因此,就像我们从 Python 控制台在我们的 PC 上运行它一样。继续通过请求斐波纳契数列中的第十二个值来测试程序。您应该会看到如图 4-3 所示的输出。

A447395_1_En_4_Fig3_HTML.jpg

图 4-3。

Output of Fibonacci Example (MicroPython board)

注意,我对查询做出了响应,显示了整个系列。还要注意,我们看到代码已经使用了我们编写的两个版本的函数:迭代和递归。最后,我们看到两个函数的值或输出是相同的数据。

Note

如果您还没有 MicroPython 板,您可以在 PC 上使用命令python ./fibonacci.py运行代码。您将看到类似的输出,如图所示。

如果再次运行代码,只需按 CTRL-D 进行软重置,然后再次发出 import 语句。这将重新运行整个代码。或者,如果您想运行其中一个函数,您可以通过使用下面的代码导入它来再次调用它。图 4-4 显示了在 MicroPython 板上执行时的样子。

A447395_1_En_4_Fig4_HTML.jpg

图 4-4。

Experimenting with the Fibonacci Functions

from fibonacci import fibonacci_iterative
fibonacci_iterative(6)
from fibonacci import fibonacci_recursive
fibonacci_recursive(6)

以这种方式导入后,您可以再次运行这些函数。继续尝试使用不同的值来显示它们的行为。回想一下,函数的实现是不同的——迭代版本返回列表,递归版本只返回最后一个或第 n 个值。

当您完成了示例的实验后,记得在拔下驱动器之前关闭终端并弹出闪存驱动器。

你的挑战

为了让这个例子更有用,修改代码,在斐波那契数列中搜索特定的整数。要求用户提供一个整数,然后确定该值是否是有效的斐波那契值。例如,如果用户输入 144,代码应该告诉用户该值是有效的,并且是序列中的第十二个值。虽然这个挑战将要求您为“主要”功能重写大部分代码,但是您必须找出如何以新的方式使用这些功能。

示例 4:使用类

这个例子通过引入面向对象的编程概念:类,大大增加了复杂性。回想一下前面的内容,类是模块化代码的另一种方式。类用于建模数据和数据上的行为。此外,类通常放在它们自己的代码模块(文件)中,进一步模块化代码。如果您需要修改一个类,您可能只需要更改类模块中的代码。

我们在这个例子中探索的问题是如何使用类和代码模块开发解决方案。我们将创建两个文件:一个用于类,另一个用于主代码。

写代码

这个例子是用来把罗马数字 8 转换成整数的。也就是说,我们将输入一个类似 VIII 的值,即 8,并期望看到整数 8。为了让事情变得更有趣,我们还将把我们得到的整数转换回罗马数字。罗马数字是用字符 I 表示 1,V 表示 5,X 表示 10,L 表示 50,C 表示 100,D 表示 500,M 表示 1000 组成的字符串。其他数字的组合是通过将字符数值加在一起(例如,3 = III),或者在另一个字符之前加一个单独的低位字符来表示代表减去该字符(例如,4 = IV)。下面展示了一些如何工作的例子。

3 = III
15 = XV
12 = XII
24 = XXIV
96 = LXLVI
107 = CVII

这听起来像是很多额外的工作,但是考虑一下:如果我们可以从一种格式转换到另一种格式,我们应该能够没有错误地转换回来。更具体地说,我们可以使用一个转换的代码来验证另一个转换。如果我们在把它转换回来的时候得到了不同的值,我们知道我们有一个需要解决的问题。

为了解决这个问题,我们将把转换罗马数字的代码放到一个单独的文件(代码模块)中,并构建一个名为Roman_Numerals的类来包含这些方法。在这种情况下,数据是整数到罗马数字的映射。

# Private dictionary of roman numerals
__roman_dict = {
    'I': 1,
    'IV': 4,
    'V': 5,
    'IX': 9,
    'X': 10,
    'XL': 40,
    'L': 50,
    'XC': 90,
    'C': 100,
    'CD': 400,
    'D': 500,
    'CM': 900,
    'M': 1000,
}

注意字典名称前的两个下划线。这是一种特殊的符号,它将字典标记为类中的私有变量。这是用于信息隐藏的 Python 方面,是设计对象时推荐使用的技术;总是努力隐藏在类内部使用的数据。

还要注意,我没有使用基本字符及其值,而是使用了其他几个值。我这样做是为了让转换变得更容易(也有一点欺骗)。在本例中,我添加了表示先前转换的一个值的条目,如 4 (IV)、9 (IX)等。这使得转换更容易(也更准确)。

我们还将添加两个方法;convert_to_int(),取一个罗马数字串,转换成整数;和convert_to_roman(),它接受一个整数并将其转换为罗马数字。我没有解释方法中的每一行代码,而是让您阅读代码,看看它是如何工作的。

简单地说,convert to integer 方法获取每个字符,并从字典中对这些值求和得到它的值。这里有一个技巧,需要对出现在较高值之前的较低值字符进行特殊处理(例如,IX)。convert to Roman 方法稍微容易一些,因为我们只需将该值除以字典中的最高值,直到达到零。清单 4-9 显示了类模块的代码,它保存在一个名为roman_ numerals .py的文件中。

#
# MicroPython for the IOT
#
# Example: Roman numerals class
#
# Convert integers to roman numerals
# Convert roman numerals to integers
#
# Dr. Charles Bell
#

class Roman_Numerals:

    # Private dictionary of roman numerals
    __roman_dict = {
        'I': 1,
        'IV': 4,
        'V': 5,
        'IX': 9,
        'X': 10,
        'XL': 40,
        'L': 50,
        'XC': 90,
        'C': 100,
        'CD': 400,
        'D': 500,
        'CM': 900,
        'M': 1000,
    }

    def convert_to_int(self, roman_num):
        value = 0
        for i in range(len(roman_num)):
            if i > 0 and self.__roman_dict[roman_num[i]] > self.__roman_dict[roman_num[i - 1]]:
                value += self.__roman_dict[roman_num[i]] - 2 * self.__roman_dict[roman_num[i - 1]]
            else:
                value += self.__roman_dict[roman_num[i]]
        return value

    def convert_to_roman(self, int_value):
        # First, get the values of all of entries in the dictionary
        roman_values = list(self.__roman_dict.values())
        roman_keys = list(self.__roman_dict.keys())
        # Prepare the string
        roman_str = ""
        remainder = int_value
        # Loop through the values in reverse
        for i in range(len(roman_values)-1, -1, -1):
            count = int(remainder / roman_values[i])
            if count > 0:
                for j in range(0,count):
                    roman_str += roman_keys[i]
                remainder -= count * roman_values[i]
        return roman_str

Listing 4-9.
Roman Numeral Class

如果你按照这一章来做,那么就在你的 PC 上为这段代码创建一个文件,并把它命名为roman_numerals.py。在下一节中,我们将把它复制到我们的 MicroPython 板上。

现在让我们看看主要代码。为此,我们只需从代码模块导入新的类,如下所示。这是进口指令的一种稍微不同的形式。在这种情况下,我们告诉 Python 包含名为Roman_Numerals的文件中的roman_ numerals类。

from roman_numerals import Roman_Numerals

Note

如果代码模块在一个子文件夹中,比如说roman,我们将把 import 语句写成from roman import Roman_Numerals,在这里我们使用点符号而不是斜线列出文件夹。

代码的其余部分很简单。我们首先要求用户输入一个有效的罗马数字字符串,然后将其转换为整数,并使用该值转换回罗马数字,打印结果。所以,你可以看到,将类放在一个单独的模块中简化了我们的代码,使它更短,更容易维护。清单 4-10 显示了保存在一个简单命名为roman.py的文件中的完整主代码。

#
# MicroPython for the IOT
#
# Example: Convert roman numerals using a class
#
# Convert integers to roman numerals
# Convert roman numerals to integers
#
# Dr. Charles Bell
#

from roman_numerals import Roman_Numerals

roman_str = input("Enter a valid roman numeral: ")
roman_num = Roman_Numerals()

# Convert to roman numberals
value = roman_num.convert_to_int(roman_str)
print("Convert to integer:        {0} = {1}".format(roman_str, value))

# Convert to integer
new_str = roman_num.convert_to_roman(value)
print("Convert to Roman Numerals: {0} = {1}".format(value, new_str))

print("bye!")

Listing 4-10.Converting Roman Numerals

如果你按照这一章来做,那么就在你的 PC 上为这段代码创建一个文件,并把它命名为roman.py。在下一节中,我们将把它复制到我们的 MicroPython 板上。

Tip

要更深入地了解如何使用 Python 中的类,请参见 https://docs.python.org/3/tutorial/classes.html

现在,让我们看看运行这个脚本会发生什么。回想一下,我们将在我们的 MicroPython 板上运行这段代码,因此如果您正在跟进,请确保设置好您的板并将其连接到您的 PC。

执行代码

在这个例子中,我们将看到如何在 Windows 10 上将代码复制到我们的 MicroPython 板上。像上一个例子一样,我们将首先在我们的 PC 上创建文件,连接 MicroPython 板,复制文件,然后执行它们。

当您在 Windows 10 上连接您的 MicroPython 板时,该驱动器将显示在文件资源管理器中,如图 4-5 所示。

A447395_1_En_4_Fig5_HTML.jpg

图 4-5。

MicroPython board Flash Drive Mounted (Windows 10)

如果您还没有创建文件,现在就创建。准备好后,我们会把它们复制到 MicroPython 板上。我没有使用文件浏览器,而是选择演示如何从控制台(命令提示符)复制文件。打开控制台,然后切换到文件资源管理器中显示的驱动器号。接下来,将文件roman_numerals.pyroman.py复制到驱动器。图 4-6 显示了一个复制文件的例子。

A447395_1_En_4_Fig6_HTML.jpg

图 4-6。

Copying the File to the MicroPython board Flash Drive (Console)

现在,我们准备好连接到 MicroPython 板并运行代码。回想一下,连接到电路板的方法之一是在 Windows 上使用 PuTTY。图 4-7 为油灰控制台。注意,我选择了 COM3 和串行连接。回想一下,您可以从设备管理器中找到 COM 端口。准备就绪后,单击打开。

连接完成后,PuTTY 将打开 REPL 控制台。您应该会看到来自 MicroPython 的欢迎消息,后面是 RELP 提示符(> > >)。在 REPL 提示符下,发出以下代码语句。

import roman

这段代码导入我们刚刚复制的roman.py文件,当它这样做时,它执行这段代码,如果您还记得的话,这段代码将调用 roman _ numerals.py 代码模块中的代码。同样,发布导入就像我们在 PC 上从 Python 控制台运行它一样。

A447395_1_En_4_Fig7_HTML.jpg

图 4-7。

Connecting with PuTTy (Windows 10)

一旦发出 import 语句,代码就会执行。去试试吧。从值 LXLIV 开始,它是罗马数字中的 SSS。图 4-8 显示了输出的样子。

A447395_1_En_4_Fig8_HTML.jpg

图 4-8。

Executing the Roman Numerals Example

您还可以尝试从终端导入代码并再次执行它,或者只是弹出驱动器,重置板,并重新连接以使用其他值再次运行它。

当您完成执行示例时,记得在拔下驱动器之前关闭终端并弹出闪存驱动器。

你的挑战

除了一些用户友好性(更好使用)之外,这个例子没有太多需要改进的地方。如果您想改进代码或类本身,我建议添加一个名为validate()的新方法,用于验证罗马数字字符串。该方法可以获取一个字符串,并确定它是否包含有效的字符序列。提示:首先,检查字符串是否只包含字典中的字符。

但是,您可以使用该模板构建其他用于转换格式的类。例如,作为练习,您可以创建一个新的类来将整数转换为十六进制甚至八进制。是的,有一些功能可以帮我们做到这一点,但是自己构建它会很有启发性和令人满意。去吧,试一试——创建一个新的类来将整数转换成其他格式。我建议先做一个十六进制到整数的函数,当这个函数工作正常时,创建一个倒数函数来把整数转换成十六进制。

更高级的挑战是重写类以接受构造函数中的字符串(当类变量被创建时),并使用该字符串进行转换,而不是使用convert_to*方法传递字符串或整数。例如,该类可以有一个构造函数和私有成员,如下所示。

__roman_str = ""
...
def __init__(self, name):
        self.name = name

当您创建实例时,您将需要传递该字符串,否则您将会得到一个缺少必需参数的错误。

roman_str = input("Enter a valid roman numeral: ")
roman_num = Roman_Numerals(roman_str)

更多信息

如果您需要更深入的 Python 知识,有几本关于这个主题的优秀书籍。下面我列出了几个我最喜欢的。Python 站点上的文档是一个很好的资源:python.org/doc/

  • Pro Python,第二版(apress,2014 年)J. Burton Browning,Marty Alchin
  • 学习 Python,第五版(O'Reilly Media,2013)马克·卢茨
  • 用 Python 自动化枯燥的东西:完全初学者实用编程(无淀粉出版社,2015),Al Sweigart

摘要

哇哦!那是一次疯狂的旅行,不是吗?我希望这个简短的 Python 速成课程已经对到目前为止展示的示例程序进行了足够的解释,现在您已经知道它们是如何工作的了。这个速成课程也是理解本书中其他 Python 例子的基础。

如果您正在学习如何使用 IOT 项目,但不知道如何使用 Python 编程,鉴于 Python 易于理解的语法,学习 Python 会很有趣。虽然互联网上有很多例子可以使用,但是很少有以这种方式记录的,可以为 Python 新手提供足够的信息来理解,更不用说开始使用和部署示例了!但至少代码很容易阅读。

本章提供了 Python 速成课程,涵盖了您在研究大多数较小的示例项目时将会遇到的基本内容。我们发现了 Python 应用的基本语法和结构,包括构建一个闪烁 LED 的真实 Python 应用的演练。通过这个例子,我们学习了如何使用无头应用,包括如何管理一个启动后台应用。

在下一章,我们将深入探究 MicroPython 编程。我们将看到更多关于在 MicroPython 板上运行的 IOT 项目中可用的特殊库。

Footnotes 1

https://en.wikipedia.org/wiki/String_interpolation

  2

https://www.python.org/dev/peps/pep-0008/

  3

没错,字典就是对象!元组和列表以及许多其他数据结构也是如此。

  4

从技术上来说,它被称为名称篡改,它模拟了将某些内容设为私有,但是如果您提供了正确数量的下划线,仍然可以被访问。更多信息请参见 https://en.wikipedia.org/wiki/Name_mangling

  5

http://www.json.org/

  6

[T0](en.wikipedia.org/wiki/Recurs…

  7

https://en.wikipedia.org/wiki/Fibonacci_number

  8

https://en.wikipedia.org/wiki/Roman_numerals

五、MicroPython 库

现在我们已经很好地掌握了如何用 Python 和 MicroPython 编写代码,是时候看看组成固件的支持库了。正如您将看到的,MicroPython 中可用的库反映了 Python 中的库。事实上,固件中的库(有时称为应用编程接口或 API 或固件 API)包含大量 Python 中的相同库。

对于标准库来说,有一些值得注意的例外,在 MicroPython 中有一个等价的库,但它已经被重命名,以区别于 Python 库。在这种情况下,要么通过删除不常用的特性来缩小库的范围,要么通过某些方式进行修改以适应 MicroPython 平台——所有这些都是为了节省空间(内存)。

还有一些特定于 MicroPython 的库,以及一些硬件,这些硬件提供的功能在一些通用 Python 版本中可能有,也可能没有。这些库旨在简化微控制器和硬件的使用。

因此,固件中有三种类型的库:标准的,与 Python 中的基本相同;专用于 MicroPython 的;专用于硬件的。还有另一种类型的库,有时称为用户提供的库或简单的自定义库。这些是我们自己创建的库(API ),我们可以将它们部署到我们的板上,从而使我们所有的项目都可以使用这些功能。在本章中,我们将会看到所有类型的库的概述。

Note

模块这个词,或者代码模块,有时用来指一个库;然而,模块通常是单个代码文件,而库可能由许多代码文件组成。因此,如果库是一个单独的文件(代码模块),交换名字是可以的。有时用于库的另一个词是包,它意味着多个文件。你可能也会遇到这个术语。

我们不是简单地解释或复制现有的文档,而是以快速参考表的形式查看这些库的概述,您可以使用这些表来熟悉可用的内容。简单地知道什么是可用的,通常可以加速开发并使您的程序更强大,而不必重新发明一些东西或花费大量时间去寻找您可能(或可能不)需要的库。然而,有几个库对于学习如何开发 MicroPython IOT 项目至关重要。我们将通过代码片段来更详细地发现它们,以帮助您学习它们。因此,虽然本章旨在向您介绍更常见的库,但它也是一个参考指南,可以通过官方文档的链接更深入地了解 MicroPython 固件。

让我们先来看看 MicroPython 中的那些“标准”Python 库。

内置和标准库

众所周知,MicroPython 是建立在 Python 之上的。人们做了大量的工作来精简内容,以便大部分 Python 库可以放在一个芯片上。由此产生的 MicroPython 固件比你电脑上的 Python 小得多,但一点也不差。也就是说,MicroPython 是 Python,因此 MicroPython 有许多与 Python 相同的库。

有些人可能称这些库为“内置的”,但更正确的说法是称它们为“标准”库,因为这些库与 Python 中的库是一样的。更具体地说,它们与 Python 中的类具有相同的类和相同的功能。因此,你可以在你的 PC 上写一个脚本并在那里执行它,然后在你的 MicroPython 板上执行相同的脚本。不错!正如您所猜测的,这在开发一个复杂的项目时非常有帮助。

回想一下,我们在上一章中看到过这种技术的演示。在那里我们看到了开发你的脚本的一部分——那些使用标准库的部分——并在你的 PC 上调试它们是可能的。也就是说,一旦您让它们正常工作,您就可以进入下一个需要 MicroPython 库或硬件库的部分。这是因为在我们的 PC 上开发要容易得多。我们不需要打开主板电源,将它连接到我们的 WiFi 等。,让它发挥作用。另外,个人电脑的速度要快得多。只是到处都更容易。通过实践这一实践,我们可以通过确保我们项目的“标准”部分正确工作来潜在地为我们自己节省一些挫折。

在这一节中,我们将探索标准的 Python 库,首先简要概述可用的库,然后详细介绍如何使用一些更常见的库。

Tip

参见 http://docs.micropython.org/en/latest/pyboard/library/index.html#python-standard-libraries-and-micro-libraries 了解更多关于任何标准库的细节。

概观

MicroPython 中的标准库包含一些对象,您可以使用这些对象来执行数学函数、操作编程结构、通过 JSON 处理可传输的文档(文档存储)、与操作系统和其他系统函数进行交互,甚至按时执行计算。表 5-1 包含了当前标准 MicroPython 库的列表。第一列是我们在import语句中使用的名称,第二列是简短的描述,第三列包含指向在线文档的链接。

表 5-1。

Standard Python Libraries in MicroPython

| 名字 | 描述 | 文件 | | --- | --- | --- | | `array` | 使用数组 | [`http://docs.micropython.org/en/latest/pyboard/library/array.html`](http://docs.micropython.org/en/latest/pyboard/library/array.html) | | `cmath` | 复杂数学 | [`http://docs.micropython.org/en/latest/pyboard/library/cmath.html`](http://docs.micropython.org/en/latest/pyboard/library/cmath.html) | | `gc` | 垃圾收集工 | [`http://docs.micropython.org/en/latest/pyboard/library/gc.html`](http://docs.micropython.org/en/latest/pyboard/library/gc.html) | | `math` | 数学函数 | [`http://docs.micropython.org/en/latest/pyboard/library/math.html`](http://docs.micropython.org/en/latest/pyboard/library/math.html) | | `sys` | 系统级功能 | [`http://docs.micropython.org/en/latest/pyboard/library/sys.html`](http://docs.micropython.org/en/latest/pyboard/library/sys.html) | | `ubinascii` | 二进制/ASCII 转换 | [`http://docs.micropython.org/en/latest/pyboard/library/ubinascii.html`](http://docs.micropython.org/en/latest/pyboard/library/ubinascii.html) | | `ucollections` | 容器 | [`http://docs.micropython.org/en/latest/pyboard/library/ucollections.html`](http://docs.micropython.org/en/latest/pyboard/library/ucollections.html) | | `uerrno` | 错误代码 | [`http://docs.micropython.org/en/latest/pyboard/library/uerrno.html`](http://docs.micropython.org/en/latest/pyboard/library/uerrno.html) | | `uhashlib` | 哈希算法 | [`http://docs.micropython.org/en/latest/pyboard/library/uhashlib.html`](http://docs.micropython.org/en/latest/pyboard/library/uhashlib.html) | | `uheapq` | 堆队列 | [`http://docs.micropython.org/en/latest/pyboard/library/uheapq.html`](http://docs.micropython.org/en/latest/pyboard/library/uheapq.html) | | `uio` | 输入/输出流 | [`http://docs.micropython.org/en/latest/pyboard/library/uio.html`](http://docs.micropython.org/en/latest/pyboard/library/uio.html) | | `ujson` | JSON 文档 | [`http://docs.micropython.org/en/latest/pyboard/library/ujson.html`](http://docs.micropython.org/en/latest/pyboard/library/ujson.html) | | `uos` | 操作系统 | [`http://docs.micropython.org/en/latest/pyboard/library/uos.html`](http://docs.micropython.org/en/latest/pyboard/library/uos.html) | | `ure` | 正则表达式 | [`http://docs.micropython.org/en/latest/pyboard/library/ure.html`](http://docs.micropython.org/en/latest/pyboard/library/ure.html) | | `uselect` | 流事件 | [`http://docs.micropython.org/en/latest/pyboard/library/uselect.html`](http://docs.micropython.org/en/latest/pyboard/library/uselect.html) | | `usocket` | 插座(网络) | [`http://docs.micropython.org/en/latest/pyboard/library/usocket.html`](http://docs.micropython.org/en/latest/pyboard/library/usocket.html) | | `ustruct` | 打包和解包 struts | [`http://docs.micropython.org/en/latest/pyboard/library/ustruct.html`](http://docs.micropython.org/en/latest/pyboard/library/ustruct.html) | | `utime` | 时间和日期函数 | [`http://docs.micropython.org/en/latest/pyboard/library/utime.html`](http://docs.micropython.org/en/latest/pyboard/library/utime.html) | | `uzlib` | 压缩算法 | [`http://docs.micropython.org/en/latest/pyboard/library/uzlib.html`](http://docs.micropython.org/en/latest/pyboard/library/uzlib.html) |

请注意,这些链接是 Pyboard 文档的链接,其中大多数也适用于其它电路板。如果有疑问,请访问您的主板的 MicroPython 文档。

Tip

这里介绍的大多数库都是 Pyboard 和 WiPy 所共有的。我们还将在下一章看到一些不同之处。

如您所见,有许多库以u开头,表示它们是 Python 等价库的特殊版本。有趣的是,一些平台可能包含原始的 Python 库。也就是说,如果您需要访问原始 Python 版本——如果它存在的话——您仍然可以通过使用原始名称(没有u前缀)来访问它。在这种情况下,MicroPython 将尝试通过原始名称查找模块,如果不存在,则默认为 MicroPython 版本。例如,如果我们想使用原始的 io 库,我们可以使用import io。但是,如果平台上没有名为io的模块,MicroPython 将使用名为uio的 MicroPython 版本。

Caution

import语句的格式允许我们指定目录。所以,如果我们使用import mylibs.io,MicroPython 将试图在mylibs文件夹中找到名为io.py的库(代码模块)。这可能会影响到如何使用不带 u 前缀的模块。如果我们使用了import io并且io.py不是一个代码模块,它将使用io作为一个文件夹的名称,如果它存在,在那个文件夹中查找模块。因此,如果您使用与 Python 库名称相同的文件夹名称,您可能会遇到麻烦。别这样。

接下来,我们将查看一些更常用的标准库,并查看每个标准库的一些代码示例。但是首先,我们应该讨论两类标准函数。

Interactive Help for Libraries

在学习 MicroPython 中的库时,一个鲜为人知的名为help()的函数会非常有帮助。您可以在 REPL 会话中使用该函数来获取有关库的信息。下面显示了uos库输出的摘录。

>>> help(uos)
<module 'uos'>object is of type module
  __name__ -- uos
  uname -- <function>
  chdir -- <function>
  getcwd -- <function>
  listdir -- <function>
  mkdir -- <function>

请注意,我们看到了所有函数的名称,以及常数(如果有的话)。当学习库和它们包含的内容时,这是一个真正的帮助。试试看!

公共标准库

现在让我们看看一些更常用的标准库的例子。接下来的只是你可以用每个库做什么的一个例子。有关所有功能的完整描述,请参见联机文档。像大多数 MicroPython 库一样,公共标准库可能是完整 Python3 (CPython)实现的子集。

Note

本章中的例子旨在运行在 MicroPython 板上。您可能需要做一些更改才能在您的电脑上运行它。

[计]系统复制命令(system 的简写)

sys 库提供对执行系统的访问,例如常量、变量、命令行选项、流(stdout、stdin、stderr)等等。该库的大多数特性都是常量或列表。可以直接访问这些流,但通常我们使用print()函数,默认情况下它会将数据发送到 stdout 流。

该库中最常用的函数包括。清单 5-1 展示了这些变量和exit()函数。

  • sys.argv:从命令行传递给脚本的参数列表
  • sys.exit(r):退出程序,返回值 r 给调用者
  • sys.modules:加载(导入)的模块列表
  • sys.path:搜索模块的路径列表——可以修改
  • sys.platform:显示 Linux、MicroPython 等平台信息。
  • sys.stderr:标准误差流
  • sys.stdin:标准输入流
  • sys.stdout:标准输出流
  • sys.version:当前执行的 Python 版本
# MicroPython for the IOT - Chapter 5
# Example use of the sys library
import sys
print("Modules loaded: " , sys.modules)
sys.path.append("/sd")
print("Path: ", sys.path)
sys.stdout.write("Platform: ")
sys.stdout.write(sys.platform)
sys.stdout.write("\n")
sys.stdout.write("Version: ")
sys.stdout.write(sys.version)
sys.stdout.write("\n")
sys.exit(1)
Listing 5-1.Demonstration of the sys library features

注意我们从import语句开始,然后我们可以使用print()函数打印 sys 库中的常量和变量。我们还将看到如何使用sys.path.append()函数将路径添加到搜索路径中。如果我们在引导驱动器(SD 驱动器)上创建自己的目录来放置代码,这将非常有帮助。没有这个添加,import语句将失败,除非代码模块在lib目录中。

在示例的最后,我们将看到如何使用 stdout 流将内容写入屏幕。注意,您必须提供回车(换行符)命令来将输出推进到新的一行(\n)。print()函数为我们处理了这个问题。下面显示了在 MicroPython 板上运行这个脚本的输出。

>>> import examples.sys_example
Modules loaded:  {'examples': <module 'examples'>, 'examples.sys_example': <module 'examples.sys_example' from 'examples/sys_example.py'>}
Path:  ['', '/flash', '/flash/lib', '/sd']
Platform: WiPy
Version: 3.4.0

请注意,我们看到了预期的数据,并且这个示例运行在一个 WiPy 模块上。你在import语句中看出了什么奇怪的地方吗?这里,代码被放在一个名为examples的新目录中,然后将代码模块复制到我的引导驱动器上的那个目录中。因此,我们可以使用语句中的目录名来查找代码模块并导入它(examples/sys_examples.py)。如果您想在自己的 MicroPython 板上运行这段代码,您可以将代码写入一个文件,比如sys_example.py,然后将该文件复制到您的引导驱动器,或者使用 ftp 来复制该文件。这允许你保持你的引导(闪存)驱动器整洁,并把你的代码放在一个共同的位置。

最后,请注意没有命令行参数。这是因为我们使用了一个import语句。然而,如果我们在 PC 上运行提供命令行参数的代码,我们会看到它们。下面显示了在 PC 上运行该脚本的输出。

$ python ./sys_example.py
Modules loaded:  {'builtins': <module 'builtins' (built-in)>, 'sys': <module 'sys' (built-in)>, ... '/sd']
darwin
Version: 3.6.0 (v3.6.0:41df79263a11, Dec 22 2016, 17:23:13)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)]

奥斯陆大学

uio库包含额外的函数来处理流和类似流的对象。有一个名为uio.open()的函数可以用来打开文件(但是大多数人使用名为open()的内置函数)以及用于字节流和字符串流的类。事实上,这些类有类似的文件函数,比如read()write()seek()flush()close(),以及一个getvalue()函数,它返回包含数据的流缓冲区的内容。让我们看一个例子。

在本例中,我们首先打开一个新的文件进行写入,并将一个字节数组写入该文件。使用的技术是将每个 bye 的十六进制值传递给write()函数。当您从传感器读取数据时,它们通常是二进制的(一个字节或一串字节)。你用转义符\x表示一个字节,如图所示。

将数据写入文件后,我们通过将 1 传递给read()函数来一次读取一个字节。然后,我们打印读取的原始 for 值(从read(1)调用返回的值)、十进制值和十六进制值。清单 5-2 展示了如何使用 uio 以二进制模式读取文件。FileIo()类。写入的字节包含一个秘密的字(一个用十六进制值模糊的字)-你能看到它吗?

# MicroPython for the IOT - Chapter 5
# Example use of the uio library
# Note: change uio to io to run this on your PC!
import uio
# Create the binary file
fio_out = uio.FileIO('data.bin', 'wb')
fio_out.write("\x5F\x9E\xAE\x09\x3E\x96\x68\x65\x6C\x6C\x6F")
fio_out.write("\x00")
fio_out.close()
# Read the binary file and print out the results in hex and char.
fio_in = uio.FileIO('data.bin', 'rb')
print("Raw,Dec,Hex from file:")
byte_val = fio_in.read(1)  # read a byte
while byte_val:
    print(byte_val, ",", ord(byte_val), hex(ord(byte_val)))
    byte_val = fio_in.read(1)  # read a byte
fio_in.close()
Listing 5-2.Demonstration of the uio library features

这就像我们在上一章看到的普通内置函数一样。清单 5-3 显示了在 WiPy 上运行时的输出。

>>> import examples.uio_example
Raw,Dec,Hex from file:
b'_' , 95 0x5f
b'\xc2' , 194 0xc2
b'\x9e' , 158 0x9e
b'\xc2' , 194 0xc2
b'\xae' , 174 0xae
b'\t' , 9 0x9
b'>' , 62 0x3e
b'\xc2' , 194 0xc2
b'\x96' , 150 0x96
b'h' , 104 0x68
b'e' , 101 0x65
b'l' , 108 0x6c
b'l' , 108 0x6c
b'o' , 111 0x6f
b'\x00' , 0 0x0
Listing 5-3.Demonstration of the uio library features (Pyboard)

如果您想知道文件是什么样子,您可以使用一个像 hexdump 这样的工具来打印内容,如下所示。你能看到隐藏的信息吗?

$ hexdump -C data.bin
00000000  5f 9e ae 09 3e 96 68 65  6c 6c 6f 00              |_...>.hello.|
0000000c

乌伊松

在 IOT 项目中处理数据时,ujson库是您可能会经常使用的库之一。它提供了 JavaScript 对象符号(JSON)文档的编码和解码。这是因为许多可用的 IOT 服务要么需要要么能够处理 JSON 文档。因此,您应该考虑养成在 JSON 中格式化数据的习惯,以便更容易与其他系统集成。该库实现了以下函数,您可以使用这些函数来处理 JSON 文档。

  • ujson.dumps(obj):返回从 JSON 对象解码的字符串。
  • ujson.loads(str):解析 JSON 字符串,返回一个 JSON 对象。如果格式不正确,将引发错误。
  • ujson.load(fp):解析文件指针(包含 JSON 文档的文件字符串)的内容。如果格式不正确,将引发错误。

回想一下,我们在上一章看到了一个 JSON 文档的简单例子。这个例子是专门为 PC 编写的,但是做了一点小小的改动就可以在 MicroPython 板上运行。让我们看一个类似的例子。清单 5-4 展示了一个使用ujson库的例子。

# MicroPython for the IOT - Chapter 5
# Example use of the ujson library
# Note: change ujson to json to run it on your PC!
import ujson

# Prepare a list of JSON documents for pets by converting JSON to a dictionary
vehicles = []
vehicles.append(ujson.loads('{"make":"Chevrolet", "year":2015, "model":"Silverado", "color":"Pull me over red", "type":"pickup"}'))
vehicles.append(ujson.loads('{"make":"Yamaha", "year":2009, "model":"R1", "color":"Blue/Silver", "type":"motorcycle"}'))
vehicles.append(ujson.loads('{"make":"SeaDoo", "year":1997, "model":"Speedster", "color":"White", "type":"boat"}'))
vehicles.append(ujson.loads('{"make":"TaoJen", "year":2013, "model":"Sicily", "color":"Black", "type":"Scooter"}'))

# Now, write these entries to a file. Note: overwrites the file
json_file = open("my_vehicles.json", "w")
for vehicle in vehicles:
    json_file.write(ujson.dumps(vehicle))
    json_file.write("\n")
json_file.close()

# Now, let's read the list of vehicles and print out their data
my_vehicles = []
json_file = open("my_vehicles.json", "r")
for vehicle in json_file.readlines():
    parsed_json = ujson.loads(vehicle)
    my_vehicles.append(parsed_json)
json_file.close()

# Finally, print a summary of the vehicles
print("Year Make Model Color")
for vehicle in my_vehicles:
    print(vehicle['year'],vehicle['make'],vehicle['model'],vehicle['color'])

Listing 5-4.Demonstration of the ujson library features

下面显示了在 WiPy 上运行的脚本的输出。

>>> import examples.ujson_example
Year Make Model Color
2015 Chevrolet Silverado Pull me over red
2009 Yamaha R1 Blue/Silver
1997 SeaDoo Speedster White
2013 TaoJen Sicily Black

终极指标

uos库实现了一组用于基本操作系统的函数。如果你为你的电脑写过程序,有些功能你可能会很熟悉。大多数函数允许您处理文件和目录操作。下面列出了几个比较常用的函数。

  • uos.chdir(path):改变当前目录
  • uos.getcwd():返回当前工作目录
  • uos.listdir([dir]):如果缺少目录,列出当前目录,或者列出指定的目录
  • uos.mkdir(path):新建一个目录
  • uos.remove(path):删除文件
  • uos.rmdir(path):删除一个目录
  • uos.rename(old_path, new_path):重命名文件
  • uos.stat(path):获取文件或目录的状态

在这个例子中,我们看到了如何更改工作目录,以便简化我们的导入语句。我们还将看到如何创建一个新目录,重命名它,在新目录中创建一个文件,列出目录,最后清理(删除)更改。清单 5-5 展示了使用uos l库函数的例子。

# MicroPython for the IOT - Chapter 5
# Example use of the uos library
# Note: change uos to os to run it on your PC!
import sys
import uos

# Create a function to display files in directory
def show_files():
    files = uos.listdir()
    sys.stdout.write("\nShow Files Output:\n")
    sys.stdout.write("\tname\tsize\n")
    for file in files:
        stats = uos.stat(file)
        # Print a directory with a "d" prefix and the size
        is_dir = True
        if stats[0] > 16384:
            is_dir = False
        if is_dir:
            sys.stdout.write("d\t")
        else:
            sys.stdout.write("\t")
        sys.stdout.write(file)
        if not is_dir:
            sys.stdout.write("\t")
            sys.stdout.write(str(stats[6]))
        sys.stdout.write("\n")

# List the current directory
show_files()
# Change to the examples directory
uos.chdir('examples')
show_files()

# Show how you can now use the import statement with the current dir
print("\nRun the ujson_example with import ujson_example after chdir()")
import ujson_example

# Create a directory
uos.mkdir("test")
show_files()

Listing 5-5.Demonstration of the uos library features

虽然这个例子有点长,但它展示了一些有趣的技巧。注意,我们创建了一个函数来打印目录列表,而不是打印返回的文件列表。我们还检查了文件的状态,以确定该文件是否是一个目录,如果是,我们打印一个 d 来表示该名称指的是一个目录。我们还使用 stdout 流来控制制表符(\t)和换行符(\n)的格式。

您还将看到如何使用更改目录来改进我们使用import语句的方式。由于我们将目录改为 examples,import ujson_example将首先尝试在当前目录中找到那个模块(库)。这是一个很好的技巧,你可以在你自己的项目中使用,将你的代码部署到你的开发板的一个单独的目录中,这意味着你可以使用它将多个项目部署到你的开发板,将每个项目放在它自己的目录中。

现在让我们看看输出。清单 5-6 显示了在 WiPy 上运行时的输出。花一些时间查看输出,看看这些函数是如何工作的。另外,请注意 JSON 示例的输出,因为我们在脚本中间导入了该代码。不错。

>>> import examples.uos_example

Show Files Output:
        name    size
        main.py 34
d       sys
d       lib
d       cert
        boot.py 336
d       examples
        data.bin        15
        my_vehicles.json        377

Show Files Output:
        name    size
        sys_example.py  395
        uio_example.py  609
        ujson_example.py        1370
        uos_example.py  1163
        data.bin        15

Run the ujson_example with import ujson_example after chdir()
Year Make Model Color
2015 Chevrolet Silverado Pull me over red
2009 Yamaha R1 Blue/Silver
1997 SeaDoo Speedster White
2013 TaoJen Sicily Black

Show Files Output:
        name    size
        sys_example.py  395
        uio_example.py  609
        ujson_example.py        1370
        uos_example.py  1163
        data.bin        15
        my_vehicles.json        377
d       test

Listing 5-6.Demonstration of the uos library features (output)

尤迪米特

utime库是另一个在许多项目中使用的流行库。该库用于获取当前时间、日期,并处理时间数据,例如计算时差。请注意,某些功能仅适用于作为硬件扩展或通过网络时间服务器安装的实时时钟(RTC)。详见第六章。下面列出了几个与在脚本中插入延迟相关的常用函数。当我们需要暂停处理以允许传感器读取或等待来自/去往其他源的数据时,延迟是有帮助的。该库的另一个常见用途是记录事件或传感器数据读取的日期和时间。

  • utime.sleep(seconds):将板卡置于睡眠模式,持续指定的秒数
  • utime.sleep_ms(ms):将板卡置于睡眠模式,持续指定的毫秒数
  • utime.sleep_us(us):将板卡置于睡眠模式,持续指定的微秒数

让我们看一个如何使用时间延迟的简短示例。这里,我们使用一个随机函数来休眠一段随机时间,并提供随机值。不用担心 RTC 代码;我们将在第六章中看到更多相关内容。

# MicroPython for the IOT - Chapter 5
# Example use of the utime library
# Note: This example only works on the WiPy
# Note: This example will not on your PC.
import machine
import sys
import utime

# Since we don't have a RTC, we have to set the current time
# initialized the RTC in the WiPy machine library
from machine import RTC

# Init with default time and date
rtc = RTC()
# Init with a specific time and date. We use a specific
# datetime, but we could get this from a network time
# service.
rtc = RTC(datetime=(2017, 7, 15, 21, 32, 11, 0, None))

# Get a random number from 0-1 from a 2²⁴ bit random value
def get_rand():
    return machine.rng() / (2 ** 24 - 1)

# Format the time (epoch) for a better view
def format_time(tm_data):
    # Use a special shortcut to unpack tuple: *tm_data
    return "{0}-{1:0>2}-{2:0>2} {3:0>2}:{4:0>2}:{5:0>2}".format(*tm_data)

# Generate a random number of rows from 0-25
num_rows = get_rand() * 25

# Print a row using random seconds, milleseconds, or microseconds
# to simulate time.
print("Datetime             Value")
print("-------------------  --------")
for i in range(0,num_rows):
    # Generate random value for our sensor
    value = get_rand() * 100
    # Wait a random number of seconds for time
    utime.sleep(int(get_rand() * 15))  # sleep up to 15 seconds
    print("{0}  {1:0>{width}.4f}".format(format_time(rtc.now()), value, width=8))

Listing 5-7.Demonstration of the utime library sleep features

Note

此示例还展示了不同主板的固件差异。这个例子只适用于 WiPy 和类似的 Pycom 板。

注意,在这个例子中有很多事情要做,而不仅仅是简单地等待几秒钟。此示例向您展示了如何做到这一点,以及如何处理时间数据和格式数据。在记录数据时,通常必须这样做。让我们走一遍。

除了 RTC 代码,我们还创建了两个函数:一个使用 WiPy 的机器库生成一个随机数(Pyboard 固件中没有这个函数),另一个以用户友好的格式格式化日期时间。

因为该函数创建了一个 24 位的随机数,所以我们用 24 位的最大值除,得到一个介于 0 和 1 之间的值。然后我们可以把它乘以一个整数,得到一个范围的值。例如,如果我们想要一个 0 到 6 之间的值,我们可以使用get_rand() * 6

format_time()函数返回一个表示日期时间的字符串。在这里,我们看到一些高级格式化选项,用每个部分的正确数字来格式化日期。具体来说,4 位数字代表年份,2 位数字代表月、日、小时、分钟和秒。:0>2选项告诉format()函数用前导零将值分隔在 2 个位置(数字)上。酷吧。注意最后一个print()语句。注意,我们使用了另一个技巧,用一个命名参数(width)来传递宽度。

最后,我们来看示例代码,其中我们生成了从 0 到 25 的随机行数,然后循环这些迭代,生成一个随机值(0-100),然后等待(休眠)随机秒数(0-15),然后打印新的日期时间和值。这模拟了我们用来从传感器读取数据的典型循环。由于大多数传感器需要时间来读取值,睡眠功能允许我们等待这段时间。 1

现在我们对这段代码的作用有了更多的了解,让我们看看它是如何工作的一个例子。下面显示了在 WiPy 上运行这段代码的输出。

>>> import examples.utime_example
Datetime             Value
-------------------  --------
2017-07-15 21:32:12  014.1520
2017-07-15 21:32:25  003.5380
2017-07-15 21:32:28  044.8298
2017-07-15 21:32:35  099.0981
2017-07-15 21:32:41  086.8839
2017-07-15 21:32:47  083.8304
2017-07-15 21:32:55  083.0670
2017-07-15 21:32:59  064.3214

Note

如前几章所述,MicroPython 有一些元素是特定于单个 MicroPython 板或硬件的。我们将在下一章讨论这些。

也有不属于任何特定库的内置函数,也有允许我们捕获错误条件的异常。在深入研究一些更常用的标准库之前,让我们先看看这些。

内置函数和类

Python 附带了许多内置函数:可以直接从脚本中调用而无需导入的函数。有许多类可以用来定义变量、处理数据等等。它们是对象,因此您可以使用它们来包含数据并对数据执行操作(功能)。到目前为止,我们已经在示例中看到了一些。

让我们看看一些主要的内置函数和类。表 5-2 包含了对每一项的简短描述。你可以在 https://docs.python.org/3/library/functions.html 找到更多关于每个的信息。虽然这是 Python 文档,但它也适用于 MicroPython。类用“类”指定;其余的都是函数。

您应该浏览这个列表,找到您感兴趣的链接,并在开发项目时参考这个列表,以便使用最合适的函数或类。你可能会惊讶有多少是“内置的”但是,请再次检查您选择的 MicroPython 板的文档,以了解固件中可用的最新功能和类。

表 5-2。

MicroPython Built-in Functions and Classes

| 名字 | 描述 | | --- | --- | | `abs(x)` | 返回一个数的绝对值。 | | `all(iterable)` | 如果 iterable 的所有元素都为 true(或者 iterable 为空),则返回 True。 | | `any(iterable)` | 如果 iterable 的任何元素为 true,则返回 True。 | | `bin(x)` | 将整数转换为二进制字符串。 | | `class bool([x])` | 返回一个布尔值,即 True 或 False 之一。 | | `class bytearray([source[, encoding[, errors]]])` | 返回新的字节数组。 | | `class bytes([source[, encoding[, errors]]])` | 返回一个新的“字节”对象,它是一个不可变的整数序列,范围是 0 <= x < 256 | | `callable(object)` | 如果对象参数是可调用的,则返回 True,否则返回 False。 | | `chr(i)` | 返回表示字符的字符串,该字符的 Unicode 码位是整数 I。 | | `classmethod(function)` | 返回函数的类方法。 | | `class complex([real[, imag]])` | 返回一个值为 real + imag*1j 的复数,或将字符串或数字转换为复数。 | | `delattr(obj, name)` | 这是`setattr()`的亲戚。参数是一个对象和一个字符串。该字符串必须是对象属性之一的名称。 | | `class dict()` | 创建新词典。 | | `dir([object])` | 不带参数,返回当前本地范围内的名称列表。使用参数,尝试返回该对象的有效属性列表。 | | `divmod(a,b)` | 使用整数除法时,将两个(非复数)数字作为参数,并返回由它们的商和余数组成的一对数字。 | | `enumerate(iterable, start=0)` | 返回一个枚举对象。Iterable 必须是序列、迭代器或其他支持迭代的对象。 | | `eval(expression, globals=None, locals=None)` | 在本地命名空间中使用全局变量和局部变量作为字典来计算表达式。 | | `exec(object[, globals[, locals]])` | 在本地名称空间中使用全局变量和局部变量作为字典来执行一组 Python 语句或对象。 | | `filter(function, iterable)` | 从 iterable 中函数返回 true 的元素构造一个迭代器。 | | `class float([x])` | 返回由数字或字符串构造的浮点数。 | | `class frozenset([iterable])` | 返回一个新的`frozenset`对象,可选地包含来自 iterable 的元素。 | | `getattr(object, name[, default])` | 返回对象的命名属性的值。名称必须是字符串。 | | `globals()` | 返回表示当前全局符号表的字典。 | | `hasattr(object, name)` | 参数是一个对象和一个字符串。如果字符串是对象属性之一的名称,则结果为 True,否则为 False。 | | `hash(object)` | 返回对象的哈希值(如果有的话)。哈希值是整数。 | | `hex(x)` | 将整数转换为以“0x”为前缀的小写十六进制字符串。 | | `id(object)` | 返回一个对象的“身份”。 | | `input([prompt])` | 如果 prompt 参数存在,它将被写入标准输出,并且没有尾随换行符。然后,该函数从 input 中读取一行,将其转换为一个字符串(去掉尾随的换行符),并返回该字符串。 | | `class int(x)` | 返回由数字或字符串 x 构成的整数对象,如果没有给定参数,则返回 0。 | | `isinstance(object, classinfo)` | 如果对象参数是`classinfo`参数或其(直接、间接或虚拟)子类的实例,则返回 true。 | | `issubclass(class, classinfo)` | 如果类是`classinfo`的子类(直接、间接或虚拟),则返回 true。 | | `iter(object[, sentinel])` | 返回一个迭代器对象。 | | `len(s)` | 返回一个对象的长度(项目数)。 | | `class list([iterable])` | 列表顺序。 | | `locals()` | 更新并返回一个表示当前局部符号表的字典。 | | `map(function, iterable, …)` | 返回一个迭代器,将函数应用于 iterable 的每一项,产生结果。 | | `max([iterable|arg*])` | 返回 iterable 中最大的项或两个或多个参数中最大的项。 | | `class memoryview(obj)` | 返回从给定参数创建的“内存视图”对象。 | | `min([iterable|arg*])` | 返回 iterable 中最小的项或两个或多个参数中最小的一个。 | | `next(iterator[, default])` | 通过调用迭代器的 __next__()方法,从迭代器中检索下一项。 | | `class objectO` | 返回一个新的无特征对象。对象是所有类的基。 | | `oct(x)` | 将整数转换为八进制字符串。 | | `open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)` | 打开文件并返回相应的 file 对象。使用`close()`关闭文件。 | | `ord(c)` | 给定一个表示一个 Unicode 字符的字符串,返回一个表示该字符的 Unicode 码位的整数。 | | `pow(x, y[, z])` | 将 x 返回到 y 的幂;如果 z 存在,返回 x 的 y 次方,以 z 为模(比`pow(x, y) % z`计算更有效)。 | | `print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)` | 将对象打印到文本流文件中,用`sep`分隔,后跟`end. sep`,end,file,flush 如果存在,必须作为关键字参数给出。 | | `class property(fget=None, fset=None, fdel=None, doc=None)` | 返回一个属性。 | | `range([stop|[start, stop[, step]]])` | 距离序列。 | | `repr(object)` | 返回包含对象的可打印表示形式的字符串。 | | `reversed(seq)` | 返回一个反向迭代器。 | | `round(number[, ndigits])` | 返回小数点后四舍五入到`ndigits`精度的数字。 | | `class set([iterable])` | 返回一个新的 set 对象,可以选择使用 iterable 中的元素。 | | `setattr(object, name, value)` | 这是 getattr()的对应物。参数是一个对象、一个字符串和一个任意值。 | | `class slice(start, stop[, step])` | 返回一个切片对象,表示由`range(start, stop, step)`指定的一组索引。 | | `sorted(iterable[, key][, reverse])` | 从 iterable 中的项目返回一个新的排序列表。 | | `staticmethod(function)` | 返回函数的静态方法。 | | `class str(object)` | 返回对象字符串版本。 | | `sum(terable[, start])` | 从左到右对 start 和 iterable 的项求和,并返回总和。 | | `super([type[, object-or-type]])` | 返回一个代理对象,该对象将函数调用委托给。 | | `class tuple([iterable])` | 元组序列。 | | `type(object)` | 返回对象的类型。 | | `zip(*iterables)` | 创建一个迭代器,聚合每个可迭代对象的元素。 |

Tip

有关内置函数和类的更多信息,请参见 https://docs.python.org/3/library/functions.html 。请参见 http://docs.micropython.org/en/latest/pyboard/library/builtins.html 了解 MicroPython 中内置函数和类的最新列表。

让我们看一个使用其中一个类的例子——字典。下面展示了我们如何使用内置类来创建一个类型为dict()的变量,并在以后使用它。因为该类是内置功能的一部分,所以它可以在 Python 和 MicroPython 上工作。

>>> my_addr = dict()
>>> print(my_addr)
{}
>>> my_addr['street'] = '123 Main St'
>>> my_addr['city'] = 'Anywhere'
>>> my_addr['zip'] = 90125
>>> print(my_addr)
{'city': 'Anywhere', 'street': '123 Main St', 'zip': 90125}
>>> my_addr = {'street':'201 Cherry Tree Road', 'city':'Gobye', 'zip':12345}
>>> print(my_addr)
{'city': 'Gobye', 'street': '201 Cherry Tree Road', 'zip': 12345}

这里我们看到我们可以使用 dictionary 类来创建该类型的变量。我们可以在第一个print()调用中看到这一点。回想一下,定义字典的语法是一组花括号。接下来,我们使用访问元素的特殊语法将值添加到字典中。最后,我们使用更熟悉的字典语法给变量重新分配一组新的数据。

现在我们来谈谈一个我们没怎么谈过的话题——异常。异常是 Python 内置模块的一部分,可能是您想要使用的一项非常重要的编程技术。也许不是马上,但是最终你会体会到在你的代码中使用异常的力量和方便。

例外

Python(和 MicroPython)中还有一个强大的机制,我们可以使用它来帮助管理或捕获发生错误时的事件,并针对特定错误执行代码。这种结构称为异常,我们可以捕获的异常(错误)称为异常类。它使用一种称为try语句的特殊语法(也称为子句,因为它需要至少一个其他子句才能形成有效的语句)来帮助我们在错误生成时捕获它们。用raise()函数可以在代码的任何地方生成异常。也就是说,如果出现问题,程序员可以“引发”一个特定的、命名的异常,并且可以使用try语句通过except子句来捕获它。表 5-3 显示了 MicroPython 中可用的异常类列表,以及何时(如何)引发异常的简短描述。

表 5-3。

MicroPython Exception Classes

| 异常类 | 使用说明 | | --- | --- | | 断言错误 | assert()语句失败 | | 属性错误 | 属性引用失败 | | 例外 | 基本异常类 | | 导入错误 | 一个或多个模块无法导入 | | 索引错误 | 下标超出范围 | | 键盘中断 | 键盘 CTRL-C 被发出或模拟 | | 键错误 | 字典中的键映射不在键列表中 | | 存储器错误 | 内存不足情况 | | 名称错误 | 局部或全局名称(变量、函数等。)未找到 | | notimplemontederror | 遇到了抽象函数(不完整) | | 操作系统错误 | 来自操作系统的任何系统相关错误 | | 运行时错误 | 执行时可能遇到致命错误 | | 停止迭代 | 迭代器的下一个函数表示可迭代对象中不再有值 | | 句法误差 | 遇到代码语法错误 | | 系统退出 | 调用或模拟了 sys.exit()函数 | | 类型错误 | 函数或操作应用于不适当的类型(如类型不匹配) | | 值错误 | 找到了正确的类型但错误的值(如超出界限) | | 零除法错误 | 数学函数的结果为/ 0 |

try语句的语法如下所示。该结构的每个部分称为一个子句。

try_stmt  ::=  try1_stmt | try2_stmt
try1_stmt ::=  "try" ":" code block
               ("except" [expression ["as" identifier]] ":" code block)+
               ["else" ":" code block]
               ["finally" ":" code block]
try2_stmt ::=  "try" ":" code block
               "finally" ":" code block

注意这里有四个子句:tryexceptelsefinallytry子句是我们放置代码(代码块)的地方——一行或多行代码将包含在异常捕获中。只能有一个tryelsefinally,但是可以有任意数量的except子句来命名一个异常类。事实上,exceptelse一起运行,如果在运行try子句中的任何代码行时检测到异常,它将搜索except子句,当且仅当不满足 except 子句时,它将执行else子句。finally子句用于在所有异常被处理和执行后执行。还要注意该语句有两个版本:一个包含一个或多个except和可选的一个elsefinally,另一个只包含tryfinally子句。

让我们看看使用语句来捕获代码中的错误的一种方法。假设您正在从一批传感器中读取数据,如果读取的值超出范围或无效,这些传感器的库(模块)将引发ValueError。如果一个或多个传感器出现故障,您也可能不想要来自任何其他传感器的数据。因此,我们可以使用如下代码“尝试”读取每个传感器,如果有一个ValueError,发出警告并继续,或者如果遇到其他错误,在读取期间将其标记为错误。注意,通常我们不会在这一点上停止程序;相反,我们通常会记录下来并继续下去。研究以下内容,直到你确信例外很酷。

values = []
print("Start sensor read.")
try:
    values.append(read_sensor(pin11))
    values.append(read_sensor(pin12))
    values.append(read_sensor(pin13))
    values.append(read_sensor(pin17))
    values.append(read_sensor(pin18))
except ValueError as err:
    print("WARNING: One or more sensors valued to read a correct value.", err)
except:
    print("ERROR: fatal error reading sensors.")
finally:
    print("Sensor read complete.")

我们使用异常的另一种方式是当我们想要导入一个模块(库)但不确定它是否存在时。例如,假设有一个名为 piano.py 的模块,它有一个名为 keys()的函数,您希望导入该模块,但是该模块可能在系统上,也可能不在系统上。在这种情况下,我们可以使用其他代码来创建我们自己版本的keys()。为了测试模块是否可以导入,我们可以将导入放在 try 块中,如下所示。然后,我们可以检测导入是否失败,并采取适当的措施。

# Try to import the keys() function from piano. If not present,
# use a simulated version of the keys() function.
try:
    from piano import keys
except ImportError as err:
    print("WARNING:", err)
    def keys():
        return(['A','B','C','D','E','F','G'])
print("Keys:", keys())

如果我们像这样添加代码,而模块不存在,我们不仅可以用警告消息来响应,还可以定义我们自己的函数在模块不存在时使用。

最后,您可以引发任何想要的异常,包括创建自己的异常。创建自定义异常是一个高级主题,但让我们看看如何引发异常,因为如果我们编写自己的自定义库,我们可能希望这样做。假设您有一个正在读取值的代码块,但是值可能会超出范围。也就是说,对于整数来说太大,对于预期值的有效范围来说太小,等等。您可以简单地使用 raise 语句和有效的异常类声明来引发 ValueError,在自定义错误消息中传递,如下所示。

raise ValueError("ERROR: the value read from the sensor ({0}) is not in range.".format(val_read))

然后,您可以使用try语句来捕获这种情况,因为您知道这是可能的,并且您的代码可以绕过它。例如,如果您正在读取数据,您可以选择跳过读取,继续循环。然而,如果在运行您的代码时遇到这个异常,并且没有try语句,您可能会得到一个类似下面的错误,尽管它是致命的,但仍然可以提供信息。

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: ERROR: the value read from the sensor (-12) is not in range.

您可以使用这里所示的类似技术来使您的 MicroPython 代码更加健壮和容错。更好的是,您可以编写代码来预测错误,并以优雅、可控的方式对错误做出反应。

Tip

要更深入地了解 Python 异常,请参见 https://docs.python.org/3/tutorial/errors.html

MicroPython 库

也有专门为 MicroPython 系统构建的库。这些库旨在帮助在硬件上使用 MicroPython。像内置库一样,有一些 MicroPython 库适用于一种或另一种板,而那些库又因板而异。也就是说,库之间存在细微的差异,这使得它们不能在多个板上工作。有关可用的 MicroPython 库的完整列表,请始终查阅您的主板固件文档。

概观

MicroPython 库提供了特定于 Python 的 MicroPython 实现的功能。有些函数库具有直接处理硬件、MicroPython 系统和网络的功能。在接下来的章节中,我们将会看到这些库的一些特性的例子。表 5-4 包含了大多数主板通用的当前 MicroPython 库的列表。第一列是我们在import语句中使用的名称,第二列是简短的描述,第三列包含指向在线文档的链接。

Tip

参见 http://docs.micropython.org/en/latest/pyboard/library/index.html#micropython-specific-libraries 了解更多关于任何 MicroPython 库的细节。

表 5-4。

MicroPython-Specific Libraries

| 名字 | 描述 | 文件 | | --- | --- | --- | | `framebuf` | 帧缓冲 | [`http://docs.micropython.org/en/latest/pyboard/library/framebuf.html`](http://docs.micropython.org/en/latest/pyboard/library/framebuf.html) | | `machine` | 硬件相关功能 | [`http://docs.micropython.org/en/latest/pyboard/library/machine.html`](http://docs.micropython.org/en/latest/pyboard/library/machine.html) | | `micropython` | MicroPython 内部构件 | [`http://docs.micropython.org/en/latest/pyboard/library/micropython.html`](http://docs.micropython.org/en/latest/pyboard/library/micropython.html) | | `network` | 建立关系网 | [`http://docs.micropython.org/en/latest/pyboard/library/network.html`](http://docs.micropython.org/en/latest/pyboard/library/network.html) | | `uctypes` | 访问二进制数据 | [`http://docs.micropython.org/en/latest/pyboard/library/uctypes.html`](http://docs.micropython.org/en/latest/pyboard/library/uctypes.html) |

接下来,我们将查看一些更常见的 MicroPython 库,并查看每个库的一些代码示例。

常见的 MicroPython 库

现在让我们看看一些更常用的 MicroPython 库的例子。接下来的只是你可以用每个库做什么的一个例子。有关所有功能的完整描述,请参见联机文档。同样,不同的主板(固件)上的 MicroPython 库可能略有不同。事实上,Pycom 文档对此表述得非常清楚( https://docs.pycom.io/chapter/firmwareapi/pycom/ ).

)。 These modules are specific to Pycom devices and may be slightly different from other variants of MicroPython (for example, for non-Pycom devices). The modules include modules that support accessing the underlying hardware, such as I2C, SPI, WLAN, Bluetooth, etc.

机器

机器库包含与硬件相关的功能,提供了一个抽象层,您可以编写代码来与硬件交互。因此,这个库是您将用来访问定时器、通信协议、CPU 等特性的主库。由于此功能直接与硬件通信,因此在试验时应小心避免改变甚至潜在地损坏主板的性能或配置。例如,不正确地使用库可能会导致锁定、重新启动或崩溃。

Caution

使用低级机库时要小心,避免改变甚至潜在地损坏主板的性能或配置。

由于机器库是一个低级的硬件抽象,我们在本章中不会深入讨论它。相反,我们将在下一章看到更多的硬件特性。同时,让我们通过向您展示如何通过 help 函数发现一个库包含什么来探索另一个有趣的 MicroPython 知识宝库。例如,清单 5-8 显示了当我们在连接到扩展板的 WiPy 上发出语句help(machine)时,通过 REPL 控制台报告的内容的摘录。help()函数将显示库中所有可用的函数和常量。虽然它不能代替详细的解释,甚至不能代替完整的例子,但在第一次遇到一个库时,它会很有用。

>>> import machine
>>> help(machine)
<module 'umachine'>object  is of type module
  __name__ -- umachine
  mem8 -- <8-bit memory>
  mem16 -- <16-bit memory>
  mem32 -- <32-bit memory>
  reset -- <function>
...
  Pin -- <class 'Pin'>
  UART -- <class 'UART'>
  SPI -- <class 'SPI'>
  I2C -- <class 'I2C'>
  PWM -- <class 'PWM'>
  ADC -- <class 'ADC'>
  DAC -- <class 'DAC'>
  SD -- <class 'SD'>
  Timer -- <class 'Timer'>
  RTC -- <class 'RTC'>
  WDT -- <class 'WDT'>
  PWRON_RESET -- 0
  HARD_RESET -- 1
  WDT_RESET -- 2
...
Listing 5-8.The machine Library Help

注意这里有很多信息!这给我们最多的是我们可以用来与硬件交互的类的列表。这里我们看到有 UART、SPI、I2C、PWM 等类。让我们对比一下 Pyboard 的相同输出。下面显示了 Pyboard 类的相同摘录。

  Pin -- <class 'Pin'>
  Signal -- <class 'Signal'>
  I2C -- <class 'I2C'>
  SPI -- <class 'SPI'>
  UART -- <class 'UART'>
  WDT -- <class 'WDT'>

注意有几个不见了。我们将在下一章探讨这些差异。在你第一次使用的板上检查帮助(机器)的输出总是一个好主意。它可以让你省去很多为不存在的硬件寻找支持的麻烦!

网络

Pyboard 上的network库用于安装网络驱动程序(与网络抽象交互的类)和配置设置。虽然讨论的其他库在不同的电路板上略有不同,但是这个库在 Pyboard 和 WiPy(以及其他)电路板上是非常不同的。事实上,WiPy 有相同的库,但是包含不同的类。

这主要是因为 WiPy 内置了 WiFi,它有自己的网络工作机制。回想一下,我们在第三章中看到了 Pyboard 的网络库,我们将在后面的示例章节中再次看到它的使用。也就是说,我们了解到 Pyboard(以及类似的)板只有两个网络驱动程序:CC3KWINNET5K。WiPy network库包含三个类:WLANBluetoothServer。还记得我们在第三章中看到的 WLAN 类示例。

我们将在下一章看到更多关于网络类和有趣的蓝牙类的内容。

自定义库

构建自己的定制库似乎是一项艰巨的任务,但事实并非如此。可能有点挑战的是弄清楚你想要这个库做什么,并使这个库抽象(足够)到可以被任何脚本使用。编程的规则和最佳实践在这里发挥了作用,比如数据抽象、API 不变性等。

我们在上一章讨论了创建你自己的模块。在这一节中,我们将看看如何将我们的代码模块组织成一个库(包),我们可以将它部署(复制)到我们的 MicroPython 板上,并在我们所有的程序中使用。这个例子虽然简单,但却是一个完整的例子,如果您决定创建自己的定制库,可以将它用作模板。

对于这个例子,我们将创建一个包含两个模块的库:一个包含为传感器执行值转换的代码,另一个包含我们项目的帮助函数——我们希望重用的通用函数。我们将这个库命名为my_helper。它将包含两个代码模块:sensor_convert.pyhelper_functions.py。回想一下,我们还需要一个__init__.py文件来帮助 MicroPython 正确地导入函数,但是我们稍后会回到这个问题上。让我们看看第一个代码模块。

我们将文件放在名为my_helper(与库名相同)的目录中。这是典型的约定,您可以输入您想要的任何名称,但是您必须记住它,因为我们将在代码中导入库时使用该名称。图 5-1 显示了文件的布局示例。这张照片是我的 Pyboard 闪存盘拍的。

A447395_1_En_5_Fig1_HTML.jpg

图 5-1。

Directory/File Layout for the my_helper Sample Library on Pyboard

请注意,我们将包含这三个文件的目录移动(复制)到了我的 Pyboard 中。如果您将文件复制到 WiPy,您将需要使用清单 5-9 中的命令来使用 ftp。还要注意,我们在连接到 WiPy 时创建了目录,并且我们位于包含我们想要复制的文件的目录中。

Note

如果您使用 Linux,您可能需要为 ftp 命令添加-p 选项(被动)。

$ ftp 192.168.4.1
Connected to 192.168.4.1.
220 Micropython FTP Server
Name (192.168.4.1:cbell): micro
Password:
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> cd flash
250
ftp> ls
227 (192,168,4,1,7,232)
150
-rw-rw-r--   1 root  root        34 Jan  1  1980 main.py
drw-rw-r--   1 root  root         0 Jan  1  1980 sys
drw-rw-r--   1 root  root         0 Jan  1  1980 lib
drw-rw-r--   1 root  root         0 Jan  1  1980 cert
-rw-rw-r--   1 root  root       336 Jan  1  2098 boot.py
drw-rw-r--   1 root  root         0 Jan  1  2098 examples
-rw-rw-r--   1 root  root        15 Jan  1  2098 data.bin
-rw-rw-r--   1 root  root       377 Jan  1  2098 my_vehicles.json
226
ftp> mkdir my_helper
250
ftp> cd my_helper
250
ftp> put __init__.py
local: __init__.py remote: __init__.py
227 (192,168,4,1,7,232)
150
100% |***********************************|   255      1.24 MiB/s    00:00 ETA
226
255 bytes sent in 00:00 (0.61 KiB/s)
ftp> put helper_functions.py
local: helper_functions.py remote: helper_functions.py
227 (192,168,4,1,7,232)
150
100% |***********************************|   703      5.49 MiB/s    00:00 ETA
226
703 bytes sent in 00:00 (1.34 KiB/s)
ftp> put sensor_convert.py
local: sensor_convert.py remote: sensor_convert.py
227 (192,168,4,1,7,232)
150
100% |***********************************|   448      4.10 MiB/s    00:00 ETA
226
448 bytes sent in 00:00 (1.27 KiB/s)
Listing 5-9.Using ftp to copy files to the WiPy

现在让我们看看代码。第一个模块被命名为helper_functions.py,包含前面例子中的两个助手函数。因为这些是通用的,所以我们把它们放在这个代码模块中。然而,我们想在我所有的板上使用这个代码。问题是,machine.rng()函数为随机数返回不同的大小。在 Pyboard 上是 30 位,但在 WiPy 上只有 24 位。因此,我们使用一个try语句来检测库函数是否可用,设置一个可以在get_rand()中使用的全局变量来返回正确的值。清单 5-10 显示了模块的完整代码。

# MicroPython for the IOT - Chapter 5
# Example module for the my_helper library
# This module contains helper functions for general use.

try:
    import pyb
    _PYBOARD = True
except ImportError:
    import machine
    _PYBOARD = False

# Get a random number from 0-1 from a 2²⁴ bit random value
# if run on the WiPy, return 0-1 from a 2³⁰ bit random
# value if the Pyboard is used.
def get_rand():
    if _PYBOARD:
        return pyb.rng() / (2 ** 30 - 1)
    return machine.rng() / (2 ** 24 - 1)

# Format the time (epoch) for a better view
def format_time(tm_data):
    # Use a special shortcut to unpack tuple: *tm_data
    return "{0}-{1:0>2}-{2:0>2} {3:0>2}:{4:0>2}:{5:0>2}".format(*tm_data)

Listing 5-10.The helper_functions.py module

第二个代码模块名为 sensor_convert.py,它包含的函数有助于将传感器原始值转换为字符串,以便进行定性比较。例如,函数get_moisture_level()根据原始值的阈值返回一个字符串。传感器的数据手册将定义这些值,除非能够校准传感器,否则应在代码中使用这些值。在这种情况下,如果该值小于下限,则土壤是干燥的;如果大于上限,土壤是湿的。清单 5-11 显示了模块的完整代码。

# MicroPython for the IOT - Chapter 5
# Example module for the my_helper library

# This function converts values read from the sensor to a
# string for use in qualifying the moisture level read.

# Constants - adjust to "tune" your sensor

_UPPER_BOUND = 400
_LOWER_BOUND = 250

def get_moisture_level(raw_value):
    if raw_value <= _LOWER_BOUND:
        return("dry")
    elif raw_value >= _UPPER_BOUND:
        return("wet")
    return("ok")

Listing 5-11.The sensor_convert.py module

现在让我们检查一下__init__.py文件。这是一个非常神秘的文件,开发人员经常对此感到困惑。如果您的库目录中没有包含,您应该手动导入您想要使用的内容。也就是用类似import.my_helper.helper_functions的东西。但是有了这个文件,你可以通过一个简单的import my_helper语句一次性导入所有文件。我们来看一下__init__.py文件。下面显示了该文件的内容。

# Metadata

__name__ = "Chuck's Python Helper Library"
__all__ = ['format_time', 'get_rand', 'get_moisture_level']
# Library-level imports
from my_helper.helper_functions import format_time, get_rand
from my_helper.sensor_convert import get_moisture_level

请注意,在第一行,我们使用了一个特殊的常量来设置库的名称。下一个常量限制了 import 语句的* (all)选项将导入的内容。因为它列出了所有的方法,所以这只是一个练习,但却是一个很好的习惯,特别是当你的库和模块包含许多你不想让其他人使用的内部函数时。最后两行显示了用于从模块导入函数的 import 语句,使导入库的任何人都可以使用这些函数。下面给出了一个简短的例子,说明如何做到这一点以及如何使用别名。这里,我们用myh作为my_ helper的别名。

>>> import my_helper as myh
>>> myh.get_rand()
0.2830396

我们现在可以通过别名访问(使用)所有三个函数,如下所示。

r_int = myh.get_rand() * 10
print(myh.format_time(value))
print(myh.get_moisture_level(sensor_raw))

如果您想知道,帮助功能也可以在这个自定义库上工作!

>>> import my_helper
>>> help(my_helper)
object <module 'Chuck's Python Helper Library' from 'my_helper/__init__.py'> is of type module
  __path__ -- my_helper
  helper_functions -- <module 'my_helper.helper_functions' from 'my_helper/helper_functions.py'>
  __name__ -- Chuck's Python Helper Library
  sensor_convert -- <module 'my_helper.sensor_convert' from 'my_helper/sensor_convert.py'>
  get_rand -- <function get_rand at 0x20004270>
  format_time -- <function format_time at 0x20004300>
  __file__ -- my_helper/__init__.py
  __all__ -- ['format_time', 'get_rand', 'get_moisture_level']
  get_moisture_level -- <function get_moisture_level at 0x20004920>

一旦您开始尝试 MicroPython 并完成了几个项目,您就可以开始构建一组不时重用的函数。这些是放入图书馆的完美候选。如果这些函数不是一个更大的类或对象的一部分,那就再好不过了。只要你把它们组织成功能相似的模块,你就不需要担心把它们变成类。另一方面,如果涉及到数据或者函数集作用于一组数据,那么您应该考虑将函数集作为一个类,以便更容易使用和更高质量的代码。

Wait, What about Circuitpython?

回想一下第三章中,我们在看 Adafruit 的 Circuit Playground 电路板时讨论了 CircuitPython。本章没有更详细地介绍 CircuitPython,因为它是 MicroPython 的一个端口,因此我们对 MicroPython 库的了解也适用。不同的是一些特定于电路板的库和函数,CircuitPython 有一些特定于 Adafruit 电路板的高级库。有关 CircuitPython 的更多信息,请参见 https://circuitpython.readthedocs.io/en/stable/

摘要

MicroPython 固件为 IOT 项目提供了很多功能。事实上,我们可以使用许多不同的类,从内置函数编写健壮而复杂的 MicroPython 程序,这些内置函数为语言提供了处理数据、执行计算、甚至处理时间值以及直接与硬件接口的广泛能力。

使用硬件是 MicroPython 与其他主板最大的不同。这是因为这些板非常不同。有些有网络,有些没有。有些芯片的板载特性比其他芯片更多,有些芯片的内存更少,GPIO 引脚甚至更少。因此,当涉及到硬件抽象层时,不同主板的固件会有所不同也就不足为奇了。

在这一章中,我们探讨了一些更常用的内置库和 MicroPython 库,它们通常适用于所有的主板。在下一章中,我们将更深入地探讨 MicroPython 中的底层库和硬件支持,包括我们可以使用的特定于主板的库,如 Pyboard、WiPy 等。

Footnotes 1

此外,请记住这个时间间隔——称为采样率——也必须对项目有意义。在受控气候中每秒钟对环境温度采样 30 次可能会产生大量无用的数据,因为它很少变化。