2017年5月16日 星期二

MicroPython on ESP8266 (五) : WiFi 連線與 WebREPL 測試

最近大部分閒暇時間都花在測試 ESP-01 模組上的 MicroPython, 這兩周完成了 Python 資料型態, 包括數值, 序列, 字典與集合等常用資料型別的測試. 接下來還有程式結構, 內建與自訂函數, 物件與模組等項目要測試, 但我已等不及要先進行 ESP8266 模組最重要的測試項目 : WiFi 連線測試了.

以下使用新版 ESP-01 模組 (1MB Flash) 進行測試, 舊版的 512KB 板有空再測試補充在後面. MicroPython 的網路與 WiFi 教學文件參考 :

# http://docs.micropython.org/en/latest/esp8266/esp8266/tutorial/network_basics.html
# http://docs.micropython.org/en/latest/esp8266/esp8266/tutorial/intro.html#wifi

此系列前面四篇測試參考 :

# MicroPython on ESP8266 (二) : 數值型別測試
# MicroPython on ESP8266 (三) : 序列型別測試
# MicroPython on ESP8266 (四) : 字典與集合型別測試

MicroPython 文件參考 :

# MicroPython tutorial for ESP8266  (官方教學)
# http://docs.micropython.org/en/latest/micropython-esp8266.pdf
# http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
# http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket
# https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)

一. 建立 WiFi 連線 :

ESP8266 的 WiFi 網路介面有三種工作模式 :
  1. STA 模式 (Station) : ESP8266 可連線至一路由器 (AP)
  2. AP 模式 (Access Point) : 作為熱點讓其他設備連線至 ESP8266 
  3. STA+AP模式 : 混和模式 (同時開啟 STA 與 AP 功能)
MicroPython for ESP8266 1.8.7 版韌體預設只開啟 AP 模式, 為了使用筆電抓取測試紀錄, 以下根據上面官網教學文件將 ESP-01 模組的 STA 模式也打開, 變成 STA+AP 混和模式.

MicroPython 的網路函式庫放在 network 模組中, 可用來設定 ESP8266 的 WiFi 連線, 欲設定ESP8266 的 WiFi 連線須先匯入 network 模組 :

>>> import network

呼叫 network 模組的 network.WLAN() 函數並傳入模式參數 (network.STA_IF 或 network.AP_IF) 會傳回該模式的 WiFi 介面物件, 例如 :

>>> import network
>>> sta = network.WLAN(network.STA_IF)
>>> ap = network.WLAN(network.AP_IF)

呼叫介面物件的 active() 方法 (不帶參數) 會傳回該介面的狀態, 例如 :

>>> sta.active()     #不帶參數就是讀取介面狀態
False
>>> ap.active()
True

可見剛燒好的韌體預設是開啟 AP 模式, 關閉 STA 模式. 呼叫介面物件的 ifconfig() 方法會傳回包含此 AP 網路介面的網址, 網路遮罩, 閘道器, 以及 DNS 所組成的 tuple, 例如 :

>>> ap.ifconfig()
('192.168.4.1', '255.255.255.0', '192.168.4.1', '208.67.222.222')

可見此 AP 的網址為 192.168.4.1, 網路遮罩為 255.255.255.0, 閘道器為 192.168.4.1, DNS 為 208.67.222.222.

如果要開啟或關閉 WiFi 介面, 只要在 active() 方法中傳入 True 或 False :

>>> sta.active(True)               #開啟 STA 模式
#5 ets_task(4020ed88, 28, 3fff9f88, 10)
>>> sta.active()                      #查詢 STA 模式是否開啟
True

打開 STA 模式後可用 scan() 方法掃描附近有哪些基地台 :

>>> sta.scan()
[(b'family', b'l\x19\x8f\xb7\x9ay', 1, -87, 3, 0), (b'KT220', b'\xa8N?\xa3\x8fh', 1, -91, 4, 0), (b'EDIMAX-tony', b'\x80\x1f\x02-Z\x9e', 11, -47, 4, 0), (b'ching', b'T\xb8\n\x84\xf4p', 2, -89, 4, 0), (b'CHT5668', b'\xd8\xfe\xe3\\\xc0\xfb', 11, -87, 3, 0), (b'edimax.setup', b't\xda8\x15\x16\x00', 3, -80, 2, 0), (b'dlink', b'\x00$\x01\xad\xa0e', 6, -87, 0, 0), (b'andy', b'\xfcu\x16\x02a`', 6, -84, 4, 0), (b'TP-LINK_601A04', b"\xe8\xde'`\x1a\x04", 7, -73, 4, 0), (b'Melissa-eason', b'\x04\x8d8\xa5H\xea', 9, -91, 1, 0), (b'Home', b'\xc4\xe9\x84f3\xc1', 10, -73, 3, 0), (b'DSL-6641K', b'\xf0}h\xfc\x0e<', 11, -93, 2, 0)]

然後就可以用 connect() 方法連線基地台 (路由器), 下面是連線我的手機熱點 :

>>> sta.connect('H30-L02-webbot', '1234567890')      #連線 AP (路由器)

查看手機的基地台設定, 可以看到 ESP8266 (MAC 位址) 已經連線到手機熱點了 :



呼叫 ifconfig() 方法會傳回此 STA 介面的網址, 網路遮罩, 閘道器, 以及 DNS 的 tuple :

>>> sta.ifconfig()
('192.168.43.72', '255.255.255.0', '192.168.43.1', '192.168.43.1')

可見 ESP-01 連上基地台後獲得派發網址 192.168.43.72, 網路遮罩為 255.255.255.0, 閘道器為 192.168.43.1, 而 DNS 伺服器網址為 192.168.43.1.

連線完成後呼叫 isconnected() 方法會傳回 True, 表示已經連上基地台了 :

>>> sta.isconnected()
True

注意, 以上的連線設定會寫入 ESP8266 內建的的 FLASH 裡面, 即使關機也不會消失, 下次重開機時會自動連線上次設定的基地台.


二. 利用 WebREPL 遠端連線操控 ESP8266 :

除了使用 USB 連線直接用串列埠進入 REPL 介面操控 ESP8266 外, 還可以透過 WiFi 連線, 使用網頁式的 REPL 介面操控 ESP8266. 這樣只要讓 ESP8266 模組與 PC 連上同一個路由器, 就可以在 PC 上透過 WiFi 以無頭 (Headless) 模式操控 ESP8266. 即使沒有無線基地台可用也沒關係, MicroPython 韌體預設開啟 ESP8266 的 AP 模式, 亦即 ESP8266 本身就自帶熱點 (IP 固定為 192.168.4.1), PC 可連線此 AP 來使用 webrepl 功能.

參考下列教學文件 :

# WebREPL - a prompt over WiFi
# WebREPL (web browser interactive prompt)
# Adafruit : Access WebREPL
# Running MicroPython on the ESP8266
# An Introduction to MicroPython

WebREPL 可以在 Intranet 環境 (無 Internet 連線時) 使用, 但要先到 GitHub 下載 WebREPL Client 客戶端 :

# https://github.com/micropython/webrepl

按右邊 "Clone and download" 鈕, 再按 "Download zip" 鈕下載 zip 檔後解壓縮, 用 Chrome 或 FireFox 開啟裡面的 webrepl.html 網頁檔, 即可使用 WebREPL 功能.


由於 ESP8266 的 MicroPython 韌體預設開啟 AP 模式, 因此將 PC (我是用筆電) 連線 ESP8266 的 AP, 其 SSID 為 MicroPython-xxxxxx, 後面的 xxxxxx 是 MAC 位址最後 6 個數字, 密碼是 micropythoN (注意, 最後的 N 是大寫).


參考 :

"After a fresh install and boot the device configures itself as a WiFi access point (AP) that you can connect to. The ESSID is of the form MicroPython-xxxxxx where the x’s are replaced with part of the MAC address of your device (so will be the same everytime, and most likely different for all ESP8266 chips). The password for the WiFi is micropythoN (note the upper-case N). Its IP address will be 192.168.4.1 once you connect to its network. WiFi configuration will be discussed in more detail later in the tutorial."

然後要先去 REPL 介面開啟 webrepl 功能 :

>>> import webrepl_setup

照 Adafruit 教學文件應該會出現是否要啟動 webrepl 功能的詢問選單, 選擇 Enable 的話還要設定連線密碼並重啟系統才會生效. 但是我測試的結果卻是出現錯誤, 但第二次輸入匯入指令又不再顯示錯誤, 真是奇怪 :

b▒#5 ets_task(40100164, 3, 3fff8398, 4)
could not open file 'boot.py' for reading
could not open file 'main.py' for reading

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>> import webrepl_setup
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "webrepl_setup.py", line 111, in <module>
  File "webrepl_setup.py", line 80, in main
  File "webrepl_setup.py", line 49, in get_daemon_status
OSError: [Errno 2] ENOENT
>>> import webrepl_setup
>>> import webrepl_setup
>>>

我在下面這篇文章發現原來 webrepl.start() 可以傳一個 password 參數進去手動更新密碼 (密碼最長是 9 個字元) :

# esp8266 webrepl_setup crashes #2605


這樣就可以啟動 webrepl 了 :

>>> import webrepl_setup
>>> import webrepl
>>> webrepl.start(password="1234567890")    #密碼太長 (最長 9 碼)
Traceback (most recent call last):
  File "  File "webrepl.py", line 69, in start
ValueError:
>>> webrepl.start(password="123456")       
WebREPL daemon started on ws://192.168.4.1:8266         #AP 模式網址
WebREPL daemon started on ws://192.168.43.72:8266     #STA 模式網址
Started webrepl in manual override mode


由於我同時開啟 ESP8266 的 AP 與 STA 模式, 因此會開啟兩個 webrepl 的 Daemon 程序, 其中 192.168.4.1 (固定 IP) 是 AP 模式網址, 而 192.168.43.72 (此為路由器 DHCP 指派, 不固定) 則是 STA 模式網址.

首先測試 AP 模式, 上面我已經用筆電連線 ESP8266 的 192.168.4.1 AP 了, 這樣就可以在 PC 上開啟 WebREPL Client 網頁 (即下載之 webrepl.html), 其預設 IP 就是 ESP8266 AP 的網址 192.168.4.1, 直接按 Connect 鈕就會在下方的方框中出現 "Welcome to webrepl!" 以及 "Password:" 提示號, 這時輸入上面設定的密碼按 Enter 就會出現 REPL 提示號了 :


這時在 REPL 介面上會顯示來自 PC 的連線訊息 :

WebREPL connection from: ('192.168.4.2', 2535)


在 webrepl 網頁上輸入 print('Hello') 同時 REPL 介面也會同步顯示輸入的指令與回應 : 

print('Hello')
Hello

上面這種連線 ESP8266 AP 的方式適合用在沒有 WiFi 路由器場合, 如果有一台無線基地台, 則除了可連線 ESP8266 的 AP 外, 可將 ESP8266 模組設定在 STA 或 STA+AP 模式連線此基地台, 這樣 ESP8266 會獲得路由器指派一個 IP (如上面範例的 192.168.43.72). PC 也可以利用這個 STA 模式的 IP 來連線 webrepl, 這時在 WebREPL 網頁的 IP 就必須改為 STA 模式的 IP (此處是 192.168.43.72) :



這時 REPL 介面上顯示的是來自 PC 在同一網段的 IP (此處是 192.168.43.73) :

WebREPL connection from: ('192.168.43.73', 2416)
print('Hello!')
Hello!

這種方式的好處就是 PC 在連線 webrepl 的同時, 仍然保有存取 Internet 的能力. 前面連線 ESP8266 AP 的方式會讓 PC 無法存取 Internet.

在 STA 或 STA+AP 模式下, 除了可開啟下載至本機的 WebREPL client 網頁連線 ESP8266 外, PC 也可以直接連線到 WebREPL 在 Internet 的公眾網頁進行連線 :

# http://micropython.org/webrepl/

是的沒錯, 在 Internet 上也是可以存取到 ESP8266 喔.

這個 WebREPL 功能具有很重大的意義, 只要將上面啟動 webrepl 的程式碼寫在 boot.py 裡面, 讓 ESP8266 模組一啟動就開啟 webrepl 功能, 理論上我們就可以不需要透過 USB 串列埠的硬體連接, 只要利用 WiFi 就能以 webrepl 的方式對 ESP8266 模組進行遠端存取了 (跟樹莓派的無頭存取 Headless access 異曲同工). 參考官網文件 :

# Start up scripts

"There are two files that are treated specially by the ESP8266 when it starts up: boot.py and main.py. The boot.py script is executed first (if it exists) and then once it completes the main.py script is executed. You can create these files yourself and populate them with the code that you want to run when the device starts up."

大意是說 MicroPython 在 ESP8266 上有兩個特殊檔案 : boot.py 與 main.py, 開機時會先去執行 boot.py (存在的話), 然後再執行 main.py. 因此我們可以把連線網路, 開啟 webrepl 等環境設定用的程式碼寫在 boot.py(類似 Arduino 的 setup 函數的功能), 而將主要的應用邏輯寫在 main.py (類似 Arduino 的 loop 函數的功能). 下面這篇 Adafruit 的文章對根目錄下的這兩個檔案用途解釋得很清楚 :

# Adafruit : Boot Scripts

不管是重啟系統或重開機, 這兩個檔案一定會被執行 (先 boot.py 再 main.py).

但目前遇到的問題是, 當使用 WEBREPL 網頁右方的傳檔功能時, 馬上就顯示 WEBREPL disconnected! 原因為何尚待進一步調查.

事實上按照下列文件去操作內部檔案系統, 我發現 ESP-01 模組似乎無法使用 os.open(), os.close(), os.mkdir(), os.listdir() 等函數, 會產生 OSError, 甚至 reboot :

# The internal filesystem

"If your devices has 1Mbyte or more of storage then it will be set up (upon first boot) to contain a filesystem. This filesystem uses the FAT format and is stored in the flash after the MicroPython firmware."

>>> import os
>>> os.mkdir('dir')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
OSError: [Errno 13] EACCES
>>> os.listdir()
['\x00\x00\x00\x00\x00\x00\x00\x00.\x00\x00\x00', '\x00\x00\x00\x00\x00\x00\x00\x00.\x00\x00\x00',
...........................................
(System reboot)

奇怪, 看起來 listdir() 方法沒有報錯, 是有實作且可執行的, 但是為何印出大量 \x00 後就重開機呢? \x00 到底表示檔案系統是啥狀態呢?


~~~ 2017-05-17 補充 :

從 os.listdir() 冒出一堆 \x00 以及上面用 webrepl 傳檔就斷線的情況, 我已在懷疑 1.8.7 版的 MicroPython 韌體在燒入 ESP8266 之後並沒有建立檔案系統, 這或許就是為何開機後出現無法開啟 boot.py 與 main.py 這兩個執行檔的原因了 :

b▒#4 ets_task(40100164, 3, 3fff8398, 4)
could not open file 'boot.py' for reading
could not open file 'main.py' for reading

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>>

這問題困擾我兩天了, 為何 WebREPL 終於測試成功後, 其傳檔功能卻失敗? 這問題若搞不定, ESP8266 就只能靠串列埠硬敲程式進去執行, 重開機又要重敲, 根本沒有實用價值. 今晚經過漫長的搜尋可能原因, 我用 "micropython esp8266 file permission" 去搜尋, 終於找到下面 MicroPython 論壇中的這篇文章, 給出了答案 :

# File Permission issues - OSError: [Errno 13] EACCES

原來其中這位 robos 遇到跟我一樣的問題, 就是在 os.listdir() 時得到一堆 \x00 頃印. 重點是下面這位 fdushin 先生給出了答案 : 就是重建檔案系統. 他提供了一段 Python 程式碼, 利用 flashbdev 函數在 ESP8266 的 FLASH 中寫入一個 boot.py 檔案, 我依樣畫葫蘆將其程式碼敲進去執行後, 再次執行 os.listdir() 果然列出 boot.py 這檔案了 :

 MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>> from flashbdev import bdev
>>> uos.VfsFat.mkfs(bdev)
>>> vfs = uos.VfsFat(bdev, "")
>>> with open("/boot.py", "w") as f:
...     f.write("""\
...     import gc
...     import webrepl
...     webrepl.start(password="123456")
...     gc.collect()
...     """)
...     f.close()
...
...
...
96
>>>


from flashbdev import bdev
uos.VfsFat.mkfs(bdev)
vfs = uos.VfsFat(bdev)
with open("/boot.py", "w") as f:
    f.write("""\
import gc
import webrepl
webrepl.start(password="123456")
gc.collect()
""")
    f.close()

MicroPython v1.8.7-7-gb5a1a20a3 on 2017-01-09; ESP module with ESP8266
Type "help()" for more information.
>>> import os
>>> os.listdir()
['boot.py']  
>>> import sys  
>>> sys.path  
['', '/lib', '/']

# Re: Corrupted filesystem

# How to install other micropython libs in pyboard?

而且重新於 webrepl 傳送與接收檔案都不會斷線, 都能成功完成送收作業, 真是太開心了! 下圖顯示當利用 http://micropython.org/webrepl/ 連線 ESP8266 時, 在右邊 "Get a file" 框輸入 boot.py, 再按 "Get from device" 就會從 ESP8266 根目錄下載 boot.py 開機檔案, 因為網頁是在 Internet 上, 因此會彈出視窗詢問檔案要儲存在哪個地方 :


OK! 這樣終於搞定檔案上傳下載這個大問題了.

其實在重建檔案系統之前我還試過用 ampy 去傳送檔案, 例如下列指令都可執行, 參考 :

# Ampy failure

D:\test>ampy --port COM4 ls                       (OK, 但輸出一堆小數點)

D:\test>ampy --port COM4 put --help          (OK)

D:\test>ampy --port "COM4" put --help       (OK)

但是下面指令就會報錯 (我已在目前目錄打好 boot.py 檔案) :

D:\test>ampy --port "COM4" put boot.py     (NG)

D:\test>ampy --port "COM4" get boot.py     (NG)

也試過 mpfshell, 結果也是無法傳檔 :

D:\test>mpfshell

** Micropython File Shell v0.8.0, sw@kaltpost.de **
-- Running on Python 3.6 using PySerial 3.3 --

mpfs [/]> open ws:192.168.2.107
webrepl passwd:
Connected to esp8266
mpfs [/]> pwd
/
mpfs [/]> help

Documented commands (type help <topic>):
========================================
EOF  cd     exec  get   lcd  lpwd  md    mput  mrm   put  repl
cat  close  exit  help  lls  ls    mget  mpyc  open  pwd  rm

mpfs [/]> put boot.py  (結果 REPL 輸出一堆 \x00)

原來問題都是因為沒有檔案系統之故. 經過重建之後, ampy 傳檔都沒問題了 :

D:\test>ampy --port COM4 put boot.py

D:\test>ampy --port COM4 ls
boot.py
D:\test>ampy --port COM4 get boot.py
import gc
import webrepl
webrepl.start(password="123456")
gc.collect()

而 mpfshell 的檔案操作也沒問題了 :

mpfs [/]> open ws:192.168.2.107
webrepl passwd:
Connected to esp8266
mpfs [/]> put boot.py
mpfs [/]> cat boot.py
#import esp
#esp.osdebug(None)
import gc
import webrepl
webrepl.start(password="123456")
gc.collect()
mpfs [/]> get boot.py
mpfs [/]> ls

Remote files in '/':

       boot.py

mpfs [/]>

哈! 大功告成! 這是今天最開心的一件事! 這都要感謝 fdushin 不吝提供重建檔案系統的程式碼, 要不然不知道要在這裡卡關多久呢!


~~~ 2017-05-19 補充 :

今天在 Adafruit 看到這篇 :

# Disable ESP8266 Debug Output

此文建議把下列程式碼寫入 boot.py, 禁止偵錯訊息輸出到串列埠, 以免干擾 ampy 的操作 :

import esp
esp.osdebug(None)

不過如果還要看到偵錯訊息的話, 就把這兩行 remark 掉, 我目前的 boot.py 是這樣 :

#import esp
#esp.osdebug(None)
import gc
import webrepl
webrepl.start(password="123456") 
gc.collect()

~~~ 20170609 補充 :

其實我們可以把上面連線 WiFi 基地台的程式碼寫成 main.py 裡的函數, 參考 :

# http://docs.micropython.org/en/v1.8.7/esp8266/esp8266/quickref.html#networking

我將官網的範例稍作修改, 把原先寫在函數裡面的 ssid 與 pwd 改由參數傳入, 這樣就可以在 REPL 介面呼叫此函數時修改欲連線之 WiFi 基地台了 :

def connect_wifi(ssid, pwd):
    import network
    wlan=network.WLAN(network.STA_IF)
    wlan.active(True)
    if not wlan.isconnected():
        print('connecting to network ...')
        wlan.connect(ssid, pwd)
        while not wlan.isconnected():
            pass
    print('Connected:', wlan.ifconfig())

將上面的程式碼存成 main.py, 用 ampy 上傳到 ESP8266 模組 :

D:\test>ampy --port COM4 put main.py

開啟 Putty 連線模組後, 按 Ctrl-D 軟開機就會重新執行 boot.py 與 main.py, 然後就可以在 REPL 介面呼叫此函數時傳入欲連線之基地台 :

PYB: soft reboot
#8 ets_task(40100164, 3, 3fff829c, 4)
MicroPython v1.9-43-gb24ccfc6 on 2017-06-09; ESP module with ESP8266
Type "help()" for more information.
>>> connect_wifi('EDIMAX-tony','1234567890')
Connected: ('192.168.2.112', '255.255.255.0', '192.168.2.1', '168.95.1.1')

由於 ESP8266 本來就是用來連網的, network 模組一定會用到, 因此上面的 main.py 可以將 import network 拿出來, 加上 get_ip() 函數如下 :

import network
def connect_wifi(ssid, pwd):
    sta=network.WLAN(network.STA_IF)
    sta.active(True)
    if not sta.isconnected():
        print('connecting to network ...')
        sta.connect(ssid, pwd)
        while not sta.isconnected():
            pass
    print('Connected:', sta.ifconfig())

def get_ip():
  return network.WLAN(network.STA_IF).ifconfig()[0]

ifconfig() 傳回四元素的串列, 而本身的 IP 在索引 0. 這樣呼叫 get_ip() 即可取得本身的 IP 了 :

PYB: soft reboot
#7 ets_task(40100164, 3, 3fff829c, 4)
MicroPython v1.9-43-gb24ccfc6 on 2017-06-09; ESP module with ESP8266
Type "help()" for more information.
>>> get_ip()
'192.168.2.112'

參考 :

# http://micropython.org/webrepl/
# How to setup WebREPL to connect to Python prompt (REPL) of ESP8266 over WIFI network?
# ESP8266 and MicroPython - Part 1
# WebREPL - a prompt over WiFi
# RFC: Not enabling WebREPL by default on boot in 1.8.6
# Current _boot.py not compatible with ESP8266-01 #1986
# Adafruit MicroPython Tool - Utility to interact with a MicroPython board over a serial connection
# https://forum.micropython.org/viewtopic.php?t=2591
# (lost+found) Running MicroPython on the ESP8266
# 4.2. Assigning a static IP address when booting

2017-06-26 補充 :

今天看到另一個重建檔案系統的方法 :

# Re: Corrupted filesystem

指令如下 :

import uos
import flashbdev
uos.VfsFat.mkfs(flashbdev.bdev)

但不一定有效, 還是上面的方式最有效.

2017-07-05 補充 :

ESP8266 連線基地台操作方式我已實作更好的方法, 就是利用其內建的 AP 所綁定的 192.168.4.1 網址, 利用 WEB 伺服器透過手機來設定, 參考下列文章的測試 5 :

# MicroPython on ESP8266 (十四) : 網頁伺服器測試

2017 年第 19 周記事

今年轉眼已過了 19 週, 週日是今年母親節, 今年將過半矣. 母親過世已兩年, 往年我都是準備一個大紅包與蛋糕, 帶小狐狸們一起回鄉下慶祝, 通常是周日飯後返回高雄前拿出蛋糕來切, 這種場景已經一去不復返.

週日早上巡了一下芒果樹, 發現後院與馬路旁還有蠻多芒果沒包到, 所以又去買了兩包套袋, 週日下午拿梯子想方設法盡量套, 但太高的也只好望果興嘆.

週日下午煮好晚餐的湯後, 就去巡家族墓園, 跟媽說聲母親節快樂, 順便跟她報告一下她那台山葉二行程摩托車 (美的 Mate 90) 可能必須汰換, 因為聽說最遲 110 年高雄市二行程機車不能再騎了, 明年汰換獎勵金也將取消 :

# 高市110年起禁用二行程機車 呼籲民眾盡早汰舊
# 〈南部〉新車貴補助少 高雄汰換二行程機車成效差

"高雄市環境維護管理自治條例已通過,自110年1月1日起,高雄市將全區禁行二行程機車,提醒二行程機車車主應於109年12月31日前汰換,避免車輛無法行駛,且汰舊二行程機車補助金額將逐年遞減, 106年汰舊二行程加碼補助金將減為2,500元。環保局提醒尚未完成汰舊二行程機車之民眾,補助款之預算有限,請把握機會盡快完成報廢程序並提出申請,若有相關疑問,可撥打 0800-073-073 諮詢。"


媽這台手排檔的機車在鄉下非常受歡迎, 因為它後面是載物平台, 農人用它來載肥料飼料或農產品很方便, 馬力也很足. 媽以前請人在座椅前方安裝了一個小椅子, 小狐狸們小時候都是坐在這小椅子上跟著阿嬤到處跑. 老實說要汰換掉也很捨不得.

我還記得這台車大約是我國中二年級時買的, 當時祖母看到母親買了機車, 還非常羨慕地說如果識字的話她也要學騎機車去考照. 但母親也不識字啊! 真不曉得她當時是怎樣拿到駕照的.


2017年5月10日 星期三

2017年5月9日 星期二

ACER D260 安裝 Linux Mint (一) : 拆硬碟

自從換了 INHON 筆電, 舊的 D260 就很少用了, 但是今年初發現無法開機, 一直停留在 XP 開機畫面跑不出來, 因為沒時間搭理, 所以就一直擺著不處理. 前幾天去圖書館還書時看到書架上有 Ubuntu 的書, 我突發奇想, 何不將 D260 小筆電改灌 Ubuntu?

我找到 "去吧!轉用 Linux 啦!" 這篇文章, 作者建議安裝基於 Ubuntu, 但穩定性與效能都比 Ubuntu 更好的 Linux Mint, 我想對 D260 這種效能普普的小筆電, 還是安裝這種輕巧的作業系統比較好. 下載 Linux Mint 映像檔 :

# https://www.linuxmint.com

將下載的 .iso 檔燒錄到光碟片, 準備一個外接式光碟機來灌 Linux Mint Mate.

但問題是我的 D260 硬碟裡面還有資料, 我不知道灌 Linux Mint 會不會把 D 碟也格式化掉, 為了保險起見還是拆機拔出硬碟, 再用硬碟外接盒把資料備份起來. 但要如何拆開機殼取出硬碟呢? 我用 "D260 拆 裝" 搜尋找到下面這篇文章 :

# [電腦]如何更換 Acer Aspire ONE D260 記憶體

雖然介紹的是更換記憶體, 但我想知道的是如何拆機, 訣竅就是要先頂開六個卡榫拆下鍵盤, 拔掉鍵盤纜線, 再鬆開四顆螺絲, 然後用竹筷或起子將底盤頂開後敲開底盤卡榫, 取下底盤就會看到 DRAM 與硬碟了.


硬碟是卡在插槽, 邊框用一顆螺絲鎖在機架上, 先轉開此顆螺絲後, 將硬碟往後推即可取下硬碟, 參考下面的影片 :


取下硬碟發現這顆是 WD 160G 的硬碟 :


將其裝進外接盒備份好資料後又再放回去, 與上面拆機程序反向將機器復歸原狀, 這其中我覺得要將鍵盤排線插回去最難, 凸了好久才成功. 弄好後才發現下午要來公司前忘記帶電源線, 只好明天再繼續了.

參考 :

# https://www.ubuntu-tw.org/modules/tinyd0/
# How to Install Ubuntu 11.04 to Replace Windows
# https://www.youtube.com/watch?v=lNN3e6Jz6hE
# Acer Aspire One D260 Linux Compatibility
# 請問 acer 小筆電 aspire one D250 該重灌哪個作業系統?
# Revive your netbook with Linux Mint
# What is the Best Lightweight Linux Distro?
# 5 Lightweight Linux distros for your Chromebook
# Raspberry Pi PIXEL on an Old Asus EeePC netbook with Intel Atom CPU
# 如何將老舊硬碟格式化?
# Acer Aspire One 宏碁筆記型電腦故障維修解決方案
# ASPIRE ONE 開機無畫面的問題
# Universal USB Installer – Easy as 1 2 3
# Can Aspire One D260 run Ubuntu 14.04 LTS? [duplicate]
# Acer Aspire one D255e running Ubuntu 12.10

~~~ 20170606 補充 :

一個月快過去了, 今天才有時間繼續玩 Linux Mint 的安裝. 上回取出 D260 硬碟將 D 碟資料備份起來後, 把燒錄好的 Linux Mint 18.1 光碟放入外接光碟機開機安裝, 但是顯示 Mint 開機畫面後螢幕變灰, 只剩左上角游標閃啊閃的, 閃了老半天沒動靜, 根本沒照 "去吧!轉用 Linux 啦!" 文中所說的進入光碟開機的 Linux Mint 畫面, 跟之前 XP 死掉時停在微軟國旗畫面很像.

我原以為是光碟或光碟機問題, 就清空了一顆 4GB 創見隨身碟, 用 UUI 將 Linux Mint 映像檔燒進去, 改用 USB 隨身碟開機, 結果還是一樣, 接著我忙著研究 MicroPython 就把這件事暫時擱下了. 上週回鄉下將之前以 5500 價格從露天買給二哥上大瀚機器人用的 D255 帶來高雄, 這台內建是 Win7+Android, 效能不好, 反應很慢, 二哥國二上去補習後就沒去大瀚, 這台小筆電就放在鄉下沒再用了.

有次回鄉下時有用 Linux Mint USB 給 D255 開機, 發現一切正常, 所以我懷疑 D260 主機板可能有問題, 打算將 D260 的硬碟放在 D255, 以及將 D255 的硬碟放在 D260 內開機看看, 交互測試到底問題出在哪裡. 今天先將 D260 硬碟放在 D255 內用 USB 可正常開機, 可見 D260 硬碟沒問題. 順便就把 Linux Mint 安裝起來了. 在 D255 上跑 Linux Mint 操作起來感覺比以前的 Win 7 要順暢多了.


# Linux Mint 18 MATE - 作業系統安裝教學 (1)
# Linux Mint 17 MATE 安裝教學
# 如何使用 Linux,讓你在網上保持匿名?
# Intel Atom® 處理器 N450
# What happens If I install 32 bit OS on 64 bit hardware?


MicroPython on ESP8266 (四) : 字典與集合型別測試

過去兩周對 ESP8266 上的 MicroPython 進行了初步的測試, 發現能在 ESP-01 模組上執行 Python 的感覺真好, 因為 Python 實在太好用了. 今天要下午才進辦公室, 所以早上就來測試最後的兩種容器型別 : 字典 dict 與集合 set, 這兩種型別的資料都是用大括弧 {} 包起來, 它們的元素或項目也都是無順序的.

Python 常用資料型別的繼承圖如下 :


此系列前面三篇測試參考 :

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

MicroPython 文件參考 :

# MicroPython tutorial for ESP8266  (官方教學)
# http://docs.micropython.org/en/latest/micropython-esp8266.pdf
# http://docs.micropython.org/en/latest/pyboard/library/usocket.html#class-socket
# http://docs.micropython.org/en/v1.8.7/esp8266/library/usocket.html#module-usocket
# https://gist.github.com/xyb/9a4c8d7fba92e6e3761a (驅動程式)
一. 字典 dict :

Python 的字典與 Javascript 或 PHP 的關聯式陣列很類似, 是由 {鍵:值} 對 (key-value pair) 組成的多值資料結構, 使用獨一無二 (不可重複) 的鍵當索引來定位資料 (值). 鍵通常使用整數或字串等不可變型別資料, 而值則可以是任何型別資料.

理論上任何 Python 不可變型別的資料 (布林值, 整數, 浮點數, 元組, 字串) 都可以用作字典的鍵, 但因為布林值當鍵使用場合不多; 浮點數因無法精確表示, 所以也不適合拿來當鍵; 而元組的元素若為串列 (可變) 也不可以當鍵, 因此實際上最常用作鍵的是整數與字串.

不可變型別如 int, float, str, tuple 才能用來當作鍵的原因是這些型別的資料是可雜湊的 (hashable), 因為它們都繼承自 Hashable 這個抽象型別; 而可變型別如 list, dict, set 則是不可雜湊的. 所謂可雜湊是指可以利用雜湊演算法將資料進行壓縮與摘要, 算出一個獨一無二可代表該資料的的數字, 此數值稱為其雜湊值, 又稱為其數字指紋, 這獨一無二的特性正是可做為 key 的必要條件. Python 有一個內建函數 hash() 可計算雜湊值, 例如 :

 >>> hash(0)
0
>>> hash(1)
1
>>> hash(2)
2
>>> hash(3)
3
>>> hash(True)    #可見 True 就是整數 1
1
>>> hash(False)   #可見 False 就是整數 0
0
>>> hash('a')
46532
>>> hash('b')
46535
>>> hash('c')
46534
>>> hash('a')
46532
>>> hash('abc')
12933
>>> hash(1.0)
1
>>> hash(1.0001)
1
>>> hash(1.2)
1
>>> hash(1.99)
1

浮點數雖然可雜湊, 但由於大部分的浮點數無法精確表示, 所以實務上很少拿來做鍵. 上面範例中的浮點數在 MicroPython 實作的雜湊演算法算起來都是 1. 但是在 CPython 執行結果不同 :

Microsoft Windows [版本 10.0.14393]
(c) 2016 Microsoft Corporation. 著作權所有,並保留一切權利。

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.
>>> hash(1.0)
1
>>> hash(1.0001)
230584300921345
>>> hash(1.2)
461168601842738689
>>> hash(1.99)
2282784579121556993

不像序列型別是依序排列存放, 可以用索引定位出資料的位置, 字典與集合的資料是無順序的, 無法用索引存取, 而是靠鍵的雜湊值當作參考去定位出資料的正確的位址.


1. 建立字典 : 

建立字典的第一種方法是使用大括號包住以逗號分隔的鍵:值對, 例如 :

>>> user={'name':'Peter', 'age':16, 'gender':'male'}
>>> user
{'name': 'Peter', 'age': 16, 'gender': 'male'}          #以字串當鍵
>>> type(user)
<class 'dict'>
>>> ascii={61:'a',62:'b',63:'c',64:'d'}  #以整數當鍵
>>> ascii
{64: 'd', 61: 'a', 62: 'b', 63: 'c'}
>>> constants={3.14:'pi',2.718:'e'}       #以浮點數當鍵
>>> constants
{2.718: 'e', 3.14: 'pi'}

注意, 浮點數雖然可以當鍵, 但由於 MicroPython 的浮點數只能表示到小數後五位, 如果用浮點數當鍵會變成無實用價值, 例如 :

>>> a={3.14:'pi1',3.14159:'pi2',3.1415926:'pi3'}
>>> a
{3.14: 'pi1', 3.14159: 'pi2', 3.14159: 'pi3'}   #MicroPython 只能表示到小數後五位
>>> a[3.14]                                   #結果看起來竟然有兩個相同的鍵 3.14159
'pi1'
>>> a[3.14159]                             #事實上只要知道真正的鍵, 還是可以正確取得值
'pi2'
>>> a[3.1415926]
'pi3'

建立字典的第二種方法是先建立一個空字典, 再用賦值方式新增字典的元素, 字典的存取使用中括號 [key], 而對鍵賦值 dict_name[key]=value 即是建立一個鍵:值對, 例如 :

>>> user={}                        #建立一個空字典
>>> user['name']='Peter'     #指定鍵-值對
>>> user['age']=16              #指定鍵-值對
>>> user['gender']='male'    #指定鍵-值對
>>> user
{'gender': 'male', 'name': 'Peter', 'age': 16}     #按照字串鍵字母排序, 小寫先, 大寫後
>>> constants={}
>>> constants[3.14]='pi'
>>> constants[2.718]='e'
>>> constants
{2.718: 'e', 3.14: 'pi'}                             #按照數值鍵的大小升冪排序
>>> a={}
>>> a['a']=61
>>> a['b']=62
>>> a['A']=41
>>> a['B']=42
>>> a
{'a': 61, 'A': 41, 'B': 42, 'b': 62}              #按照字串鍵字母排序, 小寫先, 大寫後

可見使用這種方式建立的字典, 其鍵:值對並非按照建立的順序排列, 而是依照 key 來排序. 鍵若為數值, 則依鍵之升冪排序; 鍵若為字串, 則依鍵的字母排序, 小寫先, 大寫後.

建立字典物件的第三種方法是利用 dict() 建構式 (內建函數), 它可以將大於 1 對的對偶物件轉成 dict 物件, 這個對偶物件可以 tuple 或 list, 例如 :

>>> a=((1,'one'),(2,'two'),(3,'three'))     #tuple 的 tuple
>>> dict(a)
{1: 'one', 2: 'two', 3: 'three'}
>>> a=[(1,'one'),(2,'two'),(3,'three')]     #tuple 的 list
>>> dict(a)
{1: 'one', 2: 'two', 3: 'three'}
>>> a=([1,'one'],[2,'two'],[3,'three'])     #list 的 tuple
>>> dict(a)
{1: 'one', 2: 'two', 3: 'three'}
>>> a=[[1,'one'],[2,'two'],[3,'three']]     #list 的 list
>>> dict(a)
{1: 'one', 2: 'two', 3: 'three'}

以上不管是串列的元組, 或元組的串列, 傳入 dict() 後都轉成相同的 dict 物件. 但注意, 如果只有一個對偶物件時, tuple 對偶物件的後面要加一個逗號, 否則會產生錯誤, 這是 tuple 只有單一項目時必須注意之處, 串列無此問題 :

>>> a=((1,'one'))                 #只有一個 tuple 對偶物件項目
>>> dict(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not iterable  #少一個逗號無法迭代
>>> a=((1,'one'),)                #tuple 只有一個項目時必須以逗號結尾
>>> dict(a)
{1: 'one'}                                             #有結尾逗號順利轉成 dict
>>> a=[(1,'one')]                  #串列只有一個項目時無結尾逗號問題
>>> dict(a)
{1: 'one'}

另外, 項目為雙字元字串的元組或串列也是對偶物件, 傳入 dict() 時會被拆成兩個字元的鍵:值對, 例如 :

>>> a=('a1','b2','c3')    #雙字元字串 'a1', 'b2', 'c3' 被視為對偶
>>> dict(a)
{'a': '1', 'c': '3', 'b': '2'}
>>> a=['a1','b2','c3']  
>>> dict(a)
{'a': '1', 'c': '3', 'b': '2'}

建立字典的第四種方法是用字典物件的 setdefault() 方法, 這方法是為指定的鍵設定預設值, 對於空字典而言, 此方法實際上是新增一個項目, 例如 :

>>> a={}                         #空字典
>>> a.setdefault(1,'a')      #為 key=1 設定預設值為 'a'
'a'
>>> a
{1: 'a'}
>>> a.setdefault(2,'b')
'b'
>>> a.setdefault(3,'c')
'c'
>>> a                                #項目依設定先後順序存放
{1: 'a', 2: 'b', 3: 'c'}
>>> len(a)
3

可見使用 setdefault() 的好處是項目會以設定的先後順序儲存.

建立字典的第四種方法是用字典的類別方法 dict.fromkeys(seq[, default]), 此方法會以傳入的第一參數 (序列型別的串列, 元組, 或字串) 當作鍵, 以第二參數 default 作預設值, 若未傳入第二參數預設為 None. 然後再用指派方式更改項目之值, 例如 :

>>> seq=(1,2,3)                
>>> a=dict.fromkeys(seq)       #以元組為鍵建立字典
>>> a
{3: None, 1: None, 2: None}                  #未傳入第二參數, 預設值為 None
>>> a[1]='a'                             #設值
>>> a[2]='b'
>>> a[3]='c'
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> a=dict.fromkeys(seq,'a')    #傳入預設值 'a'
>>> a
{3: 'a', 1: 'a', 2: 'a'}
>>> seq=[1,2,3]
>>> a=dict.fromkeys(seq)        #以串列為鍵建立字典
>>> a
{3: None, 1: None, 2: None}                   #未傳入第二參數, 預設值為 None
>>> seq='123'                           #以字串為鍵建立字典 (字串會被拆成字元)
>>> a=dict.fromkeys(seq)
>>> a
{'2': None, '3': None, '1': None}               #字串被拆成字元當鍵

另外在測試串列時有提到內建函數 zip(), 此函數會將兩個 tuple 或 list 物件內的項目依序配對後以 tuple 的串列傳回一個 zip 物件 (是一個迭代物件), 可以很方便地用 dict() 轉成字典, 例如 :

>>> char=('A','B','C')        #用元組物件配對
>>> ascii=(41,42,43)
>>> a=zip(char,ascii)
>>> type(a)
<class 'zip'>
>>> dict(a)
{'A': 41, 'C': 43, 'B': 42}
>>> dict(a)                         #經 dict() 轉換後, zip 物件已被迭代完故為空
{}

注意, zip 物件經 dict() 轉換後會因為已經被迭代完而變成空. 迭代物件 zip 除了可用 dict() 轉成字典外, 也可以用 list() 或 tuple() 轉換成串列或元組. 下面使用 list 物件做配對 :

>>> char=['A','B','C']          #用串列物件配對
>>> ascii=[41,42,43]
>>> dict(zip(char,ascii))     #轉成字典
{'A': 41, 'C': 43, 'B': 42}
>>> list(zip(char,ascii))       #轉成串列
[('A', 41), ('B', 42), ('C', 43)]
>>> tuple(zip(char,ascii))    #轉成元組
(('A', 41), ('B', 42), ('C', 43))


2. 字典的基本操作 :

字典的基本操作如下 :
  1. 字典內容更改 
  2. 刪除項目 (用 del 指令)
  3. 查詢項目的鍵 (使用 in 與 not in 運算子)
字典物件建立後, 可用 [key] 運算子來存取其項目之值. 指派一個值給此項目時, 若字典裡已存在此鍵, 則既有的值就會被新指派的值替換; 若鍵是新的 (不存在), 則此鍵:值對會被新增到字典裡成為一個新的項目, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> a[1]            #擷取字典項目  
'a'
>>> a[1]='A'      #變更項目的值
>>> a
{3: 'c', 1: 'A', 2: 'b'}
>>> a[4]='d'       #新增項目
>>> a
{4: 'd', 1: 'A', 2: 'b', 3: 'c'}

欲刪除字典中的一個項目, 與串列一樣, 可使用 del 指令並指定該項目之鍵, 如果鍵不存在會產生 KeyError 錯誤, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> del a[2]       #刪除鍵=2 的項目
>>> a
{3: 'c', 1: 'a'}                      #已經無鍵=2 之項目了
>>> del a[2]       #欲刪除項目之鍵不存在產生錯誤
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 2

因此在刪除一個字典項目之前, 最好用 in 或 not in 指令查詢此鍵是否存在, 這兩個指令會傳回 True/False, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> 2 in a        #key=2 之項目存在
True
>>> if 2 in a:   #若 key=2 存在就刪除此項目
...     del a[2]
...
...
...
>>> a
{3: 'c', 1: 'a'}                    #key=2 之項目已被刪除
>>> 4 in a         #key=4 之項目不存在
False
>>> if 4 in a:    #若 key=4 存在就刪除此項目
...     del a[4]
...
...
...
>>> a
{3: 'c', 1: 'a'}

3. 用內建函數操作字典 :

下列內建函數可以用在字典 :

 函數 說明
 len(obj) 傳回字典項目的個數
 min(obj) 傳回字典項目中鍵最小的元素
 max(obj) 傳回字典項目中鍵最大的元素
 sum(obj) 傳回字典項目中所有鍵之和 
 sorted(obj)  傳回鍵由小到大排列組成之串列

這些函數都是作用在鍵上, 適用於 key 為數值的字典, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> len(a)        #傳回字典項目數目
3
>>> min(a)       #傳回最小的 key
1
>>> max(a)      #傳回最大的 key
3
>>> sum(a)      #傳回所有 key 的和
6
>>> sorted(a)   #依據 key 之值做升冪排序
[1, 2, 3]

如果用在鍵為字串的字典, 除了 sum() 無法運算會產生錯誤外, 都以字串的 ASCII 為比較之準則, 即數字先, 大寫字母次之, 小寫字母最後.

>>> a={'c':1,'a':2,'b':3}
>>> min(a)        #傳回 ASCII 最小的 key
'a'
>>> max(a)       #傳回 ASCII 最大的 key
'c'
>>> sum(a)       #字串無法求和
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __add__: 'int', 'str'
>>> sorted(a)    #依據 key 之 ASCII 值做升冪排序
['a', 'b', 'c']
>>> a={'aA':1,'AA':2,'aa':3,'Aa':4}
>>> sorted(a)
['AA', 'Aa', 'aA', 'aa']         #依據 key 之 ASCII 值做升冪排序


4. 字典物件的方法 :

 方法 說明 
 keys()  傳回字典的鍵 (傳回 dict_keys 迭代物件)
 values() 傳回字典的鍵 (傳回 dict_values 迭代物件)
 items() 傳回字典的項目 (傳回 dict_items 迭代物件)
 get(key [,default]) 傳回字典中鍵為 key 之值, 若鍵不存在傳回預設鍵 default 之值
 setdefault(key [,default]) 若鍵已存在傳回其值, 否則填入預設值
 pop(key) 從字典取出指定項目傳回, 並從字典中刪除該項目
 update(obj) 將字典 obj 內容加入目前字典中, 若鍵重複則以 obj 更新字典的值
 copy() 複製字典所有項目至另一字典複本, 並傳回其參考
 dict.fromkeys(seq [, default]) 以序列物件 seq 內容為鍵建立一個字典並傳回其參考 (類別方法)
 clear() 移除字典內的全部項目

注意上表中的 dict.fromkeys() 為字典型別的方法, 不是物件方法.

方法 keys() 會傳回一個 dict_keys() 可迭代物件, 裡面儲存字典所有的鍵. 傳回 dict_keys 物件而非像 Python 2.x 那樣直接傳回一個串列物件的原因是為了節省記憶體與時間. 不過我們可以用 list() 函數將 dict_keys 轉成 list 物件 :

>>> a={1:'a',2:'b',3:'c'}
>>> b=a.keys()                   #傳回字典全部的鍵
>>> b
dict_keys([3, 1, 2])
>>> type(b)
<class 'dict_view'>
>>> list(b)                           #轉成串列
[3, 1, 2]
>>> tuple(b)                        #轉成元組
(3, 1, 2)

方法 values() 會傳回儲存字典所有值的一個可迭代物件 dict_values(), 同樣也可以用 list() 將其轉成串列 :

>>> a={1:'a',2:'b',3:'c'}  
>>> c=a.values()                #傳回字典全部的值
>>> c
dict_values(['c', 'a', 'b'])    
>>> type(c)                      
<class 'dict_view'>
>>> list(c)                          #轉成串列
['c', 'a', 'b']
>>> tuple(c)                       #轉成元組
('c', 'a', 'b')

方法 items() 會傳回全部字典項目, 放在一個可迭代物件 dict_items() 裡, 可用 tuple() 或 list() 轉成元組或串列, 但每個項目是以 tuple 方式表示, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> d=a.items()                  #傳回字典的所有項目
>>> d
dict_items([(3, 'c'), (1, 'a'), (2, 'b')])    #傳回 dict_items() 物件
>>> type(d)
<class 'dict_view'>
>>> list(d)                            #轉成串列
[(3, 'c'), (1, 'a'), (2, 'b')]
>>> tuple(a.items())             #轉成元組
((3, 'c'), (1, 'a'), (2, 'b'))

如果字典為空, 則上面 keys(), values(), 與 items()  三個方法也將傳回空的迭代物件, 用 tuple() 或 list() 轉換將得到一個空元組或空的串列.

如果要取得指定鍵所對應的值, 可用 get() 方法, 此方法可傳入兩個參數, 第一參數是要尋找的 key, 第二參數可有可無, 用來設定萬一傳入的鍵不存在時要傳回之值, 如果傳入的 key 不存在, 又沒有指定預設傳回值的話, get() 會傳回 None :

>>> a={1:'a',2:'b',3:'c'}
>>> a.get(1)                #鍵存在:傳回鍵對應之值
'a'
>>> a.get(2)
'b'
>>> a.get(3)
'c'
>>> a.get(4)                #鍵若不存在, 預設傳回 None
>>> print(a.get(4))
None
>>> a.get(4,'z')            #鍵若不存在, 傳回指定之預設值 'z'
'z'

方法 setdefault() 顧名思義是為字典不存在的 key 設定預設值, 實際上是為字典添加一個項目, 因此可以用來建立一個字典物件, 如上面建立字典物件的第三種方法所示. 此方法可以有兩個參數, 第一個必須的 key, 第二個為可有可無之預設值. 如果只傳入 key, 且此鍵已存在於字典內, 則此方法會傳回鍵所對應之值, 這時其作用就跟 get() 方法一樣; 若此鍵不存在, 就會為字典添加一個以此鍵為 key 的項目, 預設值為 None, 並傳回此預設值 None, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a.setdefault(2)                #只傳入 key, 則傳回此 key 對應之值
'b'
>>> a.setdefault(4)                #只傳入一個不存在的鍵, 傳回 None 並新增 key:None 項目
>>> print(a.setdefault(4))      #要用 print() 才看得到 None
None
>>> a                                  
{4: None, 1: 'a', 2: 'b', 3: 'c'}                  #新增了 key:None 項目

注意, setdefault() 只是用來設定預設值,  因此只有在鍵不存在時才有設值之作用 (因此可用來建立字典), 若鍵已存在則 setdefault() 無法更改其值, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a.setdefault(1,'A')    #鍵 1 已存在, 無法用傳入之第二參數更改其值
'a'                                                    #仍傳回鍵 1 原本之值 'a'
>>> a
{3: 'c', 1: 'a', 2: 'b'}

方法 pop() 是從字典中取出指定鍵之值, 同時將該項目從字典中移除. 若鍵不存在會產生 KeyError 錯誤, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a.pop(2)
'b'
>>> a
{3: 'c', 1: 'a'}                   #鍵=2項目 pop 後就被移除了
>>> a.pop(3)
'c'
>>> a
{1: 'a'}
>>> a.pop(3)   #pop 不存在的鍵會產生錯誤
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 3
>>> a.pop(1)
'a'
>>> a
{}                                     #項目全部被取出後變成空字典

方法 update() 可以用來合併兩個字典物件, 事實上就是將傳入的字典物件內容加入目前的字典中更新其內容, 如果傳入的字典內容中的 key 與目前字典重複, 就以傳入字典為主更新目前字典中該艦所對應之值, 這是 update() 名稱的原意. 注意, 傳入的字典本身不受 update() 方法的影響, 例如 :

>>> a={1:'a',2:'b',3:'c'}  
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> b={1:'A',4:'E'}
>>> b
{4: 'E', 1: 'A'}
>>> a.update(b)      #把字典 b 合併到字典 a
>>> a
{4: 'E', 1: 'A', 2: 'b', 3: 'c'}      #以字典 b 為主更新字典 a
>>> b                      #字典 b 是被合併者, 不受 update() 影響
{4: 'E', 1: 'A'}

字典為 mutable 可變資料型態, 如果有兩個變數同時指向一個字典的參考, 則會有別名 (alias) 問題, 即更改其中一個內容, 另一個也會改變, 可以使用內建函數 id() 來查看物件的參考 (相當於記憶體位址, 有些實作例如 CPython 就是物件的記憶體位址), 如果兩個物件的 id 一樣, 表示是同一物件, 用 is 運算會回傳 True, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> id(a)              #傳回字典 a 的 id (參考)
1073673600    
>>> b=a                #將字典 a 的參考設給物件 b, 兩者指向同一物件
>>> b
{3: 'c', 1: 'a', 2: 'b'}
>>> id(b)              #傳回字典 b 的 id (參考) : 跟字典 b 完全一樣
1073673600
>>> b is a              #檢查字典 a 與字典 b 是否指向同一物件 : 是
True
>>> b == a            #檢查字典 a 與字典 b 內容是否相同 : 是
True
>>> b[2]='A1'       #更改 b 的內容也會改到 a
>>> b
{3: 'c', 1: 'a', 2: 'A1'}
>>> a
{3: 'c', 1: 'a', 2: 'A1'}

而方法 copy() 可以複製一個字典的內容到複本, 並傳回此複本之參考, 這樣就可以得到一個內容完全一樣的新字典 (兩者參考的 id 不同), 彼此互不干擾, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a
{3: 'c', 1: 'a', 2: 'b'}
>>> id(a)              #傳回原字典的 id (參考)
1073673600    
>>> b=a.copy()    #複製字典內容到複本並傳回其參考
>>> b
{3: 'c', 1: 'a', 2: 'b'}
>>> id(b)              #傳回新字典的 id (參考)
1073673936                         #與字典 a 是不同的參考
>>> b is a             #檢查字典 a 與字典 b 是否指向同一物件 : 不是
False
>>> b == a           #檢查字典 a 與字典 b 內容是否相同 : 是
True
>>> b[1]='A'         #更改字典 b 不會影響字典 a
>>> b
{3: 'c', 1: 'A', 2: 'b'}
>>> a
{3: 'c', 1: 'a', 2: 'b'}

方法 dict.fromkeys() 為字典型別方法, 不是物件方法, 主要用來以傳入之序列物件為鍵建立一個字典, 如上面建立字典物件的第四種方法所示.

方法 clear() 會清除字典全部內容使其成為一個空字典, 例如 :

>>> a={1:'a',2:'b',3:'c'}
>>> a.clear()
>>> a
{}                       #變成空字典


二. 集合 set :

集合是一種內容可變, 元素無順序與不可重複的容器, 與字典一樣使用大括號 {} 將元素括起來. 集合的元素跟字典的鍵一樣不可重複, 因此集合也被視為等同於無值的字典. 集合的元素可以是任何不可變物件, 例如 int, tuple 與 str, 這些都是可雜湊 (hashable) 物件. 但要注意, tuple 容器中若含有可變元素, 則此 tuple 將無法計算雜湊值.

沒有元素的集合稱為空集合 (null set), 而集合的運算包括交集, 聯集, 差集, 以及互斥或. 注意, 集合本身雖是可變型別 (即元素可增減), 但其元素卻必須是不可變型別, 這樣的元素才能符合不可重複 (可雜湊) 的要求.

1. 建立集合 :

建立集合的第一種方法是使用大括號包住以逗號隔開的不可變物件字面值, 例如 :

>>> a={1,2,3,'a','b','c',True,False,3.14159}
>>> a
{False, 1, 2, 3, 'a', 'b', 'c', 3.14159}
>>> type(a)
<class 'set'>

集合的元素必須是不可變型別資料如 int, float, str, tuple 這四種, 其中 tuple 本身雖為不可變, 但若 tuple 內的元素含有 list, dict, set 等可變型別資料的話, 這個 tuple 就不算是不可變資料了, 這樣無法建立集合, 因其中的可變型別元素為不可雜湊 (inhashable), 例如 :

>>> a={0,1,(2,[3,4])}         #元組的元素是可變的串列, 無法計算雜湊值
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported type for __hash__: 'list'
>>> a={0,1,{2:'b',3:'c'}}     #元組的元素是可變的字典, 無法計算雜湊值
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported type for __hash__: 'dict'
>>> a={0,1,{2,3}}               #元組的元素是可變的集合, 無法計算雜湊值
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported type for __hash__: 'set'

集合與字典都是使用大括號將元素或項目資料包起來, 但 Python 將 {} 優先給了字典, 即 {} 是空字典而非空集合, 建立一個空集合必須呼叫內建函數 set(), 例如 :

>>> a={}         #這是建立一個空字典, 不是空集合
>>> type(a)
<class 'dict'>    
>>> a=set()      #建立空集合必須使用 set()
>>> type(a)
<class 'set'>
>>> a
set()

建立集合的第二種方法是使用內建函數 set() 與集合物件的 add() 方法, 例如 :

>>> a=set()      #建立空集合
>>> a.add(1)    #為集合添加元素
>>> a.add(2)
>>> a.add(3)
>>> a.add('a')
>>> a.add('b')
>>> a.add('c')
>>> a
{'c', 1, 'a', 2, 3, 'b'}

可見集合元素的排列與加入之順序無關.

建立集合的第三種方法是將 list, tuple, str, dict 等容器物件傳給內建函數 set(), 其中串列與元組是直接轉換, 但會去除重複之元素; 而字串則被拆成字元的集合, 同樣也會去除重複之字元; 而字典則是將字典的 key 轉成集合傳回. 例如 :

>>> set([1,2,3,'a','b','c',2,'a'])      #將串列轉成集合 (會去除重複之元素)
{'c', 1, 'a', 2, 3, 'b'}
>>> set([1,2,3,'a','b','c',2,'a'])      #將元組轉成集合 (會去除重複之元素)
{'c', 1, 'a', 2, 3, 'b'}
>>> set('Hello')                          #將字串轉成集合  (會去除重複之字元)
{'e', 'H', 'l', 'o'}
>>> set({1:'a',2:'b',3:'c'})           #將字典轉成集合  (只轉 key)
{1, 2, 3}

內建函數 range() 產生的序列型別物件也可以傳給 set() 來產生集合, 例如 :

>>> set(range(5))
{0, 1, 2, 3, 4}


2. 集合的基本操作 :

集合的基本操作如下 :
  1. 檢查元素是否存在 (用 in 或 not in 指令)
  2. 交集  (使用 & 運算子)
  3. 聯集  (使用 | 運算子)
  4. 差集  (使用 - 運算子)
例如 :

>>> a={1,2,3,'a','b','c'}
>>> 2 in a
True
>>> 'c' in a
True
>>> 'd' in a
False
>>> 'b' not in a
False
>>> 'd' not in a
True

集合的數學運算包括交集, 聯集, 與差集, 例如 :

>>> a={1,2,3,'a','b'}
>>> b={2,3,'a','c'}
>>> a  & b
{'a', 2, 3}
>>> a | b
{'c', 1, 2, 3, 'a', 'b'}
>>> a - b
{'b', 1}

3. 用內建函數操作集合 : 

集合可用的內建函數如下 :

 函數 說明
 len(obj) 傳回物件元素個數
 min(obj) 傳回物件中數值最小的元素 
 max(obj) 傳回物件中數值最大的元素
 sum(obj) 傳回物件所有元素之和
 sorted(obj)  傳回升冪排列之串列

若元素全部是數值, 則每一個函數均可運算 :

>>> a={5,4,3,2,1}
>>> len(a)
5
>>> min(a)
1
>>> max(a)
5
>>> sum(a)
15
>>> sorted(a)  
[1, 2, 3, 4, 5]

若元素都是字串, 則除了 sum() 無法運算外, 其他函數都是從左至右逐一比對 ASCII 碼, 例如 :

>>> a={'aA','AA','aa','Aa'}
>>> len(a)
4
>>> min(a)    #'AA' 的 ASCII 最小
'AA'
>>> max(a)   #'aa' 的 ASCII 最小
'aa'
>>> sum(a)   #字串無法加總
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __add__: 'int', 'str'
>>> sorted(a)     #按照 ASCII 碼升冪排序
['AA', 'Aa', 'aA', 'aa']

但是如果元素是數值與字串混雜, 則只有 len() 可執行, 其他函數均無法運算 :

>>> a={1,2,3,'a','b','c'}   #元素有數值也有字串
>>> len(a)     #只有 len() 能執行
6
>>> min(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __lt__: 'int', 'str'
>>> max(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __gt__: 'int', 'str'
>>> sum(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __add__: 'int', 'str'
>>> sorted(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __lt__: 'int', 'str'

4. 元素物件的方法 :

 方法 說明 
 add(obj) 新增元素 obj 至集合中
 remove(obj) 移除元素 obj, 移除元素 obj, 若不存在會產生錯誤
 discard(obj) 移除元素 obj, 若不存在則 pass, 不會產生錯誤
 pop() 從集合左方取出一個元素, 若不存在會產生錯誤
 clear()  刪除集合內的全部元素 (成為空集合)
 issubset(obj) 檢查集合 obj 是否為子集合 (傳回 True/False), 等同於 <= 運算子
 issuperset(obj) 檢查集合 obj 是否為超集合 (傳回 True/False), 等同於 >= 運算子
 union(obj) 傳回與集合 obj 之聯集, 等同於 | 運算子
 intersection(obj) 傳回與集合 obj 之交集, 等同於 & 運算子
 difference(obj) 傳回與集合 obj 之差集, 等同於 - 運算子

由於集合為可變型別, 因此可以用 add(), remove(), discard(), pop(), remove(), 與 discard() 方法更改集合的內容, 其中 remove() 若刪除不存在的元素會產生 KeyError 錯誤, 而 discard() 則不會. 而 pop() 方法會從集合左方取出指定元素傳回, 例如 :

>>> a={1,2,3}
>>> a.add('a')        #新增元素
>>> a.add('b')        #新增元素
>>> a.add('c')        #新增元素
>>> a
{'c', 1, 'a', 2, 3, 'b'}
>>> a.remove('c')          #刪除元素
>>> a
{1, 'a', 2, 3, 'b'}
>>> a.remove('c')          #刪除不存在的元素 (產生 KeyError 錯誤)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError:
>>> a.discard('c')           #刪除不存在的元素 (不會產生錯誤)
>>> a
{1, 'a', 2, 3, 'b'}
>>> a.pop()                    #取出並刪除集合左方元素後傳回
1
>>> a.pop()
'a'
>>> a                              #取出 1, 'a' 後剩下 2,3,'b'
{2, 3, 'b'}
>>> a.pop()
2
>>> a                              #取出 1, 'a', 2 後剩下 3,'b'
{3, 'b'}
>>> a.clear()                   #移除集合中的全部元素
>>> a
set()

另外, union(), intersection(), 與 difference() 三個方法則是用來做集合之聯集, 交集, 以及差集運算, 分別等同於 |, &, 以及 - 運算子的功能, 例如 :

>>> a={1,2,3,'a','b'}
>>> b={2,3,'a','c'}
>>> a.union(b)             #聯集
{'c', 1, 2, 3, 'a', 'b'}
>>> a | b                       #聯集
{'c', 1, 2, 3, 'a', 'b'}
>>> a.intersection(b)    #交集
{'a', 2, 3}
>>> a & b                     #交集
{'a', 2, 3}
>>> a.difference(b)       #差集
{'b', 1}
>>> a - b                       #交集
{'b', 1}

終於完成字典與集合測試! 這部分 MicroPython 都有實作.

2017年5月7日 星期日

2017 年第 18 周記事

因姊姊下周段考, 故本周只有我回鄉下了. 早上沿著水圳小路去市集, 想到媽走後的這兩年多, 每個周日菁菁都會跟我上市集, 但這學期開始週日早上去柏碩補數學, 不能回鄉下, 上市集買菜也就我一個人去了, 最近賣菜的, 賣魚的, 賣豆腐的都問, 妹妹怎麼沒來了, 我說要補習, 其實就算沒補習, 長大後都不會喜歡跟了. 有時會覺得有很大的失落感. 其實, 習以為常的事也許在某個節骨眼之後就不會再出現了, 人生中每個場景可能只出現一次, 或者 n 次而已.

早上打電話給豆腐嫂幫我預留豆腐時, 她說不好意思賣完了, 因為今天要上台北探望她剛出生的孫女 (呵呵, 初任的阿嬤喔), 所以豆腐做得比較少. 她做的豆腐真是好, 媽以前就是非她做的豆腐不買, 所以換我上市集買菜後, 想起她以前說過高橋邊的豆腐攤, 我按圖索驥果然找到了. 豆腐嫂的小兒子很會讀書, 現在在台大當教授.

下午小舅來菜園整理甘蔗以及把絲瓜棚再往北沿伸, 我弄完晚餐的湯後就抓緊時間開始給芒果套袋, 上週就想套, 但資材行的套袋缺貨, 早上去買菜時順路去買了一包 (100 個 50 元), 結果到傍晚時竟然全部都用完! 這兩個月內我已買了兩包, 加上去年的舊袋子, 今夏以來估計已套了 300 個芒果! 而且集中在 3 棵有順利結果的芒果樹. 我正在忙的時候, 信宏與張小姐來了, 但我可沒時間陪他們, 因為今天不套完的話, 下周就太晚了, 果蠅大軍已經蠢蠢欲動.

週五參加了一堂目前正熱門的 Fintech 相關課程, 邀請了資策會專家介紹 Fintech 的焦點 : 區塊鏈技術, 這是比特幣運作的基礎, 剛好本期商周就以比特幣價格日前超越黃金, 發了三篇專文探討目前的發展現況. 據說日本, 菲律賓都已經將比特幣視為合法貨幣, 美國也可能即將跟進. 專家預期區塊鏈技術未來將翻轉金融支付體系, 甚至於顛覆貨幣系統, 值得密切注意.

2017年5月5日 星期五

MicroPython on ESP8266 (三) : 序列型別測試

一直想要好好地對 CPython 測試一番都未動手, 這次因為測試 ESP8266 上的 MicroPython 之便, 乾脆連 CPython 也一併測試以資對照, MicroPython 沒有實作的部分再另外單獨進行測試, 此系列的前兩篇參考 :


Python 常用的序列型別有下列三種, 均繼承自 sequence 這個抽象型別, 它們的元素都是照順序存放的 :
  1. list (串列)
  2. tuple (元組)
  3. str (字串)
其中只有 list 是可變 (mutable) 資料型別, 其資料內容可以變更; 而 tuple 與 str 都是不可變 (immutable) 資料型別, 物件一旦建立就無法更改其內容. 序列型別物件的共同特性就是其元素都是有序的, 都可以利用索引來擷取其元素. 另外, 因 sequnce 型別繼承自 container 型別, 所以序列型別的物件也都是容器型別物件, 其繼承關係如下 :


Python 有五種容器型別 : str, list, tuple, dict, 以及 set, 容器皆以 cell 為基本儲存單位, 除了 str 只能儲存 unicode 字元外, 其他四種都可以儲存不同型態 (異質) 的資料.

一. 字串 (str) :

1. 字串的建立 :

Python 3 的字串全面採用 Unicode 表示, 不必像 Python 2.x 那樣在處理 Unicode 字串時還要在前面冠一個 "u". Python 的字串使用成對的單引號或雙引號括起來, 例如 :

>>> "Hello World!"
'Hello World!'
>>> 'Hello World'
'Hello World'
>>> print("Hello World!")
Hello World!
>>> print('Hello World!')
Hello World!

如果字串中必須用到引號本身, 可以用另一種引號作為外引號括起來, 例如 :

>>> print("It's mine")
It's mine
>>> print("he said:'Go!'")     #雙引號當外引號
he said:'Go!'
>>> print('he said:"Go!"')     #單引號當外引號
he said:"Go!"

或者可以使用跳脫 (Escape) 字元 "\" 來轉義 (改變字元原有意義), 原本引號是字串的開始或結束字元, 前面加倒斜線將其改變為引號本身 :

>>> print('It\'s mine')
It's mine
>>> print('he said:\'Go!\'')
he said:'Go!'
>>> print("he said:\"Go!\"")
he said:"Go!"

Python 的跳脫字元包括 \' 與 \" 總共有 12 個如下 :

 跳脫字串 說明
 \' 單引號
 \" 雙引號
 \\ 倒斜線
 \n 換行 (LF)
 \r 回車 (CR)
 \t 水平定位 (TAB)
 \v 垂直定位 (VT) 
 \a 響鈴 (BEL) 
 \b 退格 (BS) 
 \f 插入一頁 (FF) 
 \x41 16 進位 ASCII  碼 A
 \101 8 進位 ASCII  碼 A

這些跳脫字元大都是 ASCII 字元編碼中的控制字元, 例如 :

>>> print('123\\456\n\r789')       #印出倒斜線與跳行
123\456
789
>>> print('123\\456\n789')          #只用 \n 即可跳行
123\456
789
>>> print('123\\456\r789')           #\r 只有回車功能, 不會跳行
789\456    

可見跳行可以用 \n\r 或者單用 \n 皆可, \r 只是像打字機那樣的單純回車 (carrage return) 而已, 所以上面最後一例 \r 會讓列印位置回到最前面印出 789, 就把原先已經印出的 123 蓋掉了.

>>> print('123\t456\t789')    #水平定位
123     456     789
>>> print('123\v456\v789')  #垂直定位
123
   456
      789

換頁字元 \f  會刷新頁面 (換上新頁),  從最上面開始列印, 響鈴字元 \a 會讓系統發出噹的一聲, 而退位字元會往回一格列印, 蓋掉前一字元, 例如下面範例中開頭的 \f 會換新頁, 從第一列開始印出 0, 接著響鈴並印出 123, 再響鈴後印出 456, 然後 \b 會倒退一格到 6 的位置, 印出 789, 所以 6 就被蓋掉不見了 :

>>> print('\f0\a123\a456\b789')
012345789

下面範例測試 8 進位與 16 進位的跳脫字元表示法, 字母 A 的 ASCII 碼以 8 進制表示為 101, 以 16 進制表示則為 41. 注意 16 進制必須用小寫 x, 不可用大寫 X :

>>> '\101'
'A'
>>> '\x41'  
'A'

Python 有兩個內建函數用來處理 ASCII 碼 :

 函數 說明
 ord(char) 傳回字元 char 的 ASCII 碼 (10 進制)
 chr(code) 傳回 ASCOII 碼 code 的字元

注意, ord() 的參數只能傳入一個字元, 且傳回值是 10 進位的 ASCII 碼, 要取得 8 進制與 16 進制 ASCII 碼須使用 oct() 與 hex() 轉換, 例如 :

>>> ord('A'), oct(ord('A')), hex(ord('A'))
(65, '0o101', '0x41')
>>> ord('\n'), oct(ord('\n')), hex(ord('\n'))
(10, '0o12', '0xa')
>>> chr(65), chr(0o101), chr(0x41)
('A', 'A', 'A')
>>> ord('AB')        #ord 只能傳入一個字元
Traceback (most recent call last):
  File "<stdin>", line 1, in TypeError: ord() expected a character, but string of length 2 found

當字串中含有大量跳脫字元時會使可讀性變差, 這時可以用 Python 的原始字串 (raw string) 來表示, 在字串的開頭引號前面加個 r 就表示引號內為原始字串, 例如 :

>>> '\n'
'\n'
>>> r'\n'      #引號內為原始字串, \ 會以 \\ 取代
'\\n'
>>> r'\\'      #引號內為原始字串, \ 會以 \\ 取代
'\\\\'
>>> R'\n'     #MicroPython 未實作大寫 R 的原始字串功能
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: invalid syntax

注意, Cpython 可用 r 或 R, 但 MicroPython 沒有實作 R, 只能用 r : 

C:\Users\Tony>py -2
Python 2.7.8 (default, Jun 30 2014, 16:03:49) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> R'\n'
'\\n'
>>> R'\\n'
'\\\\n'

原始字串在表示 Windows 的路徑時可以派上用場 (使用倒斜線做分隔符號), 例如下面範例中, 字串中的 \U 會被認為要跳脫, 但又無法解譯這個 \U 跳脫字元, 所以產生執行造成錯誤, 前面冠 r 將其變成原始字串後就可以取消跳脫功能了 :

>>> 'C:\Users\tony'             #沒有 \U 這個跳脫字元
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: invalid syntax
>>> r'C:\Users\tony'            #用 r 避免跳脫
'C:\\Users\\tony'
>>> print('C:\Users\tony')   #沒有 \U 這個跳脫字元
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: invalid syntax
>>> print(r'C:\Users\tony')  #用 r 取消跳脫
C:\Users\tony

2. 長字串的跨行串接

對於較長的字串, 為了可讀性可以用 \ 串接延續到下一行, 例如 :

>>> 'This is \
... a book. That is\
... a pen.'
'This is a book. That isa pen.'

注意, 這裡的倒斜線 \ 只是單純的跨行串接符號而已, 沒有跳行作用, 跳行須自行加入 \n 字元. 例如 :

>>>a='This is \
... a pen.\nThat is\
... a book.'
>>> a
'This is a pen.\nThat isa book.'
>>> print(a)
This is a pen.
That isa book.

除了用行尾倒斜線跨行串接長字串外, Python 還可以用三個連續引號來表示跨行的長字串 (單引號或雙引號均可), 不過連續引號並非只能在跨行時用, 單行也可以用. 例如: 

>>> a='''Hello
...  World!'''
>>> a
'Hello\n World!'
>>> print(a)
Hello
 World!
>>> '''Hello World!'''
'Hello World!'

注意, 與上面用 \ 做跨行串接長字串不同的是, 使用三個連續引號時會保存輸入的格式, 這對處理像 JSON 或 HTML 那樣的長字串很方便. 例如上面 Hello 後是跳行, 這個 \n 字元會被認為是字串的一部分; 而上面用 \ 跨行串接長字串則只是單純串接, 並非跳行, 要跳行需自行加入 \n.   

3. 字串的長度 : 

由於字串繼承抽象型別 sized, 因此可以使用內建函數 len() 取得字串長度 (即 unicode 字元數) :

 函數 說明
 len(obj) 傳回物件元素個數

注意, 跳脫 (轉義) 字元雖然看起來由兩個字元組成, 但實際上算是一個字元. 例如 :

>>> len('Hello')
5
>>> len('\n')
1

4. 字串的基本操作 :

字串的基本操作有三種 :
  1. 串接
  2. 複製
  3. 切片 (slicing)
兩個字串常數 (字面值) 可以一前一後直接串接, 也可以用 + 運算子串接, 例如 :

>>> a='abc' '123'   #字串常數可以前後直接串接
>>> a
'abc123'
>>> a='abc' + '123' #用 + 運算子串接
>>> a
'abc123'

但是如果有一個是變數的話就必須使用 + 運算子串接, 例如 :

 >>> a='abc123'
>>> a '456'           #字串變數不可直接串接
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: invalid syntax
>>> a + '456'
'abc123456'

運算子 + 雖然身兼加號與自串串接兩種功能, 但 Python 是強型別語言, 不會像 Javascript 那樣進行隱藏的自動轉型, 必須強制轉型才可以, 例如 :

>>> '123' + 123          #Python 不會自動轉型
Traceback (most recent call last):
  File "<stdin>", line 1, in TypeError: unsupported types for __add__: 'str', 'int'
>>> int('123') + 123    #必須強制轉型
246

字串的複製使用 * 運算子, 例如 :

>>> 'abc123' * 3                #複製三次
'abc123abc123abc123'
>>> 'abc123\n' * 3             #含跳行字元複製三次
'abc123\nabc123\nabc123\n'
>>> print('abc123\n' * 3)
abc123
abc123
abc123

字串繼承自抽象型別 sequecnce, 由有序的 unicode 字元組成, 所以字串其實可以看做是不可變的字元串列. 雖然其內容為不可變, 但是可以利用中括號 [] 運算子擷取其元素 (字元), 或用切片 [start:end:step] 運算子擷取裡面的子字串成為新字串. 


所謂切片 (slicing) 是指使用中括號運算符 [] 以索引從字串中擷取子字串, 此用法是源自 Python 的序列 (Sequence) 抽象型別, 而 str, list, tuple 這三種型別都繼承自 Sequence 型別, 其每一個元素都可以使用索引操作之故. 切片的存取方式不僅用在字串, 也用在所有繼承 Sequence 型別之物件如串列與元組等.

索引值由左至右從 0 起算, 0 代表字串的開始字元; 若由字串尾端 (由右往左) 的話, 則從 -1 起算, 即最後一個字元索引為 -1, 倒數第二字原為 -2 等等. 但要注意索引若超出界線將產生錯誤, 例如 :

>>> a='0123456789'
>>> a[0]
'0'
>>> a[-1]
'9'
>>> a[9], a[-1]           #a[9] 與 a[-1] 是同一個字元
('9', '9')
>>> a[0], a[-10]         #a[0] 與 a[-10] 是同一個字元
('0', '0')
>>> a[10]                   #索引超出界限產生錯誤
Traceback (most recent call last):
  File "<stdin>", line 1
SyntaxError: invalid syntax

Python 的字串是不可變資料, 因此利用索引去更改字串內容將引發錯誤, 例如 :

>>> a='0123456789'
>>> a[0]
'0'
>>> a[0]='a'                 #字串內容不允許變更
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

使用切片從字串中取得子字串的語法為 [start:end:step], 其中 start 為開始索引, end 為結束索引, step 為步階 (即跳幾格取得下一個字元), 這三個索引以及冒號都是可以省略的, 但不能全部省略, 至少必須有一個冒號.

  1. [:] 會擷取整個字串
  2. [start:] 會擷取索引 start 到字串結尾
  3. [:end] 會擷取字串開頭至索引 end-1 (不包含索引 end)
  4. [start:end] 會擷取索引 start 至索引 end-1 (不包含索引 end)
  5. [start:end:step] 會擷取索引 start 至索引 end-1 (不包含索引 end), 跳 step 次
但是 MicroPython 不支援 step 大於 1 的步階.

例如 :

>>> a='0123456789'
>>> a[:]         #擷取整個字串
'0123456789'
>>> a[5:]       #擷取索引 5 到字串尾
'56789'
>>> a[:5]       #擷取字串首至索引 4
'01234'
>>> a[3:8]     #擷取索引 3 到索引 7
'34567'
>>> a[3:8:2]  #MicroPython 不支援步階大於 1
Traceback (most recent call last):
  File "<stdin>", line 1, in NotImplementedError: only slices with step=1 (aka None) are supported
>>> a[3:8:1]   #與 [3:8] 效果相同
'34567'

步階 step 大於 1 在 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.
>>> a='0123456789'
>>> a[3:8:2]    #索引 3~7, 跳 2 格
'357'

注意, Python 的切片功能在索引的使用上不會檢查是否超限, 即使超過也不會產生錯誤, 比使用單一索引容忍度高, 例如 :

>>> a='0123456789'
>>> a[:20]       #結束索引 20 已超限, 但不會出現錯誤
'0123456789'
>>> a[-20:]      #結束索引 -20 已超限, 但不會出現錯誤
'0123456789'

切片也可以使用負數索引, 表示從字串尾 (索引 -1) 起算, 例如 :

>>> a='0123456789'
>>> a[-3:]                   #a[-3:] 即 a[7:]
'789'
>>> a[:-3]                   #a[:-3] 即 a[:7]
'0123456'
>>> a[-6:-3]                #a[-6:-3] 即 a[4:7
'456'

間隔 step 也可以是負數, 表示由後往前 (或由右往左) 跳躍, 但是在 MicroPython 卻未實作負的間隔功能, 例如 :

>>> a='0123456789'
>>> a[::-1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NotImplementedError: only slices with step=1 (aka None) are supported
>>>

這個 [::-1] 是指整個字串 [::] 從後面每次取一個 , 在 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.
>>> a='0123456789'
>>> a[::-1]       #間隔 -1 表示從右到左每次取一個
'9876543210'


5. 字串物件的方法 :

除了上面三種基本的字串操作外, Python 的字串物件提供了豐富的方法來處理字串, 但須注意, Python 的字串是不可變 (immutable) 資料類型, 因此這些字串物件方法的傳回值如果是字串, 它都是傳回一個新字串, 並非改變了原來的字串.

(1). 大小寫操作

 方法 說明
 capitalize() 將首字母大寫, 其餘小寫 (未實作)
 lower() 將全部字元轉成小寫
 upper() 將全部字元轉成大寫
 swapcase() 將字元大寫變小寫, 小寫變大寫  (未實作)
 title() 將每個單字的首字元大寫, 其餘小寫   (未實作)

這五個方法在 MicroPython 只時做了 lower() 與 upper(), 例如 :

>>> a='This is a book. That is a pen.'
>>> a
'This is a book. That is a pen.'
>>> a.capitalize()
Traceback (most recent call last):
ppributeError: 'str' object has no attribute 'capitalize'
>>> a.lower()
'this is a book. that is a pen.'
>>> a                          #原字串不變
'This is a book. That is a pen.'
>>> a.upper()
'THIS IS A BOOK. THAT IS A PEN.'
>>> a.swapcase()
Traceback (most recent call last):
  File "AttributeError: 'str' object has no attribute 'swapcase'
>>> a.title()
Traceback (most recent call last):
  File "AttributeError: 'str' object has no attribute 'title'


(2). 字串內容檢查 :

 方法 說明
 islower() 字串中的英文字母都是小寫傳回真, 否則傳回假
 isupper() 字串中的英文字母都是大寫傳回真, 否則傳回假
 istitle() 字串中每個單字的首字元大寫其餘小寫傳回真, 否則傳回假   (未實作)
 isspace() 字串只含有空白字元 ('\t','\v','\n','\r','\f',' ') 傳回真, 否則傳回假
 isalpha() 字串只含有英文字母傳回真, 否則傳回假
 isdigit() 字串只含有數字 (0~9) 傳回真, 否則傳回假
 isdecimal() 字串只含有 10 進位數字傳回真, 否則傳回假 (未實作)
 isalnum() 字串只含有英文字母與數字傳回真, 否則傳回假 (未實作)
 isidentifier() 字串是否為合法的識別字 (未實作)
 isprintable() 字串只含有可見字元 (未實作)

這十個方法在 MicroPython 只實作了 5 個, istitle(), isdecimal(), isalnum(), isidentifier(), 以及 isprintable() 這 5 個未實作. 例如 :

>>> 'abc123'.islower()
True
>>> 'Abc123'.islower()
False
>>> 'Hello World'.istitle()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'istitle'
>>> '\t\v\n\r\f '.isspace()      #最後一個字元是空格
True
>>> '\t\v\n\r\f \a'.isspace()        #響鈴字元 '\a' 不是空白 (white space)
False
>>> 'abcxyzABCXYZ'.isalpha()
True
>>> 'abcxyzABCXYZ123'.isalpha()
False
>>> '0123456789'.isdigit()
True
>>> '0123456789abc'.isdigit()
False
>>> '0123456789'.isdecimal()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'isdecimal'
>>> '0123456789abcABC'.isalnum()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'isalnum'
>>> 'str'.isidentifier()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'isidentifier'
>>> 'abc123'.isprintable()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'isprintable'

(3). 尋找子字串 :

 方法 說明
 find(sub [,start [, end]]) 尋找子字串 sub, 找到傳回第一組之起始索引, 否則傳回 -1
 rfind(sub [,start [, end]]) 尋找子字串 sub, 找到傳回最後一組之起始索引, 否則傳回 -1
 index(sub [,start [, end]]) 與 find 相同, 但找不到子字串時會產生 valueError 錯誤
 rindex(sub [,start [, end]]) 與 rfind 相同, 但找不到子字串時會產生 valueError 錯誤
 count(sub [,start [, end]]) 在字串中尋找子字串 sub, 傳回子字串出現的次數
 startswith(pfx [,start [, end]]) 若字串以子字串 pfx 開頭傳回真, 否則傳回假 
 endswith(sfx [,start [, end]]) 若字串以子字串 sfx 結尾傳回真, 否則傳回假

上面七個方法都有備選的第二與第三參數, 不過在 MicroPython 中, startswith() 與 endswith()  只實作了一個參數的用法, 無法指定切片範圍去尋找. 其次, index()/rindex() 功能與 find()/rfind() 一樣, 差別是沒找到時, index()/rindex() 會傳回 -1, 而 find()/rfind() 則是產生 ValueError 錯誤, 例如 :

>>> '0123456789'.find('345')         #在索引 3 找到 '345'
3
>>> '0123456789'.find('345',2)       #從索引 2 開始找 (有找到)
3
>>> '0123456789'.find('345',4)       #從索引 4 開始找 (沒找到)
-1
>>> '0123456789'.find('345',2,5)     #切片 [2:5] 不包括索引 5 故沒找到
-1
>>> '0123456789'.find('345',2,6)     #切片 [2:6] 有包括索引 5 故有找到
3
>>> '0123456345'.find('345')         #有兩組匹配, 但只傳回第一組開始索引
3
>>> '0123456789'.rfind('345')        #第 1 組即最後一組
3
>>> '0123456345'.rfind('345')        #有兩組匹配, 但只傳回最後一組開始索引 7
7
>>> '0123456345'.rfind('345',2)      #從索引 2 開始找
7
>>> '0123456345'.rfind('345',2,7)    #在切片 [2:7] 中只找到第一組
3
>>> '0123456345'.rfind('345',2,5)    #在切片 [2:5] 中找不到傳回 -1
-1
>>> '0123456789'.index('345')        #在索引 3 找到 '345'
3
>>> '0123456789'.index('345',2,7)    #切片 [2:7] 有包括索引 5 故有找到
3
>>> '0123456789'.index('345',4,7)    #切片 [4:7] 不包括索引 3 故沒找到 (傳回錯誤)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: substring not found
>>> '0123456345'.index('345')        #與 find() 一樣只傳回第一組開始索引
3
>>> '0123456789'.rindex('345')       #在索引 3 找到 '345'
3
>>> '0123456789'.rindex('345',2)     #從索引 2 開始找 (有找到)
3
>>> '0123456789'.rindex('345',4,7)   #切片 [4:7] 不包括索引 3 故沒找到 (傳回錯誤)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: substring not found
>>> '0123456345'.rindex('345')       #與 rfind() 一樣只傳回最後一組開始索引
7
>>> '0123456789'.count('345')        #找到一組
1
>>> '0123456345'.count('345')        #找到兩組
2
>>> '0123456789'.count('345',2)      #從索引 2 開始找 (找到 1 組)
1
>>> ''0123456789'.count('345',4,7)   #切片 [4:7] 不包括索引 3 故沒找到
0
>>> '0123456789'.startswith('0123')
True
>>> '0123456789'.startswith('345',3)
True
>>> '0123456789'.startswith('345',2)
False
>>> '0123456789'.startswith('345',3,6)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: function expected at most 3 arguments, got 4    #奇怪的錯誤訊息
>>> '0123456789'.endswith('789')
True
>>> '0123456789'.endswith('789',5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NotImplementedError: start/end indices                #endswith() 未實作多參數
>>> '0123456789'.endswith('789',5,9)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: function expected at most 3 arguments, got 4    #奇怪的錯誤訊息

(4). 字串取代 :

 方法 說明
 replace(old, new [, count]]) 將子字串 old 全部用 new 替換, 或指定替換 count 個

注意, 這個方法會將字串中符合的子字串全部替換掉.

例如 :

>>> 'abc123abc123'.replace('abc','xyz')    #將字串中的 'abc' 以 'xyz' 取代
'xyz123xyz123'
>>> 'abc123abc123'.replace('abc','xyz',1)  #限制取代次數 1 次
'xyz123abc123'
>>> 'abc123abc123'.replace('abc','xyz',0)  #取代次數 0 表示不取代
'abc123abc123'
>>> 'abc123abc123'.replace('abc','xyz',3)  #取代次數超過沒作用
'xyz123xyz123'

(5). 字串去除 :

 方法 說明
 strip([chars]) 去除字串首尾的指定字元集合 chars, 預設刪除空白 
 lstrip([chars]) 去除字串開頭的指定字元集合 chars, 預設刪除空白 
 rstrip([chars]) 去除字串結尾的指定字元集合 chars, 預設刪除空白

注意, 若傳入參數指定欲刪除的字元, 預設刪除六種空白字元 (white space) :  \t\v\n\r\f 與空格.

例如 :

>>> ' abc '.strip()        #預設刪除 white space (\t\v\n\r\f 與空格)
'abc'
>>> 'abc123'.strip('a3')   #刪除首尾的 'a' 或 '3'
'bc12'
>>> 'abc123'.strip('c')    #刪除首尾的 'c' (沒有)
'abc123'
>>> 'abc123'.strip('32ba') #刪除首尾的 '3' 或 '2' 或 'b' 或 'a'
'c1'
>>> '\t\v\n\r\f abc\n123 \t\v\n\r\f'.strip()   #刪除首尾的 white space
'abc\n123'

(6). 字串填充 :

 方法 說明
 center(width [,fillchar]) 將字串居中擴展為 width 個字元, 左右以 fillchar 填充, 預設為空格 (未實作)
 ljust(width [,fillchar]) 將字串靠左擴展為 width 個字元, 右方以 fillchar 填充, 預設為空格 (未實作)
 rjust(width [,fillchar]) 將字串靠右擴展為 width 個字元, 左方以 fillchar 填充, 預設為空格 (未實作)
 zfill(width) 在字串左方補 0 使總長度擴展為 width 字元  (未實作)
 expandtabs([tabsize]) 將字串中的 tab 字元以 tabsize 個空格取代, 預設 8 格  (未實作)

字串填充功能在 MicroPython 全部未實作. 例如 :

>>> 'abc'.center(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'center'
>>> 'abc'.ljust(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'ljust'
>>> 'abc'.rjust(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'rjust'
>>> 'abc'.zfill(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'zfill'
>>> 'a\tbc'.expandtabs(10)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'expandtabs'

這五個方法在 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.
>>> 'abc'.center(10)             #欄寬 10 格, 前後補空格
'   abc    '
>>> 'abc'.ljust(10)                #欄寬 10 格, 後面補空格
'abc       '
>>> 'abc'.rjust(10)                #欄寬 10 格, 前面補空格
'       abc'
>>> 'abc'.zfill(10)                 #欄寬 10 格, 前面補 0
'0000000abc'
>>> 'a\tbc'.expandtabs(10)    #實際 9 個空格
'a         bc'
>>> 'a\tbc'.expandtabs(3)      #實際 2 個空格
'a  bc'
>>> 'a\tbc'.expandtabs(2)      #實際 1 個空格
'a bc'
>>> 'a\tbc'.expandtabs(1)      #實際 1 個空格
'a bc'

觀察 expandtabs() 傳入不同參數結果, 發現傳入 1 與 2 實際空格為 1,  傳入 3 以上實際空格為 tabsize-1, 與我預期者不同, 不知作何解釋.

(7). 字串切割 :

 方法 說明
 partition(sep) 以 sep 為界由左至右分割字串為三部分, 傳回 sep 居中的 tuple (未實作)
 rpartition(sep) 以 sep 為界由右至左分割字串為三部分, 傳回 sep 居中的 tuple (未實作)
 split(sep [,maxsplit]) 以 sep 為界由左至右分割字串 maxsplit 次, 結果以 list 傳回
 rsplit(sep [,maxsplit]) 以 sep 為界由右至左分割字串 maxsplit 次, 結果以 list 傳回
 splitlines([keepends]) 以 \n 為界分割字串, 預設不保留跳行字元 (keepends=False) (未實作)

這五個方法在 MicroPython 僅實作了 split() 與 rsplit() 這兩個, 例如 :

>>> a='123,456,789'
>>> a.partition(',')                #未實作 partition()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'partition'
>>> a.rpartition(',')               #未實作 rpartition()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'str' object has no attribute 'rpartition'
>>> a.split(',')        #以 ',' 分拆整個字串, 拆開後以串列傳回
['123', '456', '789']
>>> a.split(',',2)     #指定最多拆 2 次
['123', '456', '789']
>>> a.split(',',1)     #指定最多拆 1 次
['123', '456,789']
>>> a.rsplit(',')       #從右邊往左拆
['123', '456', '789']
>>> a.rsplit(',',1)    #指定最多拆 1 次
['123,456', '789']
>>> a='123\n456\n789\n'     #含有跳行字元的字串
>>> print(a)
123
456
789

>>> a.splitlines()     #未實作 splitlines()
Traceback (most recent call last):
  File "AttributeError: 'str' object has no attribute 'splitlines'

如果有連續兩個分割字串, 則會分割出空字串, 例如 :

>>> a='123,,456,,789'    #連續的分割字串 ,
>>> a.split(',')
['123', '', '456', '', '789']

上面三個 MicroPython 未實作的方法在 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.
>>> a='123,456,789'
>>> a='123,456,789'
>>> a.partition(',')       #以第一個 ',' 為界拆成 3 份
('123', ',', '456,789')
>>> a.rpartition(',')      #以最後一個 ',' 為界拆成 3 份
('123,456', ',', '789')
>>> a='123\n456\n789\n'
>>> a.splitlines()          #預設不保留跳行字元
['123', '456', '789']
>>> a.splitlines(True)   #保留跳行字元
['123\n', '456\n', '789\n']

(8). 字串組合 :

 方法 說明
 join(iterable) 以字串為分隔字元串接 iterable 物件各元素為新字串後傳回

此 join() 函數使用上較特別, 是以分隔字元當操作母體, 以要串接的可迭代物件 (如串列, 元組等) 當參數, 例如 :

>>> a=['123','456','789']    #要組合的對象為串列元素
>>> ''.join(a)     #以空字串串接
'123456789'
>>> ' '.join(a)    #以空格串接
'123 456 789'
>>> '-'.join(a)    #以 '-' 串接
'123-456-789'
>>> ','.join(a)    #以 ',' 串接
'123,456,789'
>>> a=('123','456','789')    #要組合的對象為元組元素
>>> ''.join(a)     #以空字串串接
'123456789'
>>> ' '.join(a)    #以空格串接
'123 456 789'
>>> '-'.join(a)    #以 '-' 串接
'123-456-789'
>>> ','.join(a)     #以 ',' 串接
'123,456,789'

(9). 字串格式化 : 

Python 的字串最早是採用像 C 語言那樣以 % 運算子來格式化 (這也是餘數運算子), 較新的方式是使用 format() 函數. % 運算子搭配格式指定子後嵌入字串中, 就可以利用字串後面以 % 帶領的物件變數或 tuple 變數將值映射到對應的位置, 其組成方式如下 :

%[flag][width][.precision][specifier]

中括號表示每個組成成分都是可有可無的. 其中 [flag] (旗標) 有五個字元可用 :

 旗標 說明
 '#' 顯示另一種格式, 例如 16 進制的 ff 改為 0xff
 '0' 數值前面補 '0'
 '-' 靠左對齊 (若與 '0' 旗標同時出現會蓋掉欲補之 '0')
 ' ' 空格旗標 : 在正數前面補一個空格
 '+' 前面補 '+' (若與空格旗標同時出現會蓋掉欲補之空格)

[width] 用來設定要顯示的資料欄寬 (即字元個數); [.precision] 用來設定浮點數的精確度, 以一個小數點後面帶一個整數, 表示要精確到小數點後幾位 (CPython 預設 6 位); 而 [specifier] 用來設定資料要用何種格式輸出, 共有 15 種格式如下表 :

 格式 說明
 'd' 有號 10 進位整數
 'i' 有號 10 進位整數 (同 'd')
 'o' 有號 8 進位整數
 'x' 有號 16 進位整數 (小寫)
 'X' 有號 16 進位整數 (大寫)
 'f' 小數格式的浮點數
 'F' 小數格式的浮點數
 'e' 指數格式的浮點數
 'E' 指數格式的浮點數
 'g' 自動變換格式的浮點數, 指數 < -4 用指數表示 (小寫)
 'G' 自動變換格式的浮點數, 指數 < -4 用指數表示 (大寫)
 'c' 顯示一個字元 
 's' 顯示一個字串
 'r' 顯示一個用 repr() 轉換的字串
 '%' 顯示字元 '%'

其中 'd' 與 'i' 是完全一樣的功能. 例如 :

>>> '%d' %1234567890    #10 進位整數
'1234567890'
>>> '%i' %1234567890     #與 %i 相同
'1234567890'
>>> print("Hello %s" %'Tony')     #字串
Hello Tony
>>> '%d %i %o %x %X' %(255, 255, 255, 255, 255)    #10/8/16 進位整數
'255 255 377 ff FF'
>>> '%f %F %e %E' %(0.123456789, 0.123456789, 0.123456789, 0.123456789)  #浮點數
'0.123457 0.123457 1.234568e-01 1.234568E-01'
>>> '%g %G' %(1.2345678e-4, 1.23456789e-4)    #浮點數 (自動判斷格式)
'0.000123457 0.000123457'
>>> '%g %G' %(1.2345678e-5, 1.23456789e-5)    #浮點數 (自動判斷格式)
'1.23457e-05 1.23457E-05'
>>> '%c %c' %(65, 'A')                #字元也可用整數
'A A'
>>> '%c %c %r' %(65, 'A', 'A')    #%r 會加上引號
"A A 'A'"

Python 的物件都可以轉換成字串, %s 格式也可以用在 list 與 tuple 等物件, 例如 :

>>> '%s %s' %([1,2,3], (4,5,6))    #list 與 tuple 被轉成字串輸出了
'[1, 2, 3] (4, 5, 6)'

旗標 '#' 用來以另類 (alterbative) 格式顯示資料, 例如在顯示 16 進位數值時會冠上 '0x' 或 '0X'; 顯示 8 進位數值時會冠上 '0o' :

>>> '%#x %#X %#o' %(255, 255, 255)
'0xff 0XFF 0o377'
>>> '%x %X %o' %(255, 255, 255)
'ff FF 377'

旗標 '0' 會在數值資料前面補 0 (不可用字串), 例如 :

>>> '%05d' %123         #欄寬 5, 前面補 0
'00123'
>>> '%05d' %123456   #超過欄寬當然不補 0
'123456'
>>> '%05x' %0xabc      #16 進制整數
'00abc'
>>> '%05d' %34.567     #小數會被捨去
'00034'

旗標 '-' 會將資料向左對齊, 與旗標 '0' 同時使用時, 在 CPython  是旗標 '-' 優先, 旗標 '0' 無作用, 但在 MicroPython 卻是向左靠齊後又在後面補 0, 例如 :

>>> '%-5d' %123       #向左靠齊
'123  '
>>> '%-05d' %123     #向左靠齊後右邊補 0
'12300'
>>> '%0-5d' %123     #向左靠齊後右邊補 0
'12300'
>>> '%0-5d' %123456
'123456'

而在 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.
>>> '%0-5d' %123     #旗標 '-' 優先, 即向左靠齊不補 0
'123  '
>>> '%-05d' %123     #旗標 '-' 優先, 即向左靠齊不補 0
'123  '
>>> '%-05d' %123456
'123456'

旗標 '+' 則會在數值前面冠上正負號, 例如 :

>>> '%5d' %123
'  123'
>>> '%+5d' %123      #強制冠正負號
' +123'
>>> '%+5d' %-123     #強制冠正負號
' -123'
>>> '%5d' %-123       #負數不用旗標 '+' 本來就會有負數
' -123'

除了用 % 運算子做字串格式化外, Python 的 str 物件還提供了比 % 功能更強大的格式化函數 format(), 此函數也是建立在 % 運算子基礎上, 其置換欄位採用大括號 {}, 裡面可用 0 起始的索引指定套用的參數索引, 如果沒有指定索引, 就依據參數出現順序一一套用, 如果要顯示大括號本身, 就用 {{}}, 例如 :

>>> '{0} {1} {2}'.format(123, 4.567, 'abc')    #依指定之索引順序套用參數
'123 4.567 abc'
>>> '{} {} {}'.format(123, 4.567, 'abc')          #依參數出現順序套用
'123 4.567 abc'
>>> '{2} {0} {1}'.format(123, 4.567, 'abc')     #依指定之索引套用參數
'abc 123 4.567'

大括號內除了使用索引來定位要套用的參數外, 也可以像字典那樣使用 key, 這樣有 key 的參數在 format() 內可以放在任何位置, 但無 key 參數一定要放在對應位置, 例如 :

>>> '{} {name} {greetings}'.format('Hello!', name='Tony', greetings='You are welcome!')
>>> '{} {name} {greetings}'.format('Hello!', greetings='You are welcome!', name='Tony')
'Hello! Tony You are welcome!'
>>> '{} {name} {greetings}'.format(greetings='You are welcome!', name='Tony', 'Hello!')
Traceback (most recent call last):
  File "SyntaxError: non-keyword arg after keyword arg
如果要印出的是 list 或 tuple 等序列性物件, 也可以在大括號的索引後使用中括號索引來擷取物件內的元素, 但是 MicroPython 還沒有實作這種用法, 例如 :

>>> name=['tony','peter']
>>> gender=('male','femal')
>>> '{0[1]} {1[1]}'.format(name, gender)   #印出 name[1] 與 gender[1] (未實作)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NotImplementedError: attributes not supported yet
>>> '{0} {1}'.format(name[1], gender[1])   #索引放在 format() 裡就 OK
'peter femal'

這在 CPython 執行結果如下 :

Microsoft Windows [版本 10.0.14393]
(c) 2016 Microsoft Corporation. 著作權所有,並保留一切權利。

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.
>>> name=['tony','peter']
>>> gender=('male','femal')
>>> '{0[1]} {1[1]}'.format(name, gender)   #在 {} 內使用序列物件索引
'peter femal'
>>> '{1[1]} {0[1]}'.format(name, gender)   #{} 索引順序調換
'femal peter'
>>> '{[1]} {[1]}'.format(name, gender)       #不指定 {} 索引就依對應關係
'peter femal'
>>> '{0[3]} {1[1]}'.format(name, gender)   #索引超出界限了
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

這裡 {} 內的第一個數字是 {} 的索引, 而 [] 內的則是物件元素的索引.


二. 串列 (list) :

Python 的串列是有序可變的物件集群 (Collections), 可以儲存任何資料型態, 包括串列本身. 其功能比傳統的陣列更強大, 因為傳統語言如 C 或 Java 的陣列只能儲存相同的資料型態. Python 的串列比較像 Java 的 ArrayList 類別. 串列與集合 (set) 不同之處是, 串列是有序的, 而集合是無序的, 且串列同一個元素可以出現一次以上, 而集合的元素則必須是唯一的.

1. 建立串列 :

建立串列的方法不只一種, 但主要是使用 [] 運算子, 例如 :

>>> a=[]                     #建立一個空的串列
>>> type(a)                #型態為 list
<class 'list'>
>>> a=[0,1,2,3,4,5]    #建立一個數值串列
>>> a
[0, 1, 2, 3, 4, 5]
>>> print([1.728, 3.14, 6.02e23])      #印出一個數值串列
[1.728, 3.14, 6.02e+23]
>>> a=['a', 'b', 'c']        #建立字串串列
>>> a
['a', 'b', 'c']
>>> a=[123, 3.14, "abc", True]          #建立異質元素的串列
>>> a
[123, 3.14, 'abc', True]

由於串列是一種容器型別, 其元素可以是任何資料型態, 包括串列本身, 例如 :

>>> a=[(1,2,3),[4,5,6],{'x':7,'y':8,'z':9},{'a','b','c'}]  #元素包括 tuple, list, dict, 與 set
>>> a
[(1, 2, 3), [4, 5, 6], {'x': 7, 'z': 9, 'y': 8}, {'b', 'c', 'a'}]

第二種建立串列的方法是使用內建函數 list(), 只能傳入一個元素當作參數 (tuple 或字串), 例如 :

>>> a=list()
>>> type(a)
>>> a=list((123, 3.14, "abc", True))
>>> a
[123, 3.14, 'abc', True]


內建函數 list() 也可以將其他容器型別資料轉成串列, 例如會將傳入的字串拆成字元串列 :

>>> a='abc123'
>>> list(a)
['a', 'b', 'c', '1', '2', '3']          #字串被轉成字元串列

傳入不可變 (immutable) 的元組 (tuple) 會將其轉成可變的串列 :

>>> a=(1,2,3)      #元組
>>> list(a)
[1, 2, 3]    #將元組變成串列

傳入字典物件則會傳回其 key 所組成的串列 :

>>> a={'x':1,'y':2,'z':3}   #字典
>>> list(a)
['x', 'z', 'y']    #傳回字典的鍵組成之串列

傳入無順序的集合也會轉成串列 :

>>> a={1,2,3}    #集合
>>> list(a)
[3, 1, 2]  

第三種建立串列的方法是使用串列物件的 append() 方法, 例如 :

>>> a=[]
>>> a.append(123)
>>> a.append(3.14)
>>> a.append('abc')
>>> a.append(True)
>>> a
[123, 3.14, 'abc', True]

另外, 有一些內建函數其傳回值是串列, 例如字串的 split() 函數 :

>>> date="2017-05-05"
>>> date.split('-')
['2017', '05', '05']


還有一些內建函數也常被用來建立串列, 例如範圍函數 range([start], stop[, step]), 它會傳回一個整數序列物件 (但不是 list, 而是型態為 range 的序列物件), 其中 start (預設為 0) 與 stop 分別是起訖整數, step 則是間隔整數 (預設為 1), 利用 list() 函數可以將其轉換成串列, 例如 :

>>> r=range(10)             #預設 start=0
>>> r
range(0, 10)
>>> type(r)                     #range() 會傳回 range 型別物件
<class 'range'>
>>> list(r)                       #將 range 轉成 list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(3,9))        #間隔預設為 1
[3, 4, 5, 6, 7, 8]
>>> list(range(0,10,2))   #間隔=2
[0, 2, 4, 6, 8]
>>> list(range(0,-10,-2)) #間隔為負數時, stop 須小於 start
[0, -2, -4, -6, -8]
>>> list(range(0))            #0:0 為空串列
[]
>>> list(range(1,0))         #1:0 為空串列
[]


2. 串列的基本操作 :

串列的基本操作有下面 7 種 :
  1. 串列內容更改
  2. 串接 (使用 + 或 += 運算子)
  3. 複製 (使用 * 運算子)
  4. 切片 (使用 [] 運算子)
  5. 刪除元素 (使用 del 指令)
  6. 查詢元素 (使用 in 與 not in 運算子)
  7. 多重指定
串列是可變資料型態, 其內容可以用指定運算子加以變更, 例如 :

>>> a=[1,2,3]
>>> b=a          #b 也指向 a 的參考 (兩個內容一樣)
>>> a[0]='a'    #更改 a[0] 元素
>>> a
['a', 2, 3]                         #a[0] 被更改了
>>> b
['a', 2, 3]                         #b[0] 也同時被改了 (相同實體)
>>> b[0]='b'    #更改 b[0]
>>>b
['b', 2, 3]
>>> a
['b', 2, 3]                          #a[0] 也同時被改了 (相同實體)

注意, 變數是指向物件實體的參考, b=a 表示 b 變數的參考也是指向  a, 所以兩者都指向同一實體, 改任何一個另一個也同時被改了.

多個串列可用 + 或 += 運算子串接為新的串列 (另一個方法是使用 extend), 例如 :

>>> [1,2,3] + ['a','b','c']
[1, 2, 3, 'a', 'b', 'c']               #產生新串列
>>> a=[1,2,3]
>>> b=['a','b','c']
>>> a += b           #即 a=a+b, a 指向串接後的新串列
>>> a
[1, 2, 3, 'a', 'b', 'c']  

一個串列要複製其元素可使用 * 運算子 :

>>> [1,2,3]*3
[1, 2, 3, 1, 2, 3, 1, 2, 3]       #元素複製 3 次

但是 MicroPython 卻不支援複合運算 *= :

>>> a=[1,2,3]
>>> a *= 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for : 'list', 'int'

這在 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.
>>> a=[1,2,3]
>>> a *= 3
>>> a
[1, 2, 3, 1, 2, 3, 1, 2, 3]

串列繼承 sequence 型別, 其元素是按照順序排列的, 因此可以用索引來擷取串列的單一元素, 而用切片 (slice) 則可以擷取多個元素. 如同字串, 其索引由左至右為 0 起始的連續整數 (索引必須是整數, 不可以為浮點數), 由右至左為 -1 起始 (即倒數第一個元素索引為 -1), 索引超出界限會產生 IndexError, 例如 :

>>> a=[0,1,2,3,4,5]
>>> a[0]
0
>>> a[1]
1
>>> a[2]
2
>>> a[1.5]    #索引須為整數, 不可以為浮點數
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: list indices must be integers, not float
>>> a[-1]
5
>>> a[-2]
4
>>> a[-3]
3
>>> a[6]     #索引超出界限
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> a[-6]
0
>>> a[-7]    #索引超出界限
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
IndexError: list index out of range

與字串一樣, 可以用切片 (slice) 來擷取串列中的多個元素 (傳回子串列), 格式如下 :

[start:end:step] 會擷取索引 start 至索引 end-1 (不包含索引 end), 跳 step 次

例如 :

>>> a=[0,1,2,3,4,5]
>>> a[:]              #擷取整個串列
[0, 1, 2, 3, 4, 5]
>>> a[3:]            #索引 3 至結尾
[3, 4, 5]
>>> a[:3]            #開頭至索引 2 (不包含 end)
[0, 1, 2]
>>> a[2:4]          #索引 2 至 3 (不包含 end)
[2, 3]
>>> a[0:5:2]       #索引 0 至 5 間隔 2
[0, 2, 4]
>>> a[0:5:3]       #索引 0 至 5 間隔 3
[0, 3]

刪除串列中指定索引之元素要用 del 指令, 後面的元素索引會往前挪, 例如 :

>>> a=[0,1,2,3,4,5]
>>> del a[3]   #刪除 a[3]
>>> a
[0, 1, 2, 4, 5]
>>> a[3]          #a[3] 由後面的 4 遞補了
4
>>> del a[-1]   #負索引 : 刪除倒數第 1 個元素
>>> a
[0, 1, 2, 4]

注意,  del 既非內建函數, 亦非串列的方法, 它是 Python 指令 del, 不只可用來刪除串列元素, 也可用來刪除物件以釋放記憶體, 例如 :

>>> a=[1,2,3]
>>> a
[1, 2, 3]
>>> del a     #從記憶體中刪除 a 變數
>>> a
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'a' is not defined      #變數 a 的記憶體已經被釋放了

若要查詢串列中是否含有某元素可用 in 與 not in 運算子, 傳回值為 True/False, 語法如下 :

運算元 in 串列
運算元 not in 串列

例如 :

>>> 'a' in ['a','b','c']
True
>>> 'a' not in ['a','b','c']
False
>>> 'abc' not in ['a','b','c']
True
>>> 'abc' in ['a','b','c']
False

串列也常被用在多重指定 (又稱為開箱 unpacking), 但是等號兩邊變數與元素個數必須相同, 否則會產生錯誤, 例如 :

>>> x,y,z=['a','b','c']     #多重指定
>>> x
'a'
>>> y
'b'
>>> z
'c'
>>> w,x,y,z=['a','b','c']   #等號兩邊個數不一致
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: need more than 3 values to unpack
>>> x,y=['a','b','c']         #等號兩邊個數不一致
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

上面提到的 range() 函數也可以用在多重指定賦值, 例如 :

>>> a,b,c=range(3)   #列舉 0,1,2 給 a,b,c
>>> a
0
>>> b
1
>>> c
2
>>> type(a)
<class 'int'>
>>> a,b,c=range(5)     #但不可超出所需要之列舉
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 3)

3. 用內建函數操作串列 :

下列的內建函數可用在串列的操作, 它們會傳回新的串列 : 

 函數 說明
 len(obj) 傳回物件元素個數
 min(obj) 傳回物件中數值最小的元素
 max(obj) 傳回物件中數值最大的元素
 sum(obj) 傳回物件所有元素之和 
 sorted(obj)  傳回由小到大排列之物件
 reversed(obj) 傳回與原串列元素順序相反之迭代物件
 next(iterator) 傳回迭代物件的下一個元素
 zip(obj1, obj2) 傳回兩個物件元素一對一配對 tuple 組成的串列

函數 len() 會傳回串列中元素的個數, 例如 : 

>>> len([1,2,3,4,5])
5
>>> len([123, 3.14, "abc", True])
4

函數 min(), max(), 與 sum() 是用在串列元素為數值的情況, 分別傳回元素的最小值, 最大值, 以及全部元素和 :

>>> min([1,2,3])
1
>>> max([1,2,3])
3
>>> sum([1,2,3])
6

不過 min() 與 max() 也可以用在元素全部為字串的情況, 這時是逐一比較字串中的 ASCII 碼 :

>>> max(['a','b','c'])     #'c' 的 ASCII 碼較大
'c'
>>> min(['a','b','c'])      #'a' 的 ASCII 碼較小
'a'
>>> max(['aa','aA','Aa','AA'])       #'aa' 的 ASCII 碼較小
'aa'
>>> min(['aa','aA','Aa','AA'])        #'AA' 的 ASCII 碼較小
'AA'

但是, 不論 min(), max(), 還是 sum() 都無法處理元素為數字與字串混雜的串列, 因為 Python 不知道要如何比較 :

>>> min([1,'b','c'])             #無法對數字與字串進行比較
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __lt__: 'str', 'int'
>>> sum([1,'b',3])              #無法對數字與字串進行加總
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __add__: 'int', 'str'

函數 sorted() 會將串列元素由小至大排序後傳回新串列, reversed() 則由大至小排序, 原串列不受影響 (不是 in-place 運算), 例如 :

>>> a=[5,1,0,3,2,4]
>>> sorted(a)  #由小至大排序
[0, 1, 2, 3, 4, 5]
>>> a            
[5, 1, 0, 3, 2, 4]               #原串列不受影響

sorted() 也可以對字串串列排序, 依據 ASCII 碼的由小至大先後排列, 因此數字會比字母優先 (因 0~9 之 ASCII 為 48~57), 大寫字母 (ASCII 為 65~90) 又比小寫字母 (ASCII 為 97~122) 優先, reversed() 則反過來, 不過 reversed() 函數傳回的不是串列, 而是一個迭代物件 (iterator), 可用 next() 函數取得下一個元素, 也可以用 list() 將其轉成串列, 例如 :

>>> a=['a','3','B','1','c','A','b','2','C']
>>> sorted(a)
['1', '2', '3', 'A', 'B', 'C', 'a', 'b', 'c']                 #數字先, 其次大寫, 最後是小寫
>>> a=['aA','aa','AA','Aa','BB','Bb','bB','bb']
>>> sorted(a)
['AA', 'Aa', 'BB', 'Bb', 'aA', 'aa', 'bB', 'bb']    #第一個字母先排序, 再排序第二個字母
>>> a
['aA', 'aa', 'AA', 'Aa', 'BB', 'Bb', 'bB', 'bb']    #原串列內容不便 (因不是 in-place)
>>> reversed(a)                             #reversed() 傳回的是 iterator 物件, 不是串列
>>> list(reversed(a))                     #用 list() 轉成串列
['bb', 'bB', 'Bb', 'BB', 'Aa', 'AA', 'aa', 'aA']    #順序跟原串列相反
>>> a
['aA', 'aa', 'AA', 'Aa', 'BB', 'Bb', 'bB', 'bb']    #原串列內容不便 (因不是 in-place)


next() 函數會從迭代物件 reversed 中取出下一個元素, 每取出一個裡面就少一個, 直到沒有可迭代之元素為止, 例如 :

>>> a=[1,2,3]
>>> r=reversed(a)
>>> r
<reversed>             #傳回迭代物件
>>> next(r)       #取出迭代元素
3
>>> next(r)
2
>>> next(r)
1
>>> next(r)      #迭代物件已空, 出現錯誤
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration:
>>> r
<reversed>             #迭代物件還在, 但內容已空
>>> list(r)         #用 list() 轉成串列, 是空串列
[]

但是要注意, 不要將含有字串與數字的串列傳給 sorted() 去排序, 因為 Python 不知道要採用甚麼準則去排序 :

>>> a=['a',3,'B',1,'c','A','b',2,'C']      #元素有數字也有字串
>>> sorted(a)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __lt__: 'str', 'int'

但是傳給 reversed() 卻不會產生錯誤, 可以正確地將元素順序反轉

>>> a=['a',3,'B',1,'c','A','b',2,'C']    #混和字串與數字元素的串列
>>> r=reversed(a)                          #reversed() 不會出現錯誤
>>> r
<reversed>
>>> list(r)                                       #將迭代物件轉成串列
['C', 2, 'b', 'A', 'c', 1, 'B', 3, 'a']                        #順序與原串列相反

最後來測試 zip(), 此函數接受兩個元素個數相等的串列, zip() 會將相對應元素兩兩配成一個 tuple, 然後組成一個串列傳回, 例如 :

>>> a=[1,2,3]
>>> b=['a','b','c']
>>> c=zip(a,b)      #把對應元素 (1,'a') 等送作堆
>>> c
<zip>                           #傳回 zip 物件 (也是迭代物件)
>>> type(c)
<class 'zip'>
>>> list(c)             #將 zip 物件轉成串列
[(1, 'a'), (2, 'b'), (3, 'c')]
>>> next(c)           #轉成串列會完成迭代, 迭代物件將變空
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration:
>>> c=zip(a,b)      #重新產生 zip 物件
>>> next(c)           #用 next() 迭代取出下一個迭代元素
(1, 'a')
>>> next(c)
(2, 'b')
>>> next(c)
(3, 'c')
>>> list(c)             #迭代 3 次就成空了
[]

3. 串列物件的方法 :

Python 的串列物件也提供了幾個方法來操作陣列 :

 方法 說明 
 append(obj) 將一個元素添加至串列尾端
 extend(seq) 將一個序列物件添加至串列尾端
 insert(index, obj) 將一個元素 obj 添加至串列中的指定索引 index 前面
 remove(obj) 從串列中刪除指定之元素
 pop(index) 取出並傳回指定索引之元素 (預設索引 -1)
 sort() 將串列元素排序 (由小到大)
 reverse() 將串列元素反向排序 (由大到小)
 count(obj) 傳回串列中出現 obj 元素的次數
 index(obj) 傳回元素 obj 在串列中的索引
 copy() 複製串列元素
 clear() 刪除串列中的全部元素

注意, 這些方法都是就地 (in-place) 運作的, 亦即這些方法是直接在串列上操作, 而非在複本中操作. 除了 pop() 外都沒沒有傳回值 (或者說傳回 None), 因此不要把傳回值設給原串列, 這樣會讓原串列變成 None.

例如 :

append() 的作用是將傳入的元素加入串列的尾端, 串列的元素可以是任何型態物件 :

>>> a=['a','b','c']
>>> a.append('d')      #在尾端加入 'd'
>>> a
['a', 'b', 'c', 'd']
>>> a.append([1,2])  #在尾端加入串列
>>> a
['a', 'b', 'c', 'd', [1, 2]]
>>> a.append((3,4))   #在尾端加入元組
>>> a
['a', 'b', 'c', [1, 2], (3, 4)]

extend() 用來串接兩個串列, a.extend(b) 是把 b 串列串接到 a 的尾端, 例如 :

>>> a=[1,2,3]
>>> b=['a','b','c']
>>> a.extend(b)     #b 串接到 a 後面
>>> a
[1, 2, 3, 'a', 'b', 'c']
>>> b
['a', 'b', 'c']
>>> b.extend(a)     #a 串接到 b 後面
>>> b
['a', 'b', 'c', 1, 2, 3, 'a', 'b', 'c']

串列串接也可以用 + 或 += 運算子, 例如 :

>>> a=[1,2,3]
>>> b=['a','b','c']
>>> a=a + b             #b 串接到 a 後面
>>> a
[1, 2, 3, 'a', 'b', 'c']
>>> b
['a', 'b', 'c']
>>> b += a               #a 串接到 b 後面
>>> b
['a', 'b', 'c', 1, 2, 3, 'a', 'b', 'c']

insert() 是在指定索引前面加入元素, 如果索引超出界線不會出現錯誤, 而是自動放在最後面. 索引也可以是負值, -1 是倒數第一個 :

>>> a=['a','b','c']
>>> a.insert(2,1)     #在索引 2 前面加入 1
>>> a                    
['a', 'b', 1, 'c']                           #加在原索引 2 元素 'c' 前面
>>> a.insert(6,'e')    #指定索引 6 超出界限, 會被放在最後面
>>> a
['a', 'b', 1, 'c', 'e']
>>> a.insert(-2,2)    #在倒數第 2 個 'c' 前插入 2
>>> a
['a', 'b', 1, 2, 'c', 'e']

remove() 則是從串列中刪除指定之元素, 若串列中沒有該元素會產生錯誤 :

>>> a=['a','b','c']
>>> a.remove('b')    #刪除元素 'b'
>>> a
['a', 'c']
>>> a.remove('d')     #刪除一個不存在的元素會產生錯誤
Traceback (most recent call last):
  File "<stdin>", line 1, in ValueError: object not in sequence


使用 remove() 方法跟上面使用 del 指令的差別是 : remove() 方法是刪除指定的元素, 必須知道元素內容是甚麼; 而 del 指令是刪除指定索引, 必須知道要刪除的元素索引是多少.

pop() 函數會從串列中取出指定索引之元素, 並從串列中刪除此元素後將其傳回, 這是串列方法中唯一有傳回值者. 如果沒有指定索引, 預設為 -1, 亦即取出最後一個元素傳回. 若索引超出界限將產生錯誤, 例如 :

>>> a=[0,1,2,3,4,5]
>>> a.pop()       #取出末端元素 5
5
>>> a
[0, 1, 2, 3, 4]
>>> a.pop(-1)    #取出末端元素 4
4
>>> a
[0, 1, 2, 3]
>>> a.pop(2)     #取出索引 2 元素 2
2
>>> a
[0, 1, 3]
>>> a.pop(5)     #取出引 5 元素 (超出界限)
Traceback (most recent call last):
  File "<stdin>", line 1, in IndexError: list index out of range

sort() 函數會將串列元素做升冪排序, 如果傳入參數 reverse=True 的話就會做降冪排序.


>>> a=[5,1,3,2,0,4]
>>> a.sort()
>>> a
[0, 1, 2, 3, 4, 5]
>>> a.sort(reverse=True)
>>> a
[5, 4, 3, 2, 1, 0]

如果串列的元素是字串, 則排序是依據 ASCII 碼做升冪排序, 因此數字會比字母優先 (因 0~9 之 ASCII 為 48~57), 大寫字母 (ASCII 為 65~90) 又比小寫字母 (ASCII 為 97~122) 優先, reversed() 則反過來, 

>>> a=['a','3','B','1','c','A','b','2','C']
>>> a.sort()
>>> a
['1', '2', '3', 'A', 'B', 'C', 'a', 'b', 'c']      #字串依據 ASCII 碼升冪排序

若傳入 reverse=True 則做降冪排序 :

>>> a=['a','3','B','1','c','A','b','2','C']
>>> a.sort(reverse=True)   #字串依據 ASCII 碼排序
>>> a
['c', 'b', 'a', 'C', 'B', 'A', '3', '2', '1']

與內建函數 sorted() 一樣, 不要對混和數字與字串的串列排序, 這樣會產生錯誤, 例如 :

>>> a=['a',3,'B',1,'c','A','b',2,'C']     #串列元素有數值也有字串
>>> a.sort()
Traceback (most recent call last):
  File ">stdin>", line 1, in >module>
TypeError: unsupported types for __lt__: 'str', 'int'

注意, 串列物件的 sort() 方法與內建函數 sorted() 最大的不同是, sort() 是就地 (in-place) 操作, 直接在串列物件上排序, 執行後串列元素的順序被改變, 沒有傳回值; 而 sorted() 函數是在一個副本上排序, 執行後傳回此副本的參考, 元串列不受影響.

count() 方法會傳回指定之元素在串列中出現的次數 (整數), 例如 :

>>> a=[1,2,3,3,3,2]
>>> a.count(1)
1
>>> a.count(2)
2
>>> a.count(3)
3
>>> a=['a','b','c',1,2,3,'a','b']
>>> a.count('a')
2
>>> a.count('b')
2
>>> a.count('c')
1
>>> a.count('d')    #無此元素
0

index() 方法會傳回傳入元素在串列中的索引, 例如 :

>>> a=[0,1,2,3,4,5]
>>> a.index(0)
0
>>> a.index(3)
3
>>> a.index(5)
5

copy() 方法會將串列複製一份副本, 並傳回此副本的參考. 這跟用切片的 [:] 效果是一樣的, 都是產生一個新串列, 例如 :

>>> a=[1,2,3]
>>> b=a.copy()   #傳回 a 串列副本的參考, 指定給串列 b
>>> b
[1, 2, 3]
>>> b == a          #用 == 比較兩者內容相同
True
>>> b is a            #用 is 比較兩者參考不同 (不同物件)
False
>>> c=a[:]           #用切片複製串列
>>> c == a          #用 == 比較 a,c 兩者內容相同
True
>>> c == b          #用 == 比較 b,c 兩者內容相同
True
>>> c is a            #用 is 比較 a,c 兩者參考不同 (不同物件)
False
>>> c is b            #用 is 比較 b,c 兩者參考不同 (不同物件)
False
>>> b[0]='b'        #更改 b 串列內容
>>> c[0]='c'        #更改 c 串列內容
>>> b             
['b', 2, 3]
>>> c
['c', 2, 3]
>>> a                   #串列 a,b,c 互不影響 
[1, 2, 3]

clear() 會清除串列內所有元素, 使其成為一個空字串, 例如 ;

>>> a=[1,2,3]
>>> a
[1, 2, 3]
>>> a.clear()
>>> a
[]                         #變成空串列

字串有 2 個方法與串列密切相關 : join() 與 split(), 兩者為反運算, join() 是將一個串列以一個膠合字串串接成一個新字串後傳回; 而 split() 則是以一個分拆字串為界, 將一個字串拆成數個子字串組成的串列後傳回, 例如 :

>>> a=['123','456','789']
>>> b='-'.join(a)        #以 '-' 串接
>>> b
'123-456-789'
>>> c=b.split('-',1)    #以 '-' 自左向右分拆 1 次
>>> c
['123', '456-789']
>>> c=b.split('-')       #以 '-' 自左向右全部分拆
>>> c
['123', '456', '789']
>>> c=b.rsplit('-')      #以 '-' 自右向左全部分拆
>>> c
['123', '456', '789']
>>> c=b.rsplit('-',1)   #以 '-' 自右向左分拆 1 次
>>> c
['123-456', '789']

三. 元組 (tuple) :

元組是串列的不可變版 (可視為常數串列), 其物件建立後內容即不可變更, 亦即, 在上述串列中會改變物件內容的指定運算不能使用, 而且物件方法也只有 index() 與 count() 兩個而已. 既然已經有串列這麼好用的型別了, 為何 Python 又要定義 tuple 型別呢? 理由如下 :
  1. tuple 比 list 占用的記憶體較少
  2. 在元素必須固定不可以被變更場合 tuple 最保險
  3. 函數的引數是以 tuple 形式傳入的
  4. tuple 很容易轉成字典 dict
  5. 具名元組 (named tupe) 可作為簡化之物件用
1. 建立元組 :

通常元組使用 () 運算子來標示, 元素之間用半形逗號隔開, 即使只有一個元素, 也必須在結尾加上逗號. 事實上, () 運算子是非必要的, Python 是用逗號來辨別這是一個 tuple, 例如 :

>>> name=('peter',)        #元組只有一個元素時結尾務必有逗號
>>> type(name)
<class 'tuple'>
>>> name='peter',          #即使不用 (), 只要有逗號就會被認為是 tuple
>>> type(name)
<class 'tuple'>
>>> name=('peter')         #沒有結尾逗號會被認為是字串
>>> name
'peter'
>>> type(name)
<class 'str'>

可見真正定義一個元組的是逗號而非小括弧 (), 用小括弧把元組的元素包起來只是讓我們更容易識別這是一個 tuple 而已 (當然若使用中括號就被視為串列, 大括弧表示集合或字典). 如果元組有多個元素, 則結尾的逗號可有可無 (因為已經至少有一個逗號足以識別為 tuple 了), 例如 :

>>> name='peter','amy','kelly'       #多於一個元素時結尾逗號可省略
>>> type(name)
<class 'tuple'>
>>> name
('peter', 'amy', 'kelly')
>>> name='peter','amy','kelly',       #結尾有逗號當然沒問題
>>> name
('peter', 'amy', 'kelly')
>>> type(name)
<class 'tuple'>
>>> name=('peter','amy','kelly')      #用小括弧宣揚其 tuple 身分
>>> name
('peter', 'amy', 'kelly')
>>> type(name)
<class 'tuple'>
>>> name=('peter','amy','kelly',)     #結尾有逗號當然沒問題
>>> name
('peter', 'amy', 'kelly')
>>> type(name)
<class 'tuple'>
>>>

元組乃是常數串列, 所以無法使用 append() 方法從空的元組建立 tuple.

第二種建立 tuple 的方法是使用內建函數 tuple(), 它會將其他型別物件如字串, 串列, 字典, 以及集合轉成元組, 例如 :

>>> tuple('abc')           #字串轉元組:每個字元會被打散成字元 tuple
('a', 'b', 'c')
>>> tuple(['a','b','c'])    #串列轉元組
('a', 'b', 'c')
>>> tuple({'a':1,'b':2,'c':3})   #字典轉元組
('b', 'c', 'a')
>>> tuple({'a','b','c'})    #集合轉元組
('b', 'c', 'a')


2. 元組的基本操作 :

由於元組是常數串列, 所以元組的可操作方式比串列少, 總之要更改 tuple 內容的操作是不允許的 :
  1. 串接 (使用 + 運算子)
  2. 複製 (使用 * 運算子)
  3. 切片 (使用 [] 運算子)
  4. 查詢元素 (使用 in 與 not in)
  5. 多重指定
與串列相同, tuple 可以使用 + 運算子串接與 * 運算子複製, 結果會傳回一個新 tuple, 例如 :

>>> a=(1,2,3,)
>>> b=(4,5,6,)
>>> c=a+b
>>> c
(1, 2, 3, 4, 5, 6)
>>> a
(1, 2, 3)
>d=a*3
>>> d
(1, 2, 3, 1, 2, 3, 1, 2, 3)

元組與串列一樣是繼承 sequence 之序列型別, 因此可以使用整數索引去擷取個元素, 例如 :

>>> a=0,1,2,3,4,5
>>> a[0]
0
>>> a[3]
3
>>> a[5]
5

但是元組屬於不可變型別 (immutable), 所以物件建立後無法再用指定運算子更改其內容, 例如 :

>>> a=(0,1,2,3,4,5)
>>> a[0]='a'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

元組也可以利用索引擷取 tuple 中的切片 (slice), 例如 :

>>> a=0,1,2,3,4,5,
>>> a
(0, 1, 2, 3, 4, 5)
>>> a[:]         #擷取全部元素
(0, 1, 2, 3, 4, 5)
>>> a[3:]       #擷取索引 3 至結尾的元素
(3, 4, 5)
>>> a[:3]       #擷取開頭至索引 2 (不包括結束索引 3)
(0, 1, 2)
>>> a[2:4]     #擷取索引 2 至 3 (不包括結束索引 4)
(2, 3)
>>> a[0:5:1]  #擷取索引 0 至索引 5 步階 1 (不包括結束索引 5)
(0, 1, 2, 3, 4)
>>> a[0:5:2]  #MicroPython 不支援步階大於 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NotImplementedError: only slices with step=1 (aka None) are supported

注意, 上面串列的測試顯示, 串列的切片步階可大於 1, 但在元組卻沒有, MicroPython 的元組在切片的步階 (step) 參數上只支援 step=1, 不支援大於 1 的步階, 這在 CPython 執行結果為 :

C:\Users\cht>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.
>>> a=0,1,2,3,4,5
>>> a[0:5:2]
(0, 2, 4)

要查詢指定元素有無在 tuple 中可用 in 與 not in 指令, 例如 :

>>> a=('a','b','c')
>>> 'a' in a
True
>>> 'a' not in a
False
>>> 'abc' not in a
True

與 list 一樣, tuple 也可以做多重指定 (又稱為開箱, unpacking), 例如 :

>>>x,y,z='a','b','c',        
>>> x
'a'
>>> y
'b'
>>> z
'c'
>>> w,x,y,z=('a','b','c')     #兩邊不一致
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: need more than 3 values to unpack
>>> y,z=('a','b','c')           #兩邊不一致
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: too many values to unpack (expected 2)

3. 用內建函數操作元組 :

雖然元組是串列的不可變版, 但下列內建函數並非就地 (in-place) 操作, 不會去更改元組內容, 因此也可以用在元組 :

函數 說明
 len(obj) 傳回物件元素個數
 min(obj) 傳回物件中數值最小的元素
 max(obj) 傳回物件中數值最大的元素
 sum(obj) 傳回物件所有元素之和 
 sorted(obj)  傳回由小到大排列之物件
 reversed(obj) 傳回與原物件元素順序相反之迭代物件
 next(iterator) 傳回迭代物件的下一個元素
 zip(obj1, obj2) 傳回兩個物件元素一對一配對的 zip 物件

>>> len((1,2,3,4,5))
5
>>> len((123, 3.14, "abc", True))
4
>>>

函數 min(), max(), 與 sum() 會傳回元素為數值的元組中的最小值, 最大值, 以及加總值 :

>>> min((1,2,3))
1
>>> max((1,2,3))
3
>>> sum((1,2,3))
6

這三個函數也可以用在全部元素是字串情況 (依 ASCII 碼逐一比對), 但不能用在元素包含數值與字串混合情況 :

>>> max(('a','b','c'))
'c'
>>> min(('a','b','c'))
'a'
>>> max(('aa','aA','Aa','AA'))
'aa'
>>> min(('aa','aA','Aa','AA'))
'AA'
>>> min((1,'b','c'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __lt__: 'str', 'int'
>>> sum((1,'b',3))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported types for __add__: 'int', 'str'

函數 sorted() 會將元素升冪排序後以 list 傳回, 而 reversed() 則是將元素順序反向排序後傳回一個 reversed 物件 (迭代物件), 可用 next() 取出其迭代元素, 也可以用 list() 將其轉成串列 :

>>> a=(5,1,0,3,2,4)
>>> sorted(a)         #升冪排序, 傳回串列
[0, 1, 2, 3, 4, 5]
>>> a
(5, 1, 0, 3, 2, 4)
>>> reversed(a)     #反序排序, 傳回 reversed 物件
<reversed>
>>> list(reversed(a))    #將 reversed 物件轉成串列
[4, 2, 3, 0, 1, 5]
>>> b=reversed(a)       #反序排序, 傳回 reversed 物件
>>> next(b)                  #取出 reversed 物件元素
4
>>> next(b)
2
>>> next(b)
3
>>> next(b)
0
>>> next(b)
1
>>> next(b)
5
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration:

而 zip() 函數可將傳入的兩個 tuple 物件對應元素配對後傳回 zip 物件 (迭代物件), 可以用 next() 函數檢視此 zip 物件之元素, 也可以用 tuple() 函數轉成元組, 或者用 list() 函數轉成串列 :

>>> a=1,2,3,
>>> b=4,5,6,
>>> zip(a,b)            #傳回 zip 物件
<zip>
>>> list(zip(a,b))     #轉成 list
[(1, 4), (2, 5), (3, 6)]
>>> tuple(zip(a,b))  #轉成 tuple
((1, 4), (2, 5), (3, 6))
>>> c=zip(a,b)      
>>> type(c)
<class 'zip'>
>>> next(c)               #取出下一個迭代元素
(1, 4)
>>> next(c)               #取出下一個迭代元素
(2, 5)
>>> next(c)               #取出下一個迭代元素
(3, 6)

4. 元組物件的方法 :

因為元組是不可變的串列, 所以串列物件的方法中只有 count() 的 index() 在元組中可用 :

 方法 說明 
 count(obj) 傳回串列中出現 obj 元素的次數
 index(obj) 傳回元素 obj 在串列中的索引

例如 :

>>> a=(1,2,3,3,3,2)
>>> a.count(1)
1
>>> a.count(2)
2
>>> a.count(3)
3
>>> a=('a','b','c',1,2,3,'a','b')
>>> a.count('a')
2
>>> a.count('b')
2
>>> a.count('c')
1
>>> a.count('d')
0
>>> a=[0,1,2,3,4,5]
>>> a.index(0)
0
>>> a.index(3)
3
>>> a.index(5)
5

以上便是 MicroPython 序列型別的完整測試, 還真長.