Hogyan ellenőrizzük, hogy egy egész számokból álló sorozat számtani sorozatot alkot?

Számos esetben lehet szükség egy egész számokból álló sorozat feldolgozására. Az ilyen értékek nem ritkán számtani sorozat elemei lehetnek. Többek között ilyenek például egy Unicode blokk egymást követő kódpontjai, egy statisztikai adatsorban az évszámok, vagy akár a menetrendekben a percben mért indulási idők.

A kérdés az, hogy ha van egy iterálható objektum, ami egész számokat szolgáltat, hogyan bizonyosodjunk meg arról, hogy ezek a sorozatértékek számtani sorozatot alkotnak vagy sem. Ehhez célszerű felidézni a számtani sorozat definícióját és az ebből következő képleteket, amelyeket alább foglaltuk össze:

Az itt látható mindegyik összefüggés használható olyan ellenőrzőfüggvény definiálására, amely a sorozatot, illetve az azt szolgáltató iterálható objektumot fogadja, és True értékkel tér vissza, ha a sorozatértékek egy számtani sorozat elemei. E függvény megvalósítására több változatot mutatunk alább. A közös mindegyikben, hogy a differenciát az első két elemből számítják. Az ezt követő logika és implementáció azonban már eltér.

Az első három változat az első, rekurzív képletet használja, a negyedik viszont az utolsó, nem rekurzív képletet.

Az első függvényben a két szomszédos érték különbségének vizsgálata egy olyan szokásos for-ciklusban történik, amelyben az aktuálishoz képesti előző sorozatelem egy segédváltozó ciklusonkénti felülírásával van tárolva.

A második függvény hasonló elvet követ, de itt egyszerűsítjük a ciklusszervezést bevetve az itertools modul pairwise() függvényét. Ez az argumentumként megadott iterálható objektumból páronként szolgáltat értéket mégpedig úgy, hogy a következő pár első eleme az előző pár második eleme lesz. És ez a működési mód az, amely most pontosan illik a feladatunkhoz. De, ha ezért amúgy is be kell importálni az itertools modult, akkor az első két sorozatelem kinyerésére a két next() hívás helyett használjuk az islice() függvényt.

A harmadik függvényben tovább egyszerűsítjük a kódot. Továbbra is használjuk a pairwise()-t, de az eddigi explicit ciklus helyett az iteráció most az all() beépített függvény argumentumában történik, és az all() eredményével térünk vissza.

A negyedik változatban a számtani sorozat n. elemének meghatározására szolgáló nem rekurzív képletet alkalmazzuk. Az implementáció az előzőhöz hasonlóan kevés kódsort igényel, ráadásul nem szükséges az itertools modul eszköztárából semmit igénybe venni.

A példatesztek igazolják, hogy mindegyik függvény az elvárásoknak megfelelően működik.

A működéshez Python 3.10+ szükséges.

Alkalmazási példa

Az ellenőrzőfüggvények használatának bemutatására vegyünk egy alkalmazási példát. Tegyük fel, hogy valamilyen forrásból származó, és egy iterálható objektumon keresztül kikérhető egész számok sorozatát kell feldolgozni az alkalmazásunkban. Ráadásul ezt a program több pontján kell megtenni, ezért a sorozatot tárolni kell mindaddig, amíg minden feldolgozási fázis le nem fut. Ha hosszú sorozatról van szó, akkor jó lenne, ha a tárolás a legkevesebb memóriafelhasználással járna. Ezt az igényt a következő módon fogjuk teljesíteni.

Mivel egész számok sorozatáról van szó, ezért ezt kihasználhatjuk olyan módon, hogy a számokat nem list vagy tuple típusú konténerekben tartjuk, hanem számok tárolása esetén memóriatakarékosabb array típusú tömbben, amit a szabványos könyvtár array modulja kínál. E tömb létrehozásakor meg kell adni egy, a tárolni kívánt értékekhez illeszkedő egykarakteres típuskódot, amely meghatározza, hogy hány bájton lesznek tárolva az elemek. Vagyis ezáltal elérhetjük, hogy az egész számok sorozata az ábrázolásukhoz szükséges minimumnál ne foglaljon több memóriát.

Ezen felül is tudunk még a memóriahasználatban megtakarítást elérni, ha nem csak azt használjuk ki, hogy a sorozat elemei egészek, hanem megvizsgáljuk, hogy nem alkotnak-e számtani sorozatot. Miért jó ez? Azért mert, ha minden érték egy számtani sorozat eleme, akkor a sorozat tárolása helyett használhatunk egy olyan range objektumot, amely pontosan a szóbanforgó sorozat elemeit szolgáltatja kevés memóriafelhasználás mellett, mert a range nem tárolja az elemeket, hanem kikéréskor állítja elő.

Tehát nem kell mást tenni, mint egy olyan függvényt definiálni, ami a sorozatot, pontosabban az azt szolgáltató iterálható objektumot fogadja és egy sorozattípusú konténert (vagy egy array, vagy egy range objektumot) ad vissza. Legyen e függvény neve convert_iterable_to_sequence.

A függvény megvalósítása elsőre nem tűnik nagy kihívásnak, mert első lépésben az argumentumból származó sorozatról el kell dönteni, hogy számtani sorozat-e vagy sem. Ehhez a vizsgálathoz használjuk az előbb tárgyalt ellenőrző függvények egyikét. Ha számtani sorozatról van szó, akkor második lépésként előállítjuk és visszaadjuk a megfelelő range objektumot vagy, ha pedig nem számtani sor, akkor az array példányt.

Van azért egy apró megoldandó részfeladat. Az array példány előállításához meg kell határozni azt a típuskódot, amely a minimális memóriaigényt biztosítja. Mivel nem ismerjük előre a sorozatértékeket, ezért  az array példányt nem lehet egy fix típuskóddal létrehozni. Hanem az értékek ismeretében kell a nekik megfelelő típuskódot meghatározni. Ezt egy külön, typecode_for_minsize_array nevű függvényben végezzük el, ami a következőt teszi: az egész értékekhez megadható típuskódokon sorban végighalad a legkisebb elemmérethez tartozótól a legnagyobbig, és megpróbálja az aktuálissal létrehozni a tömböt. Ha ez nem sikerül, akkor lép a következő, hosszabb elemméretet biztosító típuskódra. Amelyikkel már sikerül a tömb létrehozása, azzal a típuskóddal tér vissza.

Még egy dologra kell figyelni. A számsorozat iterátorból is származhat, ami egy teljes kiolvasás után kimerül.  Viszont a sorozatelemeket a convert_iterable_to_sequence függvényben a különböző műveletekhez többször kell felhasználni. Ezért a sorozatértékeket a függvényen belül szükséges ideiglenesen eltárolni. Ez átmenetileg növeli ugyan a memóriaigényt, de az így lefoglalt memória a függvény lefutása után felszabadul.

Mindezeket figyelembe véve most már elkészíthetjük a convert_iterable_to_sequence() függvényt. Ennek, valamint az általa használt typecode_for_minsize_array() és is_arithmetic_sequence() segédfüggvény definíciója alább látható. A tesztsorok mutatják, hogy ha számtani sorozat elemeiről van szó, akkor előáll a megfelelő range objektum, ha pedig nem ilyenek az értékek, akkor egy, az értékekhez igazodó legkisebb memóriaigényű array példányban tárolódnak.

A  bemutatott programkódokban az itertools és array modulok használatán volt elsődlegesen a hangsúly. Ezekről a Python tudásépítés lépésről lépésre című e-könyv „Készétel fogyasztás – a szabványos könyvtár moduljainak használata” fejezet „Speciális iterátorok”, valamint „Memóriahatékony számtömb – array” című alfejzetekben lehet részletekbe menően olvasni példákkal illusztrálva a használatot.

Érdekel a Python tudásépítés lépésről lépésre az alapoktól az első asztali alkalmazásig című e-könyv.