לדלג לתוכן

3.3 למבדה, דקורטור, וגנרטור הרצאה

למבדה, map, filter ו - sorted

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

add = lambda x, y: x + y
print(add(3, 5)) # Will output: 8
print(add(2, 4)) # Will output: 6

- כאשר אנחנו מגדירים פונקצית למבדה אנחנו קודם מציינים את הפרמטרים של הפונקציה על ידי הפרדתם בעזרת פסיקים ונקודותיים. אחרכך אנחנו כותבים את הביטוי חזרה של הפונקציה.
- הנה דוגמה לlambda:
func = lambda x: x + 1

- כאשר נעביר לפונקציה func מספר, נקבל אותו ועוד 1.

למה למבדה?

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

פעולות נפוצות עם lambda

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

numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
print(squared) # Will output: 1, 4, 9, 16, 25

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

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

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)

- בדומה למעלה הוצאנו את כל המספרים שמתחלקים ב2. (באמצעות הפעולה מודולו %)
- הפונקציה filter דומה לmap, רק שמטרתה להוציא איברים מהרשימה.

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

books = [
    {'title': 'The Great Gatsby', 'year': 1925},
    {'title': 'To Kill a Mockingbird', 'year': 1960},
    {'title': '1984', 'year': 1949},
    {'title': 'The Catcher in the Rye', 'year': 1951},
]

sorted_books = sorted(books, key=lambda book: book['year'])

- יסדר את הספרים לפי השנה שלהם.

מתי להשתמש בלמבדה

  • פונקציות קצרות מועד: כאשר יש צורך בפונקציה רק לזמן קצר או בהיקף מוגבל.
    result = (lambda x, y: x + y)(3, 5)
    print(result)
    
  • פונקציות כפרמטר: פונקציות שמקבלות פונקציות כפרמטר: map, filter, sorted

דוקרטורים - Decorators

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

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

- ניתן לראות שכתבנו פונקציה בשם my_decorator, שמקבלת כפרמטר פונקציה (func)
- הדקורטור מגדיר פונקציה חדשה ממש בתוכו בשם wrapper, במקרה הזה אפשר לראות שהפונקציה החדשה שהוא הגדיר (wrapper) פשוט עושה 2 print-ים ומריץ את הפונקציה שהוא קיבל כפרמטר.
- אחרי שהגדרנו את הפונקציה wrapper, אנחנו מחזירים את הפונקציה החדשה שהגדרנו.

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

  • ניתן לקשט פונקציות עם הדקורטור שלנו עם התו @
    def my_decorator(func):
        def wrapper():
            print("Something is happening before the function is called.")
            func()
            print("Something is happening after the function is called.")
        return wrapper
    
    @my_decorator
    def say_hello():
        print("Hello!")
    
    @my_decorator
    def say_bye()
        print("bye")
    
    say_hello()
    
  • במקרה הזה שנינו את ההתנהגות של הפונקציות say_hello, ו - say_bye באמצעות הדקורטור my_decorator.
  • בעתיד נלמד עוד על דקורטורים

דקורטור לפונקציה עם פרמטר

  • כדי להגדיר דקורטור לפונקציה שמקבלת פרמטרים, נציין אותם בהגדרה של הדקרטור, הנה דוגמה לדקורטור כזה:
    def add_question_symbol(get_message):
        def wrapper(message):
            return get_message(message) + " ?"
        return wrapper
    
    @add_question_symbol
    def generate_question(message):
        return f"Question: {message}"
    
    question = generate_question("why humans have 2 legs")
    print(question)
    
  • הדקורטור הבא מקבל פונקציה שמקבלת פרמטר, ומחזיר פונקציה שמקבלת פרמטר.
  • נסו לעקוב אחר הקוד בעצמכם ולהבין מה הוא עושה.

גנרטורים - Generators

  • המון פעמים בפייתון, אנחנו ניצור איזשהו ליסט שעלול להיות גדול מאוד, ואחרכך נבצע עליו פעולות, כמו למשל בדוגמה הבאה:
    # here we create a big list
    def generate_list(size):
        return [number ** 2 for number in range(size)]
    
    my_list = generate_list(100)
    # here we are doing something with the list
    for item in my_list:
        print(item)
    
  • הבעיה, שהקוד הזה יהיה מאוד איטי, בגלל שקודם כל אנחנו צריכים ליצור רשימה שעלולה להיות גדולה מאוד, ורק אחרכך אנחנו נוכל לבצע עלייה פעולות, כמו למשל להדפיס את כל האיברים שלה.
  • במקרה הזה, דוגמה לקוד יותר יעיל יהיה: בזמן שאנחנו יוצרים איברים ברשימה, נדפיס אותם אחד אחרי השני.
  • כך שבזמן שאנחנו יוצרים את כל האיברים ברשימה, נבצע עלייה את הפעולות. למשל, כאשר אנחנו יוצרים רשימה, נדפיס כל איבר שיצרנו.
  • קוד כזה, יכול להיראות כך:
    # here we are printing items of the list when they initialized
    def generate_list(size):
        my_list = []
        for number in range(size):
            value = number ** 2
            my_list.append(value)
            print(value)
    
    my_list = generate_list(100)
    
  • הקוד הזה יותר מהיר מהקוד השני, למורות שהם עושים בדיוק את אותו הדבר.
  • הבעיה בקוד הזה שהוא מבולגן יותר, כי באותה פונקציה אנחנו גם יוצרים את הרשימה וגם עושים עלייה פעולה כמו הדפסה, וזה לא מאורגן.
    בכדי לפתור את הבעיה הזו - יש לנו גנרטורים.

  • גנרטור זה type מיוחד, דומה לרשימה, שיוצר איטראבל (תזכורת: type שניתן לעבור עליו עם לולאה - למשל רשימה), בזמן שאנחנו מבצעים עליו פעולות.

  • דמיינו רשימה שיוצרת את עצמה בזמן שאנחנו עוברים עלייה עם לולאה ומדפיסים את האיברים שלה, זה דוגמה לגנרטור.
  • מגדירים גנרטור עם פונקציה, זה פונקציה מיוחדת שבמקום return עושה yeild.
    def square_numbers(size):
        for number in range(size):
            yield number ** 2
    
    squares = square_numbers(100)
    for square in squares:
        print(square)
    
  • למעשה, כאשר הקוד שלנו עובר עם for על הגנרטור squares_numbers, בכל יצירה של איבר, הוא גם מדפיס אותו.
  • כלומר, הקוד שלנו יקפוץ בין הfor שיש בפונקציה square_numbers לfor שיש למטה סיכרונית.
  • שימו לב, רשימה היא לא גנרטור, אז אם אנחנו נעשה type casting בין גנרטור לרשימה, זה פשוט יבטל את הגנרטור ויהפוך אותו לרשימה.
    squares = square_numbers(5)
    squares = list(squares)
    
  • עוד נקודה מעניינת: range הוא גנרטור, אז הוא מאוד יעיל.

נקודה חשובה

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