Guide för deskriptorer

Författare:

Raymond Hettinger

Kontakt:

<python at rcn dot com>

Deskriptorer låter objekt anpassa sökning, lagring och borttagning av attribut.

Denna guide består av fyra huvudavsnitt:

  1. ”Primer” ger en grundläggande översikt och går försiktigt vidare från enkla exempel och lägger till en funktion i taget. Börja här om du är nybörjare på deskriptorer.

  2. I det andra avsnittet visas ett komplett, praktiskt exempel på en descriptor. Om du redan kan grunderna kan du börja där.

  3. I det tredje avsnittet finns en mer teknisk handledning som går in i detalj på hur deskriptorer fungerar. De flesta människor behöver inte denna detaljnivå.

  4. Det sista avsnittet har rena Python-ekvivalenter för inbyggda deskriptorer som är skrivna i C. Läs detta om du är nyfiken på hur funktioner blir till bundna metoder eller om implementeringen av vanliga verktyg som classmethod(), staticmethod(), property() och __slots__.

Tändhatt

I den här guiden börjar vi med ett så grundläggande exempel som möjligt och sedan lägger vi till nya funktioner en efter en.

Enkelt exempel: En deskriptor som returnerar en konstant

Klassen Ten är en deskriptor vars __get__()-metod alltid returnerar konstanten 10:

class Ten:
    def __get__(self, obj, objtype=None):
        return 10

För att kunna använda deskriptorn måste den lagras som en klassvariabel i en annan klass:

class A:
    x = 5                       # Regular class attribute
    y = Ten()                   # Descriptor instance

En interaktiv session visar skillnaden mellan normal attributuppslagning och deskriptoruppslagning:

>>> a = A()                     # Make an instance of class A
>>> a.x                         # Normal attribute lookup
5
>>> a.y                         # Descriptor lookup
10

I sökningen efter attributet a.x hittar punktoperatorn 'x': 5 i klassordlistan. I sökningen efter a.y hittar punktoperatorn en deskriptorinstans, som känns igen genom metoden __get__. När den metoden anropas returneras 10.

Observera att värdet 10 inte lagras i vare sig klassordlistan eller instansordlistan. Istället beräknas värdet 10 på begäran.

Detta exempel visar hur en enkel descriptor fungerar, men det är inte särskilt användbart. För att hämta konstanter skulle normal attributuppslagning vara bättre.

I nästa avsnitt ska vi skapa något mer användbart, en dynamisk uppslagning.

Dynamiska uppslagningar

Intressanta deskriptorer kör vanligtvis beräkningar istället för att returnera konstanter:

import os

class DirectorySize:

    def __get__(self, obj, objtype=None):
        return len(os.listdir(obj.dirname))

class Directory:

    size = DirectorySize()              # Descriptor instance

    def __init__(self, dirname):
        self.dirname = dirname          # Regular instance attribute

En interaktiv session visar att uppslagningen är dynamisk - den beräknar olika, uppdaterade svar varje gång:

>>> s = Directory('songs')
>>> g = Directory('games')
>>> s.size                              # The songs directory has twenty files
20
>>> g.size                              # The games directory has three files
3
>>> os.remove('games/chess')            # Delete a game
>>> g.size                              # File count is automatically updated
2

Förutom att visa hur deskriptorer kan utföra beräkningar, avslöjar detta exempel också syftet med parametrarna i __get__(). Parametern self är size, en instans av DirectorySize. Parametern obj är antingen g eller s, en instans av Directory. Det är parametern obj som gör att metoden __get__() kan lära sig målkatalogen. Parametern objtype är klassen Directory.

Hanterade attribut

Ett populärt användningsområde för deskriptorer är att hantera åtkomst till instansdata. Deskriptorn tilldelas ett offentligt attribut i klassordlistan medan de faktiska uppgifterna lagras som ett privat attribut i instansordlistan. Deskriptorns metoder __get__() och __set__() utlöses när det publika attributet används.

I följande exempel är age det publika attributet och _age det privata attributet. När det publika attributet används loggar deskriptorn sökningen eller uppdateringen:

import logging

logging.basicConfig(level=logging.INFO)

class LoggedAgeAccess:

    def __get__(self, obj, objtype=None):
        value = obj._age
        logging.info('Accessing %r giving %r', 'age', value)
        return value

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', 'age', value)
        obj._age = value

class Person:

    age = LoggedAgeAccess()             # Descriptor instance

    def __init__(self, name, age):
        self.name = name                # Regular instance attribute
        self.age = age                  # Calls __set__()

    def birthday(self):
        self.age += 1                   # Calls both __get__() and __set__()

En interaktiv session visar att all åtkomst till det hanterade attributet age loggas, men att det vanliga attributet name inte loggas:

>>> mary = Person('Mary M', 30)         # The initial age update is logged
INFO:root:Updating 'age' to 30
>>> dave = Person('David D', 40)
INFO:root:Updating 'age' to 40

>>> vars(mary)                          # The actual data is in a private attribute
{'name': 'Mary M', '_age': 30}
>>> vars(dave)
{'name': 'David D', '_age': 40}

>>> mary.age                            # Access the data and log the lookup
INFO:root:Accessing 'age' giving 30
30
>>> mary.birthday()                     # Updates are logged as well
INFO:root:Accessing 'age' giving 30
INFO:root:Updating 'age' to 31

>>> dave.name                           # Regular attribute lookup isn't logged
'David D'
>>> dave.age                            # Only the managed attribute is logged
INFO:root:Accessing 'age' giving 40
40

Ett stort problem med detta exempel är att det privata namnet _age är fastkopplat i klassen LoggedAgeAccess. Det innebär att varje instans bara kan ha ett loggat attribut och att dess namn är oföränderligt. I nästa exempel ska vi åtgärda det problemet.

Anpassade namn

När en klass använder descriptors kan den informera varje descriptor om vilket variabelnamn som användes.

I det här exemplet har klassen Person två deskriptorinstanser, name och age. När klassen Person definieras gör den ett anrop till __set_name__() i LoggedAccess så att fältnamnen kan registreras och varje deskriptor får sina egna public_name och private_name:

import logging

logging.basicConfig(level=logging.INFO)

class LoggedAccess:

    def __set_name__(self, owner, name):
        self.public_name = name
        self.private_name = '_' + name

    def __get__(self, obj, objtype=None):
        value = getattr(obj, self.private_name)
        logging.info('Accessing %r giving %r', self.public_name, value)
        return value

    def __set__(self, obj, value):
        logging.info('Updating %r to %r', self.public_name, value)
        setattr(obj, self.private_name, value)

class Person:

    name = LoggedAccess()                # First descriptor instance
    age = LoggedAccess()                 # Second descriptor instance

    def __init__(self, name, age):
        self.name = name                 # Calls the first descriptor
        self.age = age                   # Calls the second descriptor

    def birthday(self):
        self.age += 1

En interaktiv session visar att klassen Person har anropat __set_name__() så att fältnamnen skulle registreras. Här anropar vi vars() för att leta upp deskriptorn utan att trigga den:

>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}

Den nya klassen loggar nu åtkomst till både namn och ålder:

>>> pete = Person('Peter P', 10)
INFO:root:Updating 'name' to 'Peter P'
INFO:root:Updating 'age' to 10
>>> kate = Person('Catherine C', 20)
INFO:root:Updating 'name' to 'Catherine C'
INFO:root:Updating 'age' to 20

De två Person-instanserna innehåller endast de privata namnen:

>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}

Avslutande tankar

En descriptor är vad vi kallar alla objekt som definierar __get__(), __set__() eller __delete__().

Eventuellt kan deskriptorer ha en __set_name__()-metod. Denna används endast i de fall då en descriptor behöver veta antingen vilken klass den skapades i eller namnet på den klassvariabel som den tilldelades. (Denna metod, om den finns, anropas även om klassen inte är en descriptor)

Deskriptorer anropas av dot-operatorn under attributuppslagning. Om en descriptor nås indirekt med vars(some_class)[descriptor_name] returneras descriptor-instansen utan att den anropas.

Descriptors fungerar bara när de används som klassvariabler. När de läggs in i instanser har de ingen effekt.

Huvudmotivet för deskriptorer är att tillhandahålla en hook som gör det möjligt för objekt som lagras i klassvariabler att styra vad som händer under attributuppslagningen.

Traditionellt sett kontrollerar den anropande klassen vad som händer under uppslagningen. Deskriptorer vänder på det förhållandet och låter de data som söks upp ha ett ord med i laget.

Deskriptorer används i hela språket. Det är så funktioner förvandlas till bundna metoder. Vanliga verktyg som classmethod(), staticmethod(), property() och functools.cached_property() är alla implementerade som deskriptorer.

Komplett praktiskt exempel

I det här exemplet skapar vi ett praktiskt och kraftfullt verktyg för att lokalisera fel som är svåra att hitta när det gäller datakorruption.

Klass för validerare

En validator är en deskriptor för hanterad attributåtkomst. Innan data lagras verifieras att det nya värdet uppfyller olika typ- och intervallrestriktioner. Om dessa begränsningar inte uppfylls, skapar den ett undantag för att förhindra att data korrumperas vid källan.

Denna Validator-klass är både en abstrakt basklass och en hanterad attributbeskrivare:

från abc import ABC, abstractmethod

klass Validator(ABC):

    def __set_name__(self, ägare, namn):
        self.private_name = '_' + namn

    def __get__(self, obj, objtype=None):
        return getattr(obj, self.private_name)

    def __set__(self, obj, värde):
        self.validate(värde)
        setattr(obj, self.private_name, värde)

    @abstraktmetod
    def validate(self, värde):
        passera

Egna validerare måste ärva från Validator och måste tillhandahålla en validate()-metod för att testa olika begränsningar efter behov.

Anpassade validerare

Här är tre praktiska verktyg för validering av data:

  1. OneOf verifierar att ett värde är ett av en begränsad uppsättning alternativ.

  2. Number verifierar att ett värde är antingen en int eller en float. Eventuellt verifierar den att ett värde ligger mellan ett givet minimum eller maximum.

  3. String verifierar att ett värde är en str. Eventuellt validerar den en given minsta eller största längd. Den kan även validera ett användardefinierat predikat.

class OneOf(Validator):

    def __init__(self, *options):
        self.options = set(options)

    def validate(self, value):
        if value not in self.options:
            raise ValueError(
                f'Expected {value!r} to be one of {self.options!r}'
            )

class Number(Validator):

    def __init__(self, minvalue=None, maxvalue=None):
        self.minvalue = minvalue
        self.maxvalue = maxvalue

    def validate(self, value):
        if not isinstance(value, (int, float)):
            raise TypeError(f'Expected {value!r} to be an int or float')
        if self.minvalue is not None and value < self.minvalue:
            raise ValueError(
                f'Expected {value!r} to be at least {self.minvalue!r}'
            )
        if self.maxvalue is not None and value > self.maxvalue:
            raise ValueError(
                f'Expected {value!r} to be no more than {self.maxvalue!r}'
            )

class String(Validator):

    def __init__(self, minsize=None, maxsize=None, predicate=None):
        self.minsize = minsize
        self.maxsize = maxsize
        self.predicate = predicate

    def validate(self, value):
        if not isinstance(value, str):
            raise TypeError(f'Expected {value!r} to be an str')
        if self.minsize is not None and len(value) < self.minsize:
            raise ValueError(
                f'Expected {value!r} to be no smaller than {self.minsize!r}'
            )
        if self.maxsize is not None and len(value) > self.maxsize:
            raise ValueError(
                f'Expected {value!r} to be no bigger than {self.maxsize!r}'
            )
        if self.predicate is not None and not self.predicate(value):
            raise ValueError(
                f'Expected {self.predicate} to be true for {value!r}'
            )

Praktisk tillämpning

Så här kan datavaliderarna användas i en riktig klass:

class Component:

    name = String(minsize=3, maxsize=10, predicate=str.isupper)
    kind = OneOf('wood', 'metal', 'plastic')
    quantity = Number(minvalue=0)

    def __init__(self, name, kind, quantity):
        self.name = name
        self.kind = kind
        self.quantity = quantity

Deskriptorerna förhindrar att ogiltiga instanser skapas:

>>> Component('Widget', 'metal', 5)      # Blocked: 'Widget' is not all uppercase
Traceback (most recent call last):
    ...
ValueError: Expected <method 'isupper' of 'str' objects> to be true for 'Widget'

>>> Component('WIDGET', 'metle', 5)      # Blocked: 'metle' is misspelled
Traceback (most recent call last):
    ...
ValueError: Expected 'metle' to be one of {'metal', 'plastic', 'wood'}

>>> Component('WIDGET', 'metal', -5)     # Blocked: -5 is negative
Traceback (most recent call last):
    ...
ValueError: Expected -5 to be at least 0

>>> Component('WIDGET', 'metal', 'V')    # Blocked: 'V' isn't a number
Traceback (most recent call last):
    ...
TypeError: Expected 'V' to be an int or float

>>> c = Component('WIDGET', 'metal', 5)  # Allowed:  The inputs are valid

Teknisk handledning

Det som följer är en mer teknisk handledning för mekaniken och detaljerna i hur deskriptorer fungerar.

Abstrakt

Definierar deskriptorer, sammanfattar protokollet och visar hur deskriptorer anropas. Ger ett exempel som visar hur objektrelationsmappningar fungerar.

Att lära sig om deskriptorer ger inte bara tillgång till en större uppsättning verktyg, det skapar också en djupare förståelse för hur Python fungerar.

Definition och introduktion

I allmänhet är en deskriptor ett attributvärde som har en av metoderna i deskriptorprotokollet. Dessa metoder är __get__(), __set__() och __delete__(). Om någon av dessa metoder är definierade för ett attribut, sägs det vara en descriptor.

Standardbeteendet för attributåtkomst är att hämta, ställa in eller ta bort attributet från ett objekts ordbok. Till exempel har a.x en uppslagskedja som börjar med a.__dict__['x'], sedan type(a).__dict__['x'], och fortsätter genom metodupplösningsordningen för type(a). Om det sökta värdet är ett objekt som definierar en av deskriptormetoderna, kan Python åsidosätta standardbeteendet och anropa deskriptormetoden istället. Var detta sker i prioriteringskedjan beror på vilka deskriptormetoder som definierades.

Deskriptorer är ett kraftfullt protokoll för allmänna ändamål. De är mekanismen bakom egenskaper, metoder, statiska metoder, klassmetoder och super(). De används i hela Python självt. Descriptors förenklar den underliggande C-koden och erbjuder en flexibel uppsättning nya verktyg för vardagliga Python-program.

Protokoll för deskriptorer

descr.__get__(self, obj, type=None)

descr.__set__(self, obj, value)

descr.__delete__(self, obj)

Det är allt det är. Definiera någon av dessa metoder och ett objekt betraktas som en deskriptor och kan åsidosätta standardbeteendet när det söks upp som ett attribut.

Om ett objekt definierar __set__() eller __delete__(), betraktas det som en datadeskriptor. Deskriptorer som bara definierar __get__() kallas icke-datadeskriptorer (de används ofta för metoder men andra användningsområden är möjliga).

Datadeskriptorer och icke-datadeskriptorer skiljer sig åt när det gäller hur åsidosättanden beräknas i förhållande till poster i en instans ordbok. Om en instans ordbok har en post med samma namn som en datadeskriptor, har datadeskriptorn företräde. Om en instans ordbok har en post med samma namn som en icke datadeskriptor, har ordboksposten företräde.

För att skapa en skrivskyddad databeskrivare definierar du både __get__() och __set__() med __set__() som ger upphov till ett AttributeError när den anropas. Att definiera metoden __set__() med en platshållare som ger upphov till ett undantag är tillräckligt för att göra den till en databeskrivare.

Översikt över anrop av deskriptor

En deskriptor kan anropas direkt med desc.__get__(obj) eller desc.__get__(None, cls).

Det är dock vanligare att en descriptor anropas automatiskt från attributåtkomst.

Uttrycket obj.x letar upp attributet x i kedjan av namnrymder för obj. Om sökningen hittar en deskriptor utanför instansen __dict__, anropas dess metod __get__() enligt de prioritetsregler som anges nedan.

Detaljerna i anropet beror på om obj är ett objekt, en klass eller en instans av super.

Anrop från en instans

Instance lookup skannar genom en kedja av namnrymder med högsta prioritet för datadeskriptorer, följt av instansvariabler, sedan icke-datadeskriptorer, sedan klassvariabler och slutligen __getattr__() om den tillhandahålls.

Om en deskriptor hittas för a.x, så anropas den med: desc.__get__(a, type(a)).

Logiken för en prickad uppslagning finns i object.__getattribute__(). Här är en ren Python-motsvarighet:

def find_name_in_mro(cls, name, default):
    "Emulate _PyType_Lookup() in Objects/typeobject.c"
    for base in cls.__mro__:
        if name in vars(base):
            return vars(base)[name]
    return default

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = find_name_in_mro(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

Observera att det inte finns någon __getattr__()-hook i __getattribute__()-koden. Därför kommer anrop av __getattribute__() direkt eller med super().__getattribute__ att kringgå __getattr__() helt och hållet.

Istället är det punktoperatorn och funktionen getattr() som ansvarar för att anropa __getattr__() närhelst __getattribute__() ger upphov till ett AttributeError. Deras logik är inkapslad i en hjälpfunktion:

def getattr_hook(obj, name):
    "Emulate slot_tp_getattr_hook() in Objects/typeobject.c"
    try:
        return obj.__getattribute__(name)
    except AttributeError:
        if not hasattr(type(obj), '__getattr__'):
            raise
    return type(obj).__getattr__(obj, name)             # __getattr__

Anrop från en klass

Logiken för en prickad uppslagning som A.x finns i type.__getattribute__(). Stegen liknar dem för object.__getattribute__() men uppslagningen i instansordboken ersätts av en sökning genom klassens method resolution order.

Om en descriptor hittas anropas den med desc.__get__(None, A).

Den fullständiga C-implementeringen finns i type_getattro() och _PyType_Lookup() i Objects/typeobject.c.

Anrop från super

Logiken för Supers prickade uppslagning finns i metoden __getattribute__() för objekt som returneras av super().

En prickad uppslagning som super(A, obj).m söker i obj.__class__.__mro__ efter basklassen B omedelbart efter A och returnerar sedan B.__dict__['m'].__get__(obj, A). Om det inte är en deskriptor returneras m oförändrad.

Den fullständiga C-implementeringen finns i super_getattro() i Objects/typeobject.c. En ren Python-ekvivalent finns i Guido’s Tutorial.

Sammanfattning av anropslogiken

Mekanismen för deskriptorer är inbäddad i metoderna __getattribute__() för object, type och super().

De viktiga punkterna att komma ihåg är:

  • Deskriptorer anropas med metoden __getattribute__().

  • Klasser ärver detta maskineri från object, type eller super().

  • Åsidosättande av __getattribute__() förhindrar automatiska descriptor-anrop eftersom all descriptor-logik finns i den metoden.

  • object.__getattribute__() and type.__getattribute__() make different calls to __get__(). The first includes the instance and may include the class. The second puts in None for the instance and always includes the class.

  • Data descriptors har alltid företräde framför instance dictionaries.

  • Deskriptorer som inte är data kan åsidosättas av instansordlistor.

Automatisk namnavisering

Ibland är det önskvärt att en deskriptor vet vilket klassvariabelnamn den tilldelades. När en ny klass skapas skannar metaklassen type den nya klassens dictionary. Om någon av posterna är deskriptorer och om de definierar __set_name__(), anropas den metoden med två argument. owner är den klass där deskriptorn används och name är den klassvariabel som deskriptorn tilldelades.

Implementationsdetaljerna finns i type_new() och set_names() i Objects/typeobject.c.

Eftersom uppdateringslogiken finns i type.__new__(), sker notifieringar endast vid tidpunkten för klassens skapande. Om deskriptorer läggs till i klassen efteråt måste __set_name__() anropas manuellt.

Exempel på ORM

Följande kod är ett förenklat skelett som visar hur databeskrivare kan användas för att implementera en objektrelationsmappning.

Grundtanken är att data lagras i en extern databas. Python-instanserna innehåller bara nycklar till databasens tabeller. Deskriptorer tar hand om uppslagningar eller uppdateringar:

class Field:

    def __set_name__(self, owner, name):
        self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
        self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'

    def __get__(self, obj, objtype=None):
        return conn.execute(self.fetch, [obj.key]).fetchone()[0]

    def __set__(self, obj, value):
        conn.execute(self.store, [value, obj.key])
        conn.commit()

Vi kan använda Field-klassen för att definiera modeller som beskriver schemat för varje tabell i en databas:

class Movie:
    table = 'Movies'                    # Table name
    key = 'title'                       # Primary key
    director = Field()
    year = Field()

    def __init__(self, key):
        self.key = key

class Song:
    table = 'Music'
    key = 'title'
    artist = Field()
    year = Field()
    genre = Field()

    def __init__(self, key):
        self.key = key

För att använda modellerna måste du först ansluta till databasen:

>>> import sqlite3
>>> conn = sqlite3.connect('entertainment.db')

En interaktiv session visar hur data hämtas från databasen och hur den kan uppdateras:

>>> Movie('Star Wars').director
'George Lucas'
>>> jaws = Movie('Jaws')
>>> f'Released in {jaws.year} by {jaws.director}'
'Released in 1975 by Steven Spielberg'

>>> Song('Country Roads').artist
'John Denver'

>>> Movie('Star Wars').director = 'J.J. Abrams'
>>> Movie('Star Wars').director
'J.J. Abrams'

Rena Python-ekvivalenter

Descriptor-protokollet är enkelt och erbjuder spännande möjligheter. Flera användningsfall är så vanliga att de har paketerats i inbyggda verktyg. Egenskaper, bundna metoder, statiska metoder, klassmetoder och __slots__ är alla baserade på descriptor-protokollet.

Egenskaper

Anrop av property() är ett kortfattat sätt att bygga en databeskrivning som utlöser ett funktionsanrop vid åtkomst till ett attribut. Dess signatur är:

property(fget=None, fset=None, fdel=None, doc=None) -> property

Dokumentationen visar en typisk användning för att definiera ett hanterat attribut x:

class C:
    def getx(self): return self.__x
    def setx(self, value): self.__x = value
    def delx(self): del self.__x
    x = property(getx, setx, delx, "I'm the 'x' property.")

För att se hur property() implementeras i termer av descriptorprotokollet, här är en ren Python-motsvarighet som implementerar det mesta av kärnfunktionaliteten:

class Property:
    "Emulate PyProperty_Type() in Objects/descrobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        if doc is None and fget is not None:
            doc = fget.__doc__
        self.__doc__ = doc

    def __set_name__(self, owner, name):
        self.__name__ = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        if self.fget is None:
            raise AttributeError
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError
        self.fset(obj, value)

    def __delete__(self, obj):
        if self.fdel is None:
            raise AttributeError
        self.fdel(obj)

    def getter(self, fget):
        return type(self)(fget, self.fset, self.fdel, self.__doc__)

    def setter(self, fset):
        return type(self)(self.fget, fset, self.fdel, self.__doc__)

    def deleter(self, fdel):
        return type(self)(self.fget, self.fset, fdel, self.__doc__)

Inbyggda property() är till hjälp när ett användargränssnitt har gett tillgång till attribut och efterföljande ändringar kräver att en metod används.

Till exempel kan en kalkylbladsklass ge åtkomst till ett cellvärde genom Cell('b10').value. Senare förbättringar av programmet kräver att cellen räknas om vid varje åtkomst, men programmeraren vill inte påverka befintlig klientkod som har direkt åtkomst till attributet. Lösningen är att linda in åtkomst till värdeattributet i en egenskapsdatadeskriptor:

class Cell:
    ...

    @property
    def value(self):
        "Recalculate the cell before returning value"
        self.recalc()
        return self._value

Antingen den inbyggda property() eller vår Property()-ekvivalent skulle fungera i det här exemplet.

Funktioner och metoder

Pythons objektorienterade funktioner bygger på en funktionsbaserad miljö. Med hjälp av icke-datadeskriptorer sammanfogas de två sömlöst.

Funktioner som lagras i klassordböcker omvandlas till metoder när de anropas. Metoder skiljer sig från vanliga funktioner endast genom att objektinstansen läggs till de andra argumenten. Enligt konvention kallas instansen self, men den kan kallas this eller något annat variabelnamn.

Metoder kan skapas manuellt med types.MethodType vilket i stort sett motsvarar:

class MethodType:
    "Emulate PyMethod_Type in Objects/classobject.c"

    def __init__(self, func, obj):
        self.__func__ = func
        self.__self__ = obj

    def __call__(self, *args, **kwargs):
        func = self.__func__
        obj = self.__self__
        return func(obj, *args, **kwargs)

    def __getattribute__(self, name):
        "Emulate method_getset() in Objects/classobject.c"
        if name == '__doc__':
            return self.__func__.__doc__
        return object.__getattribute__(self, name)

    def __getattr__(self, name):
        "Emulate method_getattro() in Objects/classobject.c"
        return getattr(self.__func__, name)

    def __get__(self, obj, objtype=None):
        "Emulate method_descr_get() in Objects/classobject.c"
        return self

För att stödja automatiskt skapande av metoder innehåller funktioner metoden __get__() för att binda metoder under attributåtkomst. Detta innebär att funktioner är icke-databeskrivare som returnerar bundna metoder under prickad uppslagning från en instans. Så här fungerar det:

class Function:
    ...

    def __get__(self, obj, objtype=None):
        "Simulate func_descr_get() in Objects/funcobject.c"
        if obj is None:
            return self
        return MethodType(self, obj)

Genom att köra följande klass i tolken visas hur funktionsbeskrivaren fungerar i praktiken:

class D:
    def f(self):
         return self

class D2:
    pass

Funktionen har ett qualified name-attribut för att stödja introspektion:

>>> D.f.__qualname__
'D.f'

Om du öppnar funktionen via klassordlistan anropas inte __get__(). Istället returneras bara den underliggande funktionens objekt:

>>> D.__dict__['f']
<function D.f at 0x00C45070>

Punktad åtkomst från en klass anropar __get__() som bara returnerar den underliggande funktionen oförändrad:

>>> D.f
<function D.f at 0x00C45070>

Det intressanta beteendet inträffar under prickad åtkomst från en instans. Den prickade uppslagningen anropar __get__() som returnerar en bunden metod object:

>>> d = D()
>>> d.f
<bound method D.f of <__main__.D object at 0x00B18C90>>

Internt lagrar den bundna metoden den underliggande funktionen och den bundna instansen:

>>> d.f.__func__
<function D.f at 0x00C45070>

>>> d.f.__self__
<__main__.D object at 0x00B18C90>

Om du någonsin har undrat var self kommer ifrån i vanliga metoder eller var cls kommer ifrån i klassmetoder, så har du svaret här!

Olika typer av metoder

Deskriptorer för icke-data ger en enkel mekanism för variationer på de vanliga mönstren för att binda funktioner till metoder.

För att sammanfatta har funktioner en __get__()-metod så att de kan konverteras till en metod när de används som attribut. Icke-data deskriptorn omvandlar ett anrop av obj.f(*args) till f(obj, *args). Anrop av cls.f(*args) blir f(*args).

I detta diagram sammanfattas bindningen och dess två mest användbara varianter:

Omvandling

Anropas från ett objekt

Anropas från en klass

funktion

f(obj, *args)

f(*args)

statisk metod

f(*args)

f(*args)

klassmetod

f(typ(obj), *args)

f(cls, *args)

Statiska metoder

Statiska metoder returnerar den underliggande funktionen utan ändringar. Anrop av antingen c.f eller C.f motsvarar en direkt uppslagning i object.__getattribute__(c, "f") eller object.__getattribute__(C, "f"). Därmed blir funktionen identiskt tillgänglig från antingen ett objekt eller en klass.

Bra kandidater för statiska metoder är metoder som inte refererar till variabeln self.

Ett statistikpaket kan t.ex. innehålla en containerklass för experimentella data. Klassen innehåller normala metoder för att beräkna genomsnitt, medelvärde, median och annan beskrivande statistik som är beroende av data. Det kan dock finnas användbara funktioner som är begreppsmässigt relaterade men som inte är beroende av data. Till exempel är erf(x) en praktisk konverteringsrutin som förekommer i statistiskt arbete men som inte är direkt beroende av en viss dataset. Den kan anropas antingen från ett objekt eller en klass: s.erf(1.5) --> 0.9332 eller Sample.erf(1.5) --> 0.9332.

Eftersom statiska metoder returnerar den underliggande funktionen utan några ändringar är exempelanropen inte särskilt spännande:

class E:
    @staticmethod
    def f(x):
        return x * 10
>>> E.f(3)
30
>>> E().f(3)
30

Med hjälp av icke-data descriptor-protokollet skulle en ren Python-version av staticmethod() se ut så här:

import functools

class StaticMethod:
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f
        functools.update_wrapper(self, f)

    def __get__(self, obj, objtype=None):
        return self.f

    def __call__(self, *args, **kwds):
        return self.f(*args, **kwds)

    @property
    def __annotations__(self):
        return self.f.__annotations__

Anropet functools.update_wrapper() lägger till ett __wrapped__-attribut som refererar till den underliggande funktionen. Det överför också de attribut som är nödvändiga för att få omslaget att se ut som den omslutna funktionen, inklusive __name__, __qualname__ och __doc__.

Klassmetoder

Till skillnad från statiska metoder lägger klassmetoder till klassreferensen i argumentlistan innan funktionen anropas. Detta format är detsamma oavsett om den som anropar är ett objekt eller en klass:

class F:
    @classmethod
    def f(cls, x):
        return cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)

Detta beteende är användbart när metoden bara behöver ha en klassreferens och inte förlitar sig på data som lagras i en specifik instans. Ett sätt att använda klassmetoder är att skapa alternativa klasskonstruktörer. Till exempel skapar klassmetoden dict.fromkeys() en ny ordbok från en lista med nycklar. Den rena Python-ekvivalenten är:

class Dict(dict):
    @classmethod
    def fromkeys(cls, iterable, value=None):
        "Emulate dict_fromkeys() in Objects/dictobject.c"
        d = cls()
        for key in iterable:
            d[key] = value
        return d

Nu kan en ny ordbok med unika nycklar konstrueras så här:

>>> d = Dict.fromkeys('abracadabra')
>>> type(d) is Dict
True
>>> d
{'a': None, 'b': None, 'r': None, 'c': None, 'd': None}

Med hjälp av icke-data descriptor-protokollet skulle en ren Python-version av classmethod() se ut så här:

import functools

class ClassMethod:
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f
        functools.update_wrapper(self, f)

    def __get__(self, obj, cls=None):
        if cls is None:
            cls = type(obj)
        return MethodType(self.f, cls)

Anropet functools.update_wrapper() i ClassMethod lägger till ett __wrapped__-attribut som refererar till den underliggande funktionen. Det för också vidare de attribut som är nödvändiga för att få omslaget att se ut som den omslutna funktionen: __name__, __qualname__, __doc__, och __annotations__.

Medlemsobjekt och __slots__

När en klass definierar __slots__ ersätter den instansordböcker med en array av slot-värden med fast längd. Ur användarens synvinkel har detta flera effekter:

1. Provides immediate detection of bugs due to misspelled attribute assignments. Only attribute names specified in __slots__ are allowed:

class Vehicle:
    __slots__ = ('id_number', 'make', 'model')
>>> auto = Vehicle()
>>> auto.id_nubmer = 'VYE483814LQEX'
Traceback (most recent call last):
    ...
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'

2. Helps create immutable objects where descriptors manage access to private attributes stored in __slots__:

klass Oföränderlig:

    __slots__ = ('_dept', '_name') # Byt ut instansordboken

    def __init__(self, dept, name):
        self._dept = dept # Lagra till privat attribut
        self._name = name # Lagra till privat attribut

    @property # skrivskyddad beskrivare
    def dept(self):
        return self._dept

    @egenskap
    def namn(self):                         # skrivskyddad deskriptor
        returnerar self._name
>>> mark = Immutable('Botany', 'Mark Watney')
>>> mark.dept
'Botany'
>>> mark.dept = 'Space Pirate'
Traceback (most recent call last):
    ...
AttributeError: property 'dept' of 'Immutable' object has no setter
>>> mark.location = 'Mars'
Traceback (most recent call last):
    ...
AttributeError: 'Immutable' object has no attribute 'location'

3. Saves memory. On a 64-bit Linux build, an instance with two attributes takes 48 bytes with __slots__ and 152 bytes without. This flyweight design pattern likely only matters when a large number of instances are going to be created.

4. Improves speed. Reading instance variables is 35% faster with __slots__ (as measured with Python 3.10 on an Apple M1 processor).

5. Blocks tools like functools.cached_property() which require an instance dictionary to function correctly:

from functools import cached_property

class CP:
    __slots__ = ()                          # Eliminates the instance dict

    @cached_property                        # Requires an instance dict
    def pi(self):
        return 4 * sum((-1.0)**n / (2.0*n + 1.0)
                       for n in reversed(range(100_000)))
>>> CP().pi
Traceback (most recent call last):
  ...
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.

Det är inte möjligt att skapa en exakt drop-in ren Python-version av __slots__ eftersom det kräver direkt tillgång till C-strukturer och kontroll över objektminnesallokering. Vi kan dock bygga en mestadels trogen simulering där den faktiska C-strukturen för slots emuleras av en privat _slotvalues-lista. Läsningar och skrivningar till den privata strukturen hanteras av medlemsdeskriptorer:

null = objekt()

klass Medlem:

    def __init__(self, name, clsname, offset):
        'Emulera PyMemberDef i Include/structmember.h'
        # Se även descr_new() i Objects/descrobject.c
        self.name = namn
        self.clsname = clsname
        self.offset = offset

    def __get__(self, obj, objtype=None):
        "Emulera member_get() i Objects/descrobject.c
        # Se även PyMember_GetOne() i Python/structmember.c
        om obj är None:
            return self
        värde = obj._slotvalues[self.offset]
        om värdet är null:
            raise AttributeError(self.name)
        returnera värde

    def __set__(self, obj, värde):
        'Emulera member_set() i Objects/descrobject.c'
        obj._slotvalues[self.offset] = värde

    def __delete__(self, obj):
        "Emulera member_delete() i Objects/descrobject.c
        värde = obj._slotvalues[self.offset]
        om värdet är null:
            raise AttributeError(self.name)
        obj._slotvalues[self.offset] = null

    def __repr__(self):
        'Emulera member_repr() i Objects/descrobject.c'
        return f'<Medlem {self.name!r} av {self.clsname!r}>'

Metoden type.__new__() tar hand om att lägga till medlemsobjekt i klassvariabler:

klass Typ(typ):
    'Simulera hur metaklassen type lägger till medlemsobjekt för slots'

    def __new__(mcls, clsname, baser, mappning, **kwargs):
        "Emulera type_new() i Objects/typeobject.c
        # type_new() anropar PyTypeReady() som anropar add_methods()
        slot_names = mappning.get('slot_names', [])
        för offset, namn i enumerate(slot_names):
            mappning[namn] = Member(namn, clsnamn, offset)
        return type.__new__(mcls, clsname, baser, mappning, **kwargs)

Metoden object.__new__() tar hand om att skapa instanser som har slots istället för en instansordbok. Här är en grov simulering i ren Python:

class Object:
    'Simulate how object.__new__() allocates memory for __slots__'

    def __new__(cls, *args, **kwargs):
        'Emulate object_new() in Objects/typeobject.c'
        inst = super().__new__(cls)
        if hasattr(cls, 'slot_names'):
            empty_slots = [null] * len(cls.slot_names)
            object.__setattr__(inst, '_slotvalues', empty_slots)
        return inst

    def __setattr__(self, name, value):
        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
        cls = type(self)
        if hasattr(cls, 'slot_names') and name not in cls.slot_names:
            raise AttributeError(
                f'{cls.__name__!r} object has no attribute {name!r}'
            )
        super().__setattr__(name, value)

    def __delattr__(self, name):
        'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
        cls = type(self)
        if hasattr(cls, 'slot_names') and name not in cls.slot_names:
            raise AttributeError(
                f'{cls.__name__!r} object has no attribute {name!r}'
            )
        super().__delattr__(name)

För att använda simuleringen i en riktig klass är det bara att ärva från Object och sätta metaclass till Type:

class H(Object, metaclass=Type):
    'Instance variables stored in slots'

    slot_names = ['x', 'y']

    def __init__(self, x, y):
        self.x = x
        self.y = y

Vid det här laget har metaklassen laddat medlemsobjekt för x och y:

>>> from pprint import pp
>>> pp(dict(vars(H)))
{'__module__': '__main__',
 '__doc__': 'Instance variables stored in slots',
 'slot_names': ['x', 'y'],
 '__init__': <function H.__init__ at 0x7fb5d302f9d0>,
 'x': <Member 'x' of 'H'>,
 'y': <Member 'y' of 'H'>}

När instanser skapas har de en lista med slot_values där attributen lagras:

>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}

Felstavade eller icke tilldelade attribut kommer att leda till ett undantag:

>>> h.xz
Traceback (most recent call last):
    ...
AttributeError: 'H' object has no attribute 'xz'