effektiv Python: 4 bästa praxis för Funktionsargument

funktioner i Python har en mängd extrafunktioner som gör programmerarens liv enklare. Vissa liknar funktioner i andra programmeringsspråk, men många är unika för Python. Dessa extrafunktioner kan göra en funktions syfte mer uppenbart. De kan eliminera buller och klargöra uppringarnas avsikt. De kan avsevärt minska subtila buggar som är svåra att hitta. I detta utdrag från effektiv Python: 59 specifika sätt att skriva bättre Python visar Brett Slatkin dig 4 bästa praxis för Funktionsargument i Python.

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

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: print(message) else: values_str = ', '.join(str(x) for x in values) print('%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: print(message) else: values_str = ', '.join(str(x) for x in values) print('%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 i in range(10): yield idef my_func(*args): print(args)it = 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: print('%s: %s' % (sequence, message)) else: values_str = ', '.join(str(x) for x in values) print('%s: %s: %s' % (sequence, message, values_str))log(1, 'Favorites', 7, 33) # New usage is OKlog('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.5time_diff = 3flow = flow_rate(weight_diff, time_diff)print('%.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()): print('%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 when is None else when print('%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 = 5bar = decode('also bad')bar = 1print('Foo:', foo)print('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 foo is 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 default is 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 = 5bar = decode('also bad')bar = 1print('Foo:', foo)print('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 / divisor except 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)print(result)>>>0.0

detta samtal ignorerar felet från att dividera med noll och kommer att returnera oändligheten.

result = safe_division(1, 0, False, True)print(result)>>>inf

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) # OKtry: 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 2def print_args(*args, **kwargs): print 'Positional:', args print '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 2def 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.

Lämna ett svar

Din e-postadress kommer inte publiceras.