2017年4月29日 星期六

MicroPython on ESP8266 (二) : 數值型別測試

前幾天偶然發現了 MicroPython 這個專案後, 嘗試在之前拿來做為 Arduino 連網模組的 ESP-01 板子上燒錄 MicroPython for ESP8266 韌體, 發現這塊 512KB FLASH 的 Python 功能雖然受到一些限制, 而且只接出了 GPIO0 與 GPIO2 兩個輸出入埠, 但以其內建 WiFi 網路能力與 MicroPython 加持下, 在成本與體積方面, 作為小型 IOT 端設備 (兩個輸出入腳) 還是比 Arduino 方便與便宜 (ESP-01 一片才 60~80 元). 事實上 ESP8266 本身是 32 位元核心處理器, 屈就於充當 Arduino 的上網模組確實是可惜了些, ESP-01 模組本身就可以當微控器啊!

如何燒錄 MicroPython 韌體到 ESP-01 參考 :

# MicroPython on ESP8266 (一) : 燒錄韌體
韌體燒錄完畢後, 關掉 ESP-01 模組的電源, 拔掉 GPIO0 的接地線後再送電, 開啟 Putty 以 115200 速率連線 ESP-01 模組所連接的串列埠, 就會進入 MicroPython 的 REPL 命令列 (即 Python 解譯器) 介面了.

下面是我測試 512KB MicroPython 的紀錄, 主要參考了下面兩本書 :

Python 程式設計入門 (博碩出版, 葉難) :

這本是我看過 Python 中文入門書籍寫得最深入詳盡的一本 (不是之一).


# 精通 Python-運用簡單的套件進行現代運算 (碁峰出版, 賴屹民譯) :

Source : 金石堂

這本書原文就寫得很棒, 原作者 Bill Lubanovic 筆調非常風趣, 譯者賴屹民翻譯功夫也是了得, 我覺得這本是最適合入門者看的第一本 Python 書, 原文書是歐萊禮出版的 "Introducing Python" :

Source : 歐萊禮

Python 的資料型態分為數值資料型態與容器資料型態, 數值資料型態有下列四種 :
  1. int (整數)
  2. float (浮點數)
  3. bool (布林)
  4. complex (複數, MicroPython 由於記憶體限制沒有實作)  
容器類型則有五種 :
  1. str (字串)
  2. tuple (元組)
  3. list (串列)
  4. dict (字典)
  5. set (集合)  
其中數值型態與容器類型中的 tuple 與 str 是不可變 (immutable) 的資料型態, 而 list, dict 與 set 三種為可變 (mutable) 的資料型態. 所謂不可變是指物件一旦建立, 其值即不可再更改.

直接輸入 int, float, str 等資料類型名稱可檢查該類型有無實作 :

<<< int
<<< float
<<< bool
<<< str
<<< complex
Traceback (most recent call last):
  File "NameError: name 'complex' is not defined

Python 的內建函數 type() 則可檢查資料的型態 :

<<< type(1)
<<< type(1.0),type(2e10)
(<<< type(True), type(False)
(<<< type('Hello'), type("Hello")
(<<< type(1+2j)
Traceback (most recent call last):
  File "SyntaxError: complex values not supported

Python 的數值資料型態有三種 : 整數 (int), 浮點數 (float), 以及複數 (complex). 支援複數使得 Python 在科學運算上具有其他語言沒有的優勢, 不過因為 MicroPython 是 Python 3 在嵌入式設備上的精簡版, 受大小限制不支援複數 :

>>> 1+1j
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: complex values not supported

另外 MicroPython 也沒有實作 decimal 函式庫 :

>>> from decimal import *
Traceback (most recent call last):
  File "ImportError: no module named 'decimal'


一. 整數 (int) :

Python 3 的整數具有無限精準度, 可以表示極大整數直到記憶體容量的極限為止. MicroPython 的整數可以表示到多大呢? 表示 Googol (10 的 100 次方) 是沒問題的 :

>>> googol=10**100
>>> googol
10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

表示 10**200 也沒問題 :

>>> googol*googol
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

但是 googol 的 googol 次方就不行了, 好像是記憶體溢位導致系統重置 (reset) :

>>> googol ** googol

 ets Jan  8 2013,rst cause:1, boot mode:(3,7)

load 0x40100000, len 31888, room 16
tail 0
chksum 0x65
load 0x3ffe8000, len 1072, room 8
tail 8
chksum 0xa4
load 0x3ffe8430, len 3000, room 0
tail 8
chksum 0x0c
csum 0x0c
......
......
lŒŒŽbl`‚‚lû#4 ets_task(40100164, 3, 3fff827c, 4)
OSError: [Errno 2] ENOENT
OSError: [Errno 2] ENOENT

MicroPython v1.8.7-662-gf85fd79 on 2017-04-25; ESP module with ESP8266
Type "help()" for more information.
>>>

到底對 512KB 的 ESP-01 模組而言所能表示的整數是多少? 經過用二分法逐步測試發現, 10**2129 沒問題, 但 10**2130 就不行了.

>>> 10**2030

 ets Jan  8 2013,rst cause:1, boot mode:(3,7)

load 0x40100000, len 31888, room 16
tail 0
chksum 0x65
load 0x3ffe8000, len 1072, room 8


Python  的算術運算子有如下七個 :

運算子 說明
 x + y 加法運算
 x - y 減法運算
 x * y 乘法運算
 x / y 除法運算
 x // y 整數除法運算
 x ** y 冪次運算
 x % y 取餘數運算

基本的算術運算測試如下 :

>>> 1+2+3+4+5+6+7+8+9+10
55
>>> 1 - 2-3
-4
>>> 2*3
6
>>> 1+2*3      #先乘除後加減
7
>>> (1+2)*3   #用括號改變優先順序
9
>>> 11/3    #一般除法 (只精確到小數後第 5 位四捨五入)
3.66667
>>> 11//3   #整數除法 (求商)
3
>>> 11%3  #整數除法 (求餘數)
2
>>> divmod(11,3)  #求商與餘數 (傳回 tuple, 前為商, 後為餘數)
(3, 2)
>>> 11/0     #除以 0 會導致例外
Traceback (most recent call last):
  File "ZeroDivisionError: division by zero

>>> 10**3     #次方
1000

除了上述的四則運算外, Python 還有相對應的複合運算子 (也適用於浮點數運算) :

>>> a=10
>>> a += 10   #運算前 a=10
>>> a
20
>>> a -= 10    #運算前 a=20
>>> a
10
>>> a *= 10   #運算前 a=10
>>> a
100
>>> a //= 10    #運算前 a=100
>>> a
10
>>> a /= 10     #運算前 a=10
>>> a
1.0


Python 的整數字面值 (Linteral) 預設是 10 進位表示法, 除此之外也可以用 2 進位, 8 進位或 16 進位等基數系統來表示, 以 0b 或 0B 開頭的整數為 2 進位; 0o 或 0O 開頭為 8 進位; 0x 或 0X 開頭則為 16 進位, 如下所示 :

>>> 0b10   
2
>>> 0B10
2
>>> 0o10
8
>>> 0O10
8
>>> 0x10
16
>>> 0X10
16
>>> 0x1f
31


以前在 Python 2.x 時代以 0 開頭的整數會被認為是 8 進位 (即與 0o 或 0O 開頭一樣) :

C:\Users\Tony>py -2
Python 2.7.12 (v2.7.12:d33e0cf91556, Jun 27 2016, 15:19:22) [MSC v.1500 32 bit (
Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> 012    #被認為是 8 進位整數
10

但這在 Python 3 已經被廢棄了, 整數前面的 0 會被忽略去除  :

>>> 012    #被認為是 10 進位整數

12  

Python 內建函數 bin(), oct(), 與 hex() 可將 10 進位整數分別轉成 2, 8, 與 16 進位表示法 : 

>>> bin(255)
'0b11111111'
>>> oct(255)
'0o377'
>>> hex(255)
'0xff'

除了使用字面值 (Literal) 直接產生整數物件外, 也可以用 Python 內建函數 int() 來建立 int 物件, 此函數能將整數或字串轉成 int 物件, 它可以傳入一或兩個參數 :

int(p1 [,p2])

第一個參數可以是整數字面值或字串, 只有當第一參數是字串時可以用第二參數指定要用哪種基數轉換, 而且不限 2, 8, 16 進位, 可以是任何進位, 最多可到 36 進位  (0~9, A~Z, 共 36 個符號, 大小寫不分), 可用 0, 2~36, 其中 0 表示依字串格式 : 

>>> int(3.14159)
3
>>> int("0b1111",2), int("0b1111")
(15, 15)
>>> int("0o20",8), int("0o20")
(16, 16)
>>> int("0xffff",16), int("0xffff")
(65535, 65535)
>>> int('0xff',0)
255
>>> int('ABCXYZ1230',36)     #36 進位轉成 10 進位
1047646094138316
>>> int('ABCXYZ1230',37)     #第二個參數只能 2~36
Traceback (most recent call last):
  File "ValueError: int() arg 2 must be >= 2 and <= 36

但是如果格式不符合要求會產生錯誤 :

>>> int("0o20",10)
Traceback (most recent call last):
  File "ValueError: invalid syntax for integer with base 10
>>> int("0o20",16)
Traceback (most recent call last):
  File "ValueError: invalid syntax for integer with base 16


其中 ob/0B 開頭字串的 int() 是 bin() 的反運算; 0o/0O 開頭的是 oct() 的反運算; 0x/0X 開頭的是 hex() 的反運算.

與整數有關的還有位元運算, 位元運算子有六個, 其運算元只能是整數 :

 運算子 說明 
 ~x 位元 NOT 運算, 每一個位元做 0 變 1 與 1 變 0 運算
 x< 位元左移運算, 運算元 x 的每一個位元往左移 y 次, 右方補 0
 x>>y 位元右移運算, 運算元 x 的每一個位元往右移 y 次, 左方補 0
 x&y 位元 AND 運算, 運算元 x 與 y 的相對位元做 AND 運算
 x|y 位元 OR 運算, 運算元 x 與 y 的相對位元做 OR 運算
 x^y 位元 XOR 運算, 運算元 x 與 y 的相對位元做 XOR 運算

左移運算值會變大, 每左移一位元增大 2 倍 (乘以 2); 而右移運算值會變小, 每右移一位元減小 2 倍 (除以 2), 例如 :

>>> 2<<1
4
>>> 4<<1
8
>>> 8<<1
16
>>> 16>>1
8
>>> 8>>1
4
>>> 4>>1
2
>>> 2>>1
1
>>> 1>>1
0
>>> 2<<3
16



二. 浮點數 (float)  :

浮點數 (float 型別) 可以用小數點與科學表示法 e 或 E (10 的次方之意, 不是自然指數的 e) 來表示, 但小數在 MicroPython 只能表示到小數點後 5 位四捨五入 (
Win7 64 位元 PC 上可到小數後第 16 位) :

>>> 1.23456789
1.23457
>>> 3.1415926    #圓周率
3.14159
>>> 6.02e23    #亞佛加厥數
6.02e+23
>>> 6.626070040e-34     #普郎克常數
6.62607e-34
>>> 1.38064852e-23     #波茲曼常數
1.38065e-23

MicroPython 浮點數絕對值用科學表示法最大到 3e48, 最小約為 1e-40 :


>>> 3.4e38
3.4e+38
>>> 3.41e38
inf
>>> 1e-39
1e-39
>>> 1e-40
9.99967e-41      #開始有誤差
>>> 1e-43
9.52883e-44      #誤差變大
>>> 1e-44
5.60519e-45      #誤差更大
>>> 1e-45

0.0                     #太小變 0 了

由於浮點數是以二進位儲存, 大部分的浮點數其實無法以二進位完整精確表示 (事實上只有整數可以精確表示), 而只是非常接近理論值的近似值而已, 實際運算結果與理論值會有些微誤差. 例如下列程式理論上應該得到 1.0 的結果才對, 但實際上卻是 0.999999 :

>>> s=0;
>>> for i in range(10):
...     s += 0.1
...
...
...
>>> sum==1
False          #竟然不是 1
>>> sum==1.0
False         #竟然不是 1
>>> s
0.999999      #理論值為 1.0, 實際上是 0.999999

因此浮點數不應該直接拿來比較, 而是應該比較與理論值的差是否在容許的誤差內才對, 例如 :

>>> (s-1) <= 0.0000000001    #與理論值的差比較小於容許誤差就可認為相等
True

除了用字面值建立浮點數物件外, 還可以用內建函數 float() 來建立 float 物件, 它只有一個參數, 可傳入整數或浮點數字面值, 或者是一個字串, 傳回一個 float 物件 :

>>> float()      #沒有傳入參數預設為 0.0
0.0    
>>> float(123), float(-123)
(123.0, -123.0)
>>> float(3.1415926)    #精確度為小數點後五位
3.14159
>>> float('3.1415926')   #傳入字串
3.14159
>>> float('1.23456789e23')
1.23457e+23
>>> float(3.4e38)          #科學表示法最大為 3.4e38
3.4e+38
>>> float(3.41e38)
inf
>>> float(1e-38)
1e-38
>>> float(1e-39)
1e-39
>>> float(1e-40)            #開始有誤差
9.99967e-41
>>> float(1e-43)
9.52883e-44
>>> float(1e-44)
5.60519e-45
>>> float(1e-45)            #太小變 0 了
0.0    

與浮點數運算相關最常用的內建函數是 round(), 可以傳入一或兩個參數, 其中第二參數是指定四捨五入到小數點後第幾位, MicroPython 只能表示到小數後 5 位, 故第二參數大於 5 之後結果都一樣, 直到 39 時會傳回一個特殊值 nan (非數字, not a number), 例如 :

<<< round(3.1415926)
3
<<< round(3.1415926,1)
3.1
<<< round(3.1415926,2)
3.14
<<< round(3.1415926,3)
3.142
<<< round(3.1415926,4)
3.1416
<<< round(3.1415926,5)
3.14159
<<< round(3.1415926,6)
3.14159
<<< round(3.1415926,38)
3.14159
<<< round(3.1415926,39)
nan

更奇怪的是, round() 在 Python 3 有向偶數靠攏的怪癖 (參考上述葉難書中的 3-1 節), 例如 :

<<< round(1.5),round(-1.5)
(2, -2)
<<< round(2.5),round(-2.5)
(2, -2)
<<< round(3.5),round(-3.5)
(4, -4)
<<< round(4.5),round(-4.5)
(4, -4)
<<< round(5.5),round(-5.5)
(6, -6)
<<< round(6.5),round(-6.5)
(6, -6)

藍色部分是預期要四捨五入進位到個位數得到 (3, -3), (5, -5), 以及 (7,-7) 的, 但結果卻是捨去 .5 而倒向偶數. 此特性在使用 round() 進行數學計算時必須注意, 否則會得到非預期結果.

而在 Python 2.x 執行結果卻不是這樣 :

C:\Users\Tony>py -2
Python 2.7.12 (v2.7.12:d33e0cf91556, Jun 27 2016, 15:24:40) [MSC v.1500 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> round(1.5),round(-1.5)
(2.0, -2.0)
>>> round(2.5),round(-2.5)
(3.0, -3.0)
>>> round(3.5),round(-3.5)
(4.0, -4.0)
>>> round(4.5),round(-4.5)
(5.0, -5.0)
>>> round(5.5),round(-5.5)
(6.0, -6.0)
>>> round(6.5),round(-6.5)
(7.0, -7.0)

感覺 Python 2.x 的 round() 處理四捨五入方式較合理, 但據說 Python 3 改成這樣其實是符合 IEEE 754 預設規範的, 參考 :

# Python 3.x rounding behavior

此文的回應中提到 IEEE 754 裡面有五種處理浮點數 rounding 的方式, 而 Python 2.x 所用的四捨五入只是其中一種. 英文 Rounding 一般翻成四捨五入其實是有偏見的, 正確的翻譯應該是 "湊整", 就是湊成一個鄰近的整數.

Python 3 所採用的是 IEEE 754 的預設湊整方式 : rounding to even (湊整到最近的偶數). 四捨五入法 (rounding up) 是原數加 0.5 的方式進行湊整, 此法在大部分數據趨向於兩個整數中間值 (x.4, x.5, x.6 附近) 時會造成平均值與其他統計量的較明顯偏差. 參考 :

What's the difference between round up, round down and round off?

"There are two schools of thought on this. One is to round UP in this situation. Rounding off is then analogous to adding 0.5 to the number and rounding DOWN the result. This is simple and computers often use this method of rounding off. However, when dealing with a large set of numbers (many being half way between two integers), this process will introduce a bias which would affect the mean of the numbers and other statistics.

The other school of thought is to round OFF to the nearest EVEN integer. This is not that difficult to code in a computer and eliminates any bias that might be introduced in the rounding process."

另外有三個特殊的浮點數 float('nan'), float('inf'), 以及 float('-inf'), 分別用來表示非數值 (nan), 正無窮大 (inf), 以及負無窮大 (-inf). 當浮點數超過硬體所能表示最大值時就會得到 inf, 在 ESP-01 大於 3.4e34 就被認為是 inf. 而 nan 會在無法進行的數學運算時產生, 例如 :

>>> 3.4e38
3.4e+38
>>> 3.41e38
inf
>>> -3.4e38
-3.4e+38
>>> -3.41e38
-inf
>>> 3.41e38/3.41e38
nan


三. 布林值 (bool) :

布林 (bool) 是 Python 最簡單的資料型態, 其值只有 True 與 False 兩個 (注意首字母需大寫, true 與 false 都不是布林值), 布林 True 與 False 的值 (value) 其實就是整數 1 與 0, 用值的比較運算子  == 去比較會傳回真, 但是 True/False 與 1/0 的物件參考不同, 因此用參考比較運算子 is 去比較時都會傳回 False (因參考不同, 記憶體位址也不同之故), 例如 :

<<< True==1       #True 與 1 的值相同
True
<<< False==0      #False 與 0 的值相同
True
<<< True is 1       #True 與 1 的物件參考不同
False
<<< False is 0      #False 與 0 的物件參考不同
False

利用內建函數 int() 函數可以將布林值轉成 1 與 0, 轉換後值與參考均相同, 例如 :

<<< int(True)           #True 會轉成 1 的 int 物件
1               
<<< int(False)          #False 會轉成 0 的 int 物件
0
<<< int(True)==1     #值相同
True
<<< int(False)==0    #值相同
True
<<< int(True) is 1     #物件參考相同
True
<<< int(False) is 0    #物件參考相同
True
因為 True 與 False 的值為 1 與 0, 因此可以進行四則運算, 雖然看起來沒甚麼意義 :

<<< True + 1
2
<<< False + 1
1
<<< True + 1.1
2.1
<<< False + 1.1
1.1

與布林值相關的運算子有邏輯運算與關係運算, 邏輯運算子有三個 :

 運算子 說明
 x and y 邏輯 AND 運算, 須 x 與 y 均為 True 時才傳回 True, 否則為 False
 x or y 邏輯 OR 運算, 只要 x 或 y 有一個為 True 時就傳回 True
 not x 邏輯 NOT 運算, 若 x 為 True 就傳回 False, 否則傳回 True

例如 :

>>> not True
False
>>> not False
True
>>> True and False
False
>>> True and True
True
>>> False and True
False
>>> False and False
False
>>> True or False
True
>>> True or True
True
>>> False or True
True
>>> False or False
False

Python 沒有提供邏輯互斥或運算子 xor (但有提供互斥或運算子 ^), 不過互斥或運算子可以用 and 與 or 運算來組合, 其真值是只有當兩個運算元相異時才傳回真, 此為互斥名稱由來 :

x xor y=(x or y) and not (x and y)

參考 : 邏輯異或

例如 :

>>> x,y=True,True
>>> (x or y) and not (x and y)
False
>>> x,y=False,False
>>> (x or y) and not (x and y)
False
>>> x,y=True,False
>>> (x or y) and not (x and y)
True
>>> x,y=False,True
>>> (x or y) and not (x and y)
True

Python 內建函數 bool() 可以將其他資料類型轉換成布林型態, 除了空值 (空字串, 0, None) 以外都傳回 True, 例如 :

>>> bool(None)   #無值
False
>>> bool('')           #空字串
False
>>> bool("")         #空字串
False
>>> bool(())          #空元組
False
>>> bool([])          #空串列
False
>>> bool(0)
False
>>> bool(1)
True
>>> bool(2)
True

而關係運算子則有下列六個 :

 運算子 說明
 x > y 大於運算
 x < y 小於運算
 x >= y 大於等於運算
 x <= y 小於等於運算
 x != y 不等於運算
 x == y 等於運算

例如 :

>>> 1==1
True
>>> 1>1
False
>>> 2>1
True
>>> 1<1
False
>>> 1<2
True
>>> 1>=1
True
>>> 1>=2
False
>>> 1<=1
True
>>> 1<=0
False
>>> 1!=2
True
>>> 1!=1
False
>>> 1==1
True
>>> 1==2
False

四. 複數 (compound)  :

支援複數是 Python 的特色, 主要用在科學計算上面. 但對於用在嵌入式設備上的 MicroPython 來說用不到, 所以未支援 :

>>> 1+1j
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: complex values not supported

複數由實部 Real 與虛部 Imagine 組成 : R+ Ij, 其中 j 代表虛部, j 為 -1 的平方根, R 與 I 都是浮點數, 不過虛部若為 1 時不能省略, 否則 j 會被視為未宣告的變數而出現錯誤, 以下是在 CPython 上做的測試 :

C:\Users\Tony>python
Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
<<< 1+1j
(1+1j)
<<< 1+j       #虛部為 1 也必須寫出來
Traceback (most recent call last):
  File "NameError: name 'j' is not defined
<<< 1+j1     #j 必須放在最後面
Traceback (most recent call last):
  File "NameError: name 'j1' is not defined
<<< 1j**2    #j 的平方是 -1
(-1+0j)
<<< (1+j)*(1-j)       #虛部為 1 也必須寫出來
Traceback (most recent call last):
  File "NameError: name 'j' is not defined
<<< (1+1j)*(1-1j)   #複數相乘運算
(2+0j)



沒有留言 :