<jupyter_start><jupyter_text>4. Πλειάδες και λεξικάΣτην συνέχεια παρουσιάζονται δύο άλλες καθιερωμένες δομές στην Python, οι πλειάδες (tuples) και τα λεξικά (dictionaries). Πλειάδες (tuples)Οι πλειάδες είναι μια δομή δεδομένων παρόμοια με τις λίστες.Όπως και οι λίστες αποτελούν μια συλλογή αντικειμένων (objects). Όμως διαφέρουν σε ένα σημαντικό χαρακτηριστικό από τις λίστες, είναι αμετάβλητες (immutable), δηλαδή δεν μπορούμε να μεταβάλλουμε το περιεχόμενό τους εφόσον τις δημιουργήσουμε.Κατά συνέπεια ενώ για την προσπέλαση των στοιχείων της χρησιμοποιούνται ίδιες τεχνικές και μέθοδοι (indexing, slicing κτλ) δεν υποστηρίζουν ωστόσο τις αντίστοιχες μεθόδους αφαίρεσης, τροποποίησης και προσθήκης στοιχείων.Υπό την έννοια αυτή οι πλειάδες είναι μια ασφαλή δομή δεδομένων μέσω της οποίας εξασφαλίζεται ότι τα δεδομένα δεν θα τροποποιηθούν από λάθος ή ακόμα και από επιλογή.Συνήθως χρησιμοποιούνται κατά την επιστροφή πολλαπλών τιμών από μια συνάρτηση αλλά και όταν θέλουμε να "πακετάρουμε" δεδομένα  (tuple packing).Οι πλειάδες ορίζονται με παρόμοιο τρόπο σαν τις λίστες αλλά αντι για αγκύλες χρησιμοποιούνται παρανθέσεις (αν και δεν είναι απαραίτητες). Για παράδειγμα:<jupyter_code>information = ('UTH','https://www.uth.gr/')<jupyter_output><empty_output><jupyter_text>Το ίδιο μπορεί να γραφτεί και χωρίς παρενθέσεις<jupyter_code>information = 'UTH','https://www.uth.gr/'<jupyter_output><empty_output><jupyter_text>Όμως πρέπει να λαμβάνουμε υπόψιν ότι ο ορισμός μιας πλειάδες με ένα μόνο στοιχείο πρέπει να ορίζεται με το στοιχείο και να συνοδεύεται από ένα κόμμα. Σε διαφορετική περίπτωση θα επιστρέψει μια μεταβλητή με τύπο αυτόν που περάσαμε στο υποτιθέμενο πρώτο στοιχείο. Δηλαδή το παρακάτω θα δημιουργήσει μια μεταβλητή ακέραιου τύπου και όχι μια πλειάδα με ένα στοιχείο<jupyter_code>year = (2022)
print(type(year))<jupyter_output><class 'int'><jupyter_text>Ο σωστός τρόπος συγγραφής αν θέλουμε να πάρουμε μια πλειάδα με ένα μόνο στοιχείο είναι:<jupyter_code>year = (2022,)
print(type(year))<jupyter_output><class 'tuple'><jupyter_text>Ενώ αν θέλουμε να δημιουργήσουμε μία άδεια πλειάδα τότε χρησιμοποιούμε απλώς δύο παρενθέσεις:<jupyter_code>empty_tuple = ()<jupyter_output><empty_output><jupyter_text>Επιπλέον μέσω της συνάρτησης *tuple* η οποία δέχεται ως όρισμα μία συμβολοσειρά ή λίστα μπορούμε να δημιουργήσουμε νέες πλειάδες.Προσέξτε το αποτέλεσμα όταν ορίζουμε σαν παράμετρο συμβολοσειρά στην συνάρτηση *tuple*.<jupyter_code>regions= tuple("Κρήτη")
print(regions)
regions= tuple(["Κρήτη", "Ήπειρος", "Θράκη"])
print(regions)<jupyter_output>('Κρήτη', 'Ήπειρος', 'Θράκη')<jupyter_text>Με κενό όρισμα επιστρέφεται μία άδεια λίστα:<jupyter_code>regions= tuple()<jupyter_output><empty_output><jupyter_text>Όπως προαναφέρθηκε οι πλειάδες είνα αμετάβλητες δομές δεδομένων και έτσι δεν μπορούμε να χρησιμοποιήσουμε μεθόδους όπως *append*, *del*, *sort* κ.α.Μπορούμε, όπως και στις λίστες, να αναφερθούμε στα στοιχεία της πλειάδας με τους τελεστές [] και με το ευρετήριο (index) ή να εξάγουμε τμήμα από τα στοιχεία (slice) με τον τελεστή :.Λαμβάνουμε το πλήθος των στοιχείων της πλειάδας (lengh) με την συνάρτηση *len*.Οι πλειάδες χρησιμοποιούνται για να πακετάρουμε μια συλλογή δεδομένων σε ένα αντικείμενο (tuple packing) π.χ.<jupyter_code>fruit1 = "Μήλος"
fruit2 = "Πορτοκάλι"
fruit1 = "Μανταρίνι"
fruits = fruit1, fruit1, fruit2 # tuple packing
print(fruits)<jupyter_output>('Μανταρίνι', 'Μανταρίνι', 'Πορτοκάλι')<jupyter_text>Βέβαια υπάρχει και η αντίστροφη διαδικασία, το "ξε-πακετάρισμα" δεδομένων (tuple unpacking). Όταν δηλαδή αποδίδουμε τα στοιχεία της πλειάδας σε ξεχωριστές μεταβλητές.<jupyter_code>cities = "Αθήνα", "Βόλος", "Πάτρα"
capital, city1, city2 = cities # tuple packing
print(capital,  city1, city2)<jupyter_output>Αθήνα Βόλος Πάτρα<jupyter_text>Ο αμετάβλητος χαρακτήρας των πλειάδων φαίνεται στα παρακάτω παραδείγμα στα οποία επιχειρείται η τροποποίηση των δεδομένων της.<jupyter_code>cities = ("Αθήνα", "Βόλος", "Πάτρα")
cities.pop(1)
cities.append("Ρόδος")<jupyter_output><empty_output><jupyter_text>Όπως μας ενημερώνει και το σχετικό σφάλμα εκτέλεσης δεν υπάρχουν οι σχετικές μέθοδοι για αντικείμενα της κλάσης *tuple*.Αντίστοιχο σφάλμα λαμβάνουμε και με τον παρακάνω κώδικα όταν πάμε να τροποποιήσουμε ένα στοιχείο της πλειάδας:<jupyter_code>mytuple = (1, 2, 3)
mytuple[0] = 999<jupyter_output><empty_output><jupyter_text>```{admonition} Tip:class: tipΜια χρήσιμη λειτουργία των πλειάδων είναι η χρήση τους κατά την αντιμετάθεση δύο μεταβλητών.Δείτε το παρακάτω παράδειγμα.```<jupyter_code>island='Λέσβος'
island2='Χίος'
island, island2 = island2, island

print(island)
print(island2)<jupyter_output>Χίος
Λέσβος<jupyter_text>```{warning} Προσοχή!Αν και οι πλειάδες είναι αμετάβλητος τύπος δεδομένων ωστόσο στα στοιχεία τους μπορούν να περιλαμβάνουν μεταβλητούς τύπους δεδομένων.Αυτό σημαίνει ότι μπορούμε να τροποποιήσουμε τα στοιχεία αυτά. Παρακάτω δίνεται ένα παράδειγμα.```<jupyter_code>cities = ("Αθήνα", ["Βόλος", "Πάτρα"])
cities[1][0] = "Καβάλα"
print(cities)<jupyter_output>('Αθήνα', ['Καβάλα', 'Πάτρα'])<jupyter_text>Μια εξήγηση σε αυτήν την αντιφατική συμπεριφορά δίνεται στο παρακάτω νήμα:[https://stackoverflow.com/questions/9755990/why-can-tuples-contain-mutable-items](https://stackoverflow.com/questions/9755990/why-can-tuples-contain-mutable-items) Λεξικά (Dictionaries)Τα λεξικά στην Python αποτελούν συλλογές αντικειμένων, όπως οι λίστες και οι πλειάδες.Ένα σημαντικό χαρακτηριστικό των λεξικών είναι ότι αυτά αποθηκεύουν δεδομένα κατά ζεύγη, με την μορφή κλειδί-τιμή (key-value pairs).Κάθε κλειδί σε ένα λεξικό συνοδεύεται από μία τιμή. Κάθε κλειδί αποτελεί ουσιαστικά ένα μοναδικό αναγνωριστικό για την συνοδευτική τιμή και γι'αυτό τον λόγο δεν μπορεί να υπάρξει δεύτερο ίδιο κλειδί.Ακόμα, τα κλειδιά πρέπει να ορίζονται από αμετάβλητους τύπους δεδομένων δηλαδή είτε από μιά συμβολοσειρά είτε από έναν ακέραιο ή δεκαδικό. Δεν μπορεί όμως μια λίστα να είναι κλειδί. Μια πλειάδα μπορεί να είναι κλειδί αλλά με την προϋπόθεση ότι και αυτή δεν θα αποτελείται από μεταβλητούς τύπους δεδομένων.Τα λεξικά περικλείονται σε {}, τα ζεύγη ορίζονται υπό την μορφή *κλειδί:τιμή* και χωρίζονται μεταξύ τους με κόμμα (,) δηλαδή:d = {key1 : value1, key2 : value2 } Για παράδειγμα:<jupyter_code>phones ={"Χρήστος": "69936565", "Κώστας":"246541353", "Βαγγέλης":"546546536"}
print(phones)<jupyter_output>{'Χρήστος': '69936565', 'Κώστας': '246541353', 'Βαγγέλης': '546546536'}<jupyter_text>Τα κλειδιά (keys) σε αυτήν την περίπτωση είναι τα ονόματα και οι αριθμοί τηλεφώνων (ως συμβολοσειρές ορισμένες) οι τιμές (values).Εναλλακτικά μπορούμε να δημιουργήσουμε ένα άδειο λεξικό και να προσθέσουμε στην συνέχεια ζεύγη.<jupyter_code>phones ={}
phones["Χρήστος"] ="69936565"
phones["Κώστας"]  ="246541353"
phones["Βαγγέλης"]="546546536"<jupyter_output><empty_output><jupyter_text>Ένας άλλος τρόπος δημιουργίας λεξικών είναι με την συνάρτηση *dict*.<jupyter_code>phones =dict(Χρήστος="69936565",Κώστας="246541353", Βαγγέλης="546546536")<jupyter_output><empty_output><jupyter_text>Με αυτόν τον τρόπο ορίζουμε τα κλειδιά μέσω μεταβλητών και γι' αυτό τον λόγο εφαρμόζονται οι περιορισμοί που αφορούν την ονοματολογία των μεταβλητών.Στα λεξικά τα ζεύγη αυτά δεν ταξινομούνται με κάποια συγκεκριμένη σειρά αλλά με έναν μηχανισμό της Python που λέγεται *hashing* και αποσκοπεί στην γρήγορη ανάκτηση τους.Η ταξινόμηση αυτή αλλάζει κάθε φορά κατά τυχαίο τρόπο όταν τροποποιούμε ένα λεξικό.Για τον λόγο αυτό δεν υπάρχει η έννοια της θέσης ή του δείκτη (index) όπως στις λίστες και στις πλειάδες. Η ανάκτηση μιας τιμής από ένα ζεύγος γίνεται με βάση τo κλειδί π.χ.* d['a_key']*.Γίνεται δηλαδή με παρόμοιο τρόπο όπως στις λίστες και τις πλειάδες αλλά αντι για το ευρετήριο θέσης ορίζουμε το κλειδί.<jupyter_code>print(phones["Χρήστος"])
print(phones["Βαγγέλης"])<jupyter_output>69936565
546546536<jupyter_text>Όπως προαναφέρθηκε δεν μπορούμε να έχουμε διπλό κλειδί σε ένα λεξικό. Έτσι αν επιχειρήσουμε να προσθέσουμε ένα ζεύγος με υφιστάμενο κλειδί στην πράξη αυτό που θα γίνει είναι να αντικαταστήσουμε την παλιά τιμή με μια νέα:<jupyter_code>print(phones["Βαγγέλης"])
phones["Βαγγέλης"]="666666666"
print(phones["Βαγγέλης"])<jupyter_output>546546536
666666666<jupyter_text>Για την διαγραφή ενός ζεύγους από λεξικό χρησιμοποιείται η συνάρτηση *del* δηλ. del(d['key']). Δείτε το επόμενο παράδειγμα:<jupyter_code>del(phones["Βαγγέλης"])
print(phones)<jupyter_output>{'Χρήστος': '69936565', 'Κώστας': '246541353'}<jupyter_text>Με την συνάρτηση *len* επιστρέφεται το μέγεθος του λεξικού δηλαδή το πλήθος των ζευγαριών κλειδιών/τιμών που περιέχει:<jupyter_code>print(len(phones))<jupyter_output>2<jupyter_text>Μέσω του τελεστή *in * διαπιστώνεται αν υπάρχει ένα κλειδί σε ένα λεξικό:<jupyter_code>print("Χρήστος" in  phones)<jupyter_output>True<jupyter_text>Με την μέθοδο *keys* ενός λεξικού επιστρέφονται τα κλειδιά του.Αντίστοιχα με την μέθοδο *values* επιστρέφονται οι τιμές του ενώ με την μέθοδο items επιστρέφονται τα ζεύγη κλειδιών/τιμών.Τα επιστρεφόμενα objects είναι αντιστοιχα *dict_keys*, *dict_values*,  *dict_items*.Ο παρακάτω κώδικας περιγράφει τις παραπάνω λειτουργίες:<jupyter_code>d = {'a': 10, 'b': 20, 'c': 30}
print(d.keys())
print(d.values())
print(d.items())<jupyter_output>dict_keys(['a', 'b', 'c'])
dict_values([10, 20, 30])
dict_items([('a', 10), ('b', 20), ('c', 30)])<jupyter_text>Αν θέλουμε να ταξινομήσουμε το λεξικό με βάση τα κλειδιά τότε το κάνουμε με την συνάρτηση *sorted*:<jupyter_code>thisdict = {
  "year": 1964,
  "brand": "Ford",
  "model": "Mustang"
}

print(sorted(thisdict))<jupyter_output>['brand', 'model', 'year']<jupyter_text>Πολύ συνηθισμένη περίπτωση είναι τα λεξικά να περιλαμβάνουν σαν τιμές άλλα λεξικά.<jupyter_code>contacts = {"Χρήστος": {"Σπίτι": "457456456", "Εργασία": "48856"}, 
            "Γιάννης": {"Σπίτι": "8753778", "Εργασία": "45654656"}, 
            "Κώστας": {"Κινητό": "45475354"}}<jupyter_output><empty_output><jupyter_text>Σε αυτή την περίπτωση ορίζοντας διαδοχικά τα κλειδιά παίρνουμε τις τιμές που επιθυμούμε:<jupyter_code>print(contacts["Χρήστος"])
print(contacts["Χρήστος"]["Εργασία"])<jupyter_output>{'Σπίτι': '457456456', 'Εργασία': '48856'}
48856<jupyter_text>Μπορούμε να διατρέξουμε τα ζεύγη ενός λεξικού αλλά η σειρά που θα γίνεται η ανάκτηση μπορεί να μην είναι με την σειρά που ορίζονται και όχι πάντα η ίδια.<jupyter_code>for phone in phones:
    print(phones[phone])<jupyter_output>69936565
246541353<jupyter_text>Όπως και με τις λίστες έτσι και με τα λεξικά πρέπει να είμαστε προσεκτικοί όταν αντιγράφουμε ένα λεξικό.Η αντιγραφή μπορεί να αναφέρεται στο ίδιο αντικείμενο και τροποποίηση των δεδομένων ενός λεξικού θα επιφέρει και την αντίστοιχη τροποποίηση στο άλλο.<jupyter_code>names1 = {'name':"Κώστας"}
names2=names1 
names2['name']="Γιάννης"
print(names1)<jupyter_output>{'name': 'Γιάννης'}<jupyter_text>Σε αυτή την περίπτωση χρησιμοποιείται η μέθοδος *copy* για την αντιγραφή των δεδομένων ενός λεξικού σε ένα άλλο.<jupyter_code>names1 = {'name':"Κώστας"}
names2=names1.copy()
names2['name']="Γιάννης"
print(names1)<jupyter_output>{'name': 'Κώστας'}
{'fraction_non_alphanumeric': 0.09181365897783808, 'fraction_numerical': 0.029127091813658978, 'mean_word_length': 7.238450074515648, 'pattern_counts': {'":': 14, '<': 96, '<?xml version=': 0, '>': 96, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '28', 'dir': 'jupyter-structured-clean-dedup', 'id': '159841', 'license': 'MIT', 'n_tokens_mistral': 8937, 'n_tokens_neox': 5880, 'n_words': 1163, 'path': '_build/jupyter_execute/notebooks/Lesson_4.ipynb', 'repo_name': 'kokkytos/programming', 'size': '11055'}