Seite 1 von 3

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Dienstag 7. Februar 2017, 14:42
von BlackJack
@PythonRookie9000: Eine weitere Idee aufgrund dieser Aufgabe wäre es mal zu schauen wie sich das bei den anderen 9999 Möglichkeiten neben 1986 verhält. Wie lange brauchen andere Kombinationen und wie viele Kombinationen gibt es, die nie wieder zum Ausgangscode zurückkehren?

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Mittwoch 8. Februar 2017, 11:38
von sebastian0202
Dann will ich mal wieder 8)
Hoffe die Namen sind jetzt eindeutiger..

Code: Alles auswählen

#!/usr/bin/python3
# -*- coding: utf-8 -*-



class Mathe_Functions(object):
    def __init__(self):
        super(Mathe_Functions, self).__init__()
        # Zahl hat mindestens eine Stelle
        self.stellen = 1

    def anzahl_ziffern(self, zahl):
        # die Anzahl der Stellen von der Zahl ermitteln
        anzahl = 1
        while zahl >= 10:
            zahl /= 10
            anzahl += 1
        return anzahl
     
    def quersumme(self, zahl):
        # Quersumme der Zahl bilden
        stellen = self.anzahl_ziffern(zahl)
        summe = 0
        while stellen > 0:
            summe += zahl % 10
            zahl = (zahl - zahl % 10) / 10
            stellen -= 1
        return int(summe)

    def stellen_kuerzen(self, zahl, kuerzen = 1):
        # Zahl um x Stellen kuerzen (standard eine Stelle)
        # dabei muss die Zahl mindestens x Stellen lang 'self.stellen' sein
        stellen = self.anzahl_ziffern(zahl)
        if stellen - self.stellen >= kuerzen:
            zahl = zahl % 10**(stellen-kuerzen)
        return zahl

    def zahl_links_shiften(self, zahl, einer):
        # Zahl um eine Stelle nach Links verschieben 
        zahl *= 10
        zahl += einer
        zahl = self.stellen_kuerzen(zahl, 1)
        return zahl

    def letzte_zahl(self, zahl):
        # die letzte Ziffer der Zahl ermitteln 
        if zahl >= 10:
            return zahl % 10
        return zahl

 
class Code_X_Stellig(Mathe_Functions):
    def __init__(self, stellen):
        super(Code_X_Stellig).__init__()
        self.stellen = stellen
        self.codes = {}
        self.groups = []
    
    def __str__(self):
        liste = [int(zahl) for zahl in self.codes]
        liste.sort()
        for code in liste:
            tage = str(self.codes[str(code)]['tage']).ljust(4)
            code_string = ('000'+str(code))[-4:]
            print("Code: %s Tage: %s Gruppe: %i" % (code_string, tage, self.codes[str(code)]['gruppe']))

        return ''

    def auswertung(self):
        gruppen_anzahl = len(self.groups)
        kombinationen = set()
        ohne_wiederkehr = []
        for code in self.codes:
            kombinationen.add(self.codes[code]['tage'])
            if self.codes[code]['tage'] == -1:
                ohne_wiederkehr.append(code)
        print("Es existieren %i unterschiedliche Gruppierungen" % gruppen_anzahl)
        print("Folgende Komibnationen traten dabei auf: %s" % kombinationen)
        print("Folgende Zahlen kehren nie wieder zurück: %s" % ohne_wiederkehr)

    def recheck_groups(self):
        liste = [int(zahl) for zahl in self.codes]
        liste.sort()
        for zahl in liste:
            for nr, group in enumerate(self.groups):
                if zahl in group:
                    self.codes[str(zahl)]['gruppe'] = nr
                    break
        
    def add_group(self, zahl, liste):
        gefunden = False
        for group in self.groups:
            if zahl in group:
                gefunden = True
                break

        if not gefunden:
            self.groups.append(liste)
    
    def mathe(self, ur_code):
        # nach wievielen durchlaeufen kommt die zahl wieder vor
        # bedingung:  1. links-shiften um eine Stelle 
        #             2. quersumme (einer-stelle) anhaengen
        code = ur_code
        tage = 0
        zahlen_liste = []
        while tage < 10000:
            summe = self.quersumme(code)
            zahl = self.letzte_zahl(summe)
            code = self.zahl_links_shiften(code, zahl)
            zahlen_liste.append(code)
            tage += 1
            if code == ur_code:
                break
        if tage == 10000: tage = -1
        return tage, zahlen_liste
    
    def experimental(self, zahlen_liste, tage):
        # da der vorige Code nach X Durchlaeufen wiederkehrt, 
        # kehren auch alle Zahlen in der zahlen_liste nach diesen X Durchlaeufen wieder
        for zahl in zahlen_liste:
            sammeln.codes[str(zahl)] = {'tage' : tage, 'gruppe': -1}

if __name__ == '__main__':

    stellen = 4
    sammeln = Code_X_Stellig(stellen)
    # tage, zahlen_liste = sammeln.mathe(1986)
    # print("Es vergehen %i Tage, bis sich der Code %i wiederholt!" % (tage, 1986))

    for code in range(1,10000):
        if not str(code) in sammeln.codes:
            tage, zahlen_liste = sammeln.mathe(code)
            sammeln.codes[str(code)] = {'tage' : tage, 'gruppe': -1}
            sammeln.add_group(code, zahlen_liste)
            sammeln.experimental(zahlen_liste, tage)
    
    sammeln.recheck_groups()
    # print(sammeln)
    sammeln.auswertung()


Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Mittwoch 8. Februar 2017, 13:14
von BlackJack
@sebastian0202: Das sind keine Klassen die Du da geschrieben hast. Das ist so gar keine objektorientierte Programmierung. Bei `Mathe_Functions` sagt es ja schon der Name das es keine Klasse mit Methoden ist, sondern einfach nur Funktionen die ohne Grund in eine Klasse gesteckt wurden.

`Code_X_Stellig` ist auch ein komischer Name. Da würde ich erwarten das *ein* Exemplar dieser Klasse für *einen* x-stelligen Code steht. Unterstriche haben in Klassennamen übrigens nichts zu suchen.

Vererbung ist eine „ist-ein(e)“-Beziehung. Was `Code_X_Stellig` ist ein(e) `Mathe_Functions` überhaupt bedeuten soll ist mir an sich schon schleierhaft. Aber es macht ja sowieso schon keinen Sinn Funktionen in eine Klasse zu stecken.

Die `__str__()`-Methode ist dazu da eine Zeichenkettendarstellung des Objekts zu erzeugen und zurück zu geben. *Nicht* um irgendwelche Ausgaben oder andere Effekte zu haben. Du wirst ja sogar gezwungen eine Zeichenkette zurück zu geben, wie man an dem hier sinnlosen ``return ''`` sehen kann.

Die Ausrichtungen der Tage und des Codes (inklusive führender Nullen) hätte man beim Platzhalter in der Zeichenkette wo die Werte hinein formatiert werden, erledigen können.

`sorted()` hätte eine bzw. sogar zwei Zeilen sparen können.

Das ganze ständige hin und her wandeln zwischen `int()` und `str()` bei den Codes ist verwirrend und hätte man ziemlich sicher einfach weglassen können in dem man nur Zahlen nimmt und die einzig für die Ausgabe mit 0en aufgefüllt formatiert.

Wenn das `codes`-Attribut auf `collections.namedtuple` abbilden würde, dann hätte man im Code eine Dokumentation wie die Struktur aussieht, also welche Attribute/Schlüssel ein Wert in diesem Wörterbuch hat.

Programmlogik und Benutzerinteraktion sind vermischt.

Den Namen `recheck_groups()` verstehe ich nicht im Zusammenhang mit dem was die Methode tut. Bei `mathe()` das gleiche.

Für `add_group()` könntest Du Dir mal `any()` und `all()` anschauen und wie man eine der beiden Funktionen verwenden kann um da einen Ein- oder Zweizeiler draus zu machen.

Zur ``while``-Schleife in `mathe()` könntest Du Dir mal ``else``-Anschauen, dann muss man den Test mit der 10000 nach der Schleife nicht machen.

Einerseits scheint `stellen` etwas variables zu sein, andererseits funktioniert es nicht etwas anderes als 4 dafür zu verwenden, weil die 10000 als magische Zahl im Quelltext steht und nicht zu anderen Werten als 4 passt.

Ist das `ur` bei `ur_code` die deutsche Vorsilbe „Ur“ wie in „Ursprung“ oder Slangabkürzung für das englische `your`? Im ersten Fall: Keine natürlichen Sprachen mischen, schon gar nicht in *einem* Bezeichner (betrifft auch `Mathe_Functions`). Im zweiten Fall: Keine kryptischen Abkürzungen verwenden.

`experimental()` ist eindeutig keine Methode, wie so einiges andere auch auf `Code_X_Stellig` wahrscheinlich als Funktion geschrieben werden könnte, und die ”Methode” greift auf `sammeln` zu was nicht möglich sein dürfte, denn das Hauptprogramm sollte unter anderem genau aus diesem Grund in einer Funktion stehen.

`sammeln` ist auch wieder ein Name den ich nicht verstehe. Das wäre ein Name für eine Funktion oder eine Methode weil es eine Tätigkeit beschreibt. Aber `Code_X_Stellig` ist ja schon ein unverständlicher Name. Wenn Dir für die beiden nichts besseres, passendes einfällt, dann liegt das daran das die Klasse IMHO keinen Sinn macht. Das was da an Daten und Funktionen zusammengefasst wurde sieht völlig willkürlich aus. Also im Grunde alles was das Programm so können muss abzüglich einiger Hilfsfunktionen die sinnloser Weise in einer ererbten Klasse stehen. Irgendwie habe ich den Eindruck das Ziel war es keine Funktionen zu schreiben und alles *irgendwie* in Klassen zu stopfen. Das ist falsch! Funktionen sind nichts böses, und Klassen sollte man verwenden um *sinnvoll* Daten und Funktionen zu einem Objekt zusammen zu fassen.

Im Hauptprogramm wird dann umgekehrt in einer Art und Weise auf `codes` zugegriffen die man so auch nicht unbedingt erwarten würde.

Eine `Code`-Klasse die *tatsächlich* *einen* x-stelligen Code als Werttyp modelliert, könnte beispielsweise Sinn machen. Die könnte dann zum Beispiel eine Methode haben, welche den nächsten Code liefert. Und Methoden um die Quersumme abzufragen und einen verschobenen Code zu liefern.

Mal am Beispiel der CoffeeScript-Lösung objektorientiert, dort allerdings nicht mit einer zusätzlichen Klasse, sondern einfach die vorhandene `Number`-Klasse um entsprechende Methoden erweitert:
[codebox=coffeescript file=Unbenannt.coffee]#!/usr/bin/env coffee
'use strict'

Number::lastDigit = -> this % 10

Number::crossSum = ->
sum = 0
n = this
while n > 0
digit = n.lastDigit()
sum += digit
n = (n - digit) / 10
sum

Number::shiftCode = (digit) -> this * 10 % 10000 + digit

Number::nextCode = -> this.shiftCode(this.crossSum().lastDigit())

main = ->
startCode = 1986
day = 0
code = startCode
loop
code = code.nextCode()
day++
break if code is startCode
console.log(day, code)

main() if require.main == module[/code]

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Mittwoch 8. Februar 2017, 13:18
von Sirius3
@sebastian0202: auf die Schnelle habe ich nichts gefunden, was das Verwenden von Klassen rechtfertigen würde. Mathe_Functions ist nur eine Sammlung von einzelnen Funktionen, die entweder zu kompliziert gedacht, oder gleich ganz überflüssig sind. Man will eben nicht eine 5 stellige Zahl um eine Ziffer kürzen, sondern nur dafür sorgen, dass man immer eine 4 stellige Zahl hat. Bei Code_X_Stellig.__str__: es gibt Formatangaben um Zahlen 4 stellig (mit und ohne führende Nullen) auszugeben (wobei Du da die 4 fest codiert hast, statt die Stellenanzahl zu benutzen). __str__ sollte auch einen String zurückgeben, und nicht etwas per print ausgeben! Die ständige Umwandlung von Zahl zu String und zurück ist unnötig kompliziert. Mit Sets könnte man sich wohl 99% der Rechenzeit sparen. Es ist sinnvoller, eine Listen mit Gruppen anzulegen, da dort die Anzahl der Tage implizit durch die Länge gegeben ist und die Gruppennummer als Index.

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Mittwoch 8. Februar 2017, 22:48
von BlackJack
Ein C-Programm das die Tage für alle Codes berechnet. Läuft grundsätzlich auch auf dem C64, allerdings quälend langsam.
[codebox=c file=Unbenannt.c]#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

uint8_t cross_sum(uint16_t code)
{
uint8_t sum = 0;
div_t tmp;

while (code) {
tmp = div(code, 10);
code = tmp.quot;
sum += tmp.rem;
}
return sum;
}

uint16_t shift_code(uint16_t code, uint8_t digit) {
return (code % 1000) * 10 + digit;
}

bool calculate_days(uint16_t start_code, uint16_t *days) {
register uint16_t day = 0;
register uint16_t code = start_code;
bool is_code_used[10000];

memset(is_code_used, 0, sizeof(is_code_used));
do {
code = shift_code(code, cross_sum(code) % 10);
if (is_code_used

Code: Alles auswählen

) return false;
        is_code_used[code] = true;
        ++day;
    } while (code != start_code);
    *days = day;
    return true;
}

int main(void)
{
    uint16_t start_code;
    uint16_t days;
    
    puts("start_code,days");
    for (start_code = 0; start_code <= 9999; ++start_code) {
        if (calculate_days(start_code, &days)) {
            printf("%04u,%u\n", start_code, days);
        } else {
            printf("%04u,inf\n", start_code);
        }
    }

    return 0;
}

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Donnerstag 9. Februar 2017, 12:23
von sebastian0202
Dann weiß ich immer noch nicht wann es sich lohnt ein Objekt zu erstellen.
Vielleicht lese ich die falschen Bücher.

Bei Klassennamen sollte der Anfangsbuchstabe jedes Wortes Groß und aneinander gereiht sein, richtig?
Bei Methoden und Funktionen sollten die Wörter dann kleingeschrieben und mit Unterstrich verbunden sein, richtig?
https://www.python.org/dev/peps/pep-000 ... tion-names :lol:

Bei der Formatierung hätte ich ja "%4d" verwenden können, aber was ist, wenn ich anstelle von 4 auf 6 Zahlen auffüllen möchte?
Abhängig von der Anzahl der Stellen des Codes eben. Es wird ja kein %ANZAHL_STELLENd geben.
Deshalb ('0'*anzahl_stellen+str(zahl))[anzahl_stellen*-1:], aber das liest sich ja katastrophal.

Sorted() konnte ich nicht nehmen, da er die Zahlen als Strings sortiert. Und so kommt 57 vor 571 etc.
Habe die Zahlen vor zum string konvertiert um sie als dict Key zu nutzen :oops:

Es sieht schon wirr aus was ich da geschrieben habe :lol:
Da bekomme ich glatt Angst, wenn ich darüber nachdenke wie ich auf Arbeit programmiere.
Dabei habe ich mehrfach über den Code geschaut.

Ich habe mir any und all angeschaut. Keine Ahnung wie ich die Funktionen nutzen soll,
um über mehrere Listen zu iterieren um darin ein Element an beliebiger Stelle zu suchen. Zumal ich dann ja den Index der Gruppe brauche.

@Sirius3
Wie meinst du das mit den Sets?
Wie kann ich denn 99% Rechenzeit sparen?



Ich habe mich eben an einem zweiten Versuch gewagt :?

Code: Alles auswählen


class Code(object):
    def __init__(self, laenge, zahl = -1):
        self._generation = 0
        self.laenge = laenge
        if zahl >= 0: 
            self.ziffern = list(map(int, str(zahl)))
        else:
            self.ziffern = [random.randint(0,9) for x in laenge]
        self.codes = [self.get_code()]

    def next_code(self, generations = 1):
        while generations > 0:
            zahl = sum(self.ziffern) % 10        
            self.ziffern.append(zahl)
            self.ziffern = self.ziffern[self.laenge*-1:]
            self._generation += 1
            self.codes.append(self.get_code())
            generations -= 1
        return self.get_code()

    def get_code(self):
        return int(''.join(map(str, self.ziffern)))
    
    def get_codes(self):
        return self.codes

    def get_generation(self):
        return self._generation

    def show_generations(self, von=0, bis=None):
        text = ''
        if not bis: bis = self._generation
        for generation in range(von, bis + 1):
            text += "Gen: %s - %i\n" % (generation, self.codes[generation])
        return text
    
    def __str__(self):
        return "Code: %4d - Generation: %i" % (self.get_code(), self._generation)

class CodeGruppe(object):
    def __init__(self):
        self.gruppen = []
        self._codes = []

    def add_group(self, codes):
        if not self.number_exists(codes[0]):
            self.gruppen.append(codes)
            self._codes.extend(codes)
    
    def number_exists(self, code):
        if code in self._codes:
            return True
        return False
    
    def get_group(self, number):
        return self.gruppen[number]

    def get_group_number(self, code):
        if not code in self._codes:
            return False
        for nr in self.get_group_numbers():
            if code in self.gruppen[nr]:
                return nr
    
    def get_group_numbers(self):
        return [nr for nr, liste in enumerate(self.gruppen)]

    def get_group_info(self, *arg):
        text = ''
        if not arg:
            arg = self.get_group_numbers()
        for nr in arg:
            text += "Gruppe: %i laenge: %i\n" % (nr, len(self.gruppen[nr]))

        return text

def main():
    codegruppe = CodeGruppe()

    for i in range(0, 1000):
        if not codegruppe.number_exists(i):
            code = Code(4, i)
            anfang = code.get_code()

            while code.get_generation() < 4800:
                if anfang == code.next_code():
                    break
            codegruppe.add_group(code.get_codes())
    
    
    print(codegruppe.get_group_info())
    number = codegruppe.get_group_number(1986)
    print(codegruppe.get_group_info(number))
    print(codegruppe.get_group(number))

if __name__ == '__main__':
    main()

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Donnerstag 9. Februar 2017, 13:36
von BlackJack
@sebastian0202: Es gibt kein '%0ANZAHL_STELLENd' aber '%0*d'. Und bei `format()` kann man Platzhalter auch verschachteln. Notfalls gäbe es auf Zeichenketten auch noch die `zfill()`-Methode.

Code: Alles auswählen

In [13]: '%0*d' % (4, 42)
Out[13]: '0042'

In [14]: '{0:0{1}d}'.format(42, 4)
Out[14]: '0042'

In [15]: str(42).zfill(4)
Out[15]: '0042'
`sorted()` kannst Du natürlich trotzdem nehmen wenn Du es auf die umgewandelten Daten anwendest, also einfach statt der „list comprehension“ die `sorted()`-Funktion mit einem Generatorausdruck als Argument:
sorted_numbers = sorted(int(s) for s in strings)

Bezüglich `any()`/`all()`: Aus diesem hier:

Code: Alles auswählen

    def add_group(self, zahl, liste):
        gefunden = False
        for group in self.groups:
            if zahl in group:
                gefunden = True
                break
 
        if not gefunden:
            self.groups.append(liste)
wird das hier (ungetestet):

Code: Alles auswählen

    def add_group(self, zahl, liste):
        if all(zahl not in group for group in self.groups):
            self.groups.append(liste)
`Code` macht IMHO schon wieder zu viel. Das `codes`-Attribut hat da nichts zu suchen. `_generation` eigentlich auch nicht, denn ich würde erwarten das `Code` ein Wertobjekt ist und der Wert nur aus dem Zahlwert und der Länge eines Codes besteht. Du packst da gleich noch alle Codes die aus dem Code folgen mit hinein, wobei das aber abhängig davon ist ob und wie viele man davon hat generieren lassen.

`next_code()` würde ich `advance_code()` nennen, weil man nicht nur den nächsten Code bekommen kann, sondern auch beliebig weite Sprünge machen kann. Das und auch das mit dem Zufallswert in der `__init__()` scheint mir hier überflüssig zu sein‽

Wenn `Code` kein Werttyp ist, dann würde ich da einen Iterator draus machen statt eine `next_code()`-Methode zur Verfügung zu stellen. `CodeIterator` wäre dann auch ein passenderer Name IMHO. Denn es ist ja kein Code sondern enthält einen (aktuellen) Code (und eventuell alle seine Vorgänger).

``while generations > 0:`` sollte eine ``for``-Schleife sein.

`get_codes()` und `get_generation()` sind trivialer Getter → weg damit. Aus `get_code()` könnte man ein `property()` machen.

`show_generations()` wäre in Haskell vielleicht der passende Namen, aber in Python eher nicht. Da würde man bei `show_irgendwas()` erwarten das etwas ausgegeben oder angezeigt wird. Die Methode ist vom Inhalt her auch mehr was für die Benutzerinteraktion und nicht für die Programmlogik.

”Magische” Methoden wie `__str__()` erwartet man eigentlich am Anfang der Klasse vor Properties und normalen Methoden und nicht versteckt als letzte Methode.

``self.ziffern[self.laenge*-1:]`` würde ich als ``self.ziffern[-self.laenge:]`` schreiben. Eine `collections.deque` statt einer Liste würde die Sache vereinfachen und eventuell auch effizienter machen.

`CodeGruppe` enthält mehr als eine Gruppe, sollte also `CodeGruppen` heissen. Das `_group` kann aus allen Methodennamen verschwinden, denn es doppelt nur noch mal die Typinformation.

Wenn ``if``/``else`` in den zweigen nur `True` oder `False` zurück gibt, dann ist das Konstrukt unnötig, denn die Bedingung vom ``if`` ist ja schon der Wert den man haben möchte, oder seine Negation, die man mit ``not`` bekommt. Also wird `number_exists()` zu einem Einzeiler: ``return code in self._codes``.

Eine Funktion sollte immer nur einen (Duck)Typ als Ergebnis liefern, also beispielsweise nicht mal `False` und mal eine Zahl. Das lässt sich in diesem Fall auch nur über einen Typtest prüfen, denn `False` ist eine Zahl und hat den gleichen Wert wie 0 beim Vergleichen und allen anderen Operationen die auf `int` definiert sind:

Code: Alles auswählen

In [21]: isinstance(False, int)
Out[21]: True

In [22]: False == 0
Out[22]: True

In [23]: False + 42
Out[23]: 42

In [24]: False * 42
Out[24]: 0
Statt spezielle Fehlercodes zurück zu geben, verwendet man in Python Ausnahmen. `get_group_number()` sollte eine Auslösen wenn der `code` nicht enthalten ist. `KeyError` würde sich anbieten.

`get_group_numbers()` nutzt `enumerate()` wo ein `range()` ausreichen würde. In Python 3 mit einem `list()` um da tatsächlich eine Liste draus zu machen. Sofern das benötigt wird tatsächlich eine Liste vorliegen zu haben.

Die API von `get_group_info()` ist mir zu magisch. Warum nicht einfach eine Liste mit Nummern statt ein *-Parameter? Was ist der Anwendungsfall für einen Aufruf mit mehr als einer Nummer bei der beim Aufruf *kein* Sternchen gebraucht wird? Der zudem einen besseren Namen als `*arg` haben könnte. Der Leser will nicht wissen das da irgendwelche Argumente übergeben werden können, sondern was sie bedeuten.

Eventuell würde es die API von `CodeGruppen` schöner machen, wenn man da die ”magischen” Methoden für einen Containertyp implementiert. Wohl eine Abbildung in diesem Fall.

Im Hauptprogramm sind 1000, 4, und 4800 Zahlen die zusammenhängen. Wobei 4800 IMHO einer Erklärung bedarf. Warum ist diese Zahl sicher? Wie berechnet die sich aus der Stellenanzahl? Ist der Code robust gegen den Fall das eine Kombination niemals wieder erreicht würde? Was wäre da das Gegenargument?

Zur Frage wann es sich lohnt ein Objekt zu erstellen: Implementiere das doch mal nur mit Funktionen. Dann sieht man in der Regel leichter welche Daten und welche Funktionen man zusammenfassen kann/sollte. Wie gesagt Funktionen sind nicht böse und Klassen sind kein Selbstzweck. Wenn man sie nicht benötigt, sollte man sie auch nicht schreiben und man sollte auch nicht versuchen krampfhaft jede ``def``\inition in eine Klasse stecken zu wollen.

Edit: Man braucht die Fähigkeit Code sinnvoll auf Funktionen aufzuteilen auch bei OOP, denn dort muss man ihn sinnvoll auf Klassen und Methoden aufteilen. Wenn man also schon bei der Aufteilung auf Funktionen Probleme hat, dann wird das bei OOP nur noch schwieriger. Ich erwähne das weil Dein Versuch weiter oben im Thema die ursprüngliche Aufgabe auf Funktionen zu verteilen auch schon nicht gut war. Auch ein Grund erst einmal Funktionen zu üben.

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 00:05
von BlackJack
@sebastian0202: Mal ein Beispiel wie man zu Objekten kommen kann. Ausgangspunkt ist eine funktionale Lösung mit Codes die nicht auf 4 Stellen festgelegt sind, denn sonst gibt es hier fast nichts was man zu einem Objekt zusammenfassen kann:

Code: Alles auswählen

#!/usr/bin/env python
from __future__ import absolute_import, division, print_function


def get_cross_sum(number):
    result = 0
    while number:
        number, digit = divmod(number, 10)
        result += digit
    return result


def code_as_string(code_length, code_value):
    return str(code_value).zfill(code_length)


def shifted_code(code_length, code_value, digit):
    return code_value * 10 % 10**code_length + digit


def get_next_code(code_length, code_value):
    return shifted_code(code_length, code_value, get_cross_sum(code_value) % 10)


def calculate_days(code_length, start_code_value):
    days = 0
    code_value = start_code_value
    while True:
        days += 1
        code_value = get_next_code(code_length, code_value)
        if code_value == start_code_value:
            return days


def main():
    code_length = 4
    for code_value in range(10**code_length):
        print(
            code_as_string(code_length, code_value),
            calculate_days(code_length, code_value)
        )


if __name__ == '__main__':
    main()
Wie man sieht besteht ein Code aus zwei Komponenten, der Länge und dem Zahlwert, die durch viele Funktionen durchgereicht werden müssen. Das wären im ersten Schritt die Kandidaten die man zu einem Objekt zusammenfassen könnte. Machen wir das mal mit `collections.namedtuple()` (die geänderten Funktionen):

Code: Alles auswählen

Code = namedtuple('Code', 'length value')


def code_as_string(code):
    return str(code.value).zfill(code.length)


def shifted_code(code, digit):
    return Code(code.length, code.value * 10 % 10**code.length + digit)


def get_next_code(code):
    return shifted_code(code, get_cross_sum(code.value) % 10)


def calculate_days(start_code):
    days = 0
    code = start_code
    while True:
        days += 1
        code = get_next_code(code)
        if code == start_code:
            return days


def iter_codes(length):
    return (Code(length, i) for i in range(10**length))


def main():
    length = 4
    for code in iter_codes(length):
        print(code_as_string(code), calculate_days(code))
Jetzt kann man sich überlegen, welche von den Funktionen zu dem Objekt gehören könnten um das `namedtuple()` zu beerben und um Methoden zu erweitern. `code_as_string()` ist sinnvoll als `__str__()` auf der Klasse. Des weiteren würde ich in diesem Beispiel alle Funktionen dort hin verschieben die ein `Code`-Objekt als Argument bekommen *und* die interne Repräsentation kennen (müssen). Dazu gehört `calculate_days()` *nicht*, denn diese Funktion weiss nicht, das ein `Code` aus Länge und Wert besteht. Die `iter_codes()`-Funktion bekommt zwar keinen Code übergeben, erzeugt aber welche, und wäre damit ein Kandidat für eine Klassenmethode:

Code: Alles auswählen

class Code(namedtuple('Code', 'length value')):

    def __str__(self):
        return str(self.value).zfill(self.length)

    def shifted(self, digit):
        return Code(self.length, self.value * 10 % 10**self.length + digit)

    def get_next(self):
        return self.shifted(get_cross_sum(self.value) % 10)

    @classmethod
    def iter_all(cls, length):
        return (cls(length, i) for i in range(10**length))


def calculate_days(start_code):
    days = 0
    code = start_code
    while True:
        days += 1
        code = code.get_next()
        if code == start_code:
            return days


def main():
    length = 4
    for code in Code.iter_all(length):
        print(code, calculate_days(code))

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 03:03
von karolus
Hallo

Ich hab mal einen Code als tuple von vier Zahlen (0 … 9) aufgefasst, und das ganze nach Größe und Anzahl von geschlossenen Codefolgen aufgedröselt.

Code: Alles auswählen

from itertools import product
from collections import Counter

alle = set(product(range(10),repeat=4))
seen = set()

def next_code(code):
    return code[1:] + (sum(code) % 10,)

def main():
    ringcounter = Counter()
    for code in alle:
        if code in seen:
            continue
        codering = []
        start = code[:]
        c = 0
        while True:
            code = next_code(code)
            codering.append(code)
            c+=1
            if code == start:
                break:
        seen.update(codering)
        ringcounter[c] += 1
    kontrolle = 0
    for key, value in ringcounter.items():
        kontrolle += key * value
        print( "{} Codereihen aus {} Codes".format(value, key))
    print("kontrollsumme ist {}".format(kontrolle))
                
            
main()

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 04:28
von karolus
Hallo

Oder ein wenig übersichtlicher:

Code: Alles auswählen

from itertools import product
from collections import Counter

def next_code(code):
    return code[1:] + (sum(code) % 10,)        

def main():
    pool = set(product(range(10),repeat=4))
    ringcounter = Counter()    
    while pool:
        code = pool.pop()
        codering = []
        start = code[:]
        c = 0
        while True:
            code = next_code(code)
            codering.append(code)
            c += 1
            if code == start:
                break
        pool = pool.difference(codering)
        ringcounter[c] += 1
            
    kontrolle = 0
    for key, value in ringcounter.items():
        kontrolle += key * value
        print( "{} Codereihen aus {} Codes".format(value, key))
    print("kontrollsumme ist {}".format(kontrolle))

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 05:23
von karolus
Version 0.0.3 mit Generatorfunktion:

Code: Alles auswählen

from itertools import product
from collections import Counter

def iter_code(code): 
    nextcode = code
    while True:
        nextcode = nextcode[1:] + (sum(nextcode) % 10,)
        if code == nextcode:
            return
        yield nextcode        

def main():
    pool = set(product(range(10),repeat=4))
    ringcounter = Counter()    
    while pool:
        code = pool.pop()
        codering = [code]
        for icode in iter_code(code):
            codering.append(icode)
        pool = pool.difference(codering)
        ringcounter[len(codering)] += 1
            
    kontrolle = 0
    for key, value in ringcounter.items():
        kontrolle += key * value
        print( "{} Codereihen aus {} Codes".format(value, key))
    print("kontrollsumme ist {}".format(kontrolle))

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 08:54
von sebastian0202
@BlackJack
Der Code kann mit einer Leichtigkeit gelesen werden, dass ist wahnsinn.
Jetzt ist mir einiges klarer.
Ich werde mir den Leitfaden ausdrucken und für neue Projekte als Vorlage hinlegen. :mrgreen:

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 11:11
von snafu
Hier eine Variante, die deque() verwendet:

Code: Alles auswählen

from itertools import count
from collections import deque

def count_days(code):
    digits = deque(map(int, code), len(code))
    old = deque(digits)
    for num_days in count(1):
        digits.append(sum(digits) % 10)
        if digits == old:
            return num_days

def main():
    num_days = count_days('1986')
    print('Code wiederholt sich nach', num_days, 'Tagen.')

if __name__ == '__main__':
    main()
Der Einsatz von deque() hat den Vorteil, dass man beim Verschieben nichts herum kopieren muss. Gleichzeitig erspart einem die Betrachtung der Ziffern als Sequenz die entsprechende Rechnerei. Nur das Hinzufügen der letzten Stelle aus der Quersumme habe ich rechnerisch gelöst, da dies ein "billiger" Einzeiler ist.

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 12:30
von BlackJack
@snafu: Kleiner Bug in der Funktion: die funktioniert nur für Codes ≥1000.

Edit: Und geht auch davon aus das der Code jemals wieder erreicht wird, was man ohne Begründung IMHO nicht so einfach annehmen darf.

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 12:42
von BlackJack
@karolus: Beim ersten Quelltext ist es äusserst unschön das `alle` und `seen` globale Variablen sind. Wobei `alle` auch gar kein `set` sein muss, denn es wird im `Code` ja nur darüber iteriert. Das ``product(range(10),repeat=4)`` hätte man gleich in die ``for``-Schleife schreiben können.

Der Code geht davon aus das man disjunkte Ringe/Kreise hat. Das muss man ja schon vorher wissen, oder irgendwie begründen können, dass der Graph so aussieht.

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 14:33
von snafu
BlackJack hat geschrieben:@snafu: Kleiner Bug in der Funktion: die funktioniert nur für Codes ≥1000.

Edit: Und geht auch davon aus das der Code jemals wieder erreicht wird, was man ohne Begründung IMHO nicht so einfach annehmen darf.
Woran machst du fest, dass der Code nicht für Zahlen unter 1000 funktioniert? Der Code wird ja als String übergeben und danach werden die Ziffern einzeln betrachtet. Ich sehe da kein Problem mit führenden Nullen, falls das gemeint ist.

Und dass der Code theoretisch endlos laufen könnte, ist doch bei allen bisher gezeigten Lösungen so. Das ist ja vom Prinzip her ein typisches Halteproblem.

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 15:46
von snafu
Man könnte natürlich einbauen, dass man nach 1 Million Versuchen aufgibt oder sowas. Ansonsten stelle ich mir das Erkennen von Zirkeln schwierig vor, da diese prinzipiell sehr groß sein können. Nur den Fall, dass alle Stellen auf Null stellen könnte man mit aufnehmen bzw wenn sich der Code unmittelbar wiederholt (bei 11 Einsen und ähnliches).

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 15:50
von Sirius3
@snafu: eine n-stellige Zahl kann maximal 10^n Zustände annehmen. Da der interne Zustand des Automaten auch eine n-stellige Zahl ist, muß sich die Folge spätestens nach 10^n Schritten wiederholen.

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 15:56
von karolus
Hallo
Der Code geht davon aus das man disjunkte Ringe/Kreise hat. Das muss man ja schon vorher wissen, oder irgendwie begründen können, dass der Graph so aussieht.
Bei der Betrachtung spätestens der Zweiten Zahlenreihe sollte einem schon auffallen das:
1.) jede Kombination nur jeweils einen eindeutigen Vorläufer|Nachfolger hat. ( haben kann!! )
2.) nur über einen begrenzten Vorat (hier 10**4) an Kombinationen iterieren kann.
3.) →irgenwann (<=10**4) Iterationen wieder jeder mögliche Ausgangspunkt erreicht wird.
4.) Aus 1.) folgt auch das es keine Schnittmengen zwischen verschiedenen Ringen geben kann.

Letzlich beweisst mein Code das ja empirisch.

Re: AUFGABE: Jeden tag eine Änderung des Bankcodes...

Verfasst: Freitag 10. Februar 2017, 16:04
von BlackJack
@snafu: Okay ich hatte das nicht vorhandene Problem der führenden Nullen gesehen. :oops:

Das der Code endlos laufen könnte ist nicht bei allen Lösungen so. Der könnte zudem nicht nur theoretisch endlos laufen sondern auch praktisch, sofern man nicht vorher erklären kann warum jeder Code irgendwann wieder zu sich selbst führt.

Meine erweiterte Frage war ja: „Wie lange brauchen andere Kombinationen und wie viele Kombinationen gibt es, die nie wieder zum Ausgangscode zurückkehren?“

Wenn man also nicht (vorher) erklären kann warum es keine Kombination gibt, bei der nie wieder zum Ausgangscode zurückgekehrt wird, dann ist ein Programm das diese Frage nicht explizit berücksichtigt IMHO fehlerhaft, weil es die Frage nur dadurch beantwortet, weil es nicht hängengeblieben ist.

Und ja, den Fehler habe ich in meinem „von funktional nach OOP“ auch gemacht.

Die Lösungen von sebastian0202 und mein C-Programm berücksichtigen den Fall aber. Seins verwendet die -1 als Kennzeichen und meins 'inf'.

Halteproblem triffts nicht ganz weil es dort ja darum geht, dass ganz allgemein nicht beweisen kann ob ein beliebiges gegebenes Programm zum Ende kommt. Hier haben wir aber ein konkretes Programm, sogar ohne Eingaben von aussen und mit einer endlichen Wertemenge. Ob es hängen bleibt oder nicht, kann man als Grapgenproblem formulieren. Knoten sind die 10.000 Codes. Kanten bestehen zwischen Knoten die für einen Code und dessen Folgecode stehen. Zu beweisen wäre jetzt, dass jeder Code genau einen möglichen Vorgänger hat. Denn er hat auf jeden Fall einen Nachfolger. Wenn jeder genau einen Vor- und Nachgänger hat, dann muss der Graph aus einem oder mehreren Kreisen bestehen und somit käme auch jeder Code wieder zu sich selbst.

Ein Kreis kann nur maximal so gross sein wie es verschiedene Kombinationen gibt. Bei vier Stellen kann er also maximal 10.000 Codes umfassen. Eine Million Durchläufe wäre demnach eine sinnvolle Begrenzung für sechsstellige Codes. Alternativ muss man sich nur merken welche Codes man schon gesehen hat, damit man keinen, beziehungsweise nicht die falschen, mehr als einmal berechnet. Das ist eine Abwägung zwischen Laufzeit und Speicherverbrauch.

@karolus: 3. Sehe ich so ohne weitere Erklärung nicht. Es sind Graphen möglich die 1, 2, und 4 erfüllen, bei denen 3 nicht gilt.