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:
”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.
I det andra avsnittet visas ett komplett, praktiskt exempel på en descriptor. Om du redan kan grunderna kan du börja där.
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å.
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:
OneOf
verifierar att ett värde är ett av en begränsad uppsättning alternativ.Number
verifierar att ett värde är antingen enint
eller enfloat
. Eventuellt verifierar den att ett värde ligger mellan ett givet minimum eller maximum.String
verifierar att ett värde är enstr
. 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
ellersuper()
.Åsidosättande av
__getattribute__()
förhindrar automatiska descriptor-anrop eftersom all descriptor-logik finns i den metoden.object.__getattribute__()
andtype.__getattribute__()
make different calls to__get__()
. The first includes the instance and may include the class. The second puts inNone
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'