Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 359 – Die „make“-Anweisung

Autor:
Steven Bethard <steven.bethard at gmail.com>
Status:
Zurückgezogen
Typ:
Standards Track
Erstellt:
05. Apr. 2006
Python-Version:
2.6
Post-History:
05. Apr. 2006, 06. Apr. 2006, 13. Apr. 2006

Inhaltsverzeichnis

Zusammenfassung

Diese PEP schlägt eine Verallgemeinerung der Syntax für Klassendeklarationen vor, die make-Anweisung. Die vorgeschlagene Syntax und Semantik parallelen der Syntax für Klassendefinitionen, und so

make <callable> <name> <tuple>:
    <block>

wird in die Zuweisung übersetzt

<name> = <callable>("<name>", <tuple>, <namespace>)

wobei <namespace> das Dictionary ist, das durch Ausführen von <block> erstellt wird. Dies ist hauptsächlich syntaktischer Zucker für

class <name> <tuple>:
    __metaclass__ = <callable>
    <block>

und soll die Absicht der Anweisung klarer ausdrücken, wenn etwas anderes als eine Klasse erstellt wird. Natürlich ist auch eine andere Syntax für eine solche Anweisung möglich, aber es wird gehofft, dass durch die starke Parallele zur Klassenanweisung das Verständnis, wie Klassen und Metaklassen funktionieren, sich auf das Verständnis der Funktionsweise der make-Anweisung übertragen lässt.

Die PEP basiert auf einem Vorschlag [1] von Michele Simionato aus der python-dev-Liste.

Rücknahmehinweis

Diese PEP wurde auf Guidos Wunsch [2] zurückgezogen. Guido mochte sie nicht, und insbesondere mochte er nicht, wie der Anwendungsfall der Eigenschaft die Instanzmethoden einer Eigenschaft auf einer anderen Ebene als andere Instanzmethoden platziert und feste Namen für die Eigenschaftsfunktionen erfordert.

Motivation

Klassenanweisungen bieten Python zwei nützliche Einrichtungen

  1. Sie führen einen Block von Anweisungen aus und stellen die daraus resultierenden Bindungen als Dictionary an die Metaklasse.
  2. Sie fördern DRY (Don't Repeat Yourself), indem sie der zu erstellenden Klasse erlauben, den Namen zu kennen, dem sie zugewiesen wird.

Somit in einer einfachen Klassenanweisung wie

class C(object):
    x = 1
    def foo(self):
        return 'bar'

erhält die Metaklasse (type) einen Aufruf mit etwas wie

C = type('C', (object,), {'x':1, 'foo':<function foo at ...>})

Die Klassenanweisung ist nur syntaktischer Zucker für die obige Zuweisungsanweisung, aber eindeutig ein sehr nützlicher syntaktischer Zucker. Sie vermeidet nicht nur die Wiederholung von C, sondern vereinfacht auch die Erstellung des Dictionaries, indem sie es als eine Reihe von Anweisungen ausdrückt.

Historisch gesehen waren Typinstanzen (auch bekannt als Klassenobjekte) die einzigen Objekte, die diese Art von syntaktischer Unterstützung erhielten. Die make-Anweisung zielt darauf ab, diese Unterstützung auf andere Arten von Objekten auszudehnen, bei denen eine solche Syntax ebenfalls nützlich wäre.

Beispiel: einfache Namespaces

Nehmen wir an, ich habe einige Attribute in einem Modul, auf die ich wie folgt zugreife:

mod.thematic_roletype
mod.opinion_roletype

mod.text_format
mod.html_format

und da „Namespaces sind eine verdammt gute Idee“, möchte ich stattdessen über diese Attribute zugreifen können als

mod.roletypes.thematic
mod.roletypes.opinion

mod.format.text
mod.format.html

Derzeit habe ich zwei Hauptoptionen

  1. Das Modul in ein Paket umwandeln, roletypes und format in Untermodule umwandeln und die Attribute in die Untermodule verschieben.
  2. Die Klassen roletypes und format erstellen und die Attribute in die Klassen verschieben.

Ersteres ist eine beträchtliche Refactoring-Arbeit und erzeugt zwei winzige Module ohne viel Inhalt. Letzteres behält die Attribute lokal im Modul, erstellt aber Klassen, von denen keine Instanzen jemals erstellt werden sollen.

In Situationen wie dieser wäre es schön, einfach einen „Namespace“ deklarieren zu können, um die wenigen Attribute zu speichern. Mit der neuen make-Anweisung könnte ich meine neuen Namespaces mit etwas wie diesem einführen:

make namespace roletypes:
    thematic = ...
    opinion = ...

make namespace format:
    text = ...
    html = ...

und meine Attribute lokal im Modul behalten, ohne Klassen zu erstellen, von denen keine Instanzen erstellt werden sollen. Eine Definition von Namespace, die dies ermöglichen würde, ist

class namespace(object):
    def __init__(self, name, args, kwargs):
        self.__dict__.update(kwargs)

Mit dieser Definition wären am Ende der obigen make-Anweisungen roletypes und format Namespace-Instanzen.

Beispiel: GUI-Objekte

In GUI-Toolkits sind Objekte wie Frames und Panels oft mit Attributen und Funktionen verbunden. Mit der make-Anweisung könnte Code, der etwa so aussieht:

root = Tkinter.Tk()
frame = Tkinter.Frame(root)
frame.pack()
def say_hi():
    print "hi there, everyone!"
hi_there = Tkinter.Button(frame, text="Hello", command=say_hi)
hi_there.pack(side=Tkinter.LEFT)
root.mainloop()

könnte umgeschrieben werden, um die Funktion des Buttons mit seiner Deklaration zu gruppieren:

root = Tkinter.Tk()
frame = Tkinter.Frame(root)
frame.pack()
make Tkinter.Button hi_there(frame):
    text = "Hello"
    def command():
        print "hi there, everyone!"
hi_there.pack(side=Tkinter.LEFT)
root.mainloop()

Beispiel: benutzerdefinierte Deskriptoren

Da Deskriptoren verwendet werden, um den Zugriff auf ein Attribut anzupassen, ist es oft nützlich, den Namen dieses Attributs zu kennen. Das aktuelle Python bietet keine einfache Möglichkeit, diesen Namen zu finden, und so müssen viele benutzerdefinierte Deskriptoren, wie Ian Bickings setonce-Deskriptor [3], dies irgendwie umgehen. Mit der make-Anweisung könnten Sie ein setonce-Attribut wie dieses erstellen:

class A(object):
    ...
    make setonce x:
        "A's x attribute"
    ...

wobei der setonce-Deskriptor wie folgt definiert wäre:

class setonce(object):

    def __init__(self, name, args, kwargs):
        self._name = '_setonce_attr_%s' % name
        self.__doc__ = kwargs.pop('__doc__', None)

    def __get__(self, obj, type=None):
        if obj is None:
            return self
        return getattr(obj, self._name)

    def __set__(self, obj, value):
        try:
            getattr(obj, self._name)
        except AttributeError:
            setattr(obj, self._name, value)
        else:
            raise AttributeError("Attribute already set")

    def set(self, obj, value):
        setattr(obj, self._name, value)

    def __delete__(self, obj):
        delattr(obj, self._name)

Beachten Sie, dass im Gegensatz zur ursprünglichen Implementierung der Name des privaten Attributs stabil ist, da er den Namen des Deskriptors verwendet, und daher Instanzen der Klasse A picklebar sind.

Beispiel: Eigenschafts-Namespaces

Pythons Eigenschaftstyp nimmt drei Funktionsargumente und ein Docstring-Argument entgegen, das, obwohl nur für die Eigenschaft relevant, vor ihr deklariert und dann als Argument an den Eigenschaftsaufruf übergeben werden muss, z.B.:

class C(object):
    ...
    def get_x(self):
        ...
    def set_x(self):
        ...
    x = property(get_x, set_x, "the x of the frobulation")

Dieses Problem wurde bereits angesprochen, und Guido [4] und andere [5] haben kurz über alternative Eigenschaftssynthesen nachgedacht, um die Deklaration von Eigenschaften zu erleichtern. Mit der make-Anweisung könnte folgende Syntax unterstützt werden:

class C(object):
    ...
    make block_property x:
        '''The x of the frobulation'''
        def fget(self):
            ...
        def fset(self):
            ...

mit der folgenden Definition von block_property:

def block_property(name, args, block_dict):
    fget = block_dict.pop('fget', None)
    fset = block_dict.pop('fset', None)
    fdel = block_dict.pop('fdel', None)
    doc = block_dict.pop('__doc__', None)
    assert not block_dict
    return property(fget, fset, fdel, doc)

Beispiel: Schnittstellen

Guido [6] und andere haben gelegentlich vorgeschlagen, Schnittstellen in Python einzuführen. Die meisten Vorschläge boten eine Syntax im Stil von:

interface IFoo:
    """Foo blah blah"""

    def fumble(name, count):
        """docstring"""

Da es derzeit keine Möglichkeit in Python gibt, eine Schnittstelle auf diese Weise zu deklarieren, verwenden die meisten Implementierungen von Python-Schnittstellen stattdessen Klassenobjekte, z.B. Zopes

class IFoo(Interface):
    """Foo blah blah"""

    def fumble(name, count):
        """docstring"""

Mit der neuen make-Anweisung könnten diese Schnittstellen stattdessen wie folgt deklariert werden:

make Interface IFoo:
    """Foo blah blah"""

    def fumble(name, count):
        """docstring"""

was die Absicht (dass dies eine Schnittstelle und keine Klasse ist) viel klarer macht.

Spezifikation

Python übersetzt eine make-Anweisung

make <callable> <name> <tuple>:
    <block>

in die Zuweisung

<name> = <callable>("<name>", <tuple>, <namespace>)

wobei <namespace> das Dictionary ist, das durch Ausführen von <block> erstellt wird. Der Ausdruck <tuple> ist optional; wenn er nicht vorhanden ist, wird ein leeres Tupel angenommen.

Ein Patch zur Implementierung dieser Semantik ist verfügbar [7].

Die make-Anweisung führt ein neues Schlüsselwort ein: make. In Python 2.6 muss die make-Anweisung daher über from __future__ import make_statement aktiviert werden.

Offene Fragen

Schlüsselwort

Bricht das make-Schlüsselwort zu viel Code? Ursprünglich verwendete die make-Anweisung das Schlüsselwort create (ein Vorschlag von Alyssa Coghlan). Untersuchungen der Standardbibliothek [8] und des Zope+Plone-Codes [9] zeigten jedoch, dass create viel mehr Code brechen würde, sodass stattdessen make als Schlüsselwort übernommen wurde. Es gibt jedoch immer noch einige Fälle, in denen make Code brechen würde. Gibt es ein besseres Schlüsselwort für die Anweisung?

Einige mögliche Schlüsselwörter und ihre Anzahlen in der Standardbibliothek (plus einige installierte Pakete)

  • make - 2 (beide in Tests)
  • create - 19 (einschließlich bestehender Funktion in imaplib)
  • build - 83 (einschließlich bestehender Klasse in distutils.command.build)
  • construct - 0
  • produce - 0

Die make-Anweisung als alternativer Konstruktor

Derzeit gibt es nicht viele Funktionen mit der Signatur (name, args, kwargs). Das bedeutet, dass etwas wie

make dict params:
    x = 1
    y = 2

derzeit unmöglich ist, da der Dictionary-Konstruktor eine andere Signatur hat. Muss eine solche Sache unterstützt werden? Ein Vorschlag von Carl Banks wäre, eine __make__-Magie-Methode hinzuzufügen, die, wenn sie gefunden wird, anstelle von __call__ aufgerufen wird. Für Typen wäre die __make__-Methode identisch mit __call__ und somit unnötig, aber Dictionaries könnten die make-Anweisung unterstützen, indem sie eine __make__-Methode für den Dictionary-Typ definieren, die ungefähr so aussieht:

def __make__(cls, name, args, kwargs):
    return cls(**kwargs)

Anstatt eine weitere Magie-Methode hinzuzufügen, könnte der Dictionary-Typ natürlich auch einfach eine Klassenmethode wie dict.fromblock erhalten, die wie folgt verwendet werden könnte:

make dict.fromblock params:
    x = 1
    y = 2

Die Frage ist also, werden viele Typen die make-Anweisung als alternativen Konstruktor verwenden wollen? Und wenn ja, muss dieser alternative Konstruktor denselben Namen wie der ursprüngliche Konstruktor haben?

Anpassen des Dictionaries, in dem der Block ausgeführt wird

Sollen Benutzer der make-Anweisung bestimmen können, in welchem Dictionary-Objekt der Code ausgeführt wird? Dies würde die Verwendung der make-Anweisung in Situationen ermöglichen, in denen ein normales Dictionary-Objekt nicht ausreicht, z.B. wenn Reihenfolge und wiederholte Namen zulässig sein müssen. Die Ermöglichung einer solchen Anpassung könnte die Erstellung von XML ohne wiederholte Elementnamen ermöglichen, wobei die Verschachtelung von make-Anweisungen der Verschachtelung von XML-Elementen entspricht.

make Element html:
    make Element body:
        text('before first h1')
        make Element h1:
            attrib(style='first')
            text('first h1')
            tail('after first h1')
        make Element h1:
            attrib(style='second')
            text('second h1')
            tail('after second h1')

Wenn die make-Anweisung versuchen würde, das Dictionary, in dem sie ihren Block ausführen soll, durch Aufruf der __make_dict__-Methode des Aufrufbaren zu erhalten, würde der folgende Code die Verwendung der make-Anweisung wie oben ermöglichen:

class Element(object):

    class __make_dict__(dict):

        def __init__(self, *args, **kwargs):
            self._super = super(Element.__make_dict__, self)
            self._super.__init__(*args, **kwargs)
            self.elements = []
            self.text = None
            self.tail = None
            self.attrib = {}

        def __getitem__(self, name):
            try:
                return self._super.__getitem__(name)
            except KeyError:
                if name in ['attrib', 'text', 'tail']:
                    return getattr(self, 'set_%s' % name)
                else:
                    return globals()[name]

        def __setitem__(self, name, value):
            self._super.__setitem__(name, value)
            self.elements.append(value)

        def set_attrib(self, **kwargs):
            self.attrib = kwargs

        def set_text(self, text):
            self.text = text

        def set_tail(self, text):
            self.tail = text

    def __new__(cls, name, args, edict):
        get_element = etree.ElementTree.Element
        result = get_element(name, attrib=edict.attrib)
        result.text = edict.text
        result.tail = edict.tail
        for element in edict.elements:
            result.append(element)
        return result

Beachten Sie jedoch, dass der Code zur Unterstützung dessen etwas fragil ist – er muss den Namespace magisch mit attrib, text und tail füllen und geht davon aus, dass jede Namensbindung innerhalb des make-Anweisungsrumpfes ein Element erstellt. In seiner jetzigen Form würde dieser Code mit der Einführung einer einfachen for-Schleife in irgendeinen der make-Anweisungsrümpfe fehlschlagen, da die for-Schleife einen Namen an ein Nicht-Element-Objekt binden würde. Dies könnte durch Hinzufügen einer Art isinstance-Prüfung oder Attributuntersuchung umgangen werden, aber dies führt immer noch zu einer etwas fragilen Lösung.

Es wurde auch darauf hingewiesen, dass die with-Anweisung eine äquivalente Verschachtelung mit einer viel expliziteren Syntax bieten kann:

with Element('html') as html:
    with Element('body') as body:
        body.text = 'before first h1'
        with Element('h1', style='first') as h1:
            h1.text = 'first h1'
            h1.tail = 'after first h1'
        with Element('h1', style='second') as h1:
            h1.text = 'second h1'
            h1.tail = 'after second h1'

Und wenn die Wiederholung der Elementnamen hier zu stark gegen DRY verstößt, ist es auch möglich, alle as-Klauseln bis auf die erste zu eliminieren, indem ein paar Methoden zu Element hinzugefügt werden. [10]

Gibt es also echte Anwendungsfälle für die Ausführung des Blocks in einem Dictionary eines anderen Typs? Und wenn ja, sollte die make-Anweisung erweitert werden, um sie zu unterstützen?

Optionale Erweiterungen

Entfernen des make-Schlüsselworts

Es wäre möglich, das make-Schlüsselwort zu entfernen, sodass solche Anweisungen mit dem Aufrufbaren beginnen würden, z.B.:

namespace ns:
    badger = 42
    def spam():
        ...

interface C(...):
    ...

Fast alle anderen Python-Anweisungen beginnen jedoch mit einem Schlüsselwort, und das Entfernen des Schlüsselworts würde es schwieriger machen, diesen Konstrukt in der Dokumentation nachzuschlagen. Außerdem würde dies die Grammatik etwas verkomplizieren und ich (Steven Bethard) konnte das Feature bisher nicht ohne das Schlüsselwort implementieren.

Entfernen von __metaclass__ in Python 3000

Als Nebeneffekt seiner Allgemeinheit eliminiert die make-Anweisung weitgehend die Notwendigkeit des __metaclass__-Attributs in Klassenobjekten. In Python 3000 könnte anstelle von

class <name> <bases-tuple>:
    __metaclass__ = <metaclass>
    <block>

Metaklassen durch die Verwendung der Metaklasse als Aufrufbare in einer make-Anweisung unterstützt werden:

make <metaclass> <name> <bases-tuple>:
    <block>

Das Entfernen des __metaclass__-Hooks würde den BUILD_CLASS-Opcode etwas vereinfachen.

Entfernen von class-Anweisungen in Python 3000

In der extremsten Anwendung von make-Anweisungen könnte die Klassenanweisung selbst zugunsten von make type-Anweisungen veraltet werden.

Referenzen


Quelle: https://github.com/python/peps/blob/main/peps/pep-0359.rst

Zuletzt geändert: 2025-02-01 08:59:27 GMT