Itt vagy: Kezdőlap Ugorj fejest a Python 3-ba

Nehézségi szint: ♦♦♦♦♢

Speciális iterátorok

A nagy bolhák hátán kisebb bolhák lakmároznak,
A kisebb bolhák hátán pedig még kisebb bolhák, és így tovább a végtelenségig.
– Augustus De Morgan

 

Ugorj fejest

Ahogyan a reguláris kifejezések felturbózzák a karakterláncokat, az itertools modul az iterátorokat turbózza fel. De előbb szeretnék mutatni egy klasszikus, angol nyelvű rejtvényt.

HAWAII + IDAHO + IOWA + OHIO == STATES
510199 + 98153 + 9301 + 3593 == 621246

H = 5
A = 1
W = 0
I = 9
D = 8
O = 3
S = 6
T = 2
E = 4

Az ilyen rejtvényeket kriptaritmusoknak vagy alfametikáknak nevezik. A betűk valódi szavakká állnak össze, de ha az egyes betűket egy 0–9 közti számmal helyettesíted, akkor egy aritmetikai egyenlet áll elő. A feladat a betűkhöz tartozó számjegyek megtalálása. Az egyes betűk minden előfordulása ugyanannak a számjegynek felel meg, a számjegyek nem ismétlődhetnek, és egyik „szó” sem kezdődhet a 0 számjeggyel.

Ebben a fejezetben egy hihetetlen Python programba ugrunk fejest, amelyet eredetileg Raymond Hettinger írt. Ez a program alfametikus feladatokat old meg összesen 14 sor kóddal.

[az alphametics.py letöltése]

import re
import itertools

def solve(puzzle):
    words = re.findall('[A-Z]+', puzzle.upper())
    unique_characters = set(''.join(words))
    assert len(unique_characters) <= 10, 'Too many letters'
    first_letters = {word[0] for word in words}
    n = len(first_letters)
    sorted_characters = ''.join(first_letters) + \
        ''.join(unique_characters - first_letters)
    characters = tuple(ord(c) for c in sorted_characters)
    digits = tuple(ord(c) for c in '0123456789')
    zero = digits[0]
    for guess in itertools.permutations(digits, len(characters)):
        if zero not in guess[:n]:
            equation = puzzle.translate(dict(zip(characters, guess)))
            if eval(equation):
                return equation

if __name__ == '__main__':
    import sys
    for puzzle in sys.argv[1:]:
        print(puzzle)
        solution = solve(puzzle)
        if solution:
            print(solution)

A programot a parancssorból futtathatod. Linuxon ez így néz ki. (A program futása eltarthat egy ideig, a számítógéped sebességétől függően, és nincs folyamatjelző. Légy türelmes!)

te@localhost:~/diveintopython3/examples$ python3 alphametics.py "HAWAII + IDAHO + IOWA + OHIO == STATES"
HAWAII + IDAHO + IOWA + OHIO = STATES
510199 + 98153 + 9301 + 3593 == 621246
te@localhost:~/diveintopython3/examples$ python3 alphametics.py "I + LOVE + YOU == DORA"
I + LOVE + YOU == DORA
1 + 2784 + 975 == 3760
te@localhost:~/diveintopython3/examples$ python3 alphametics.py "SEND + MORE == MONEY"
SEND + MORE == MONEY
9567 + 1085 == 10652

Egy minta összes előfordulásának megkeresése

A program első lépésként megkeresi a feladvány összes betűjét (A–Z).

>>> import re
>>> re.findall('[0-9]+', '16 2-by-4s in rows of 8')  
['16', '2', '4', '8']
>>> re.findall('[A-Z]+', 'SEND + MORE == MONEY')     
['SEND', 'MORE', 'MONEY']
  1. A re modul a Python reguláris kifejezés megvalósítása. Ez tartalmazza a findall() nevű függvényt, amely egy reguláris kifejezésként megadott mintát és egy karakterláncot vár, és megkeresi a minta összes előfordulását a karakterláncban. Ebben az esetben a minta számok sorozatára illeszkedik. A findall() függvény a mintára illeszkedő összes részkarakterlánc listáját adja vissza.
  2. Itt a reguláris kifejezés minta betűk sorozatára illeszkedik. A visszatérési érték ismét egy lista, és minden eleme a reguláris kifejezés mintára illeszkedő karakterlánc.

A következő példa is egy pici gondolkodásra fog késztetni.

>>> re.findall(' s.*? s', "The sixth sick sheikh's sixth sheep's sick.")
[' sixth s', " sheikh's s", " sheep's s"]

Meglepődtél? A reguláris kifejezés egy szóközt, egy s betűt, majd tetszőleges karakterek lehetséges legrövidebb sorozatát (.*?), majd egy szóközt, és egy újabb s betűt keres. Nos, a bemeneti karakterláncra nézve, öt találatot látok:

  1. The sixth sick sheikh's sixth sheep's sick.
  2. The sixth sick sheikh's sixth sheep's sick.
  3. The sixth sick sheikh's sixth sheep's sick.
  4. The sixth sick sheikh's sixth sheep's sick.
  5. The sixth sick sheikh's sixth sheep's sick.

Azonban a re.findall() függvény csak három találatot adott vissza. Ezen belül az első, harmadik és ötödik találatot adta vissza. Miért van ez így? Mert nem adja vissza az egymást átfedő találatokat. Az első találat átfedi a másodikat, így az első visszaadásra kerül, a második pedig kimarad. Ezután a harmadik átfedi a negyediket, így a harmadik visszaadásra kerül, a negyedik pedig kimarad. Végül az ötödik is visszaadásra kerül. Három találat, nem öt.

Ennek semmi köze az alfametikus megoldóhoz, csak érdekesnek találom.

Egy sorozat egyedi elemeinek megkeresése

A halmazok triviálissá teszik egy sorozat egyedi elemeinek megtalálását.

>>> a_list = ['The', 'sixth', 'sick', "sheik's", 'sixth', "sheep's", 'sick']
>>> set(a_list)                      
{'sixth', 'The', "sheep's", 'sick', "sheik's"}
>>> a_string = 'EAST IS EAST'
>>> set(a_string)                    
{'A', ' ', 'E', 'I', 'S', 'T'}
>>> words = ['SEND', 'MORE', 'MONEY']
>>> ''.join(words)                   
'SENDMOREMONEY'
>>> set(''.join(words))              
{'E', 'D', 'M', 'O', 'N', 'S', 'R', 'Y'}
  1. Egy több karakterláncból álló lista esetén a set() függvény visszaadja a lista egyedi karakterláncainak halmazát. Ennek van értelme, ha úgy gondolsz rá, mint egy for ciklusra. Vegyük a lista első elemét, tegyük be a halmazba. A másodikat. A harmadikat. A negyediket. Az ötödiket – várj, ilyen már van a halmazban, így most kimarad, mert a Python nem engedélyezi a másodpéldányokat. A hatodikat. A hetediket – újra egy másodpéldány, ismét kimarad. A végeredmény? Az eredeti lista összes egyedi eleme másodpéldányok nélkül. Az eredeti listát nem is kell rendezni előtte.
  2. Ugyanez a módszer működik karakterláncokkal is, mert a karakterlánc csak karakterek sorozata.
  3. Karakterláncok listáját véve a ''.join(a_list) összefésüli az összes karakterláncot egyetlen eggyé.
  4. Karakterláncok egy listáját átadva, ez a kódsor visszaadja a karakterláncok összes egyedi karakterét, másodpéldányok nélkül.

Az alfametikus megoldó ezt a módszert használja a feladvány összes egyedi karakterét tartalmazó halmaz felépítéséhez.

unique_characters = set(''.join(words))

Ez a lista később a számjegyek karakterekhez rendelésénél lesz felhasználva, amikor a megoldó végigjárja a lehetséges megoldásokat.

Kijelentések tétele

Sok programozási nyelvhez hasonlóan a Python is rendelkezik egy assert utasítással. Így működik:

>>> assert 1 + 1 == 2                                     
>>> assert 1 + 1 == 3                                     
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError
>>> assert 2 + 2 == 5, "Csak a 2 nagyon nagy értékeihez"  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: Csak a 2 nagyon nagy értékeihez
  1. Az assert utasítást tetszőleges érvényes Python kifejezés követheti. Ebben az esetben az 1 + 1 == 2 kifejezés True-ra értékelődik ki, így az assert utasítás nem csinál semmit.
  2. Ha azonban a Python kifejezés False-ra értékelődik ki, akkor az assert utasítás AssertionError kivételt dob.
  3. Megadhatsz egy emberek által olvasható üzenetet is, amely az AssertionError dobásakor kiírásra kerül.

Emiatt ez a sor kód:

assert len(unique_characters) <= 10, 'Túl sok betű'

…ezzel egyenértékű:

if len(unique_characters) > 10:
    raise AssertionError('Túl sok betű')

Az alfametikus megoldó pontosan ezt az assert utasítást használja, hogy időben megálljon, ha a feladvány tíz egyedi betűnél többet tartalmazna. Mivel minden betűhöz egyedi számjegy kerül hozzárendelésre, és csak tíz számjegy van, a tíz betűnél többet tartalmazó feladványoknak nem lehet megoldásuk.

Generátorkifejezések

A generátorkifejezés olyan, mint egy függvény nélküli generátorfüggvény.

>>> unique_characters = {'E', 'D', 'M', 'O', 'N', 'S', 'R', 'Y'}
>>> gen = (ord(c) for c in unique_characters)  
>>> gen                                        
<generator object <genexpr> at 0x00BADC10>
>>> next(gen)                                  
69
>>> next(gen)
68
>>> tuple(ord(c) for c in unique_characters)   
(69, 68, 77, 79, 78, 83, 82, 89)
  1. Egy generátorkifejezés olyan, mint egy névtelen függvény, amely értékeket ad vissza. Maga a kifejezés úgy néz ki, mint egy listafeldolgozó, de szögletes zárójelek helyett zárójelek közt van.
  2. A generátorkifejezés egy… iterátort ad vissza.
  3. A next(gen) hívása az iterátorból származó következő értéket adja vissza.
  4. Ha szeretnéd, végiglépkedhetsz az összes lehetséges értéken, és visszaadhatsz egy tuplet, listát vagy halmazt a generátorkifejezés átadásával a tuple(), list() vagy set() függvénynek. Ezekben az esetekben nincs szükséged extra zárójelekre – csak add át a „nyers” ord(c) for c in unique_characters kifejezést a tuple() függvénynek, és a Python megállapítja, hogy ez egy generátorkifejezés.

Listafeldolgozó helyett generátorkifejezést használva egyaránt spórolhatsz a CPU és a RAM használatán is. Ha csak azért építesz egy listát, hogy rögtön eldobhasd (például a tuple() vagy set() függvénynek átadva), használj generátorkifejezést.

Ugyanezt máshogy is elérheted egy generátorfüggvény használatával:

def ord_map(a_string):
    for c in a_string:
        yield ord(c)

gen = ord_map(unique_characters)

A generátorkifejezés tömörebb, de funkcionálisan egyenértékű.

Permutációk kiszámítása… lusta módra!

Először is, mi a csuda az a permutáció? A permutáció egy matematikai fogalom. (Valójában több meghatározás van, az épp használt matematikai rendszertől függően. Itt a kombinatorikáról van szó, de ha ez nem jelent neked semmit, akkor se aggódj. Mint mindig, a Wikipédia a barátod.)

Az alapötlet a következő: veszed elemek (számok, betűk, táncoló medvék) listáját, és az összes lehetséges módon felosztod kisebb listákra. Az összes kisebb lista azonos méretű, akár egy eleműek is lehetnek, de az elemek teljes számával egyenlő méretűek is. Ja és egyáltalán nem lehetnek ismétlődések. A matematikusok olyanokat mondanak, mint például „keressük meg 3 különböző elem permutációit egyszerre kettőt véve”, ami azt jelenti, hogy egy 3 elemből álló listád van, és meg akarod találni az összes lehetséges rendezett párt.

>>> import itertools                              
>>> perms = itertools.permutations([1, 2, 3], 2)  
>>> next(perms)                                   
(1, 2)
>>> next(perms)
(1, 3)
>>> next(perms)
(2, 1)                                            
>>> next(perms)
(2, 3)
>>> next(perms)
(3, 1)
>>> next(perms)
(3, 2)
>>> next(perms)                                   
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
  1. Az itertools modul mindenféle jópofa dolgot tartalmaz, beleértve egy permutations() függvényt, amely elvégzi a permutációk keresésének fáradtságos munkáját.
  2. A permutations() függvény egy sorozatot (itt egy három számból álló listát) és egy számot vár, amely a kisebb csoportokba beosztandó elemek száma. A függvény egy iterátort ad vissza, amelyet felhasználhatsz egy for ciklusban vagy bármely más helyen, ami iterál. Itt saját kezűleg lépkedek végig az iterátoron az értékek megjelenítéséhez.
  3. Az [1, 2, 3] első permutációja egyszerre 2 elemet véve az (1, 2).
  4. Figyeld meg, hogy a permutációk rendezettek: a (2, 1) eltér az (1, 2)-től.
  5. Ennyi az egész! Ez az [1, 2, 3] összes permutációja egyszerre két elemet véve. Az (1, 1) és (2, 2) párok soha nem jelennek meg, mert ismétléseket tartalmaznak, így nem érvényes permutációk. Ha nincs több permutáció, akkor az iterátor egy StopIteration kivételt dob.

A permutations() függvénynek nem kötelező listát átadni. Tetszőleges sorozatot kaphat – akár egy karakterláncot is.

>>> import itertools
>>> perms = itertools.permutations('ABC', 3)  
>>> next(perms)
('A', 'B', 'C')                               
>>> next(perms)
('A', 'C', 'B')
>>> next(perms)
('B', 'A', 'C')
>>> next(perms)
('B', 'C', 'A')
>>> next(perms)
('C', 'A', 'B')
>>> next(perms)
('C', 'B', 'A')
>>> next(perms)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> list(itertools.permutations('ABC', 3))    
[('A', 'B', 'C'), ('A', 'C', 'B'),
 ('B', 'A', 'C'), ('B', 'C', 'A'),
 ('C', 'A', 'B'), ('C', 'B', 'A')]
  1. Egy karakterlánc csupán karakterek sorozata. A permutációk keresése szempontjából az 'ABC' egyenértékű a ['A', 'B', 'C'] listával.
  2. A 3 elem első permutációja ['A', 'B', 'C'] egyszerre 3 elemet véve ('A', 'B', 'C'). Öt másik permutáció is van – ugyanaz a három karakter minden elképzelhető sorrendben.
  3. Mivel a permutations() függvény mindig egy iterátort ad vissza, a permutációk hibakeresése egyszerűen megoldható az iterátor átadásával a beépített list() függvénynek, így az összes permutáció azonnal megjeleníthető.

További menő dolgok az itertools modulban

>>> import itertools
>>> list(itertools.product('ABC', '123'))   
[('A', '1'), ('A', '2'), ('A', '3'), 
 ('B', '1'), ('B', '2'), ('B', '3'), 
 ('C', '1'), ('C', '2'), ('C', '3')]
>>> list(itertools.combinations('ABC', 2))  
[('A', 'B'), ('A', 'C'), ('B', 'C')]
  1. Az itertools.product() függvény a két sorozat Descartes-szorzatát tartalmazó iterátort ad vissza.
  2. Az itertools.combinations() függvény az adott sorozat összes adott hosszú kombinációját adja vissza. Ez olyan, mint az itertools.permutations() függvény, csak a kombinációk nem tartalmaznak olyan elemeket, amelyek ugyanazon elemek más sorrendben lévő másodpéldányai. Így az itertools.permutations('ABC', 2) visszaadja mind az ('A', 'B') és ('B', 'A') elemeket (többek közt), de az itertools.combinations('ABC', 2) nem adja vissza a ('B', 'A') elemet, mert az az ('A', 'B') másodpéldánya más sorrendben.

[a favorite-people.txt letöltése]

>>> names = list(open('examples/favorite-people.txt', encoding='utf-8'))  
>>> names
['Dora\n', 'Ethan\n', 'Wesley\n', 'John\n', 'Anne\n',
'Mike\n', 'Chris\n', 'Sarah\n', 'Alex\n', 'Lizzie\n']
>>> names = [name.rstrip() for name in names]                             
>>> names
['Dora', 'Ethan', 'Wesley', 'John', 'Anne',
'Mike', 'Chris', 'Sarah', 'Alex', 'Lizzie']
>>> names = sorted(names)                                                 
>>> names
['Alex', 'Anne', 'Chris', 'Dora', 'Ethan',
'John', 'Lizzie', 'Mike', 'Sarah', 'Wesley']
>>> names = sorted(names, key=len)                                        
>>> names
['Alex', 'Anne', 'Dora', 'John', 'Mike',
'Chris', 'Ethan', 'Sarah', 'Lizzie', 'Wesley']
  1. Ez a kifejezés a szövegfájl sorainak listáját adja vissza.
  2. Sajnos (jelen példa szempontjából) a list(open(filename)) kifejezés tartalmazza a sorok végén lévő kocsivissza karaktert is. Ez a listafeldolgozó az rstrip() karakterlánc-metódust használja a záró üres hely eltávolítására a karakterláncok végéről. (A karakterláncok rendelkeznek egy lstrip() metódussal is a kezdő üres hely eltávolításához, és egy strip() metódussal mindkettő eltávolításához.)
  3. A sorted() függvény egy listát vár, és rendezve adja vissza. Alapértelmezésben ábécérendbe rendez.
  4. De a sorted() függvény kaphat függvényt is a key paraméteren keresztül, ekkor aszerint rendez. Ebben az esetben a rendezési függvény a len(), így a len(minden elem) szerint rendez. A rövidebb nevek jönnek előre, aztán a hosszabbak, végül a leghosszabbak.

Mi köze van ennek az itertools modulhoz? Örülök, hogy megkérdezted!

…folytatás az előző interaktív parancsértelmezőből…
>>> import itertools
>>> groups = itertools.groupby(names, len)  
>>> groups
<itertools.groupby object at 0x00BB20C0>
>>> list(groups)
[(4, <itertools._grouper object at 0x00BA8BF0>),
 (5, <itertools._grouper object at 0x00BB4050>),
 (6, <itertools._grouper object at 0x00BB4030>)]
>>> groups = itertools.groupby(names, len)   
>>> for name_length, name_iter in groups:    
...     print('{0:d} betűs nevek:'.format(name_length))
...     for name in name_iter:
...         print(name)
... 
4 betűs nevek:
Alex
Anne
Dora
John
Mike
5 betűs nevek:
Chris
Ethan
Sarah
6 betűs nevek:
Lizzie
Wesley
  1. Az itertools.groupby() függvény egy sorozatot és egy kulcsfüggvényt vár, és visszaad egy iterátort, amely párokat generál. Minden pár a kulcsfüggvény(minden elem) eredményét, és egy másik iterátort tartalmaz, amely az összes azonos kulcsú elemet tartalmazza.
  2. A list() függvény hívása „kimerítette” az iterátort, azaz előállítottad az iterátor minden elemét a lista létrehozásához. Nincs „újraindítás” gomb az iterátorokon, az egyszer már kimerített iterátoron nem kezdhetsz újra végigjárni. Ha újra végig szeretnél rajta járni (mondjuk a soron következő for ciklusban), akkor újra meg kell hívnod az itertools.groupby() metódust egy új iterátor létrehozásához.
  3. Ebben a példában a már hossz szerint rendezett névlistát tekintve az itertools.groupby(names, len) az összes 4 betűs nevet egy iterátorba teszi, az összes 5 betűset egy másikba és így tovább. A groupby() függvény teljesen általános, csoportosíthatja a karakterláncokat az első betű szerint, a számokat a tényezőik száma szerint vagy tetszőleges kulcsfüggvény szerint, amit csak el tudsz képzelni.

Az itertools.groupby() függvény csak akkor működik, ha a bemeneti sorozat már rendezve van a csoportosító függvény által. A fenti példában a nevek listáját a len() függvénnyel csoportosítottad. Ez csak azért működött, mert a bemenet már rendezve volt hossz szerint.

Nagyon figyelsz?

>>> list(range(0, 3))
[0, 1, 2]
>>> list(range(10, 13))
[10, 11, 12]
>>> list(itertools.chain(range(0, 3), range(10, 13)))        
[0, 1, 2, 10, 11, 12]
>>> list(zip(range(0, 3), range(10, 13)))                    
[(0, 10), (1, 11), (2, 12)]
>>> list(zip(range(0, 3), range(10, 14)))                    
[(0, 10), (1, 11), (2, 12)]
>>> list(itertools.zip_longest(range(0, 3), range(10, 14)))  
[(0, 10), (1, 11), (2, 12), (None, 13)]
  1. Az itertools.chain() függvény két iterátort vár, és egy olyan iterátort ad vissza, amely az első elemeit, majd azok után a második elemeit tartalmazza. (Valójában tetszőleges számú iterátort át lehet neki adni, hogy aztán a függvénynek való átadásuk sorrendjében összefűzze azokat.)
  2. A zip() függvény egy teljesen hétköznapi dolgot tesz, amely azonban különösen hasznosnak fog bizonyulni: tetszőleges számú sorozatot vár, és visszaad egy iterátort, amely az egyes sorozatokból az első, második, harmadik stb. elemeket tartalmazó tuple-kat ad vissza.
  3. A zip() függvény a legrövidebb sorozat végén áll meg. A range(10, 14) 4 elemet (10, 11, 12 és 13) tartalmaz, de a range(0, 3) csak hármat, így a zip() függvény egy 3 elemű iterátort ad vissza.
  4. Másrészről az itertools.zip_longest() függvény a leghosszabb sorozat végén áll meg, None értékeket szúrva be a rövidebb sorozatok vége utáni elemekhez.

Oké, ez mind nagyon érdekes, de mi köze van az alfametikus megoldóhoz? A következő:

>>> characters = ('S', 'M', 'E', 'D', 'O', 'N', 'R', 'Y')
>>> guess = ('1', '2', '0', '3', '4', '5', '6', '7')
>>> tuple(zip(characters, guess))  
(('S', '1'), ('M', '2'), ('E', '0'), ('D', '3'),
 ('O', '4'), ('N', '5'), ('R', '6'), ('Y', '7'))
>>> dict(zip(characters, guess))   
{'E': '0', 'D': '3', 'M': '2', 'O': '4',
 'N': '5', 'S': '1', 'R': '6', 'Y': '7'}
  1. Ha adva van egy betűkből és egy számokból álló lista (itt mindkettő 1 karakterből álló karakterláncként jelenik meg), akkor a zip függvény létrehozza a betűk és számjegyek sorba rendezett párjait.
  2. Miért menő ez? Mert ez az adatszerkezet pontosan megfelel a dict() függvénynek való átadáshoz, hogy az létrehozzon egy szótárat, amelynek kulcsai betűk, értékei pedig számjegyek. (Ez természetesen nem az egyetlen megoldás. Használhatnál szótárfeldolgozót is a szótár közvetlen létrehozásához.) Noha a szótár kiírt ábrázolása a párokat eltérő sorrendben sorolja fel (a szótárak maguk nem rendelkeznek „sorrenddel”), azért láthatod, hogy minden betű egy számjegyhez tartozik, az eredeti characters és guess sorozatok sorrendje alapján.

Az alfametikus megoldó ezt a módszert használja egy szótár létrehozásához, amely a feladvány betűit leképezi a megoldás számjegyeire, minden lehetséges megoldás esetén.

characters = tuple(ord(c) for c in sorted_characters)
digits = tuple(ord(c) for c in '0123456789')
...
for guess in itertools.permutations(digits, len(characters)):
    ...
    equation = puzzle.translate(dict(zip(characters, guess)))

De mi az a translate() metódus? Ó, most jutunk el az igazán vidám részhez.

Egy új karakterlánc-manipulálási módszer

A Python karakterláncok sok metódussal rendelkeznek. Ezen metódusok némelyikével a Karakterláncok fejezetben már megismerkedtél: lower(), count() és format(). Most szeretnék megmutatni egy hatékony, de kevéssé ismert karakterlánc-manipulálási módszert: a translate() metódust.

>>> translation_table = {ord('A'): ord('O')}  
>>> translation_table                         
{65: 79}
>>> 'MARK'.translate(translation_table)       
'MORK'
  1. A karakterláncok lefordítása egy fordítási táblával kezdődik, ez egy szótár, amely egy karaktert egy másikra képez le. Valójában a „karakter” kifejezés helytelen – a fordítási tábla egy bájtot képez le egy másikra.
  2. Emlékezz, a bájtok a Python 3-ban egész számok. Az ord() függvény visszaadja egy karakter ASCII értékét, amely az A–Z esetén mindig egy 65 és 90 közti bájt.
  3. Egy karakterlánc translate() metódusa egy fordítási táblát vár, és átfuttatja rajta a karakterláncot. Azaz a fordítási tábla kulcsainak minden előfordulását lecseréli a megfelelő értékre. Ebben az esetben „lefordítja” a MARK szót MORK-ra.

Mi köze van ennek az alfametikus feladványok megoldásához? Amint kiderül, rengeteg.

>>> characters = tuple(ord(c) for c in 'SMEDONRY')       
>>> characters
(83, 77, 69, 68, 79, 78, 82, 89)
>>> guess = tuple(ord(c) for c in '91570682')            
>>> guess
(57, 49, 53, 55, 48, 54, 56, 50)
>>> translation_table = dict(zip(characters, guess))     
>>> translation_table
{68: 55, 69: 53, 77: 49, 78: 54, 79: 48, 82: 56, 83: 57, 89: 50}
>>> 'SEND + MORE == MONEY'.translate(translation_table)  
'9567 + 1085 == 10652'
  1. Egy generátorkifejezés használatával gyorsan kiszámítjuk a karakterlánc minden egyes karakterének bájtértékét. A characters tuple a sorted_characters értékét példázza az alphametics.solve() függvényben.
  2. Egy másik generátorkifejezés használatával gyorsan kiszámítjuk a karakterlánc számjegyeinek bájtértékeit. Az eredményként kapott guess tuple az alphametics.solve() függvényben az itertools.permutations() függvény által visszaadott formátumú.
  3. Ezt a fordítási táblát a characters és guess zip metódussal való egyesítése és az eredményül kapott párok sorozatából egy szótár összeállításával kapjuk. Ez pontosan ugyanaz, mint amit az alphametics.solve() függvény csinál a for cikluson belül.
  4. Végül átadjuk ezt a fordítási táblát az eredeti feladvány karakterlánc translate() metódusának. Ez a karakterlánc minden betűjét átalakítja a megfelelő számjeggyé (a characters betűi és a guess számjegyei alapján). Az eredmény egy érvényes Python kifejezés, karakterláncként.

Ez meglehetősen meggyőző. De mit lehet kezdeni egy karakterlánccal, amely egyúttal érvényes Python kifejezés is?

Tetszőleges karakterláncok kiértékelése Python kifejezésekként

Ez a feladvány utolsó darabja (vagy inkább a feladványmegoldó utolsó darabja). A rengeteg karakterlánc-manipuláció után egy ilyen karakterláncot kapsz: '9567 + 1085 == 10652'. De ez egy karakterlánc, és mire jó egy karakterlánc? Most lép színre az eval(), az univerzális Python kiértékelési eszköz.

>>> eval('1 + 1 == 2')
True
>>> eval('1 + 1 == 3')
False
>>> eval('9567 + 1085 == 10652')
True

De várj, van még más is! Az eval() függvény nincs logikai kifejezésekre korlátozva. Képes kezelni bármely Python kifejezést és tetszőleges adattípust adhat vissza.

>>> eval('"A" + "B"')
'AB'
>>> eval('"MARK".translate({65: 79})')
'MORK'
>>> eval('"AAAAA".count("A")')
5
>>> eval('["*"] * 5')
['*', '*', '*', '*', '*']

De várj, ez nem minden!

>>> x = 5
>>> eval("x * 5")         
25
>>> eval("pow(x, 2)")     
25
>>> import math
>>> eval("math.sqrt(x)")  
2.2360679774997898
  1. Az eval() által kapott kifejezés az eval() híváson kívül definiált globális változókra is hivatkozhat. Ha függvényen belül hívod, akkor helyi változókra is hivatkozhat.
  2. És függvényekre is.
  3. És modulokra is.

Hé, várj egy percet…

>>> import subprocess
>>> eval("subprocess.getoutput('ls ~')")                  
'Asztal         Gyűjtemény         Képek \
 Dokumentumok       Videók          Nyilvános   \
 Zenék           Oldalak'
>>> eval("subprocess.getoutput('rm /valami/véletlen/fájl')")  
  1. A subprocess modul lehetővé teszi a parancsértelmező tetszőleges parancsainak futtatását, és az eredmény lekérését Python karakterláncként.
  2. A parancsértelmező bizonyos parancsainak futtatása végzetes következményekkel járhat.

Ennél is rosszabb a helyzet, mert van egy globális __import__() függvény, amely egy modulnevet vár karakterláncként, importálja a modult, és visszaad egy hivatkozást rá. Az eval() erejével egyesítve előállíthatsz egy olyan kifejezést, amely letörli az összes fájlod:

>>> eval("__import__('subprocess').getoutput('rm /valami/véletlen/fájl')")  
  1. Most képzeld el az 'rm -rf ~' kimenetét. Valójában nem lenne kimenete, de fájljaid sem maradnának.

Az eval() GONOSZ

A gonosz rész a megbízhatatlan forrásokból származó tetszőleges kifejezések kiértékelése. Az eval() függvényt csak megbízható bemeneten szabad használni. Természetesen a trükk a „megbízható” bemenet meghatározása. De van egy dolog, amiben biztos vagyok: ezt az alfametikus megoldót NEM szabad kitenni az internetre egy vicces kis webszolgáltatásként. Ne kövesd el azt a hibát, hogy úgy gondolod: „Ugyan már, a függvény rengeteg karakterlánc-manipulációt végez, mielőtt megkapja a kiértékelendő karakterláncot, nem tudom elképzelni, hogyan lehetne ezt kihasználni.” Valaki ki FOGJA találni, hogyan lehet megadni a karakterlánc-manipulációkat túlélő rosszindulatú végrehajtható kódot (furább dolgok is történtek már), és akkor elköszönhetsz a kiszolgálódtól.

De biztos van valami lehetőség a kifejezések biztonságos kiértékelésére, nem? Az eval() nem tehető be egy homokozóba, ahol nem tudja elérni vagy károsítani a külvilágot? Hát, igen is meg nem is.

>>> x = 5
>>> eval("x * 5", {}, {})               
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'x' is not defined
>>> eval("x * 5", {"x": x}, {})         
25
>>> import math
>>> eval("math.sqrt(x)", {"x": x}, {})  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'math' is not defined
  1. Az eval() függvénynek átadott második és harmadik paraméter a kifejezés kiértékeléséhez használandó globális és helyi névterekként működnek. Ebben az esetben mindkettő üres, ami azt jelenti, hogy az "x * 5" karakterlánc kiértékelésekor nincs hivatkozás az x-re a globális vagy helyi névtérben, így az eval() kivételt dob.
  2. A globális névtérbe felvehetsz egyedi értékeket azok egyenkénti felsorolásával. Ezután azok – és csak azok – elérhetők lesznek a kiértékelés során.
  3. Noha importáltad a math modult, nem vetted fel az eval() függvénynek átadott névtérbe, így a kiértékelés meghiúsult.

Hát, ez könnyű volt. Hadd csináljam meg most azt az alfametikus webszolgáltatást!

>>> eval("pow(5, 2)", {}, {})                   
25
>>> eval("__import__('math').sqrt(5)", {}, {})  
2.2360679774997898
  1. Noha csak üres szótárakat adtál át a globális és helyi névtereknek, a Python összes beépített függvénye továbbra is elérhető a kiértékelés során. Így a pow(5, 2) működik, mert az 5 és a 2 literálok, a pow() pedig beépített függvény.
  2. Sajnos (és ha nem látod miért sajnos, olvass tovább) az __import__() függvény szintén beépített, így ez is működik.

Igen, ez azt jelenti, hogy továbbra is lehet huncut dolgokat csinálni, még ha a globális és helyi névtereket kifejezetten üres szótárakra is állítottad az eval() hívásakor:

>>> eval("__import__('subprocess').getoutput('rm /valami/véletlen/fájl')", {}, {})

Hoppá. Örülök, hogy nem csináltam meg azt az alfametikus webszolgáltatást. Létezik valami módszer az eval() biztonságos használatára? Hát, igen is meg nem is.

>>> eval("__import__('math').sqrt(5)",
...     {"__builtins__":None}, {})          
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name '__import__' is not defined
>>> eval("__import__('subprocess').getoutput('rm -rf /')",
...     {"__builtins__":None}, {})          
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name '__import__' is not defined
  1. Megbízhatatlan kifejezések biztonságos kiértékeléséhez definiálnod kell egy globális névtérszótárat, amely leképezi a "__builtins__" modulnevet a Python null értékére, a None-ra. A felszín alatt a „beépített” függvényeket egy "__builtins__" nevű álmodul tartalmazza. Ez az álmodul (azaz a beépített függvények halmaza) elérhető a kiértékelt kifejezések számára, hacsak nem bírálod felül kifejezetten.
  2. Győződj meg róla, hogy a __builtins__ modult bírálod felül. Nem a __builtin__, __built-ins__ vagy valami más változatot, amely működni épp működik, de katasztrofális kockázatoknak tesz ki.

Akkor az eval() végre már biztonságos? Hát, igen is meg nem is.

>>> eval("2 ** 2147483647",
...     {"__builtins__":None}, {})          
  1. Még a __builtins__ elérése nélkül is indíthatsz egy szolgáltatás megbénításával járó támadást. Például a 2 2147483647. hatványra emelése a kiszolgálód CPU-kihasználtságát meglehetősen hosszú időre 100%-ra tornázza fel. (Ha ezt az interaktív parancsértelmezőben próbáltad ki, akkor nyomd meg párszor a Ctrl-C kombinációt a megszakításához.) Technikailag ez a kifejezés idővel vissza fog adni egy értéket, de addig a kiszolgálód rengeteg értelmetlen számolást fog végezni.

Végső soron lehetséges biztonságosan kiértékelni megbízhatatlan Python kifejezéseket, a „biztonságos” bizonyos definíciója mellett, amely a való életben nem bizonyul annyira hasznosnak. Ez rendben van, ha csak játszol, és rendben van, ha mindig csak megbízható bemenetet adsz át. De minden más csak felhívás keringőre.

Mindent összerakva

Ismételjünk: ez a program alfametikus feladványokat old meg nyers erő használatával, azaz az összes lehetséges megoldás kimerítő keresése útján. Ehhez…

  1. Megkeresi a feladvány összes betűjét a re.findall() függvénnyel
  2. Megkeresi a feladvány összes egyedi betűjét halmazokkal és set() függvénnyel
  3. Ellenőrzi, hogy 10 egyedi betűnél több van-e (ez azt jelentené, hogy a feladvány biztosan megoldhatatlan) egy assert utasítással
  4. A betűket átalakítja az ASCII megfelelőikre egy generátorobjektummal
  5. Kiszámítja az összes lehetséges megoldást az itertools.permutations() függvénnyel
  6. Minden lehetséges megoldást átalakít Python kifejezéssé a translate() karakterlánc-metódussal
  7. Teszteli az összes lehetséges megoldást a Python kifejezést kiértékelve az eval() függvénnyel
  8. Visszaadja az első megoldást, amely True-ra értékelődött ki

…mindössze 14 sor kódban.

További olvasnivaló

Hatalmas köszönet Raymond Hettingernek, amiért beleegyezett kódja újralicencelésébe, így át tudtam portolni Python 3-ra, és felhasználhattam ezen fejezet alapjául.

© 2001–11 Mark Pilgrim