Απόδοση Python, Generators και Generator Expressions

Σε αυτό το σεμινάριο, θα μάθετε πώς να δημιουργείτε επαναλήψεις εύκολα χρησιμοποιώντας τις γεννήτριες Python, πώς διαφέρει από τους επαναληπτές και τις κανονικές λειτουργίες και γιατί πρέπει να το χρησιμοποιήσετε.

Βίντεο: Γεννήτριες Python

Γεννήτριες στο Python

Υπάρχει πολλή δουλειά για την οικοδόμηση ενός επαναληπτικού στο Python. Πρέπει να εφαρμόσουμε μια τάξη με __iter__()και __next__()μέθοδο, να παρακολουθούμε τις εσωτερικές καταστάσεις και να αυξάνουμε StopIterationόταν δεν υπάρχουν τιμές που θα επιστραφούν.

Αυτό είναι και μακρόχρονο και αντίθετο. Η γεννήτρια έρχεται στη διάσωση σε τέτοιες καταστάσεις.

Οι γεννήτριες Python είναι ένας απλός τρόπος δημιουργίας επαναληπτικών. Όλη η εργασία που αναφέραμε παραπάνω αντιμετωπίζεται αυτόματα από γεννήτριες στο Python.

Με απλά λόγια, μια γεννήτρια είναι μια συνάρτηση που επιστρέφει ένα αντικείμενο (επαναληπτικό) στο οποίο μπορούμε να επαναλάβουμε (μία τιμή τη φορά).

Δημιουργήστε γεννήτριες στο Python

Είναι αρκετά απλό να δημιουργήσετε μια γεννήτρια στο Python. Είναι τόσο εύκολο όσο ο ορισμός μιας κανονικής λειτουργίας, αλλά με μια yieldδήλωση αντί για μια returnδήλωση.

Εάν μια συνάρτηση περιέχει τουλάχιστον μία yieldδήλωση (μπορεί να περιέχει άλλες yieldή returnδηλώσεις), γίνεται συνάρτηση γεννήτριας. Και τα δύο yieldκαι returnθα επιστρέψουν κάποια τιμή από μια συνάρτηση.

Η διαφορά είναι ότι ενώ μια returnδήλωση τερματίζει μια συνάρτηση εντελώς, η yieldδήλωση διακόπτει τη λειτουργία αποθηκεύοντας όλες τις καταστάσεις της και αργότερα συνεχίζεται από εκεί σε διαδοχικές κλήσεις.

Διαφορές μεταξύ λειτουργίας γεννήτριας και κανονικής λειτουργίας

Εδώ είναι πώς η λειτουργία της γεννήτριας διαφέρει από μια κανονική λειτουργία.

  • Η λειτουργία γεννήτριας περιέχει μία ή περισσότερες yieldδηλώσεις.
  • Όταν καλείται, επιστρέφει ένα αντικείμενο (επαναληπτικό) αλλά δεν ξεκινά την εκτέλεση αμέσως.
  • Οι μέθοδοι αρέσουν __iter__()και __next__()εφαρμόζονται αυτόματα. Έτσι μπορούμε να επαναλάβουμε τα στοιχεία χρησιμοποιώντας next().
  • Μόλις αποδώσει η συνάρτηση, η λειτουργία διακόπτεται και ο έλεγχος μεταφέρεται στον καλούντα.
  • Οι τοπικές μεταβλητές και οι καταστάσεις τους θυμούνται μεταξύ διαδοχικών κλήσεων.
  • Τέλος, όταν τερματιστεί η λειτουργία, StopIterationαυξάνεται αυτόματα σε περαιτέρω κλήσεις.

Εδώ είναι ένα παράδειγμα για την απεικόνιση όλων των σημείων που αναφέρονται παραπάνω. Έχουμε μια λειτουργία γεννήτριας που ονομάζεται my_gen()με πολλές yieldδηλώσεις.

 # A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n

Μια διαδραστική εκτέλεση στο διερμηνέα δίνεται παρακάτω. Εκτελέστε τα στο κέλυφος Python για να δείτε την έξοδο.

 >>> # It returns an object but does not start execution immediately. >>> a = my_gen() >>> # We can iterate through the items using next(). >>> next(a) This is printed first 1 >>> # Once the function yields, the function is paused and the control is transferred to the caller. >>> # Local variables and theirs states are remembered between successive calls. >>> next(a) This is printed second 2 >>> next(a) This is printed at last 3 >>> # Finally, when the function terminates, StopIteration is raised automatically on further calls. >>> next(a) Traceback (most recent call last):… StopIteration >>> next(a) Traceback (most recent call last):… StopIteration

Ένα ενδιαφέρον πράγμα που πρέπει να σημειωθεί στο παραπάνω παράδειγμα είναι ότι η τιμή της μεταβλητής n θυμάται μεταξύ κάθε κλήσης.

Σε αντίθεση με τις κανονικές συναρτήσεις, οι τοπικές μεταβλητές δεν καταστρέφονται όταν αποδίδεται η συνάρτηση. Επιπλέον, το αντικείμενο της γεννήτριας μπορεί να επαναληφθεί μόνο μία φορά.

Για να επανεκκινήσουμε τη διαδικασία πρέπει να δημιουργήσουμε ένα άλλο αντικείμενο γεννήτριας χρησιμοποιώντας κάτι σαν a = my_gen().

Ένα τελευταίο πράγμα που πρέπει να σημειωθεί είναι ότι μπορούμε να χρησιμοποιήσουμε γεννήτριες με βρόχους απευθείας.

Αυτό συμβαίνει επειδή ένας forβρόχος παίρνει έναν επαναληπτικό και επαναλαμβάνει πάνω του χρησιμοποιώντας τη next()συνάρτηση. Λήγει αυτόματα όταν StopIterationσηκωθεί. Ρίξτε μια ματιά εδώ για να μάθετε πώς πραγματικά εφαρμόζεται το a for loop στο Python.

 # A simple generator function def my_gen(): n = 1 print('This is printed first') # Generator function contains yield statements yield n n += 1 print('This is printed second') yield n n += 1 print('This is printed at last') yield n # Using for loop for item in my_gen(): print(item)

Όταν εκτελείτε το πρόγραμμα, η έξοδος θα είναι:

 Εκτυπώνεται πρώτα 1 Εκτυπώνεται δεύτερο 2 Εκτυπώνεται το τελευταίο 3

Γεννήτριες Python με βρόχο

Το παραπάνω παράδειγμα είναι λιγότερο χρήσιμο και το μελετήσαμε μόνο για να πάρουμε μια ιδέα για το τι συνέβαινε στο παρασκήνιο.

Κανονικά, οι λειτουργίες της γεννήτριας υλοποιούνται με έναν βρόχο που έχει την κατάλληλη κατάσταση τερματισμού.

Ας πάρουμε ένα παράδειγμα μιας γεννήτριας που αντιστρέφει μια συμβολοσειρά.

 def rev_str(my_str): length = len(my_str) for i in range(length - 1, -1, -1): yield my_str(i) # For loop to reverse the string for char in rev_str("hello"): print(char)

Παραγωγή

 olleh

Σε αυτό το παράδειγμα, χρησιμοποιήσαμε τη range()συνάρτηση για να πάρουμε το ευρετήριο σε αντίστροφη σειρά χρησιμοποιώντας το για βρόχο.

Σημείωση : Αυτή η λειτουργία γεννήτριας δεν λειτουργεί μόνο με συμβολοσειρές, αλλά και με άλλα είδη επαναληπτικών προγραμμάτων όπως λίστα, tuple κ.λπ.

Έκφραση γεννήτριας Python

Οι απλές γεννήτριες μπορούν να δημιουργηθούν εύκολα εν πτήσει χρησιμοποιώντας εκφράσεις γεννήτριας. Κάνει τις γεννήτριες κτιρίων εύκολη.

Παρόμοια με τις λειτουργίες λάμδα που δημιουργούν ανώνυμες συναρτήσεις, οι εκφράσεις της γεννήτριας δημιουργούν ανώνυμες λειτουργίες γεννήτριας.

Η σύνταξη για την έκφραση της γεννήτριας είναι παρόμοια με αυτήν της κατανόησης λίστας στο Python. Αλλά οι αγκύλες αντικαθίστανται με στρογγυλές παρενθέσεις.

Η κύρια διαφορά μεταξύ κατανόησης λίστας και έκφρασης γεννήτριας είναι ότι η κατανόηση λίστας παράγει ολόκληρη τη λίστα, ενώ η έκφραση γεννήτριας παράγει ένα στοιχείο κάθε φορά.

They have lazy execution ( producing items only when asked for ). For this reason, a generator expression is much more memory efficient than an equivalent list comprehension.

 # Initialize the list my_list = (1, 3, 6, 10) # square each term using list comprehension list_ = (x**2 for x in my_list) # same thing can be done using a generator expression # generator expressions are surrounded by parenthesis () generator = (x**2 for x in my_list) print(list_) print(generator)

Output

 (1, 9, 36, 100) 

We can see above that the generator expression did not produce the required result immediately. Instead, it returned a generator object, which produces items only on demand.

Here is how we can start getting items from the generator:

 # Initialize the list my_list = (1, 3, 6, 10) a = (x**2 for x in my_list) print(next(a)) print(next(a)) print(next(a)) print(next(a)) next(a)

When we run the above program, we get the following output:

 1 9 36 100 Traceback (most recent call last): File "", line 15, in StopIteration

Generator expressions can be used as function arguments. When used in such a way, the round parentheses can be dropped.

 >>> sum(x**2 for x in my_list) 146 >>> max(x**2 for x in my_list) 100

Use of Python Generators

There are several reasons that make generators a powerful implementation.

1. Easy to Implement

Generators can be implemented in a clear and concise way as compared to their iterator class counterpart. Following is an example to implement a sequence of power of 2 using an iterator class.

 class PowTwo: def __init__(self, max=0): self.n = 0 self.max = max def __iter__(self): return self def __next__(self): if self.n> self.max: raise StopIteration result = 2 ** self.n self.n += 1 return result

The above program was lengthy and confusing. Now, let's do the same using a generator function.

 def PowTwoGen(max=0): n = 0 while n < max: yield 2 ** n n += 1

Since generators keep track of details automatically, the implementation was concise and much cleaner.

2. Memory Efficient

A normal function to return a sequence will create the entire sequence in memory before returning the result. This is an overkill, if the number of items in the sequence is very large.

Generator implementation of such sequences is memory friendly and is preferred since it only produces one item at a time.

3. Represent Infinite Stream

Οι γεννήτριες είναι εξαιρετικά μέσα για να αντιπροσωπεύουν μια άπειρη ροή δεδομένων. Οι άπειρες ροές δεν μπορούν να αποθηκευτούν στη μνήμη και επειδή οι γεννήτριες παράγουν μόνο ένα στοιχείο κάθε φορά, μπορούν να αντιπροσωπεύουν μια άπειρη ροή δεδομένων.

Η ακόλουθη λειτουργία γεννήτριας μπορεί να δημιουργήσει όλους τους ζυγούς αριθμούς (τουλάχιστον θεωρητικά).

 def all_even(): n = 0 while True: yield n n += 2

4. Γεννήτριες σωληνώσεων

Πολλαπλές γεννήτριες μπορούν να χρησιμοποιηθούν για τον αγωγό μιας σειράς λειτουργιών. Αυτό απεικονίζεται καλύτερα χρησιμοποιώντας ένα παράδειγμα.

Ας υποθέσουμε ότι έχουμε μια γεννήτρια που παράγει τους αριθμούς στη σειρά Fibonacci. Και έχουμε μια άλλη γεννήτρια για τετράγωνο αριθμών.

Αν θέλουμε να μάθουμε το άθροισμα των τετραγώνων αριθμών στη σειρά Fibonacci, μπορούμε να το κάνουμε με τον ακόλουθο τρόπο μέσω της σωληνώσεως της παραγωγής των λειτουργιών της γεννήτριας.

 def fibonacci_numbers(nums): x, y = 0, 1 for _ in range(nums): x, y = y, x+y yield x def square(nums): for num in nums: yield num**2 print(sum(square(fibonacci_numbers(10))))

Παραγωγή

 4895

Αυτή η σωληνώσεις είναι αποτελεσματική και ευανάγνωστη (και ναι, πολύ πιο δροσερή!).

ενδιαφέροντα άρθρα...