Klassen und Kapselung

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

Wenn Sie Klassen schreiben, ist es üblich, versuchen, interne Details zu kapseln. In diesem Abschnitt werden einige Python-Programmier-Idiome dazu vorgestellt, darunter private Variablen und Eigenschaften.

Öffentlich vs Privat

Eine der primären Rollen einer Klasse besteht darin, Daten und interne Implementierungsdetails eines Objekts zu kapseln. Allerdings definiert eine Klasse auch eine öffentliche Schnittstelle, die die Außenwelt verwenden soll, um das Objekt zu manipulieren. Diese Unterscheidung zwischen Implementierungsdetails und der öffentlichen Schnittstelle ist wichtig.

Ein Problem

In Python ist fast alles über Klassen und Objekte öffentlich.

  • Sie können die internen Details eines Objekts leicht untersuchen.
  • Sie können beliebig Dinge ändern.
  • Es gibt keinen starken Zugangskontrollbegriff (d.h. private Klassenelemente)

Dies ist ein Problem, wenn Sie versuchen, die Details der internen Implementierung zu isolieren.

Python-Kapselung

Python stützt sich auf Programmierkonventionen, um die beabsichtigte Verwendung von etwas anzuzeigen. Diese Konventionen basieren auf der Namensgebung. Es besteht eine allgemeine Einstellung, dass es dem Programmierer überlassen ist, die Regeln zu befolgen, im Gegensatz dazu, dass die Sprache sie durchsetzt.

Private Attribute

Jeder Attributname mit führendem _ wird als privat betrachtet.

class Person(object):
    def __init__(self, name):
        self._name = 0

Wie bereits erwähnt, ist dies nur ein Programmierstil. Sie können es immer noch zugreifen und ändern.

>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>

Als allgemeine Regel gilt, dass jeder Name mit führendem _ als interne Implementierung betrachtet wird, ob es sich um eine Variable, eine Funktion oder einen Modulnamen handelt. Wenn Sie sich finden, dass Sie solche Namen direkt verwenden, tun Sie wahrscheinlich etwas falsch. Suchen Sie nach höherebenen Funktionalität.

Einfache Attribute

Betrachten Sie die folgende Klasse.

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

Ein überraschendes Merkmal ist, dass Sie die Attribute beliebig auf einen beliebigen Wert setzen können:

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>

Sie könnten sich das ansehen und denken, dass Sie zusätzliche Überprüfungen benötigen.

s.shares = '50'     #引发一个 TypeError,这是一个字符串

Wie würden Sie das tun?

Verwaltete Attribute

Eine Möglichkeit: Einführung von Zugangsmethoden.

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

    ## Funktion, die die "get"-Operation abdeckt
    def get_shares(self):
        return self._shares

    ## Funktion, die die "set"-Operation abdeckt
    def set_shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Erwartet einen int')
        self._shares = value

Schade, dass damit all unseren bestehenden Code kaputt geht. s.shares = 50 wird zu s.set_shares(50)

Eigenschaften

Es gibt einen alternativen Ansatz zum vorherigen Muster.

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

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Erwartet int')
        self._shares = value

Der normale Attribut-Zugriff löst jetzt die Getter- und Setter-Methoden unter @property und @shares.setter aus.

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares         ## Triggers @property
50
>>> s.shares = 75    ## Triggers @shares.setter
>>>

Mit diesem Muster sind keine Änderungen am Quellcode erforderlich. Der neue Setter wird auch aufgerufen, wenn innerhalb der Klasse eine Zuweisung erfolgt, einschließlich innerhalb der __init__()-Methode.

class Stock:
    def __init__(self, name, shares, price):
       ...
        ## Diese Zuweisung ruft den unten stehenden Setter auf
        self.shares = shares
       ...

  ...
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Erwartet int')
        self._shares = value

Es besteht oft Verwirrung zwischen einer Eigenschaft und der Verwendung privater Namen. Obwohl eine Eigenschaft intern einen privaten Namen wie _shares verwendet, kann der Rest der Klasse (nicht die Eigenschaft) weiterhin einen Namen wie shares verwenden.

Eigenschaften sind auch nützlich für berechnete Datenattribute.

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

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

Dies ermöglicht es Ihnen, die zusätzlichen Klammern zu weglassen und die Tatsache zu verbergen, dass es tatsächlich eine Methode ist:

>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares ## Instanzvariable
100
>>> s.cost   ## Berechneter Wert
49010.0
>>>

Uniform access

Das letzte Beispiel zeigt, wie man eine einheitlichere Schnittstelle für ein Objekt erstellt. Wenn man das nicht tut, kann das Verwenden eines Objekts verwirrend sein:

>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() ## Methode
49010.0
>>> b = s.shares ## Datenattribut
100
>>>

Warum ist die () für den Wert von cost erforderlich, aber nicht für shares? Eine Eigenschaft kann das beheben.

Dekorator-Syntax

Die @-Syntax wird als "Dekoration" bezeichnet. Sie gibt einen Modifizierer an, der auf die unmittelbar folgende Funktionsdefinition angewendet wird.

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

Weitere Details werden im Abschnitt 7 gegeben.

__slots__-Attribut

Sie können die Menge der Attributnamen einschränken.

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

Es wird ein Fehler für andere Attribute auslösen.

>>> s.price = 385.15
>>> s.prices = 410.2
Traceback (most recent call last):
File "<stdin>", line 1, in?
AttributeError: 'Stock' Objekt hat kein Attribut 'prices'

Obwohl dies Fehler verhindert und die Verwendung von Objekten einschränkt, wird es tatsächlich für die Leistung verwendet und lässt Python den Arbeitsspeicher effizienter nutzen.

Letzte Bemerkungen zur Kapselung

Gehen Sie nicht über die Maßen bei privaten Attributen, Eigenschaften, Slots usw. Sie dienen einem bestimmten Zweck und Sie werden sie möglicherweise bei der Lektüre anderer Python-Code sehen. Sie sind jedoch für den meisten alltäglichen Code nicht erforderlich.

Übung 5.6: Einfache Eigenschaften

Eigenschaften sind eine nützliche Möglichkeit, "berechnete Attribute" einem Objekt hinzuzufügen. In stock.py haben Sie ein Objekt Stock erstellt. Beachten Sie, dass bei Ihrem Objekt eine leichte Inkonsistenz bei der Extraktion unterschiedlicher Arten von Daten besteht:

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>

Insbesondere beobachten Sie, wie Sie die zusätzlichen Klammern () zu cost hinzufügen müssen, da es sich um eine Methode handelt.

Sie können die zusätzlichen Klammern () bei cost() entfernen, indem Sie es zu einer Eigenschaft machen. Nehmen Sie Ihre Stock-Klasse und ändern Sie sie so, dass die Kostenberechnung wie folgt funktioniert:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>

Versuchen Sie, s.cost() als Funktion aufzurufen, und beobachten Sie, dass es jetzt nicht funktioniert, da cost als Eigenschaft definiert wurde.

>>> s.cost()
... funktioniert nicht...
>>>

Dieser Änderung wird wahrscheinlich Ihr früheres pcost.py-Programm brechen. Sie müssen möglicherweise zurückgehen und die Klammern () bei der cost()-Methode entfernen.

✨ Lösung prüfen und üben

Übung 5.7: Eigenschaften und Setter

Ändern Sie das shares-Attribut so, dass der Wert in einem privaten Attribut gespeichert wird und dass eine Paar von Eigenschaftsfunktionen verwendet wird, um sicherzustellen, dass er immer als ganzzahliger Wert festgelegt wird. Hier ist ein Beispiel für das erwartete Verhalten:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'a lot'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: erwartet eine Ganzzahl
>>>
✨ Lösung prüfen und üben

Übung 5.8: Hinzufügen von Slots

Ändern Sie die Stock-Klasse so, dass sie ein __slots__-Attribut hat. Anschließend überprüfen Sie, dass neue Attribute nicht hinzugefügt werden können:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... sehen Sie, was passiert...
>>>

Wenn Sie __slots__ verwenden, verwendet Python eine effizientere interne Darstellung von Objekten. Was passiert, wenn Sie versuchen, das zugrunde liegende Wörterbuch von s wie oben zu untersuchen?

>>> s.__dict__
... sehen Sie, was passiert...
>>>

Es sollte bemerkt werden, dass __slots__ am häufigsten als Optimierung für Klassen verwendet wird, die als Datenstrukturen dienen. Das Verwenden von Slots wird solche Programme dazu bringen, viel weniger Speicher zu verwenden und etwas schneller zu laufen. Auf den meisten anderen Klassen sollten Sie jedoch möglicherweise __slots__ vermeiden.

✨ Lösung prüfen und üben

Zusammenfassung

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