הבלוג של ינון פרק

טיפים קצרים וחדשות למתכנתים

הפקודה id ב Python

13/03/2020

הפקודה id ב Python מחזירה מספר מזהה ייחודי של הדבר שנתתם לה. פקודה זו יכולה לעזור לנו להבין שינויים במהלך חיי האוביקט ולשים לב להבדל חשוב בין פעולות של שינוי מידע לשינוי משתנה. בואו נראה אותה בפעולה דרך מספר דוגמאות.

את הפקודה id אתם יכולים להפעיל על כל אוביקט פייתון:

>>> id('hello')
4298282256

>>> id([10, 20, 30])
4298103336

>>> id(4)
140548070459824

אם נפעיל את הפקודה מספר פעמים על אותו מידע נקבל תמיד את אותו ערך:

>>> id(5)
140548070459800
>>> id(5)
140548070459800
>>> id(5)
140548070459800
>>>

וזה עובד גם במבני נתונים מורכבים יותר:

>>> l = [10, 20, 30]
>>> id(l)
4298103336
>>> id(l)
4298103336
>>> id(l)
4298103336

אבל שימו לב מה קורה כשאני מנסה להפעיל את הפקודה על מידע חדש, אפילו שמבחינת הערך שלו הוא זהה לגמרי למידע הקודם:

>>> l = [10, 20, 30]
>>> t = [10, 20, 30]
>>> id(l)
4298159096
>>> id(t)
4298103336
>>>

למרות ששתי הרשימות זהות מבחינת הערכים שלהן, המזהה של הרשימה עצמה שונה מה שאומר לנו שמדובר בשתי רשימות שמתנהלות באופן עצמאי. שינוי ערכים ברשימה אחת לא ישפיע על הרשימה השניה.

ב Python יש לנו שני סוגי פעולות שאפשר להפעיל על משתנים וש id תעזור לנו לזהות טוב את ההבדל ביניהן: סוג אחד הוא פעולות המשפיעות על המשתנה והסוג השני הוא פעולות המשפיעות על המידע. כך הקוד הבא משפיע על המשתנה x - לפני הרצת הקוד הוא היה השם של המספר 4, ואחרי הרצת הקוד הוא הפך להיות השם של המספר 5:

>>> x = 4
>>> id(x)
140548070459824
>>> x += 1
>>> id(x)
140548070459800
>>>

מצד שני ברשימות אנחנו יכולים להשתמש בשני סוגי הפעולות. הקוד הבא החזיק ב x את הרשימה 10, 20, 30, 40; ואחר כך רשימה חדשה בה נוסף גם המספר 50:

>>> x = [10, 20, 30, 40]
>>> id(x)
4298242672
>>> x = x + [50]
>>> id(x)
4298242600
>>>

לעומת זאת הקוד הבא משנה ממש את הרשימה ש x מתיחס אליה, בלי לשנות את המשתנה x עצמו:

>>> x = [10, 20, 30, 40]
>>> id(x)
4298242672
>>> x += [50]
>>> id(x)
4298242672
>>>

ההבדל בין שני סוגי הפעולות בא לידי ביטוי בתוכנית אמיתית אם יש לנו משתנה נוסף שמצביע על אותו הדבר. מסתבר שכשאתם משנים את הערך (כלומר ה id נשאר זהה אחרי הפעולה), כל השמות האחרים של אותו מידע יכילו עכשיו את הערך החדש. לעומת זאת כשמשנים את המשתנה השמות האחרים עדיין מצביעים על הדבר הישן, ולא משתנים.

במילים אחרות בקוד הבא המשתנה y יכיל אחרי ריצת הקוד גם את המספר 50:

>>> x = [10, 20, 30, 40]
>>> y = x
>>> x += [50]
>>> y
[10, 20, 30, 40, 50]

לעומתו בקוד הבא המשתנה y לא יושפע מהפעולה וימשיך להתיחס לרשימה הישנה:

>>> x = [10, 20, 30, 40]
>>> y = x
>>> x = x + [50]
>>> y
[10, 20, 30, 40]

כך הפקודה id ב Python, למרות שבדרך כלל לא נשתמש בה בתוך תוכנית אמיתית, יכולה לעזור לנו להבין מה הקוד שלנו עושה ולמצוא באגים שנובעים משינוי מידע שלא התכוונו לשנות.

מה לעשות כשמקבלים לינק חשוד בווטסאפ

12/03/2020

השבוע קיבלתי את ההודעה הבאה לווטסאפ:

שופרסל לכבוד "יום שישי השחור" בחודש נובמבר, מחלק את הקופונים החופשיים בשווי 900 ש"ח. קיבלתי את הקופון https://t.co/7MH01rxfwM

לא ברור למה כל הספאמרים של העולם עדיין מתפקדים ועדיין שולחים את השטויות שלהם בכל מיני תחפושות, אבל האמת שהם היו פה הרבה לפנינו וכנראה יישארו גם אחרינו אז עדיף שנלמד להתגונן, ולכן המדריך היום "מה לעשות כשמקבלים לינק חשוד".

הנחת עבודה ראשונה היא שאתם אנשים סקרנים שרוצים ללמוד מי שולח לכם לינקים ומה הוא רוצה. אם יש לכם דברים יותר טובים לעשות עם הזמן שלכם הדבר הכי טוב לעשות כשמקבלים לינק חשוד יהיה למחוק ולהתעלם.

הנחת עבודה שניה היא שמדובר בלינק שצריך ללחוץ עליו. זה לא מובן מאליו והיום יש הרבה לינקים שרק מעצם זה שמתקבלים אצלכם במכשיר כבר קורה משהו רע. זה קורה בגלל שתוכנות כמו ווטסאפ מנסות לבד להיכנס ללינקים בשמכם ולהביא חלק מהתוכן. לפעמים אפילו מעט התוכן שהן מביאות מספיק זדוני כדי לשבור לכם את המכשיר או להתקין תוכנת ריגול. אם זה המצב אין לי איך לעזור לכם.

לכן כותרת יותר מדויקת למדריך הזה תהיה "מה לעשות כשמקבלים לינק חשוד שלא שבר לי את המכשיר אבל כנראה מוביל לאתר ספאם חובבני ואני סקרן לדעת מה יש שם, מי בנה את האתר ומה הם רוצים". עם זה אני יכול לעזור לכם.

נחזור להודעה שקיבלתי ונלך לבדוק מאיפה הלינק הגיע. הדומיין t.co הוא דומיין של טוויטר. הם משתמשים בו כדי לקצר כתובות אינטרנט ארוכות כך שיכנסו בציוץ. קל למצוא את זה בגוגל אם מחפשים what is t.co וזה דף ההסבר של טוויטר על הלינקים האלה:

https://developer.twitter.com/en/docs/basics/tco

שימוש במקצרי כתובות הוא פרקטיקה נפוצה אצל ספאמרים שלא רוצים לספר מה הכתובת האמיתית שלהם. בעזרת האתר http://checkshorturl.com/expand.php אפשר לפתוח לינקים מקוצרים מסוג זה ובמקרה שלנו לגלות שהלינק מוביל ל:

https://lvreceitas.site/il-shuffersal-1/?w 

הכתובת lvreceitas.site לא נראית כאילו הגיעה משופרסל, אבל אני עדיין לא בטוח מה כן יש שם. לכן לפני שנכנס נפעיל whois קצר:

% whois lvreceitas.site
% IANA WHOIS server
% for more information on IANA, visit http://www.iana.org
% This query returned 1 object

refer:        whois.nic.site

domain:       SITE

organisation: DotSite Inc.
address:      F/19, BC1, Ras Al Khaimah FTZ
address:      P.O Box # 16113, Ras Al Khaimah - 16113.
address:      United Arab Emirates

contact:      administrative
name:         Shweta Sahjwani
organisation: DotSite, Inc
address:      F/19, BC1, Ras Al Khaimah FTZ
address:      P.O Box # 16113, Ras Al Khaimah - 16113.
address:      United Arab Emirates
phone:        +91.2230797500x8522
fax-no:       +91.2230797508
e-mail:       admin@radixregistry.com

ונגלה שהדומיין רשום על חברה באיחוד האמירויות, שזה כבר לא סימן טוב אבל אולי לא נורא, והשרת יושב בכתובת 77.72.1.15 שממוקמת בלונדון (את הכתובת מצאתי עם dig, ובשביל לגלות שהיא בלונדון הלכתי לאתר https://www.iplocation.net/).

השלבים הבאים בחקירה שלנו יהיו לגלות עוד קצת מידע על lvreceitas.site. תחילה נחפש בגוגל אם הם מופיעים ואם יש עוד דברים מעניינים באתר שלהם באמצעות הכנסת הטקסט הבא בתיבת החיפוש של גוגל: inurl:lvreceitas.site. במקרה שלנו אנחנו מוצאים שם עוד הטבה שנראית פיקטיבית של המוסד לביטוח לאומי, ועוד כעשרים הטבות של אתרים בחו"ל.

אפשר גם להמשיך ולבדוק את תעודות ה SSL של האתר באמצעות הפקודה:

$ openssl s_client -showcerts -connect lvreceitas.site:443

תקבלו הרבה פלט על המסך אבל המילה שופרסל לא מופיעה בו. שוב זה הגיוני כי הדומיין הוא לא של שופרסל ולא נראה שקשור אליהם בשום צורה.

התחנה האחרונה לסקרנים היא האתר https://www.site-shot.com/ שייקח תמונת מסך של כל אתר בלי שתצטרכו להיכנס אליו. זה נחמד כי ככה תוכלו לראות מה מנסים למכור לכם, למרות שבדרך כלל במיזמים כאלה אנשים ישכפלו ממש קמפיין אמיתי ולכן לדעתי לא נראה הבדל אמיתי בין אתר הספאם לבין אתר הקמפיין המקורי.

אז אומנם על השאלה הגדולה "מה הם רוצים" לא הצלחנו לענות, אבל אולי זה בעצם לא כזה משנה. הם רוצים לדעת מי החברים שלך, לכמה אנשים אתה שולח את ההודעה, כמה מהם באמת נכנסו ואולי בעתיד לנסות לשלוח לכם עוד הודעות כי הם יודעים שלחצתם. 900 שקל לקניות בשופרסל לא תראו מהם.

מדיניות Referer

11/03/2020

אחת הטעויות המצחיקות של האינטרנט היא הכותרת referer (שגיאת הכתיב מכוונת). אבל חוץ מלהיות תזכורת לעובדה שקשה מאוד לתקן טעויות אחרי שכולם מתחילים לעבוד בדרך מסוימת, רפרר גם עלולה לסכן ממש את המשתמשים שלכם.

תקופה ארוכה ברירת המחדל של דפדפנים היתה שבכל פעם שאנחנו מבקרים באתר אנחנו שולחים לאותו אתר גם את כתובת האינטרנט הקודמת שהיינו בה. הרעיון היה לעזור לבעלי אתרים להבין מי מפנה אליהם תנועה וכך אנשים יוכלו לדעת למי לשלוח ברכות לחג.

התנהגות זו נשמעת תמימה אבל יש בה מספר בעיות - אפילו אם נשים בצד את המשתמשים שאולי לא רוצים שכל אתר ידע איפה הם היו קודם (אם אתם כאלה אתם בטח כבר יודעים איך לבטל את שליחת ה Referer בהגדרות הדפדפן) - עדיין יש לנו בעיה עם כתובות אינטרנט המשלבות מידע אישי.

נסו למשל להעתיק קישור מדרופבוקס לגישה ישירה לקובץ. התוצאה היא משהו שנראה כך:

https://www.dropbox.com/s/4g6hce1gs57un9g/%D7%9C%D7%95%D7%97%20%D7%97%D7%95%D7%A4%D7%A9%D7%95%D7%AA%20%D7%AA%D7%A9%D7%A2%D7%96.doc?dl=0

את הקישור הזה אני כנראה רוצה לשמור בסוד ולתת אותו רק לחברים ספציפיים שצריכים לגשת לקובץ. עכשיו נניח שחבר כזה שיש לו את הקישור נכנס דרכו לדרופבוקס ואז ממשיך לאתר חדש, והדפדפן שלו שולח לאתר החדש את הכתובת "הקודמת" שהיה בה, אז הכתובת הקודמת כוללת מידע אישי ומנהל האתר החדש יכול לראות את הקישור ואף לגלוש באמצעותו לקובץ.

לכן דפדפנים תומכים היום בתגית meta שעוזרת להם להבין האם צריך או לא צריך לשלוח מידע בשדה Referer ואיזה מידע בדיוק צריך לשלוח. אתם יכולים לדוגמא לשתול ב HTML שלכם את הקוד הבא:

<meta name="referrer" content="no-referrer">

ואז הדפדפן לא ישלח בכלל כותרת Referer לאתר הבא שמשתמש יגיע אליו. אפשרות טובה נוספת היא המדיניות origin כלומר התגית:

<meta name="referrer" content="origin">

שתגרום לדפדפן לשלוח כותרת Referer אבל רק עם שם הדומיין בלי כתובת הדף הספציפי ממנו הגולש הגיע.

לאופציות נוספות ומידע נוסף על Referer שווה לבקר בדף התיעוד מ MDN.

אני אתחיל ב...

10/03/2020

יש המון חומרי לימוד באינטרנט ובעיה אמיתית להבין איזה מדריך יעזור לנו ללמוד הכי מהר את מה שאנחנו צריכים ללמוד. אחת הדרכים היא להסתכל על המרחק כלומר לראות איפה המדריך מתחיל ומה אבנה בו, ומתוך זה להבין כמה אני הולך ללמוד.

לדוגמא לא מזמן מצאתי מדריך שמסביר איך לזהות רגשות מתוך משפטים באמצעות AI. זה מדליק, והרבה אנשים (גם אני) חושבים לעצמם - אוקיי אני אתחיל בלהבין איך לזהות רגשות מתוך משפטים, ומתוך זה אני כבר אצליח להמשיך ולהבין איך AI עובד.

ואותו הגיון מופיע בכל דבר שאנחנו רוצים ללמוד:

  • אני אתחיל בלבנות אפליקציה עם React ו Redux, ומתוך זה אני כבר אבין איך ריאקט עובד

  • אני אתחיל בלהתקין שרת Linux עם nginx ו Web Application שכתבתי וזה כבר ייתן לי רעיונות לאיך Linux עובד.

  • אני אתחיל בלהעביר את הפרויקט שלי ל git ואחרי כמה שבועות בטח אבין איך גיט עובד.

הבעיה עם ההגיון הזה היא שבתוכנה זה לא ממש עובד. כשאנחנו כותבים קוד קשה לנו לראות את פערי הידע שאנחנו צוברים במהלך הכתיבה, ובסיום ה Tutorial כשיש לנו מערכת שעובדת קל להגיד לעצמך "אני בניתי את זה, אני יודע את זה". האמת היא שיש פער עצום (בתוכנה, אולי גם בתחומים אחרים אין לי מושג) בין מי שיודע לכתוב קוד לפי ההוראות לבין מי שיודע לכתוב את ההוראות. ב Tutorial אנחנו מנסים בכל שלב להבין את המינימום שצריך בשביל שנוכל לעבור לשלב הבא, כדי שנוכל בזמן נתון לבנות את הדבר הכי גדול שאפשר.

במקום זה מה שכן עובד לי היא הגישה ההפוכה: הנה שני דברים קטנים שאפשר לעשות עם React (בלי create-react-app, בלי TypeScript, בלי Redux, בלי MobX, בלי useEffect ובלי Styled Components). בוא נראה כמה דברים מעניינים אני מצליח לבנות רק עם שני הדברים הקטנים האלה. וכך, לאט לאט, אני מוסיף כל פעם עוד מילה חדשה ורואה איך היא משתלבת בדבר שאני בונה. הכלל היחיד - לפני שמוסיפים את המילה הבאה מנסים למצוא את מקסימום האפשרויות שאפשר לבנות עם הדברים שלמדנו עד עכשיו, כדי שנוכל להבין באמת את הכלים בהם אנחנו משתמשים.

אין מצב שזה אי פעם עבד

09/03/2020

כשאני מתחיל להסתכל בקוד בחיפוש אחר באג, לעתים קרובות אני אפעיל git blame כדי לראות מי הכניס את הבאג הזה למערכת (זה בדרך כלל אני) ולמה (זו בדרך כלל תהיה הודעת קומיט גנרית להחריד). יותר מדי פעמים, ובמיוחד במערכות גדולות, אני הולך לגלות שהבאג היה שם חודשים ארוכים ואולי אפילו שנים. אז איך זה שעד עכשיו אף אחד לא התלונן?

התשובה כמובן היא שלא כל באג בא לידי ביטוי. הנה תוכנית פייתון קצרה שמקבלת מספר ומדפיסה אותו בבינארי:

import sys

def print_binary(n):
    if n > 1:
        print_binary(n // 2)

    print(n % 2, end="")

num = sys.argv[1]
print_binary(int(num))
print()

היא עובדת ותדפיס לכם את הייצוג הבינארי של כל מספר שתעבירו כקלט. או לפחות כל מספר מספיק קטן, כי בגלל הרקורסיה הפונקציה שלנו לא תעבוד על מספרים גדולים מדי.

ברוב המקרים המצב אפילו יותר גרוע: הבאג יתחיל להופיע רק כשתשדרגו איזו ספריה, תעברו מערכת הפעלה, תתחברו לשרת מיותר לקוחות במקביל או תגיעו לשנה מסוימת. וזו אולי הסיבה הכי חשובה לכתוב בדיקות אוטומטיות: הבדיקה תזהה את הבאג בשניה שהוא יתחיל להשפיע, ולפני שהלקוחות יראו את הבעיה.

שברת קיבלת

08/03/2020

החלק הכי קשה בללמוד משהו חדש הוא המעבר מ"אני יודע לבנות לפי ההוראות" ל"אני יודע לכתוב את ההוראות", או היכולת לשלב כמה דברים שאנחנו יודעים לדבר חדש שלא הופיע בספר. דמיינו לדוגמא שאתם לא מכירים את השפה האנגלית כלל ואני מספר לכם שהמשפט I have a cat מתרגם ל"יש לי חתול", המשפט I have a dog מתרגם ל"יש לי כלב", חתול זה cat, כלב זה dog ותפוח זה apple. עכשיו אם תנסו להגיד "יש לי תפוח" באנגלית אני חושב שכולכם תטעו ותגידו I have a apple, כי מי בכלל חושב שאנגלית לא מסוגלים להגיד שתי אותיות ניקוד אחת אחרי השניה אז ממציאים n באמצע. זה פשוט כלל אחר.

ואותו דבר בתכנות - אם אתם לא יודעים פייתון בכלל אני יכול לספר לכם שהתוכנית הבאה מדפיסה על המסך את הטקסט hello ynon:

name = "ynon"
print("hello " + name)

ואספר לכם שהתוכנית הבאה מדפיסה על המסך את המספר 42:

x = 42
print(x)

סיכוי לא רע שבשביל להדפיס את הטקסט hello 42 תחליטו לכתוב את הקוד הבא:

x = 42
print("hello " + x)

שכמובן לא יעבוד, כי מי בכלל חושב שמסביב לפלוס חייבים לכתוב שני דברים מאותו סוג.

מה שיפה שכל טעות כזאת מקרבת אותנו עוד צעד להבנה של המערכת: כשפייתון צועק עליי שאני לא יכול לשים מסביב לפלוס שני דברים מסוגים שונים למדתי משהו חדש על פייתון ועל סוגי משתנים. גיליתי כלל חדש של השפה.

הדרך ללמוד משהו חדש לכן היא פשוטה, משעממת ועובדת - בונים משהו קיים לפי ההוראות, משנים קצת לפי מה שנראה לנו הגיוני מתוך כוונה לשבור את המערכת, ואם שברנו להסתכל על הטעות וללמוד ממנה כלל נוסף. תחזרו על זה מספיק פעמים ותוכלו גם אתם לכתוב את ההוראות, במקום רק לעקוב אחריהן.

הייתי רוצה להיות טבח

07/03/2020

בחיים לא שמעתי על טבח שקם בבוקר ואומר "היום אני הולך לנסות לבשל פסטה. אני אתחיל - אולי זה יהיה מוכן לארוחת צהריים, אולי לארוחת ערב, ואולי רק לשבוע הבא". וגם אין מצב כשהפסטה כבר על האש שהטבח יסתכל עליה בקטע של "טוב זה נראה שעוד 5 דקות היא תהיה מוכנה, אבל גם יש מצב שזה ייקח עוד שעתיים או עוד שבועיים, אי אפשר לדעת".

כשאתה מתיישב להכין פסטה יש לוח זמנים ברור מה הולך לקרות מתי. אתה יכול לפשל עם הפסטה, אבל אין מצב להסתכל על הסיר ולהתלבט מה הדבר הבא שצריך לעשות.

ואנחנו? איפה. בן אדם קם בבוקר לכתוב פיצ'ר למערכת, עוברות שעתיים ואין לך מושג אם הפיצ'ר הולך להיות מוכן עד הצהריים, עד הערב או עד שבוע הבא. ובשום שלב בתהליך (אפילו אחרי שהפיצ'ר כבר מוכן ועובד) אתה לא יודע אם עוד שניה לא יתגלה איזה באג שישלח אותך לעוד שבוע עבודה.

תסכול בצד - חוסר היכולת לקבוע לוחות זמנים לעבודה הוא חלק מובנה בעבודה של מתכנתים. או כמו שאומרים אצלנו "זה לא באג, זה פיצ'ר". עכשיו כדאי שנתחיל להתרגל ולבנות לעצמנו שיטות עבודה שמחבקות את חוסר הידיעה.

ידע חוצה שפות

06/03/2020

ביטויים רגולאריים יעבדו כמעט בלי שינוי בכל שפת תכנות שתגיעו אליה. גם כשתנסו לפתוח קובץ בכל שפת תכנות תמצאו את עצמכם מעבירים פרמטר בשם Open Mode שיכול לקבל תמיד את אותו סט של ערכים. הפקודה strftime של Python מקבלת מחרוזת שמייצגת פורמט תאריך ומורכבת מאחוזים ואותיות, וזהה לגמרי לזו של strftime של רובי וגם של C++, perl ושפות רבות נוספות.

מי שיודע לעבוד עם Promises ב JavaScript יקלוט מהר מאוד איך להשתמש ב asyncio ב Python ואפילו ב std::async של C++ (למרות שבינינו שום דבר ב C++ לא נקלט מהר מאוד).

אותו דבר לגבי עבודה עם REST ו GraphQL, כתיבת שאילתות SQL, פיענוח של קבצי XML, ואפילו Multi Threaded Programming. הרבה מאוד מהידע שלנו יכול להיות בעל ערך במעבר לשפה חדשה.

בהתלבטות אם כדאי ללמוד נושא מסוים, כדאי לקחת בחשבון את מספר שפות התכנות שנושא זה מופיע בהן בצורה כזו או אחרת. ככל שנשקיע באיסוף מיומנויות חוצות שפות כך זה הולך להשתלם ולהחזיר את ההשקעה ברווח גדול בעתיד.

יוניקוד כל הדרך ב Python2

05/03/2020

אחד הדברים שמבלבלים ב Python2 הוא שמחרוזות אינן Unicode כברירת מחדל. הדבר מייצר המון טעויות של מתכנתים עם המון הודעות שגיאה שונות ומשונות. הנה כמה דוגמאות ופיתרונות שלהן.

המשך קריאה

המכשול

04/03/2020

מה שמפריד בין אנשים שמצליחים להרים פרויקטים לאנשים שנתקעים הוא לא העובדה שהראשונים יודעים מה הם עושים ותמיד הכל הולך להם, אלא מה כל קבוצה עושה אל מול המכשול.

המכשול זה הקוד שכתבת בדיוק כמו בתיעוד אבל משום מה אצלך לא עובד.

זה הדבר המוזר הזה שאתה מנסה לעשות שלא מופיע בתיעוד ושנראה שאף אחד באינטרנט לא עשה קודם.

זה הגילוי שהספריה שרצית לבחור בה בעצם לא יכולה לעשות את הדבר הזה שרצית.

זו ההבנה שמה שנראה כמו יומיים עבודה הולך לקחת חודש במקרה הטוב.

בכל פרויקט יש מכשול והרבה פעמים יש יותר מאחד. המכשול זאת הנקודה בה אנשים מתייאשים, זה החודש הראשון בחדר כושר, או השנה הראשונה בעבודה כעצמאי. אבל המכשול הוא גם מה שהופך את הדבר שאתה מנסה לבנות לבעל ערך, והוא הסיבה שלא כולם עושים את הפרויקט הזה. והדרך היחידה להתמודד עם המכשול היא להגביר מהירות, להשקיע יותר שעות, להיכנס לעובי הקורה ולפתור את הבעיה בכלים שאפילו לא חשבת שיש לך.

התגברות על מכשולים ובניה מוצלחת של הפרויקטים שהתחלתם הם הדברים שלאורך זמן הופכים אתכם למתכנתים טובים יותר.