Grundlagen des Python-Objektsystems

PythonPythonBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Das Python-Objektsystem basiert weitgehend auf einer Implementierung, die auf Wörterbüchern beruht. In diesem Abschnitt wird dies diskutiert.

Dictionaries, Revisited

Denken Sie daran, dass ein Wörterbuch eine Sammlung von benanntenen Werten ist.

stock = {
    'name' : 'GOOG',
    'shares' : 100,
    'price' : 490.1
}

Wörterbücher werden üblicherweise für einfache Datenstrukturen verwendet. Sie werden jedoch für kritische Teile des Interpreters verwendet und können der wichtigste Datentyp in Python sein.

Dicts and Modules

Innerhalb eines Moduls enthält ein Wörterbuch alle globalen Variablen und Funktionen.

## foo.py

x = 42
def bar():
  ...

def spam():
  ...

Wenn Sie foo.__dict__ oder globals() untersuchen, werden Sie das Wörterbuch sehen.

{
    'x' : 42,
    'bar' : <function bar>,
   'spam' : <function spam>
}

Dicts and Objects

Benutzerdefinierte Objekte verwenden ebenfalls Wörterbücher für Instanzdaten und Klassen. Tatsächlich besteht das gesamte Objektsystem größtenteils aus einer zusätzlichen Schicht, die auf Wörterbüchern aufgesetzt ist.

Ein Wörterbuch enthält die Instanzdaten, __dict__.

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG','shares' : 100, 'price': 490.1 }

Sie füllen dieses Dict (und die Instanz) bei der Zuweisung an self.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Die Instanzdaten, self.__dict__, sehen so aus:

{
    'name': 'GOOG',
   'shares': 100,
    'price': 490.1
}

Jede Instanz bekommt ihr eigenes privates Wörterbuch.

s = Stock('GOOG', 100, 490.1)     ## {'name' : 'GOOG','shares' : 100, 'price': 490.1 }
t = Stock('AAPL', 50, 123.45)     ## {'name' : 'AAPL','shares' : 50, 'price': 123.45 }

Wenn Sie 100 Instanzen einer Klasse erzeugen, gibt es 100 Wörterbücher, die Daten speichern.

Klasseneigenschaften

Ein separates Wörterbuch enthält auch die Methoden.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Das Wörterbuch befindet sich in Stock.__dict__.

{
    'cost': <function>,
   'sell': <function>,
    '__init__': <function>
}

Instanzen und Klassen

Instanzen und Klassen sind miteinander verknüpft. Das Attribut __class__ verweist zurück auf die Klasse.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG','shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>

Das Instanzwörterbuch enthält Daten, die für jede Instanz einzigartig sind, während das Klassenwörterbuch Daten enthält, die von allen Instanzen gemeinsam geteilt werden.

Attributzugriff

Wenn Sie mit Objekten arbeiten, greifen Sie auf Daten und Methoden mit dem .-Operator zu.

x = obj.name          ## Abrufen
obj.name = value      ## Festlegen
del obj.name          ## Löschen

Diese Operationen sind direkt an die darunter liegenden Wörterbücher gebunden.

Ändern von Instanzen

Operationen, die ein Objekt ändern, aktualisieren das zugrunde liegende Wörterbuch.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG','shares': 100, 'price': 490.1 }
>>> s.shares = 50       ## Festlegen
>>> s.date = '6/7/2007' ## Festlegen
>>> s.__dict__
{ 'name': 'GOOG','shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares        ## Löschen
>>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>

Lesen von Attributen

Nehmen wir an, Sie lesen ein Attribut auf einer Instanz.

x = obj.name

Das Attribut kann an zwei Stellen vorhanden sein:

  • Lokales Instanzwörterbuch.
  • Klassenwörterbuch.

Beide Wörterbücher müssen überprüft werden. Zunächst wird im lokalen __dict__ nachgeschaut. Wenn es nicht gefunden wird, wird im __dict__ der Klasse über __class__ gesucht.

>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>

Dieses Suchschema ist die Art und Weise, wie die Mitglieder einer Klasse von allen Instanzen geteilt werden.

Wie Vererbung funktioniert

Klassen können von anderen Klassen erben.

class A(B, C):
  ...

Die Basisklassen werden in einem Tupel in jeder Klasse gespeichert.

>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>

Dies liefert einen Link zu den Elternklassen.

Lesen von Attributen mit Vererbung

Logisch gesehen verläuft der Prozess zum Finden eines Attributs wie folgt. Zunächst wird im lokalen __dict__ nachgeschaut. Wenn es nicht gefunden wird, wird im __dict__ der Klasse gesucht. Wenn es in der Klasse nicht gefunden wird, wird in den Basisklassen über __bases__ gesucht. Es gibt jedoch einige subtilere Aspekte, die im Folgenden besprochen werden.

Lesen von Attributen mit einfacher Vererbung

In Vererbungs hierarchien werden Attribute indem man in der Reihenfolge den Vererbungsbaum hinauf läuft, gefunden.

class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass

Bei einfacher Vererbung gibt es einen einzigen Pfad zum obersten Element. Man stoppt bei der ersten Übereinstimmung.

Method Resolution Order oder MRO

Python berechnet eine Vererbungsfolge im Voraus und speichert sie im MRO-Attribut der Klasse. Sie können es anzeigen.

>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
 <class '__main__.B'>, <class '__main__.A'>,
 <type 'object'>)
>>>

Diese Folge wird als Method Resolution Order bezeichnet. Um ein Attribut zu finden, läuft Python die MRO in der angegebenen Reihenfolge ab. Die erste Übereinstimmung gewinnt.

MRO bei Mehrfachvererbung

Beim Mehrfachvererbung gibt es keinen einzigen Pfad zum obersten Element. Schauen wir uns ein Beispiel an.

class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass

Was passiert, wenn Sie auf ein Attribut zugreifen?

e = E()
e.attr

Es wird ein Attributsucheprozess durchgeführt, aber welche ist die Reihenfolge? Das ist ein Problem.

Python verwendet kooperatives Mehrfachvererbung, die bestimmte Regeln zur Klasseneinordnung befolgt.

  • Kinder werden immer vor Eltern überprüft
  • Eltern (wenn mehrere) werden immer in der aufgelisteten Reihenfolge überprüft.

Die MRO wird berechnet, indem alle Klassen in einer Hierarchie gemäß diesen Regeln sortiert werden.

>>> E.__mro__
(
  <class 'E'>,
  <class 'C'>,
  <class 'A'>,
  <class 'D'>,
  <class 'B'>,
  <class 'object'>)
>>>

Der zugrunde liegende Algorithmus wird als "C3 Linearization Algorithm" bezeichnet. Die genauen Details sind nicht wichtig, solange Sie sich daran erinnern, dass eine Klassenhierarchie die gleichen Einordnungsregeln befolgt, denen Sie folgen würden, wenn Ihr Haus in Flammen steht und Sie evakuieren müssen - Kinder zuerst, gefolgt von Eltern.

Ein sonderbarer Code-Wiederverwendungseffekt (mit Mehrfachvererbung)

Betrachten Sie zwei völlig unverbundene Objekte:

class Dog:
    def noise(self):
        return 'Bark'

    def chase(self):
        return 'Chasing!'

class LoudDog(Dog):
    def noise(self):
        ## Code-Ähnlichkeit mit LoudBike (siehe unten)
        return super().noise().upper()

Und

class Bike:
    def noise(self):
        return 'On Your Left'

    def pedal(self):
        return 'Pedaling!'

class LoudBike(Bike):
    def noise(self):
        ## Code-Ähnlichkeit mit LoudDog (siehe oben)
        return super().noise().upper()

Es gibt eine Code-Ähnlichkeit in der Implementierung von LoudDog.noise() und LoudBike.noise(). Tatsächlich ist der Code genau derselbe. Natürlich wird ein solcher Code sicherlich Softwareingenieure ansprechen.

Das "Mixin"-Muster

Das Mixin-Muster ist eine Klasse mit einem Codefragment.

class Loud:
    def noise(self):
        return super().noise().upper()

Diese Klasse kann nicht in isolation verwendet werden. Sie mischt sich mit anderen Klassen über Vererbung.

class LoudDog(Loud, Dog):
    pass

class LoudBike(Loud, Bike):
    pass

Wunderbarerweise wurde die Lautstärke jetzt nur einmal implementiert und in zwei völlig unverbundenen Klassen wiederverwendet. Solch ein Trick ist eine der primären Anwendungen der Mehrfachvererbung in Python.

Warum super()?

Verwenden Sie immer super() beim Überschreiben von Methoden.

class Loud:
    def noise(self):
        return super().noise().upper()

super() delegiert an die nächste Klasse in der MRO.

Das Knackige dabei ist, dass Sie nicht wissen, was es ist. Vor allem wissen Sie es nicht, wenn Mehrfachvererbung verwendet wird.

Einige Vorsichtsmaßnahmen

Mehrfachvererbung ist ein leistungsstarkes Werkzeug. Denken Sie daran: Mit Macht kommt auch Verantwortung. Frameworks / Bibliotheken verwenden es manchmal für fortgeschrittene Funktionen, die die Komposition von Komponenten betreffen. Vergessen Sie jetzt, dass Sie das gesehen haben.

Im Abschnitt 4 haben Sie eine Klasse Stock definiert, die einen Aktienbesitz repräsentiert. In dieser Übung werden wir diese Klasse verwenden. Starten Sie den Interpreter neu und erstellen Sie einige Instanzen:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm  = Stock('IBM',50, 91.23)
>>>

Übung 5.1: Darstellung von Instanzen

Im interaktiven Shell untersuchen Sie die zugrunde liegenden Wörterbücher der beiden Instanzen, die Sie erstellt haben:

>>> goog.__dict__
... schauen Sie sich das Ergebnis an...
>>> ibm.__dict__
... schauen Sie sich das Ergebnis an...
>>>

Übung 5.2: Änderung von Instanzdaten

Versuchen Sie, einem der obigen Instanzen ein neues Attribut zuzuweisen:

>>> goog.date = '6/11/2007'
>>> goog.__dict__
... schauen Sie sich das Ergebnis an...
>>> ibm.__dict__
... schauen Sie sich das Ergebnis an...
>>>

In der obigen Ausgabe werden Sie feststellen, dass die goog-Instanz ein Attribut date hat, während die ibm-Instanz es nicht hat. Es ist wichtig zu beachten, dass Python eigentlich keine Beschränkungen für Attribute stellt. Beispielsweise sind die Attribute einer Instanz nicht auf die in der __init__()-Methode eingerichteten beschränkt.

Anstatt ein Attribut zuzuweisen, versuchen Sie, einen neuen Wert direkt in das __dict__-Objekt zu platzieren:

>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>

Hier erkennen Sie wirklich die Tatsache, dass eine Instanz einfach eine Schicht über einem Wörterbuch ist. Hinweis: Es sollte betont werden, dass das direkte Manipulieren des Wörterbuchs ungewöhnlich ist - Sie sollten immer Ihren Code so schreiben, dass Sie die (.)-Syntax verwenden.

Übung 5.3: Die Rolle von Klassen

Die Definitionen, die zu einer Klassendefinition gehören, werden von allen Instanzen dieser Klasse geteilt. Beachten Sie, dass alle Instanzen einen Link zurück zu ihrer zugehörigen Klasse haben:

>>> goog.__class__
... schauen Sie sich das Ergebnis an...
>>> ibm.__class__
... schauen Sie sich das Ergebnis an...
>>>

Versuchen Sie, eine Methode auf den Instanzen aufzurufen:

>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>

Beachten Sie, dass der Name 'cost' weder in goog.__dict__ noch in ibm.__dict__ definiert ist. Stattdessen wird er von dem Klassenwörterbuch bereitgestellt. Versuchen Sie das:

>>> Stock.__dict__['cost']
... schauen Sie sich das Ergebnis an...
>>>

Versuchen Sie, die cost()-Methode direkt über das Wörterbuch aufzurufen:

>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>

Beachten Sie, wie Sie die in der Klassendefinition definierte Funktion aufrufen und wie das self-Argument die Instanz erhält.

Versuchen Sie, einem neuen Attribut der Stock-Klasse hinzuzufügen:

>>> Stock.foo = 42
>>>

Beachten Sie, wie dieses neue Attribut jetzt auf allen Instanzen erscheint:

>>> goog.foo
42
>>> ibm.foo
42
>>>

Beachten Sie jedoch, dass es kein Teil des Instanzenwörterbuchs ist:

>>> goog.__dict__
... schauen Sie sich das Ergebnis an und bemerken Sie, dass es kein 'foo'-Attribut gibt...
>>>

Der Grund, warum Sie das foo-Attribut auf Instanzen zugreifen können, ist, dass Python immer das Klassenwörterbuch überprüft, wenn es etwas auf der Instanz selbst nicht finden kann.

Hinweis: Dieser Teil der Übung veranschaulicht etwas, das als Klassenvariable bekannt ist. Nehmen Sie beispielsweise an, dass Sie eine Klasse wie diese haben:

class Foo(object):
     a = 13                  ## Klassenvariable
     def __init__(self,b):
         self.b = b          ## Instanzvariable

In dieser Klasse ist die Variable a, die im Körper der Klasse selbst zugewiesen wird, eine "Klassenvariable". Sie wird von allen erstellten Instanzen geteilt. Beispielsweise:

>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a          ## Überprüfen Sie die Klassenvariable (gleich für beide Instanzen)
13
>>> g.a
13
>>> f.b          ## Überprüfen Sie die Instanzvariable (unterschiedlich)
10
>>> g.b
20
>>> Foo.a = 42   ## Ändern Sie den Wert der Klassenvariablen
>>> f.a
42
>>> g.a
42
>>>

Übung 5.4: Gebundene Methoden

Ein subtiler Aspekt von Python ist, dass das Aufrufen einer Methode tatsächlich zwei Schritte umfasst und etwas, das als gebundene Methode bekannt ist. Beispielsweise:

>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>

Gebundene Methoden enthalten tatsächlich alle Bestandteile, die für das Aufrufen einer Methode erforderlich sind. Beispielsweise behalten sie einen Verweis auf die Funktion, die die Methode implementiert:

>>> s.__func__
<function sell at 0x10049af50>
>>>

Dies ist der gleiche Wert wie der, der im Stock-Wörterbuch gefunden wird.

>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>

Gebundene Methoden verzeichnen auch die Instanz, die das self-Argument ist.

>>> s.__self__
Stock('GOOG',75,490.1)
>>>

Wenn Sie die Funktion mit () aufrufen, kommen alle Bestandteile zusammen. Beispielsweise führt das Aufrufen von s(25) tatsächlich folgendes aus:

>>> s.__func__(s.__self__, 25)    ## Identisch mit s(25)
>>> goog.shares
50
>>>

Übung 5.5: Vererbung

Erstellen Sie eine neue Klasse, die von Stock erbt.

>>> class NewStock(Stock):
        def yow(self):
            print('Yow!')

>>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>

Die Vererbung wird durch Erweiterung des Suchprozesses für Attribute implementiert. Das __bases__-Attribut hat ein Tupel der unmittelbaren Elternklassen:

>>> NewStock.__bases__
(<class'stock.Stock'>,)
>>>

Das __mro__-Attribut hat ein Tupel aller Elternklassen in der Reihenfolge, in der nach Attributen gesucht wird.

>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class'stock.Stock'>, <class 'object'>)
>>>

So würde die cost()-Methode der oben genannten Instanz n gefunden werden:

>>> for cls in n.__class__.__mro__:
        if 'cost' in cls.__dict__:
            break

>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab "Dictionaries Revisited" abgeschlossen. Sie können in LabEx weitere Labs ausprobieren, um Ihre Fähigkeiten zu verbessern.