ספריות: התקנה וכתיבה
מהפרק הראשון אנחנו כותבים import. בפרק הזה נפתח את המילה הזאת: איפה הבקר מחפש אותה? איך מתקינים ספרייה שמישהו אחר כתב, ואיך יודעים שאפשר לסמוך עליה? אחר כך נעבור לצד השני ונכתוב ספרייה בעצמנו, כך שאפשר יהיה לבדוק אותה בלי שום רכיב. לבסוף נפעיל את אותה בדיקה על קוד שכתבה לנו בינה מלאכותית.
- נסביר את תפקידם של
sys.pathושל התיקייהlib, ונפרש את ההודעהImportError. - נתקין ספרייה בעזרת
mip,mpremoteו-Thonny, ונבדוק אותה לפני השימוש: הרישיון, המקור, הגרסה והקוד. - נפריד בין הגדרת הרכיב ללוגיקה של התוכנית, ונעביר את האפיק דרך הבנאי.
- נקמפל ספרייה לקובץ
.mpyונדע מתי כדאי לעשות זאת. - נכתוב בדיקת יחידה עם אובייקט מדומה, ונבנה את המחלקה
Buttonעם טיפול בקפיצות מגע. - נבדוק קוד שכתבה בינה מלאכותית, בלי לקבל שום מידע שאין לו מקור.
11.1 איפה import מחפש (The Search Path)
import במקום להעתיק אותם לכל תוכנית. כשיש כמה קבצים עם תיאור התקנה, קוראים לזה גם חבילה.כשאנחנו כותבים import convert, מיקרו-פייתון מחפש את convert.py או את convert.mpy ברשימה של תיקיות, לפי הסדר, ולוקח את הראשון שהוא מוצא. הרשימה הזאת היא sys.path. הפלט תלוי בסביבה, ולכן כאן רק מציגים את הפקודה, בלי להריץ אותה בבניית הספר:
import sys
print(sys.path)
sys.path.append('/my_libs')לפי התיעוד, בדרך כלל יש ברשימה את הפריט הריק '', כלומר התיקייה הנוכחית, וגם .frozen למודולים שבנויים בתוך מערכת הלוח, ותיקייה שנגמרת ב-/lib. לכן שמים את הספריות בתיקייה lib ואת התוכנית בשורש. כך השורש נשאר נקי, והספרייה נשארת נפרדת מהתוכנית.
import convert print(convert.__name__) print(convert.to_percent(32768))
convert 50
אם הקובץ אינו נמצא בכל הרשימה, זו ההודעה:
import convert_v2 print(convert_v2.to_percent(32768))
ImportError: no module named 'convert_v2'
max7219.py בשורש קודם לקובץ שב-lib, ולכן התוכנית מייבאת את עצמה או גרסה ישנה, ומופיעות הודעות מוזרות.11.2 התקנת ספריות (Installing Libraries)
פקודות ההתקנה בסעיף הזה לא הורצו בבניית הספר, כי הן דורשות רשת אמיתית ולוח אמיתי. הספריות שהספר מדפיס את הפלט שלהן מגיעות לסימולציה ישירות מחבילת הקבצים. יש שלוש דרכים:
1. מעתיקים את הקובץ בעצמנו:
ספרייה שכתבנו או הורדנו מעתיקים ללוח. הכלי mpremote עובד מהמחשב דרך USB:
mpremote mkdir :lib mpremote cp max7219.py :lib/ mpremote ls :lib
גם ב-Thonny עושים את אותו דבר מחלון הקבצים: בוחרים את הקובץ ומעלים אותו לתיקייה lib שבלוח.
2. הכלי mip בלוח:
לוח שמחובר לרשת מוריד בעצמו. האינדקס ברירת המחדל הוא micropython-lib, ו-mip בוחר את התיקייה הראשונה ב-sys.path שנגמרת ב-/lib. אפשר לשנות זאת עם target, בתנאי שהתיקייה נמצאת ב-sys.path.
import time
import network
import mip
from secrets import WIFI_SSID, WIFI_PASS
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.disconnect()
wlan.connect(WIFI_SSID, WIFI_PASS)
while not wlan.isconnected():
time.sleep(0.5)
mip.install('umqtt.simple')
mip.install('umqtt.simple', mpy=False)
mip.install('github:org/repo/path/foo.py', version='v1.2')| פקודה | מה היא עושה |
|---|---|
| mip.install('name') | מהאינדקס, עם קובצי .mpy מוכנים. |
| mip.install('name', mpy=False) | עם קובצי .py: אפשר לקרוא אותם. |
| mip.install('name', version='x.y') | גרסה מסוימת. |
| mip.install('name', target='/x') | תיקייה אחרת, בתנאי שהיא נמצאת ב-sys.path. |
| mip.install('github:org/repo/foo.py', version='tag') | קובץ ממאגר, בענף או בתג מסוים. |
3. מהמחשב:
mpremote mip install umqtt.simple mpremote mip install --target=/my_libs umqtt.simple mpremote mip install github:org/repo@v1.2
ב-Thonny יש מנהל חבילות בתפריט Tools: מחפשים בו את השם ומתקינים. השמות ברשימות עשויים להשתנות מגרסה לגרסה. אחרי ההתקנה תמיד מוודאים: מסתכלים בתיקייה lib ומנסים import ישירות.
pip בסביבה וירטואלית, כפי שראינו בפרק 6.11.3 בודקים לפני שמשתמשים (Vetting a Library)
ספרייה היא קוד שירוץ בתוך המכשיר שלנו, ואדם זר כתב אותו. לכן מתרגלים לבדוק אותה לפני שבונים עליה, ושואלים ארבע שאלות:
| השאלה | איפה מוצאים את התשובה | סימן אזהרה |
|---|---|---|
| הרישיון: האם מותר לי? | הקובץ LICENSE וראש כל קובץ | אין רישיון בכלל |
| המקור: מי כתב אותה? | המאגר הרשמי ובעליו | קובץ שהועתק מאתר בלי שם |
| הגרסה: האם היא חיה? | תאריך השינויים, התגים והתקלות הפתוחות | לא נוסתה על הלוח שלנו |
| הקוד: מה הוא עושה? | קוראים אותו: התקנה עם mpy=False נותנת את המקור | רשת או קבצים שאין להם קשר לעבודתה |
הרישיון אינו פרט שולי. הוא אומר מה מותר לנו:
| הרישיון | מה הוא אומר בקצרה |
|---|---|
| MIT, BSD | שימוש, שינוי ומכירה, בתנאי שמשאירים את נוסח הרישיון ואת שם הכותב. |
| Apache 2.0 | כמוהם, עם תנאים נוספים על שינויים ועל פטנטים. |
| GPL | אם מפיצים תוכנה שנבנתה עליו, צריך להפיץ אותה באותו רישיון יחד עם הקוד שלה. |
| CC BY-NC-SA | אין שימוש מסחרי, והיצירה הנגזרת תהיה באותו רישיון. |
| אין רישיון | אין רשות: הזכויות שמורות לבעליהן גם אם הקובץ חשוף באינטרנט. |
קוראים את הקוד עם שאלות מוגדרות: מה הוא מייבא? האם הוא קובע בתוכו מספרי הדקים? האם הוא משתמש במודולים שמיוחדים ללוח אחר? האם הוא חוסם את התוכנית זמן רב ב-sleep? האם הוא מתחבר לכתובת ברשת? האם הוא מדפיס סיסמה או מבקש אותה?
11.4 כותבים ספרייה: הפרדה בין שכבות (Writing a Library: Separation)
נסתכל קודם במה שאיננו רוצים. זו מחלקה שעובדת ומדליקה מספר שורות של המטריצה לפי אחוז, אבל היא מערבבת הכול יחד. היא לא מורצת בבניית הספר, כי זו דוגמה רעה:
from machine import Pin, SPI
class Matrix:
def __init__(self):
self.spi = SPI(2, 1000000, sck=Pin(18), mosi=Pin(23))
self.cs = Pin(5, Pin.OUT, value=1)
def bar(self, percent):
lit = (percent * 8 + 50) // 100
print('lit rows:', lit)
for i in range(8):
value = 0xFF if 7 - i < lit else 0
self.cs.value(0)
self.spi.write(bytes([i + 1, value]))
self.cs.value(1)- מספרי ההדקים קבועים בתוכה: היא לא תעבוד ב-Pico W.
- היא יוצרת את האפיק בעצמה: אי אפשר לשים רכיב שני על האפיק, ואי אפשר לנסות אותה בלי לוח.
- היא מערבבת חישוב עם בייטים: כדי לבדוק נוסחה פשוטה נצטרך חומרה שלמה.
- היא מדפיסה מבפנים: התוכנית היא שצריכה להחליט מה מוצג.
הפתרון הוא שלוש שכבות, ולכל שכבה סיבה אחת בלבד להשתנות:
machine, ולכן בודקים אותה בכל מקום.MAX7219(spi, cs). כך עבד ADS1115(i2c) בפרק 6, וכך עובד כל דרייבר בספר.import# ledbar.py - turns a percentage into rows for an 8x8 matrix. No hardware.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
def bar_rows(percent):
if not 0 <= percent <= 100:
raise ValueError('percent is 0 to 100')
lit = (percent * 8 + 50) // 100
rows = bytearray(8)
for i in range(lit):
rows[7 - i] = 0xFF
return rowsהתוכנית היא המקום היחיד שמכיר את הלוח. הפוטנציומטר מחובר כמו בפרק 6, והמטריצה כמו בפרק 10:
import time
import board
from machine import Pin
from convert import to_percent
from ledbar import bar_rows
from max7219 import MAX7219
pot = board.analog_in(board.POT)
spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)
matrix = MAX7219(spi, cs)
while True:
percent = to_percent(pot.read_u16())
rows = bar_rows(percent)
matrix.show(rows)
print(percent, 'percent:', sum(1 for r in rows if r), 'rows lit')
time.sleep(1)0 percent: 0 rows lit 25 percent: 2 rows lit 50 percent: 4 rows lit 100 percent: 8 rows lit ...
כללים למי שכותב ספרייה. את רובם רואים ב-ads1115.py וב-max7219.py:
- שורה בראש הקובץ שאומרת מה הקובץ הזה, למה הוא נועד, מי כתב אותו ומה הרישיון שלו.
- הייבוא לא עושה דבר בחומרה: כל העבודה נעשית בבנאי ובפונקציות.
- אין הדפסה בתוך הספרייה. בשגיאה מעלים חריג:
raise ValueError('...'). - מעט שמות וברורים לשימוש, וקו תחתון למה שפנימי:
_send. - לא משתמשים במה שמיוחד ללוח מסוים אם אפשר להימנע.
from machine import Pin ב-max7219.py?11.5 קובצי .mpy (Precompiled Files)
בייבוא של .py הבקר מתרגם את הטקסט עצמו לקוד ביניים, וצריך לשם כך זיכרון. הכלי mpy-cross עושה זאת מראש במחשב, ונותן קובץ .mpy שמייבאים באותו שם:
pip install mpy-cross mpy-cross max7219.py mpremote cp max7219.mpy :lib/
| מתי משתמשים | מתי לא |
|---|---|
ספרייה גדולה שהייבוא שלה נכשל ב-MemoryError | כשאנחנו עדיין משנים את הספרייה כל דקה |
| ספרייה יציבה שמפיצים ללוחות רבים, בגרסה ידועה | כשרוצים לקרוא את הקוד בלוח |
.mpy חייבת להתאים למה שמיקרו-פייתון בלוח תומך בו. אחרת רואים ValueError: incompatible .mpy file. את המספר בלוח מוצאים בשדה שלהלן, ומשווים למה ש-mpy-cross --version אומר. קובץ .mpy אינו דרך להסתיר סוד: הסיסמה נשארת בתוכו קריאה..mpy. לא מורץ בבניית הספר.import sys print(sys.implementation._mpy & 0xFF)
11.6 בדיקת יחידה עם אובייקט מדומה (Unit Tests with a Fake)
את שכבת הלוגיקה בודקים ישירות בשורות assert. זו הבדיקה כולה:
from ledbar import bar_rows
assert bar_rows(0) == bytes(8)
assert bar_rows(100) == b'\xff' * 8
assert bar_rows(50) == bytes(4) + b'\xff' * 4
print('ledbar: all tests passed')ledbar: all tests passed
כדי לבדוק דרייבר של רכיב צריך אפיק מדומה. הודות להעברת האפיק דרך הבנאי, מספיקה מחלקה של כמה שורות. את המדומים אוספים בקובץ אחד, ובבדיקה לא מייבאים board ולא machine:
# fakes.py - stand-ins for the hardware, for unit tests. Runs anywhere.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
class FakeSPI:
def __init__(self):
self.sent = []
def write(self, data):
self.sent.append(bytes(data))
class FakePin:
def __init__(self, level=1):
self.level = level
self.lows = 0
def value(self, x=None):
if x is None:
return self.level
self.level = x
if x == 0:
self.lows += 1
class FakeClock:
def __init__(self):
self.t = 0
def __call__(self):
return self.tfrom max7219 import MAX7219
from fakes import FakeSPI, FakePin
spi = FakeSPI()
cs = FakePin()
m = MAX7219(spi, cs)
assert spi.sent[0] == bytes([0x0F, 0]), 'test mode must go off first'
assert spi.sent[-1] == bytes([0x0C, 1]), 'turn on must be last'
assert cs.lows == len(spi.sent), 'one CS window for each packet'
assert cs.level == 1, 'CS must stay high'
spi.sent = []
m.show((1, 2, 3, 4, 5, 6, 7, 8))
assert len(spi.sent) == 8
assert spi.sent[7] == bytes([0x08, 8])
try:
m.intensity(16)
assert False, 'no error for intensity 16'
except ValueError:
pass
print('max7219: all tests passed')max7219: all tests passed
בדיקה שמצליחה תמיד אינה שווה דבר. לכן נוודא שהיא באמת מגלה את הטעות: נשנה את _send במחלקה נגזרת כך שתחליף בין שני הבייטים, וזו טעות שאינה גורמת לשום הודעה בלוח:
from max7219 import MAX7219
from fakes import FakeSPI, FakePin
class Swapped(MAX7219):
def _send(self, reg, value):
self.spi.write(bytes([value, reg]))
spi = FakeSPI()
m = Swapped(spi, FakePin())
assert spi.sent[0] == bytes([0x0F, 0]), 'test mode must go off first'
print('never printed')AssertionError: test mode must go off first
assert מספיקה לספרייה קטנה. ב-micropython-lib יש חבילה unittest למי שרוצה יותר, ולא השתמשנו בה בספר.11.7 פרויקט: המחלקה Button (Project: A Button Class)
מגע מתכתי אינו נסגר בבת אחת, אלא קופץ כמה פעמים בזמן קצר. אלה קפיצות המגע. אילו ספרנו קצוות, לחיצה אחת הייתה נספרת כמה פעמים. הפתרון בלי לחסום את התוכנית: לא מאמינים לשינוי המצב אלא אחרי שהרמה נשארת יציבה זמן מספיק.
ms.נכתוב את המחלקה לפי מה שלמדנו: ההדק מגיע מבחוץ, וגם השעון, כדי שבבדיקה נוכל להזיז את הזמן בידיים:
# button.py - a push button with debouncing, without blocking.
# pin: an input Pin, or any object with value(). clock: returns ms.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
import time
class Button:
def __init__(self, pin, ms=30, active=0, clock=time.ticks_ms):
self.pin = pin
self.ms = ms
self.active = active
self.clock = clock
self.state = pin.value()
self.last = self.state
self.since = clock()
def pressed(self):
now = self.clock()
level = self.pin.value()
if level != self.last:
self.last = level
self.since = now
return False
if level == self.state:
return False
if time.ticks_diff(now, self.since) < self.ms:
return False
self.state = level
return level == self.activeברירת המחדל, 30 אלפיות שנייה, היא בחירה שלנו ולא תקן: לכל מתג יש התנהגות משלו, ואם מופיעות לחיצות עודפות מגדילים אותה. את הבדיקה כותבים לפני שמתקרבים ללוח:
from button import Button
from fakes import FakePin, FakeClock
def count_presses(levels):
pin = FakePin()
clock = FakeClock()
btn = Button(pin, ms=30, clock=clock)
presses = 0
for level in levels:
pin.level = level
clock.t += 5
if btn.pressed():
presses += 1
return presses
STEADY = [0] * 12
BOUNCY = [0, 1, 0, 1, 0, 1] + [0] * 10
SPIKE = [0, 0, 1, 1, 1, 1]
RELEASE = [0] * 12 + [1, 0, 1, 0] + [1] * 12
assert count_presses(STEADY) == 1, 'a clean press'
assert count_presses(BOUNCY) == 1, 'bounces are one press'
assert count_presses(SPIKE) == 0, 'too short to be a press'
assert count_presses(RELEASE) == 1, 'release is not a press'
print('button: all tests passed')button: all tests passed
עכשיו בלוח, באותו מעגל מפרק 3: מתג לאדמה, ונורית ב-LED_EXT. מתכנתים בסימולציה לחיצה קופצת ואחריה לחיצה נקייה, וסופרים את הקצוות לצד הלחיצות להשוואה:
import time
import board
from machine import Pin
from button import Button
pin = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
led = Pin(board.LED_EXT, Pin.OUT)
btn = Button(pin)
presses = 0
edges = 0
last = pin.value()
while True:
level = pin.value()
if last == 1 and level == 0:
edges += 1
last = level
if btn.pressed():
presses += 1
led.value(presses % 2)
print('presses', presses, ' falling edges', edges)
time.sleep(0.005)presses 1 falling edges 3 presses 2 falling edges 5 ...
time.sleep(0.03) אחרי כל קריאה של ההדק, שהוא הפשוט ביותר?11.8 קוד שכתב כלי AI (Reviewing AI-Written Code)
כלי בינה מלאכותית כותבים במהירות קוד שנראה נכון. אבל הם עלולים לערבב בין לוחות, או להמציא פקודה או מספר אוגר שנראים סבירים. לכן מתייחסים למה שהם כותבים כמו לספרייה שכתב אדם זר, ובודקים אותו באותה בדיקה. הטעויות הקלות לגילוי הן אלה שעוצרות את התוכנית. התוכנית הזאת מתאימה ל-Pico W ונכשלת ב-ESP32:
from machine import Timer
tick = Timer(-1)
tick.init(period=1000, callback=lambda t: print('tick'))ValueError: invalid Timer number
המסוכן יותר הוא מה שאינו גורם לשום הודעה. זו תוכנית שנכתבה בביטחון, עם שם סביר לאוגר שאינו קיים בטבלה של MAX7219:
import board
from machine import Pin
BRIGHTNESS_REG = 0x0D
spi = board.spi(1000000)
cs = Pin(board.CS, Pin.OUT, value=1)
cs.value(0)
spi.write(bytes([BRIGHTNESS_REG, 8]))
cs.value(1)
print('brightness set to 8')brightness set to 8 [spi] 0d 08
לכן נוהגים בסדר קבוע:
| מה בודקים | איך |
|---|---|
| האם היא מתקמפלת ועובדת? | מריצים אותה. זה המינימום, ואינו הוכחה לנכונות. |
| האם הפקודות קיימות? | כל פונקציה לא מוכרת מחפשים ב-docs.micropython.org, ללוח הנכון. |
| האם המספרים נכונים? | כל כתובת, אוגר וקביעת זמן משווים לגיליון הנתונים של הרכיב. |
| האם ההדקים נכונים? | משווים לקובץ הלוח: הדק קלט בלבד, הדק שמור, או התנגשות כמו בפרק 10. |
| האם המתחים בטוחים? | המתחים מגיליון הנתונים, לפני שמחברים חוט כלשהו. |
| איפה הסודות? | אין סיסמה בקוד ואין בשיחה עם הכלי. |
| מאין הגיעה? | ספרייה שהכלי מציע בודקים כמו ב-11.3, ומוודאים שהיא קיימת בכלל. |
| האם אנחנו מבינים אותה? | מסוגלים להסביר כל שורה, וכותבים לה בדיקה בעצמנו. |
עיקרי הפרק:
importמחפש ב-sys.pathלפי הסדר, ומקום הספריות הואlib.- מתקינים בעזרת
mip,mpremoteאו Thonny, או בהעתקה, וקובעים גרסה כשצריך. - לפני השימוש: הרישיון, המקור, הגרסה וקריאת הקוד. אין רישיון פירושו אין רשות.
- הדרייבר מכיר את הרכיב, הלוגיקה מחשבת, והתוכנית לבדה מכירה את הלוח.
- האפיק מגיע דרך הבנאי, ולכן בודקים עם אובייקט מדומה בלי חומרה.
.mpyחוסך זיכרון בייבוא, וחייב להתאים לגרסת הלוח.- הבדיקה בוחנת את ההנחות שלנו, וגיליון הנתונים בוחן את ההנחות עצמן.
פקודות הפרק:
| פקודה | מה היא עושה |
|---|---|
| sys.path | רשימת התיקיות ש-import מחפש בהן. |
| mip.install('name') | התקנה מהלוח. |
| mpremote mip install name | התקנה מהמחשב. |
| mpremote cp f.py :lib/ | העתקת קובץ ללוח. |
| mpy-cross f.py | קומפילציה ל-.mpy. |
| assert cond, 'text' | עוצר את התוכנית אם התנאי שגוי. |
| raise ValueError('text') | דיווח לתוכנית על ערך שגוי. |
| Button(pin, ms=30) | מתג עם טיפול בקפיצות מגע; pressed(). |
מילון מונחים:
| עברית | English | ערבית |
|---|---|---|
| ספרייה | Library | مكتبة |
| חבילה | Package | حزمة |
| רישיון | License | رخصة |
| גרסה | Version | إصدار |
| דרייבר | Driver | طبقة التعريف |
| לוגיקה | Logic Layer | طبقة المنطق |
| הזרקה דרך הבנאי | Constructor Injection | تمرير عبر الباني |
| בנאי | Constructor | الباني |
| קוד ביניים | Bytecode | شيفرة وسيطة |
| בדיקת יחידה | Unit Test | اختبار الوحدة |
| אובייקט מדומה | Fake Object | كائن بديل |
| קפיצות מגע | Contact Bounce | ارتداد التلامس |
value. כך המחלקה עובדת גם עם אובייקט מדומה בבדיקה.pressed() חוזרת מיד בכל פעם.שאלות לתרגול:
lib, כי היא נמצאת ב-sys.path, וכי השורש נשאר לתוכנית.mip.install('x') לבין mip.install('x', mpy=False)?.mpy, והשני את קובצי המקור .py שאפשר לקרוא.ADS1115 עם אפיק I2C מדומה: היא מוודאת את הערך השלילי ואת בחירת ערוץ 2.from ads1115 import ADS1115
class FakeI2C:
def __init__(self, value):
self.value = value
self.reg = 0
self.config = 0
def writeto(self, addr, data):
self.reg = data[0]
if len(data) == 3:
self.config = (data[1] << 8) | data[2]
def readfrom(self, addr, n):
v = 0x8000 if self.reg == 1 else self.value
return bytes([v >> 8, v & 0xFF])
bus = FakeI2C(0xFFFF)
adc = ADS1115(bus)
assert adc.read_raw(2) == -1, 'negative values'
assert (bus.config >> 12) & 7 == 6, 'channel 2'
print('ads1115: all tests passed')Button מקרה של שתי לחיצות נקיות ברצף. מה מצפים?[0] * 12 + [1] * 12 + [0] * 12. מצפים ל-2.(א) המרת הקריאה הגולמית לטמפרטורה.
(ב) שליחת פקודת ההפעלה לרכיב.
(ג) קביעה אם המכשיר צריך קירור.
Button זיהוי לחיצה ארוכה, כך שבודקים אותו עם FakeClock?ticks_diff למשך הנדרש ומחזירה True פעם אחת. בבדיקה מגדילים את clock.t בידיים ולא מחכים.spi.write_read(tx). איך בודקים?machine.SPI: הפקודות המתועדות הן write, read, readinto ו-write_readinto, והפקודה הזאת אינה ביניהן.