Spara 35% rabatt på listpriset* för den relaterade boken eller e-boken i flera format (EPUB + MOBI + PDF) med rabattkodartikel.
* se informit.com/terms
- punkt 18: minska visuellt brus med variabla Positionsargument
- saker att komma ihåg
- punkt 19: ge valfritt beteende med Sökordsargument
- saker att komma ihåg
- punkt 20: Använd None och Docstrings för att ange dynamiska standardargument
- saker att komma ihåg
- Item 21: Enforce Clarity with Keyword-Only Arguments
- endast sökord argument i Python 2
- saker att komma ihåg
punkt 18: minska visuellt brus med variabla Positionsargument
Acceptera valfria positionsargument (kallas ofta star args med hänvisning till det konventionella namnet för parametern, *args) kan göra ett funktionsanrop tydligare och ta bort visuellt brus.
säg till exempel att du vill logga in lite felsökningsinformation. Med ett fast antal argument behöver du en funktion som tar ett meddelande och en lista med värden.
def
log(message, values):if not
values:else
: values_str=
', '
.join(str
(x)for
xin
values)'%s: %s'
%
(message, values_str))log('My numbers are'
, )log('Hi there'
, )>>>My numbers are: 1, 2Hi there
att behöva skicka en tom lista när du inte har några värden att logga är besvärligt och bullrigt. Det skulle vara bättre att lämna ut det andra argumentet helt. Du kan göra detta i Python genom att prefixa det sista positionsparameternamnet med *. Den första parametern för loggmeddelandet krävs, medan valfritt antal efterföljande positionsargument är valfria. Funktionskroppen behöver inte ändras, bara de som ringer gör det.
def
log(message,*
values):# The only difference
if not
values:else
: values_str=
', '
.join(str
(x)for
xin
values)'%s: %s'
%
(message, values_str))log('My numbers are'
,1
,2
)log('Hi there'
)# Much better
>>>My numbers are: 1, 2Hi there
Om du redan har en lista och vill anropa en variabelargumentfunktion som logg, kan du göra det genom att använda operatorn*. Detta instruerar Python att skicka objekt från sekvensen som positionella argument.
favorites=
log('Favorite colors'
,*
favorites)>>>Favorite colors: 7, 33, 99
det finns två problem med att acceptera ett variabelt antal positionella argument.
det första problemet är att de variabla argumenten alltid förvandlas till en tupel innan de skickas till din funktion. Det betyder att om den som ringer till din funktion använder * – operatören på en generator, kommer den att upprepas tills den är uttömd. Den resulterande tupeln kommer att innehålla alla värden från generatorn, vilket kan förbruka mycket minne och få ditt program att krascha.
def
my_generator():for
iin
range
(10
):yield i
def
my_func(*
args):=
my_generator()my_func(*
it)>>>(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
funktioner som accepterar *args är bäst för situationer där du vet att antalet ingångar i argumentlistan kommer att vara ganska små. Den är idealisk för funktionsanrop som passerar många bokstäver eller variabla namn tillsammans. Det är främst för programmerarens bekvämlighet och läsbarheten för koden.
det andra problemet med * args är att du inte kan lägga till nya positionsargument till din funktion i framtiden utan att migrera varje uppringare. Om du försöker lägga till ett positionsargument framför argumentlistan bryts befintliga uppringare subtilt om de inte uppdateras.
def
log(sequence, message,*
values):if not
values:'%s: %s'
%
(sequence, message))else
: values_str=
', '
.join(str
(x)for
xin
values)'%s: %s: %s'
%
(sequence, message, values_str))log(1
,'Favorites'
,7
,33
)# New usage is OK
log('Favorite numbers'
,7
,33
)# Old usage breaks
>>>1: Favorites: 7, 33Favorite numbers: 7: 33
problemet här är att det andra samtalet till loggen använde 7 som meddelandeparameter eftersom ett sekvensargument inte gavs. Buggar som detta är svåra att spåra eftersom koden fortfarande körs utan att höja några undantag. För att undvika denna möjlighet helt bör du använda argument endast för sökord när du vill utöka funktioner som accepterar *args (se punkt 21: “verkställa tydlighet med argument endast för sökord”).
saker att komma ihåg
- funktioner kan acceptera ett variabelt antal positionsargument genom att använda * args i Def-uttalandet.
- Du kan använda objekten från en sekvens som positionsargument för en funktion med operatorn*.
- Om du använder * – operatören med en generator kan det leda till att ditt program tar slut på minnet och kraschar.
- lägga till nya positionsparametrar till funktioner som accepterar * args kan införa svåra att hitta buggar.
punkt 19: ge valfritt beteende med Sökordsargument
som de flesta andra programmeringsspråk tillåter anropning av en funktion i Python att skicka argument efter position.
def
remainder(number, divisor):return
number%
divisorassert
remainder(20
,7
)==
6
alla positionsargument till Python-funktioner kan också skickas med nyckelord, där argumentets namn används i en uppgift inom parentes för ett funktionsanrop. Sökordsargumenten kan skickas i valfri ordning så länge som alla nödvändiga positionsargument anges. Du kan mixa och matcha nyckelord och positionsargument. Dessa samtal är likvärdiga:
remainder(20
,7
)remainder(20
, divisor=
7
)remainder(number=
20
, divisor=
7
)remainder(divisor=
7
, number=
20
)
Positionsargument måste anges före sökordsargument.
remainder(number=
20
,7
)>>>SyntaxError: non-keyword arg after keyword arg
varje argument kan bara anges en gång.
remainder(20
, number=
7
)>>>TypeError: remainder() got multiple values for argument 'number'
flexibiliteten i sökordsargument ger tre betydande fördelar.
den första fördelen är att sökordsargument gör funktionssamtalet tydligare för nya läsare av koden. Med anropsresten (20, 7) är det inte uppenbart vilket argument som är numret och vilket är divisorn utan att titta på implementeringen av restmetoden. I samtalet med sökordsargument gör number=20 och divisor=7 det omedelbart uppenbart vilken parameter som används för varje ändamål.
den andra effekten av sökordsargument är att de kan ha standardvärden som anges i funktionsdefinitionen. Detta gör att en funktion kan ge ytterligare funktioner när du behöver dem men låter dig Acceptera standardbeteendet för det mesta. Detta kan eliminera repetitiv kod och minska buller.
säg till exempel att du vill beräkna vätskeflödet i ett kärl. Om momsen också är på en skala kan du använda skillnaden mellan två viktmätningar vid två olika tidpunkter för att bestämma flödeshastigheten.
def
flow_rate(weight_diff, time_diff):return
weight_diff/
time_diffweight_diff=
0.5
time_diff=
3
flow=
flow_rate(weight_diff, time_diff)'%.3f kg per second'
%
flow)>>>0.167 kg per second
i det typiska fallet är det användbart att känna till flödeshastigheten i kilogram per sekund. Andra gånger skulle det vara till hjälp att använda de senaste sensormätningarna för att approximera större tidsskalor, som timmar eller dagar. Du kan ange detta beteende i samma funktion genom att lägga till ett argument för tidsskalningsfaktorn.
def
flow_rate(weight_diff, time_diff, period):return
(weight_diff/
time_diff)*
period
problemet är att du nu måste ange periodargumentet varje gång du ringer till funktionen, även i det vanliga fallet med flödeshastighet per sekund (där perioden är 1).
flow_per_second=
flow_rate(weight_diff, time_diff,1
)
för att göra detta mindre bullrigt kan jag ge periodargumentet ett standardvärde.
def
flow_rate(weight_diff, time_diff, period=
1
):return
(weight_diff/
time_diff)*
period
periodargumentet är nu valfritt.
flow_per_second=
flow_rate(weight_diff, time_diff)flow_per_hour=
flow_rate(weight_diff, time_diff, period=
3600
)
detta fungerar bra för enkla standardvärden (det blir svårt för komplexa standardvärden—se punkt 20:”Använd ingen och Docstrings för att ange dynamiska standardargument”).
den tredje anledningen till att använda sökordsargument är att de ger ett kraftfullt sätt att utöka en funktions parametrar samtidigt som de är bakåtkompatibla med befintliga uppringare. Detta gör att du kan tillhandahålla ytterligare funktionalitet utan att behöva migrera mycket kod, vilket minskar risken för att införa buggar.
säg till exempel att du vill förlänga flow_rate-funktionen ovan för att beräkna flödeshastigheter i viktenheter förutom kilogram. Du kan göra detta genom att lägga till en ny valfri parameter som ger en omvandlingsfrekvens till dina önskade måttenheter.
def
flow_rate(weight_diff, time_diff, period=
1
, units_per_kg=
1
):return
((weight_diff*
units_per_kg)/
time_diff)*
period
standardargumentvärdet för units_per_kg är 1, vilket gör att de returnerade viktenheterna förblir som kilogram. Detta innebär att alla befintliga uppringare inte ser någon förändring i beteende. Nya uppringare till flow_rate kan ange det nya sökordsargumentet för att se det nya beteendet.
pounds_per_hour=
flow_rate(weight_diff, time_diff, period=
3600
, units_per_kg=
2.2
)
det enda problemet med detta tillvägagångssätt är att valfria sökordsargument som period och units_per_kg fortfarande kan anges som positionsargument.
pounds_per_hour=
flow_rate(weight_diff, time_diff,3600
,2.2
)
att leverera valfria argument positionellt kan vara förvirrande eftersom det inte är klart vad värdena 3600 och 2.2 motsvarar. Bästa praxis är att alltid ange valfria argument med hjälp av sökordsnamnen och aldrig skicka dem som positionsargument.
saker att komma ihåg
- Funktionsargument kan anges efter position eller nyckelord.
- nyckelord gör det klart vad syftet med varje argument är när det skulle vara förvirrande med endast positionella argument.
- Sökordsargument med standardvärden gör det enkelt att lägga till nya beteenden i en funktion, särskilt när funktionen har befintliga uppringare.
- valfria sökordsargument ska alltid skickas med nyckelord istället för efter position.
punkt 20: Använd None och Docstrings för att ange dynamiska standardargument
Ibland måste du använda en icke-statisk typ som ett sökordsargumentets standardvärde. Säg till exempel att du vill skriva ut loggningsmeddelanden som är markerade med tiden för den loggade händelsen. I standardfallet vill du att meddelandet ska inkludera tiden då funktionen anropades. Du kan prova följande tillvägagångssätt, förutsatt att standardargumenten omvärderas varje gång funktionen anropas.
def
log(message, when=
datetime.now()):'%s: %s'
%
(when, message))log('Hi there!'
)sleep(0.1
)log('Hi again!'
)>>>2014-11-15 21:10:10.371432: Hi there!2014-11-15 21:10:10.371432: Hi again!
tidsstämplarna är desamma eftersom datetime.nu körs bara en enda gång: när funktionen är definierad. Standardargumentvärden utvärderas endast en gång per modulbelastning, vilket vanligtvis händer när ETT program startar. När modulen som innehåller den här koden är laddad, datetime.nu kommer standardargumentet aldrig att utvärderas igen.
konventionen för att uppnå önskat resultat i Python är att tillhandahålla ett standardvärde för ingen och att dokumentera det faktiska beteendet i docstring (se punkt 49: “skriv Docstrings för varje funktion, klass och modul”). När din kod ser ett argumentvärde av ingen, allokerar du standardvärdet i enlighet därmed.
def
log(message, when=
None
):"""Log a message with a timestamp.
Args:
message: Message to print.
when: datetime of when the message occurred.
Defaults to the present time.
"""
when=
datetime.now()if
whenis None else
when'%s: %s'
%
(when, message))
nu kommer tidsstämplarna att vara olika.
log('Hi there!'
)sleep(0.1
)log('Hi again!'
)>>>2014-11-15 21:10:10.472303: Hi there!2014-11-15 21:10:10.573395: Hi again!
att använda Ingen för standardargumentvärden är särskilt viktigt när argumenten kan ändras. Säg till exempel att du vill ladda ett värde kodat som JSON-data. Om avkodning av data misslyckas vill du att en tom ordlista ska returneras som standard. Du kan prova detta tillvägagångssätt.
def
decode(data, default=
{}):try:
return
json.loads(data)except
ValueError:return
default
problemet här är detsamma som datetime.nu exempel ovan. Ordlistan som anges för standard kommer att delas av alla samtal för att avkoda eftersom standardargumentvärden endast utvärderas en gång (vid modulens laddningstid). Detta kan orsaka extremt överraskande beteende.
foo=
decode('bad data'
)foo=
5
bar=
decode('also bad'
)bar=
1
'Foo:'
, foo)'Bar:'
, bar)>>>Foo: {'stuff': 5, 'meep': 1}Bar: {'stuff': 5, 'meep': 1}
Du kan förvänta dig två olika ordböcker, var och en med en enda nyckel och värde. Men att ändra en verkar också ändra den andra. Den skyldige är att foo och bar båda är lika med standardparametern. De är samma ordboksobjekt.
assert
foois
bar
fixen är att ställa in standardvärdet för sökordsargumentet till ingen och sedan dokumentera beteendet i funktionens docstring.
def
decode(data, default=
None
):"""Load JSON data from a string.
Args:
data: JSON data to decode.
default: Value to return if decoding fails.
Defaults to an empty dictionary.
"""
if
defaultis None
: default=
{}try
:return
json.loads(data)except
ValueError:return
default
nu ger samma testkod som tidigare det förväntade resultatet.
foo=
decode('bad data'
)foo=
5
bar=
decode('also bad'
)bar=
1
'Foo:'
, foo)'Bar:'
, bar)>>>Foo: {'stuff': 5}Bar: {'meep': 1}
saker att komma ihåg
- standardargument utvärderas endast en gång: under funktionsdefinition vid modulens laddningstid. Detta kan orsaka udda beteenden för dynamiska värden (som {} eller ).
- Använd ingen som standardvärde för sökordsargument som har ett dynamiskt värde. Dokumentera det faktiska standardbeteendet i funktionens docstring.
Item 21: Enforce Clarity with Keyword-Only Arguments
Passing arguments by keyword är ett kraftfullt inslag i Python-funktioner (Se punkt 19: “ge valfritt beteende med Sökordsargument”). Flexibiliteten i sökordsargument gör att du kan skriva kod som kommer att vara tydlig för dina användningsfall.
säg till exempel att du vill dela ett nummer med ett annat men var mycket försiktig med speciella fall. Ibland vill du ignorera zerodivisionerror undantag och returnera oändlighet istället. Andra gånger vill du ignorera OverflowError-undantag och returnera noll istället.
def
safe_division(number, divisor, ignore_overflow, ignore_zero_division):try
:return
number/
divisorexcept
OverflowError:if
ignore_overflow:return 0
else
:raise
except
ZeroDivisionError:if
ignore_zero_division:return
float
('inf'
)else
:raise
att använda denna funktion är enkelt. Detta samtal kommer att ignorera float overflow från division och kommer att returnera noll.
result=
safe_division(1
,10
**
500
,True
,False
)
detta samtal ignorerar felet från att dividera med noll och kommer att returnera oändligheten.
result=
safe_division(1
,0
,False
,True
)
problemet är att det är lätt att förvirra positionen för de två Booleska argumenten som styr undantaget-ignorerar beteendet. Detta kan lätt orsaka buggar som är svåra att spåra. Ett sätt att förbättra läsbarheten för denna kod är att använda sökordsargument. Som standard kan funktionen vara alltför försiktig och kan alltid höja undantag igen.
def
safe_division_b(number, divisor, ignore_overflow=
False
, ignore_zero_division=
False
):# ...
sedan kan uppringare använda sökordsargument för att ange vilka av ignoreringsflaggorna de vill vända för specifika operationer, vilket åsidosätter standardbeteendet.
safe_division_b(1
,10
**
500
, ignore_overflow=
True
)safe_division_b(1
,0
, ignore_zero_division=
True
)
problemet är, eftersom dessa sökordsargument är valfritt beteende, finns det inget som tvingar uppringare av dina funktioner att använda sökordsargument för tydlighet. Även med den nya definitionen av safe_division_b kan du fortfarande kalla det på det gamla sättet med positionella argument.
safe_division_b(1
,10
**
500
,True
,False
)
med komplexa funktioner som detta är det bättre att kräva att de som ringer är tydliga om sina avsikter. I Python 3 kan du kräva tydlighet genom att definiera dina funktioner med nyckelord-bara argument. Dessa argument kan endast levereras med nyckelord, aldrig efter position.
Här omdefinierar jag safe_division-funktionen för att acceptera nyckelord-bara argument. * – Symbolen i argumentlistan anger slutet på positionsargument och början på argument som endast är nyckelord.
def
safe_division_c(number, divisor,*
, ignore_overflow=
False
, ignore_zero_division=
False
):# ...
Nu fungerar inte funktionen med positionsargument för sökordsargumenten.
safe_division_c(1
,10
**
500
,True
,False
)>>>TypeError: safe_division_c() takes 2 positional arguments but 4 were given
Sökordsargument och deras standardvärden fungerar som förväntat.
safe_division_c(1
,0
, ignore_zero_division=
True
)# OK
try
: safe_division_c(1
,0
)except
ZeroDivisionError:pass
# Expected
endast sökord argument i Python 2
tyvärr har Python 2 inte explicit syntax för att ange sökord endast argument som Python 3. Men du kan uppnå samma beteende för att höja typfel för ogiltiga funktionsanrop genom att använda * * – operatören i argumentlistor. Operatorn * * liknar operatorn * (se punkt 18: “minska visuellt brus med variabla Positionsargument”), förutom att istället för att acceptera ett variabelt antal positionsargument, accepterar det valfritt antal sökordsargument, även om de inte är definierade.
# Python 2
def
print_args(*
args,**
kwargs):'Positional:'
, args'Keyword: '
, kwargsprint_args(1
,2
, foo=
'bar'
, stuff=
'meep'
)>>>Positional: (1, 2)Keyword: {'foo': 'bar', 'stuff': 'meep'}
för att göra safe_division ta nyckelord endast argument i Python 2, har du funktionen Acceptera **kwargs. Sedan popar du sökordsargument som du förväntar dig av kwargs-ordboken, med hjälp av pop-metodens andra argument för att ange standardvärdet när nyckeln saknas. Slutligen ser du till att det inte finns fler sökordsargument kvar i kwargs för att förhindra att uppringare levererar argument som är ogiltiga.
# Python 2
def
safe_division_d(number, divisor,**
kwargs): ignore_overflow=
kwargs.pop('ignore_overflow'
,False
) ignore_zero_div=
kwargs.pop('ignore_zero_division'
,False
)if
kwargs:raise
TypeError('Unexpected **kwargs: %r'
%
kwargs)# ...
Nu kan du ringa funktionen med eller utan sökordsargument.
safe_division_d(1
,10
)safe_division_d(1
,0
, ignore_zero_division=
True
)safe_division_d(1
,10
**
500
, ignore_overflow=
True
)
att försöka skicka nyckelord-bara argument efter position fungerar inte, precis som i Python 3.
safe_division_d(1
,0
,False
,True
)>>>TypeError: safe_division_d() takes 2 positional arguments but 4 were given
att försöka skicka oväntade sökordsargument fungerar inte heller.
safe_division_d(0
,0
, unexpected=
True
)>>>TypeError: Unexpected **kwargs: {'unexpected': True}
saker att komma ihåg
- Sökordsargument gör avsikten med ett funktionsanrop tydligare.
- använd argument endast för sökord för att tvinga uppringare att leverera sökordsargument för potentiellt förvirrande funktioner, särskilt de som accepterar flera Booleska flaggor.
- Python 3 stöder explicit syntax för sökord endast argument i funktioner.
- Python 2 kan emulera sökord endast argument för funktioner genom att använda **kwargs och manuellt höja TypeError undantag.