Zum Inhalt
wannacode

Was alle Objekte teilen: Klassenattribute

Kurz gesagt: Ein Klassenattribut gehört zur Klasse, nicht zum Objekt: Alle Objekte sehen einen gemeinsamen Wert, und @staticmethod und @classmethod sind Methoden ohne Objekt.

Problem

Jedes Produkt ist Nr. 1

Der Shop will, dass jedes neue Produkt die nächste Nummer bekommt: 1, 2, 3… Du legst einen Zähler _next_id in das Produkt. __init__ gibt dem Produkt die aktuelle Nummer und erhöht den Zähler:

class Product:
def __init__(self, name):
self._next_id = 1
self._name = name
self._id = self._next_id
self._next_id += 1
def __str__(self):
return f"Nr. {self._id} {self._name}"
bread = Product("Brot")
milk = Product("Milch")
coffee = Product("Kaffee")
print(bread)
print(milk)
print(coffee)
Ausgabe
Nr. 1 Brot
Nr. 1 Milch
Nr. 1 Kaffee

Alle sind Nr. 1. Der Grund ist, dass _next_id ein Attribut des Objekts ist, genau wie der Name. Jedes neue Produkt bekommt seinen eigenen Zähler, der bei 1 beginnt. Der Zähler des Brotes kommt nie bei der Milch an.

Die Idee

Ein Attribut der Klasse, nicht des Objekts

Der Zähler gehört nicht zu einem Produkt. Alle Produkte teilen ihn sich. Schreib ihn in die Klasse selbst, außerhalb von __init__:

class Product:
_next_id = 1

Ein Attribut, das in der Klasse steht, ist ein Klassenattribut. Es gibt eines, egal wie viele Objekte du erzeugst, und alle Objekte teilen es sich. In __init__ erreichst du es über den Namen der Klasse: Product._next_id.

shop.py
class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = Product._next_id
Product._next_id += 1
def __str__(self):
return f"Nr. {self._id} {self._name}"
bread = Product("Brot")
milk = Product("Milch")
coffee = Product("Kaffee")
print(bread)
print(milk)
print(coffee)
Ausgabe
Nr. 1 Brot
Nr. 2 Milch
Nr. 3 Kaffee

Das Brot nimmt die 1 und macht den Zähler zu 2. Die Milch sieht denselben Zähler, nimmt die 2 und macht ihn zu 3.

class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = Product._next_id
Product._next_id += 1
bread = Product("Brot")
milk = Product("Milch")
coffee = Product("Kaffee")
Schritt für Schritt, in Worten
  1. _next_id ist ein Klassenattribut: Es gibt eines, nicht eines in jedem Objekt.
  2. Product("Brot"): ein neues Objekt. __init__ speichert den Namen.
  3. self._id = Product._next_id: Das Objekt nimmt die aktuelle Nummer, 1.
  4. Product._next_id += 1 ändert den gemeinsamen Zähler: Jetzt ist er 2.
  5. Milch: derselbe Zähler. Es nimmt die 2 und macht ihn zu 3.
  6. Kaffee: Es nimmt die 3 und macht ihn zu 4.
  7. Es gibt einen Zähler für alle, deshalb sind die Nummern der Reihe nach.
Genauer, für Neugierige

Vorsicht mit self. Beim Lesen findet self._next_id das Klassenattribut, aber self._next_id += 1 legt ein neues Attribut auf dem Objekt an, und der gemeinsame Zähler bewegt sich nicht:

class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = self._next_id
self._next_id += 1
def __str__(self):
return f"Nr. {self._id} {self._name}"
bread = Product("Brot")
milk = Product("Milch")
print(bread)
print(milk)
print(Product._next_id)
Ausgabe
Nr. 1 Brot
Nr. 1 Milch
1

Deshalb änderst du den gemeinsamen Wert nur über den Namen der Klasse: Product._next_id += 1.

Probier es aus

Gemeinsam und eigen

Was gibt das Programm aus?

class Order:
count = 0
def __init__(self):
Order.count += 1
self.items = 0
self.items += 1
a = Order()
b = Order()
c = Order()
print(Order.count, c.items)
Antwort

Die Idee

@staticmethod: Arbeit ohne Objekt

Dasselbe gilt für Methoden. Eine Methode mit @staticmethod darüber ist eine Methode der Klasse. Sie hat kein self, und du rufst sie über den Namen der Klasse auf, ohne ein Objekt zu erzeugen:

class Calculator:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def average(values):
total = 0
for v in values:
total += v
return total / len(values)
print(Calculator.add(10, 20))
grades = [5, 6, 4, 6]
print(Calculator.average(grades))
print(max(3, 8))
Ausgabe
30
5.25
8

Calculator.add braucht kein Objekt: Alles, was sie braucht, kommt als Parameter. In Python ist so eine Arbeit oft einfach eine Funktion außerhalb der Klasse, wie max und math.sqrt. In eine Klasse kommt sie, wenn sie eng zur Klasse gehört und du sie unter dem Namen der Klasse haben willst.

Python hat kein main in einer Klasse, bei dem das Programm startet: Das Programm sind die Zeilen der Datei, von oben nach unten. Deshalb brauchten die Funktionen aus dem Modul „Funktionen“ keine Klasse.

Genauer, für Neugierige

Es gibt auch @classmethod. Sie bekommt die Klasse selbst, cls, als ersten Parameter und erreicht die Klassenattribute ohne Objekt:

class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = Product._next_id
Product._next_id += 1
@classmethod
def created(cls):
return cls._next_id - 1
bread = Product("Brot")
milk = Product("Milch")
print(Product.created())
Ausgabe
2

@staticmethod bekommt nichts, @classmethod bekommt die Klasse, und eine gewöhnliche Methode bekommt das Objekt (self).

Probier es aus

Finde den Fehler

tea.label() hält das Programm an. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Konstanten

Die MwSt. ist für alle Produkte 20%, und sie ändert sich nicht, während das Programm läuft. So ein Wert ist eine Konstante:

class Prices:
VAT_RATE = 0.20
@staticmethod
def with_vat(price):
return price * (1 + Prices.VAT_RATE)
print(Prices.with_vat(50))
print(f"MwSt.: {Prices.VAT_RATE * 100}%")
Ausgabe
60.0
MwSt.: 20.0%

Python hat keine Konstanten. Es gibt eine Abmachung: Ein Name in Großbuchstaben mit Unterstrichen, VAT_RATE, bedeutet „das ändert sich nicht“. In der Klasse geschrieben, ist es ein Klassenattribut.

Wenn jemand versucht, sie zu ändern,

Prices.VAT_RATE = 0.25

schützt sie nichts, und Prices.VAT_RATE ist jetzt:

Ausgabe
0.25

Die Abmachung bringt dir, dass die Zahl 0.20 an einer Stelle mit einem klaren Namen steht. Die Großbuchstaben sagen jedem, der den Code liest, „nicht anfassen“, aber den Wert zu schützen, liegt bei dir.

Genauer, für Neugierige

Final aus dem Modul typing sieht aus wie eine Konstante, aber Python prüft es nicht. Nur der Editor oder ein Werkzeug wie mypy warnt dich:

from typing import Final
class Prices:
VAT_RATE: Final = 0.20
Prices.VAT_RATE = 0.25
print(Prices.VAT_RATE)
Ausgabe
0.25

Manchmal ist der Wert für jedes Objekt anders, darf sich aber nach dem Erzeugen des Objekts nicht ändern. So ist die Nummer des Produkts. Mach sie zu einer schreibgeschützten Property, @property ohne Setter (die Lektion „Kapselung“):

class Product:
_next_id = 1
def __init__(self, name):
self._name = name
self._id = Product._next_id
Product._next_id += 1
@property
def id(self):
return self._id

coffee.id liest die Nummer, und coffee.id = 5 bricht mit einem AttributeError ab, weil die Property keinen Setter hat.

Probier es aus

Was fehlt?

Ein Jahr hat 12 Monate, und das wird sich nicht ändern. Welcher Name steht in der Lücke?
class Calendar:
     = 12

Die Idee

Wann an der Klasse und wann am Objekt

Frag dich: „Gehört das zu einem konkreten Objekt?“

  • Ja (der Kontostand des Kontos, der Name des Produkts, das Abheben von einem Konto): ein Attribut des Objekts in __init__ oder eine Methode mit self.
  • Nein (die Anzahl aller Konten, die MwSt., die Summe zweier Zahlen): ein Klassenattribut, @staticmethod oder @classmethod.

Ein häufiger Fehler ist eine Liste als Klassenattribut. Es gibt eine Liste für alle Objekte, also taucht das, was du in einen Warenkorb legst, im anderen auf:

class Cart:
items = []
def add(self, name):
self.items.append(name)
first = Cart()
second = Cart()
first.add("Brot")
print(second.items)
Ausgabe
['Brot']

Die Liste gehört zu jedem Warenkorb, also gehört sie in __init__: self.items = []. Sonst teilen sich alle „Objekte“ dieselben Daten, und du bist wieder beim Problem mit den parallelen Listen aus der ersten Lektion.

Übung

Was alle Objekte teilen

Löse die Aufgaben in deinem Editor.

1. Bestellungen. Eine Klasse Order mit Kunde und Betrag. Jede neue Bestellung bekommt die nächste Nummer aus einem Zähler, der ein Klassenattribut ist. Eine Methode count() mit @classmethod gibt zurück, wie viele Bestellungen erzeugt wurden.

Das Programm
orders = [
Order("Maria", 45.5),
Order("Peter", 120),
Order("Elena", 18.9),
]
for o in orders:
print(o)
print(f"Bestellungen: {Order.count()}")
Ausgabe
Bestellung Nr. 1: Maria, 45.5 €
Bestellung Nr. 2: Peter, 120 €
Bestellung Nr. 3: Elena, 18.9 €
Bestellungen: 3

2. Taschenrechner. Eine Klasse Calculator mit Methoden mit @staticmethod zum Addieren, Subtrahieren und Multiplizieren ganzer Zahlen und zum Dividieren von Dezimalzahlen.

Das Programm
print(Calculator.add(10, 20))
print(Calculator.subtract(10, 15))
print(Calculator.multiply(6, 7))
print(Calculator.divide(5, 2))
Ausgabe
30
-5
42
2.5

3. Wechselstube. Eine Klasse Converter mit Konstanten für den Kurs: 1 € = 1,08 US-Dollar und 1 € = 0,85 britische Pfund (die Kurse sind Beispiele). Methoden mit @staticmethod rechnen einen Betrag in Euro in Dollar und in Pfund um. Für 20 €:

Ausgabe
20 EUR = 21.6 USD
20 EUR = 17.0 GBP

Weitere Aufgaben

  • Nummerierte Konten. Gib dem Account aus der Lektion „Kapselung“ eine Kontonummer aus einem Zähler, der ein Klassenattribut ist, und eine schreibgeschützte Property für die Nummer.
  • Umrechnen in beide Richtungen. Gib Converter Methoden, die von Dollar und von Pfund in Euro umrechnen. Rechne 20 € in Dollar um und zurück in Euro. Bekommst du genau 20?
  • Ein Limit. Der Shop nimmt höchstens 3 Produkte in einem Warenkorb an. Füge dafür eine Konstante hinzu und lass die Methode zum Hinzufügen das vierte ablehnen.

Kurz gesagt

Das Modul in kurz

  • Eine Klasse ist dein eigener Typ, und ein Objekt ist ein konkretes Exemplar davon, erzeugt mit Product(). Die Variable zeigt auf das Objekt. Klassen und Objekte
  • __init__ sorgt dafür, dass das Objekt vollständig entsteht. self ist das aktuelle Objekt. Konstruktoren und self
  • Instanzmethoden arbeiten mit den Attributen des Objekts. __str__ sagt, wie es als Text aussieht. Instanzmethoden
  • _name sagt „intern“, und @property mit einem Setter lässt Änderungen erst nach einer Prüfung durch. Kapselung
  • Klassenattribute, @staticmethod und @classmethod sind das, was die Klasse teilt: Zähler, Hilfsmethoden und Konstanten.

Daten und die Regeln dafür an einer Stelle, mit einer klaren Markierung, was intern ist: Das ist objektorientierte Programmierung. Im nächsten Modul fangen Klassen an, Code miteinander zu teilen.

Test

Teste dich

1. Du erzeugst 5 Objekte aus einer Klasse mit dem Klassenattribut count = 0. Wie viele Kopien von count liegen im Speicher?
2. Was kann eine @staticmethod NICHT?
3. Was bekommt eine @classmethod als ersten Parameter?
4. Die Klasse hat _next_id = 1. Was macht self._next_id += 1 in __init__?
5. Was passiert bei Prices.VAT_RATE = 0.25?
6. Was ist ein guter Kandidat für eine @staticmethod?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·