Skip to content

Εργαστήριο 6 στην Python

Θέματα που εξετάζονται στο εργαστήριο: συναρτησιακός προγραμματισμός με την Python68, λάμδα συναρτήσεις17 (lambda functions), συναρτήσεις υψηλότερης τάξης (higher order functions), μερικές συναρτήσεις5 (partial functions), γεννήτριες2 (generator functions και generator expressions) και οκνηρή αποτίμηση (lazy evaluation), iterators3, list comprehensions, βιβλιοθήκες itertools4 και functools5.

Άσκηση Ε6Α1 (λάμδα συναρτήσεις και συναρτήσεις υψηλότερης τάξης) - Δίνεται η ακόλουθη λίστα από πλειάδες που αναπαριστούν παραγγελίες ενός ηλεκτρονικού καταστήματος:

orders = [
    ("A101", "laptop", 1200, 1),
    ("A102", "mouse", 25, 2),
    ("A103", "keyboard", 75, 1),
    ("A104", "monitor", 300, 2),
    ("A105", "laptop", 1100, 1),
    ("A106", "mouse", 20, 3),
    ("A107", "monitor", 280, 1)
]

Κάθε πλειάδα έχει τη μορφή: (order_id, product, price_per_unit, quantity).

Να γράψετε πρόγραμμα που χωρίς χρήση for-loops και αξιοποιώντας lambda συναρτήσεις και higher-order functions (συναρτήσεις map, filter, sorted και από το functools η συνάρτηση reduce) να εκτελεί τα ακόλουθα:

  • Υπολογισμός συνολικής αξίας κάθε παραγγελίας. Δηλαδή, δημιουργήστε μια νέα λίστα όπου κάθε στοιχείο θα έχει τη μορφή: (order_id, product, total_value), όπου total_value = price_per_unit * quantity
  • Φιλτράρισμα παραγγελιών με συνολική αξία πάνω από 100 ευρώ
  • Ομαδοποίηση order_id ανά προϊόν
  • Ταξινομήστε τα προϊόντα κατά συνολική αξία σε φθίνουσα σειρά
  • Εύρεση προϊόντος με τα μεγαλύτερα συνολικά έσοδα από όλες τις παραγγελίες
Λύση άσκησης E6A1
e6a1.py
from functools import reduce

orders = [
    ("A101", "laptop", 1200, 1),
    ("A102", "mouse", 25, 2),
    ("A103", "keyboard", 75, 1),
    ("A104", "monitor", 300, 2),
    ("A105", "laptop", 1100, 1),
    ("A106", "mouse", 20, 3),
    ("A107", "monitor", 280, 1)
]

# 1. Συνολική αξία κάθε παραγγελίας
order_totals = list(map(
    lambda o: (o[0], o[1], o[2] * o[3]),
    orders
))

print("Συνολική αξία κάθε παραγγελίας:")
print(order_totals)


# 2. Παραγγελίες με συνολική αξία πάνω από 100 ευρώ
orders_over_100 = list(filter(
    lambda o: o[2] > 100,
    order_totals
))

print("\nΠαραγγελίες άνω των 100 ευρώ:")
print(orders_over_100)


# 3. Ομαδοποίηση order_id ανά προϊόν
orders_by_product = reduce(
    lambda acc, o: {
        **acc,
        o[1]: acc.get(o[1], []) + [o[0]]
    },
    orders,
    {}
)

print("\nOrder IDs ανά προϊόν:")
print(orders_by_product)


# 4. Συνολικά έσοδα ανά προϊόν
revenue_by_product = reduce(
    lambda acc, o: {
        **acc,
        o[1]: acc.get(o[1], 0) + o[2] * o[3]
    },
    orders,
    {}
)

sorted_products = sorted(
    revenue_by_product.items(),
    key=lambda x: x[1],
    reverse=True
)

print("\nΠροϊόντα κατά συνολική αξία φθίνουσα:")
print(sorted_products)


# 5. Προϊόν με τα μεγαλύτερα συνολικά έσοδα
top_product = reduce(
    lambda a, b: a if a[1] > b[1] else b,
    sorted_products
)

print("\nΠροϊόν με τα μεγαλύτερα συνολικά έσοδα:")
print(top_product)

Άσκηση Ε6Α2 (μερική αποτίμηση συναρτήσεων) - Δίνεται η παρακάτω συνάρτηση που υπολογίζει την τελική αξία μιας παραγγελίας, λαμβάνοντας υπόψη την τιμή μονάδας price, την ποσότητα quantity, την έκπτωση discount και τον φόρο tax:

def calculate_price(price, quantity, discount, tax):
    return price * quantity * (1 - discount) * (1 + tax)

Να γράψετε πρόγραμμα που να χρησιμοποιεί partial function evaluation ώστε:

  • Να δημιουργεί μια νέα συνάρτηση calculate_with_tax, στην οποία ο φόρος να είναι σταθερός στο 24%.
  • Να δημιουργεί μια νέα συνάρτηση calculate_with_discount, στην οποία η έκπτωση να είναι σταθερή στο 10%.
  • Να δημιουργεί μια νέα συνάρτηση calculate_standard_order, στην οποία τόσο ο φόρος όσο και η έκπτωση να είναι σταθερά στο 24% και 10% αντίστοιχα.
  • Να καλεί τις νέες συναρτήσεις για διαφορετικές τιμές προϊόντων και ποσοτήτων και να εμφανίζει τα αποτελέσματα με κατάλληλα μηνύματα.
Λύση άσκησης E6A2
e6a2.py
from functools import partial


def calculate_price(price, quantity, discount, tax):
    return price * quantity * (1 - discount) * (1 + tax)


# Σταθερός φόρος 24%
calculate_with_tax = partial(calculate_price, tax=0.24)

# Σταθερή έκπτωση 10%
calculate_with_discount = partial(calculate_price, discount=0.10)

# Σταθερός φόρος 24% και σταθερή έκπτωση 10%
calculate_standard_order = partial(calculate_price, discount=0.10, tax=0.24)


print("Με σταθερό φόρο 24%:")
print(calculate_with_tax(price=100, quantity=2, discount=0.10))

print("\nΜε σταθερή έκπτωση 10%:")
print(calculate_with_discount(price=100, quantity=2, tax=0.24))

print("\nΜε σταθερό φόρο 24% και έκπτωση 10%:")
print(calculate_standard_order(price=100, quantity=2))
print(calculate_standard_order(price=50, quantity=5))
print(calculate_standard_order(price=1200, quantity=1))

Άσκηση Ε6Α3 (γεννήτριες) - Να γράψετε μια γεννήτρια συνάρτηση running_totals(numbers) η οποία δέχεται μια λίστα αριθμών και παράγει σταδιακά τα αθροίσματα των στοιχείων της λίστας. Για παράδειγμα, για τη λίστα numbers = [4, 7, 2, 10] η γεννήτρια θα πρέπει να παράγει τις τιμές 4, 11, 13, 23 σταδιακά με κάθε κλήση την εντολής yield. Καλέστε την yield σε μια for.

Λύση άσκησης E6A3
e6a3.py
def running_totals(numbers):
    total = 0
    for n in numbers:
        total += n
        yield total



numbers = [4, 7, 2, 10]

for value in running_totals(numbers):
    print(value)

Άσκηση Ε6Α4 (memoization) Ο ακόλουθος κώδικας υπολογίζει αναδρομικά τον n-οστό όρο της ακολουθίας Fibonacci (0,1,1,2,3,5,8,13,...).

e6a4a.py
1
2
3
4
5
6
7
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

x = fibonacci(40)
print(x)

Ωστόσο, καθυστερεί υπερβολικά για τιμές του n από 40 και πάνω. Εντοπίστε την τιμή του n για την οποία ο χρόνος υπολογισμού του n-οστού όρου είναι περισσότερο από 10 δευτερόλεπτα.

Άσκηση E6A4 (α΄ μέρος)
e6a4b.py
def fibonacci(n):
    if n <= 1:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)


import time

n = 0
while True:
    start = time.perf_counter()
    result = fibonacci(n)
    elapsed = time.perf_counter() - start

    print(f"n={n}, fib={result}, time={elapsed:.4f} sec")

    if elapsed > 10:
        break

    n += 1

Στη συνέχεια προτείνετε λύση με το functools @cache που να λύνει το θέμα της ταχύτητας υπολογισμού.

Άσκηση E6A4 (β΄ μέρος)
e6a4c.py
import time
from functools import cache


@cache
def fibonacci_cached(n):
    if n <= 1:
        return n
    return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)

n = 50
start = time.perf_counter()
result = fibonacci_cached(n)
elapsed = time.perf_counter() - start

print(f"With cache: n={n}, fib={result}, time={elapsed:.8f} sec")

Άσκηση Ε6Α5 itertools Να υλοποιήσετε τις παρακάτω συναρτήσεις, ώστε να περνάνε τα unittests, χρησιμοποιώντας τις συναρτήσεις του module itertools: count, combinations, product, accumulate, cycle, islice.

e6a5.py
import unittest
from itertools import (
    count,
    combinations,
    product,
    accumulate,
    cycle,
    islice,
)


# TODO: Υλοποιήστε τις συναρτήσεις


def first_numbers(start, step, n):
    pass


def pair_combinations(items):
    pass


def cartesian_product(a, b):
    pass


def cumulative_sums(numbers):
    pass


def repeat_pattern(pattern, n):
    pass


class TestItertools(unittest.TestCase):

    def test_count(self):
        self.assertEqual(
            first_numbers(0, 2, 5),
            [0, 2, 4, 6, 8]
        )

    def test_combinations(self):
        self.assertEqual(
            pair_combinations(["A", "B", "C"]),
            [("A", "B"), ("A", "C"), ("B", "C")]
        )

    def test_product(self):
        self.assertEqual(
            cartesian_product([1, 2], ["x", "y"]),
            [(1, "x"), (1, "y"), (2, "x"), (2, "y")]
        )

    def test_accumulate(self):
        self.assertEqual(
            cumulative_sums([1, 2, 3, 4]),
            [1, 3, 6, 10]
        )

    def test_cycle(self):
        self.assertEqual(
            repeat_pattern(["A", "B"], 5),
            ["A", "B", "A", "B", "A"]
        )


if __name__ == "__main__":
    unittest.main()
Άσκηση E6A5
e6a5sol.py
import unittest
from itertools import (
    count,
    combinations,
    product,
    accumulate,
    cycle,
    islice,
)


def first_numbers(start, step, n):
    return list(islice(count(start, step), n))


def pair_combinations(items):
    return list(combinations(items, 2))


def cartesian_product(a, b):
    return list(product(a, b))


def cumulative_sums(numbers):
    return list(accumulate(numbers))


def repeat_pattern(pattern, n):
    return list(islice(cycle(pattern), n))


class TestItertools(unittest.TestCase):

    def test_count(self):
        self.assertEqual(
            first_numbers(0, 2, 5),
            [0, 2, 4, 6, 8]
        )

    def test_combinations(self):
        self.assertEqual(
            pair_combinations(["A", "B", "C"]),
            [("A", "B"), ("A", "C"), ("B", "C")]
        )

    def test_product(self):
        self.assertEqual(
            cartesian_product([1, 2], ["x", "y"]),
            [(1, "x"), (1, "y"), (2, "x"), (2, "y")]
        )

    def test_accumulate(self):
        self.assertEqual(
            cumulative_sums([1, 2, 3, 4]),
            [1, 3, 6, 10]
        )

    def test_cycle(self):
        self.assertEqual(
            repeat_pattern(["A", "B"], 5),
            ["A", "B", "A", "B", "A"]
        )


if __name__ == "__main__":
    unittest.main()

Άσκηση Ε6Α6 (all και any) Δίνεται μια λίστα από φοιτητές, όπου κάθε φοιτητής αναπαρίσταται από ένα tuple της μορφής (name, passed) όπου name είναι το όνομα του φοιτητή και το passed που είναι True αν ο φοιτητής πέρασε ένα μάθημα διαφορετικά είναι False. Δίνεται ο ακόλουθος κώδικας που ελέγχει α) αν όλοι οι φοιτητές έχουν περάσει το μάθημα και β) αν υπάρχει τουλάχιστον ένας φοιτητής που δεν πέρασε το μάθημα.

e6a6.py
#fmt:off
students = [
    ("Anna", True),
    ("Nikos", True),
    ("Maria", False),
    ("Giorgos", True)
]
#fmt:on

all_passed = True
someone_failed = False

for name, passed in students:
    if not passed:
        all_passed = False
        someone_failed = True

print("All students passed:", all_passed)
print("At least one student failed:", someone_failed)

Υλοποιήστε την ίδια λειτουργικότητα χρησιμοποιώντας τις συναρτήσεις all() και any().

Άσκηση E6A6
e6a6sol.py
#fmt:off
students = [
    ("Anna", True),
    ("Nikos", True),
    ("Maria", False),
    ("Giorgos", True)
]
#fmt:on

all_passed = all(passed for name, passed in students)
someone_failed = any(not passed for name, passed in students)

print("All students passed:", all_passed)
print("At least one student failed:", someone_failed)