Effective Python: 4 Best Practices for Function Arguments

funkcje w Pythonie mają wiele dodatkowych funkcji, które ułatwiają życie programiście. Niektóre są podobne do możliwości w innych językach programowania, ale wiele z nich jest unikalnych dla Pythona. Te dodatki mogą uczynić cel funkcji bardziej oczywistym. Mogą eliminować hałas i wyjaśniać intencje rozmówców. Mogą znacznie zmniejszyć subtelne błędy, które są trudne do znalezienia. W tym fragmencie Effective Python: 59 Specific Ways to Write Better Python, Brett Slatkin pokazuje 4 najlepsze praktyki dla argumentów funkcji w Pythonie.

zaoszczędź 35% od ceny katalogowej * powiązanej książki lub wieloformatowego ebooka (EPUB + MOBI + PDF) z artykułem z kodem rabatowym.
* Zobacz informit.com/terms

pozycja 18: zmniejsz szum wizualny za pomocą zmiennych argumentów pozycyjnych

akceptowanie opcjonalnych argumentów pozycyjnych (często nazywanych gwiazdkowymi argami w odniesieniu do konwencjonalnej nazwy parametru *args) może sprawić, że wywołanie funkcji będzie bardziej wyraźne i usunie szum wizualny.

na przykład, powiedz, że chcesz zalogować niektóre informacje debugowania. Z ustaloną liczbą argumentów, potrzebujesz funkcji, która pobiera wiadomość i listę wartości.

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

przekazywanie pustej listy, gdy nie ma wartości do logowania, jest uciążliwe i hałaśliwe. Lepiej byłoby całkowicie pominąć drugi argument. Możesz to zrobić w Pythonie, poprzedzając nazwę ostatniego parametru pozycyjnego *. Pierwszy parametr dla wiadomości log jest wymagany, podczas gdy dowolna liczba kolejnych argumentów pozycyjnych jest opcjonalna. Ciało funkcji nie musi się zmieniać, tylko dzwoniący.

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

Jeśli masz już listę i chcesz wywołać funkcję argumentu zmiennej, taką jak log, możesz to zrobić za pomocą operatora*. To instruuje Pythona, aby przekazywał elementy z sekwencji jako argumenty pozycyjne.

favorites = log('Favorite colors', *favorites)>>>Favorite colors: 7, 33, 99

istnieją dwa problemy z akceptacją zmiennej liczby argumentów pozycyjnych.

pierwsza sprawa polega na tym, że argumenty zmiennych są zawsze zamieniane w krotkę, zanim zostaną przekazane do funkcji. Oznacza to, że jeśli wywołujący twoją funkcję używa operatora * na generatorze, będzie on iterowany, dopóki nie zostanie wyczerpany. Wynikająca krotka będzie zawierać każdą wartość z generatora, która może pochłonąć dużo pamięci i spowodować awarię programu.

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)

funkcje akceptujące *args są najlepsze w sytuacjach, gdy wiadomo, że liczba wejść na liście argumentów będzie stosunkowo mała. Jest to idealne rozwiązanie dla wywołań funkcji, które przekazują wiele literałów lub nazw zmiennych razem. Jest to przede wszystkim dla wygody programisty i czytelności kodu.

drugi problem z *args polega na tym, że nie można dodawać nowych argumentów pozycyjnych do funkcji w przyszłości bez migracji każdego wywołującego. Jeśli spróbujesz dodać argument pozycyjny na początku listy argumentów, istniejące wywołania zostaną subtelnie złamane, jeśli nie zostaną zaktualizowane.

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

problem polega na tym, że drugie wywołanie logu używało 7 jako parametru wiadomości, ponieważ nie podano argumentu sekwencji. Błędy takie jak ten są trudne do wyśledzenia, ponieważ kod nadal działa bez żadnych WYJĄTKÓW. Aby całkowicie uniknąć tej możliwości, powinieneś użyć argumentów tylko dla słów kluczowych, gdy chcesz rozszerzyć funkcje, które akceptują *args (patrz pozycja 21: “wymuszaj jasność za pomocą argumentów tylko dla słów kluczowych”).

rzeczy do zapamiętania

  • funkcje mogą przyjmować zmienną liczbę argumentów pozycyjnych za pomocą *args w instrukcji def.
  • możesz użyć elementów z sekwencji jako argumentów pozycyjnych dla funkcji z operatorem*.
  • użycie operatora * z generatorem może spowodować wyczerpanie się pamięci i awarię programu.
  • dodawanie nowych parametrów pozycyjnych do funkcji akceptujących * args może wprowadzać trudne do znalezienia błędy.

pozycja 19: zapewnienie opcjonalnego zachowania z argumentami kluczowymi

podobnie jak większość innych języków programowania, wywołanie funkcji w Pythonie pozwala na przekazywanie argumentów według pozycji.

def remainder(number, divisor): return number % divisorassert remainder(20, 7) == 6

wszystkie argumenty pozycyjne do funkcji Pythona mogą być również przekazywane za pomocą słowa kluczowego, gdzie nazwa argumentu jest używana w przypisaniu w nawiasach wywołania funkcji. Argumenty kluczowe mogą być przekazywane w dowolnej kolejności, o ile wszystkie wymagane argumenty pozycyjne są określone. Możesz mieszać i dopasowywać argumenty kluczowe i pozycyjne. Wywołania te są równoważne:

remainder(20, 7)remainder(20, divisor=7)remainder(number=20, divisor=7)remainder(divisor=7, number=20)

argumenty pozycyjne muszą być podane przed argumentami kluczowymi.

remainder(number=20, 7)>>>SyntaxError: non-keyword arg after keyword arg

każdy argument może być podany tylko raz.

remainder(20, number=7)>>>TypeError: remainder() got multiple values for argument 'number'

elastyczność argumentów słów kluczowych zapewnia trzy znaczące korzyści.

pierwszą zaletą jest to, że argumenty kluczowe sprawiają, że wywołanie funkcji staje się jaśniejsze dla nowych czytelników kodu. Przy wywołaniu reszta(20, 7) nie jest jasne, który argument jest liczbą, a który dzielnikiem, nie patrząc na implementację metody reszta. W wywołaniu z argumentami kluczowymi number=20 i divisor = 7 od razu widać, który parametr jest używany do każdego celu.

drugi wpływ argumentów słów kluczowych polega na tym, że mogą mieć wartości domyślne określone w definicji funkcji. Pozwala to funkcji zapewnić dodatkowe Funkcje, gdy ich potrzebujesz, ale pozwala zaakceptować domyślne zachowanie przez większość czasu. Może to wyeliminować powtarzający się kod i zmniejszyć hałas.

na przykład, powiedzmy, że chcesz obliczyć szybkość przepływu płynu do kadzi. Jeśli kadzi jest również na skali, to można użyć różnicy między dwoma pomiarami wagi w dwóch różnych czasach, aby określić natężenie przepływu.

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

w typowym przypadku warto znać natężenie przepływu w kilogramach na sekundę. Innym razem pomocne byłoby użycie ostatnich pomiarów z czujnika w celu przybliżenia większych skal czasowych, takich jak godziny lub dni. Takie zachowanie można zastosować w tej samej funkcji, dodając argument dla współczynnika skalowania przedziału czasu.

def flow_rate(weight_diff, time_diff, period): return (weight_diff / time_diff) * period

problem polega na tym, że teraz musisz podać argument period za każdym razem, gdy wywołujesz funkcję, nawet w powszechnym przypadku natężenia przepływu na sekundę (gdzie okres wynosi 1).

flow_per_second = flow_rate(weight_diff, time_diff, 1)

aby uczynić to mniej hałaśliwym, mogę podać domyślną wartość argumentu period.

def flow_rate(weight_diff, time_diff, period=1): return (weight_diff / time_diff) * period

argument period jest teraz opcjonalny.

flow_per_second = flow_rate(weight_diff, time_diff)flow_per_hour = flow_rate(weight_diff, time_diff, period=3600)

to działa dobrze dla prostych wartości domyślnych (robi się trudne dla złożonych wartości domyślnych—patrz pozycja 20: “użyj None i Docstrings, aby określić dynamiczne argumenty domyślne”).

trzecim powodem użycia argumentów słów kluczowych jest to, że zapewniają one potężny sposób na rozszerzenie parametrów funkcji, zachowując kompatybilność wsteczną z istniejącymi wywołującymi. Pozwala to zapewnić dodatkową funkcjonalność bez konieczności migracji dużej ilości kodu, zmniejszając ryzyko wprowadzenia błędów.

na przykład, powiedzmy, że chcesz rozszerzyć powyższą funkcję flow_rate, aby obliczyć natężenia przepływu w jednostkach wagi oprócz kilogramów. Możesz to zrobić, dodając nowy opcjonalny parametr, który zapewnia współczynnik konwersji do preferowanych jednostek miary.

def flow_rate(weight_diff, time_diff, period=1, units_per_kg=1): return ((weight_diff * units_per_kg) / time_diff) * period

domyślną wartością argumentu dla units_per_kg jest 1, co sprawia, że zwracane jednostki wagi pozostają w kilogramach. Oznacza to, że wszystkie istniejące osoby wywołujące nie będą widzieć żadnych zmian w zachowaniu. Nowe wywołania do flow_rate mogą określać nowy argument słowa kluczowego, aby zobaczyć nowe zachowanie.

pounds_per_hour = flow_rate(weight_diff, time_diff, period=3600, units_per_kg=2.2)

jedynym problemem z tym podejściem jest to, że opcjonalne argumenty słów kluczowych, takie jak period I units_per_kg, mogą być nadal podawane jako argumenty pozycyjne.

pounds_per_hour = flow_rate(weight_diff, time_diff, 3600, 2.2)

pozycjonowanie opcjonalnych argumentów może być mylące, ponieważ nie jest jasne, do czego odpowiadają wartości 3600 i 2.2. Najlepszą praktyką jest zawsze określanie opcjonalnych argumentów za pomocą nazw słów kluczowych i nigdy nie przekazywanie ich jako argumentów pozycyjnych.

rzeczy do zapamiętania

  • argumenty funkcji mogą być określone przez pozycję lub słowo kluczowe.
  • słowa kluczowe

  • wyjaśniają, jaki jest cel każdego argumentu, gdy byłby mylony tylko z argumentami pozycyjnymi.
  • argumenty ze słowami kluczowymi o wartościach domyślnych ułatwiają dodawanie nowych zachowań do funkcji, zwłaszcza gdy funkcja ma istniejące wywołania.
  • opcjonalne argumenty słowa kluczowego powinny być zawsze przekazywane przez słowo kluczowe zamiast przez pozycję.

poz. 20: Użyj None i Docstrings, aby określić dynamiczne domyślne argumenty

czasami musisz użyć niestatycznego typu jako domyślnej wartości argumentu słowa kluczowego. Na przykład, powiedzmy, że chcesz wydrukować wiadomości logowania, które są oznaczone czasem zalogowanego zdarzenia. W domyślnym przypadku wiadomość powinna zawierać czas wywołania funkcji. Możesz spróbować następującego podejścia, zakładając, że domyślne argumenty są ponownie oceniane za każdym razem, gdy funkcja jest wywoływana.

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!

znaczniki czasu są takie same, ponieważ datetime.teraz jest wykonywany tylko jeden raz: gdy funkcja jest zdefiniowana. Domyślne wartości argumentów są obliczane tylko raz na obciążenie modułu, co zwykle ma miejsce podczas uruchamiania programu. Po załadowaniu modułu zawierającego ten kod, DateTime.teraz domyślny argument nie będzie już nigdy sprawdzany.

konwencją osiągnięcia pożądanego rezultatu w Pythonie jest podanie domyślnej wartości None i udokumentowanie rzeczywistego zachowania w docstringu (patrz pozycja 49: “Write Docstrings for Every Function, Class, and Module”). Gdy twój kod widzi wartość argumentu None, odpowiednio przydzielasz wartość domyślną.

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))

teraz znaczniki czasu będą różne.

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!

używanie None dla domyślnych wartości argumentów jest szczególnie ważne, gdy argumenty są zmienne. Na przykład, powiedzmy, że chcesz załadować wartość zakodowaną jako dane JSON. Jeśli dekodowanie danych nie powiedzie się, domyślnie ma zostać zwrócony pusty słownik. Możesz spróbować tego podejścia.

def decode(data, default={}): try: return json.loads(data) except ValueError: return default

problem jest tutaj taki sam jak datetime.teraz przykład powyżej. Słownik podany dla domyślnego będzie współdzielony przez wszystkie wywołania do dekodowania, ponieważ domyślne wartości argumentów są obliczane tylko raz (w czasie ładowania modułu). Może to powodować niezwykle zaskakujące zachowanie.

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}

można oczekiwać dwóch różnych słowników, każdy z jednym kluczem i wartością. Ale modyfikowanie jednego wydaje się również modyfikować drugie. Winowajcą jest to, że foo i bar są równe domyślnemu parametrowi. To ten sam obiekt słownikowy.

assert foo is bar

poprawką jest ustawienie domyślnej wartości argumentu słowa kluczowego na None, a następnie udokumentowanie zachowania w docstringu funkcji.

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

teraz uruchomienie tego samego kodu testowego co poprzednio daje oczekiwany wynik.

foo = decode('bad data')foo = 5bar = decode('also bad')bar = 1print('Foo:', foo)print('Bar:', bar)>>>Foo: {'stuff': 5}Bar: {'meep': 1}

rzeczy do zapamiętania

  • domyślne argumenty są obliczane tylko raz: podczas definiowania funkcji w czasie ładowania modułu. Może to powodować dziwne zachowania dla wartości dynamicznych (takich jak {} lub ).
  • używa None jako domyślnej wartości dla argumentów słów kluczowych, które mają wartość dynamiczną. Dokumentuj rzeczywiste zachowanie domyślne w docstring funkcji.

pozycja 21: wymuszanie przejrzystości za pomocą argumentów tylko dla słów kluczowych

przekazywanie argumentów za słowami kluczowymi jest potężną cechą funkcji Pythona (patrz pozycja 19: “podaj opcjonalne zachowanie za pomocą argumentów kluczowych”). Elastyczność argumentów słów kluczowych umożliwia pisanie kodu, który będzie jasny dla Twoich przypadków użycia.

na przykład, powiedzmy, że chcesz podzielić jedną liczbę przez drugą, ale bądź bardzo ostrożny w szczególnych przypadkach. Czasami chcesz zignorować wyjątki ZeroDivisionError i zamiast tego zwrócić nieskończoność. Innym razem, chcesz zignorować wyjątki OverflowError i zamiast tego zwrócić zero.

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

Korzystanie z tej funkcji jest proste. To wywołanie zignoruje float overflow z division i zwróci zero.

result = safe_division(1, 10**500, True, False)print(result)>>>0.0

to wywołanie zignoruje błąd dzielenia przez zero i zwróci nieskończoność.

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

problem polega na tym, że łatwo jest pomylić pozycję dwóch argumentów logicznych, które kontrolują zachowanie ignorujące wyjątki. Może to łatwo spowodować błędy, które są trudne do wyśledzenia. Jednym ze sposobów poprawy czytelności tego kodu jest użycie argumentów słów kluczowych. Domyślnie funkcja może być zbyt ostrożna i zawsze może ponownie podnosić wyjątki.

def safe_division_b(number, divisor, ignore_overflow=False, ignore_zero_division=False): # ...

wtedy osoby wywołujące mogą użyć argumentów słów kluczowych, aby określić, które z ignorowanych FLAG chcą odwrócić dla określonych operacji, nadpisując domyślne zachowanie.

safe_division_b(1, 10**500, ignore_overflow=True)safe_division_b(1, 0, ignore_zero_division=True)

problem polega na tym, że ponieważ te argumenty kluczowe są opcjonalnym zachowaniem, nic nie zmusza wywołujących twoje funkcje do używania argumentów kluczowych dla jasności. Nawet z nową definicją safe_division_b, nadal można nazwać ją starą metodą z pozycyjnymi argumentami.

safe_division_b(1, 10**500, True, False)

przy złożonych funkcjach takich jak ta, lepiej jest wymagać, aby rozmówcy mieli jasność co do swoich zamiarów. W Pythonie 3 możesz żądać jasności, definiując swoje funkcje za pomocą argumentów tylko dla słów kluczowych. Argumenty te mogą być dostarczone tylko przez słowo kluczowe, nigdy przez pozycję.

tutaj zmieniam definicję funkcji safe_division, aby akceptowała argumenty tylko dla słów kluczowych. Symbol * na liście argumentów wskazuje koniec argumentów pozycyjnych i początek argumentów tylko słów kluczowych.

def safe_division_c(number, divisor, *, ignore_overflow=False, ignore_zero_division=False): # ...

teraz wywołanie funkcji z pozycyjnymi argumentami dla słów kluczowych arguments nie będzie działać.

safe_division_c(1, 10**500, True, False)>>>TypeError: safe_division_c() takes 2 positional arguments but 4 were given

argumenty słów kluczowych i ich domyślne wartości działają zgodnie z oczekiwaniami.

safe_division_c(1, 0, ignore_zero_division=True) # OKtry: safe_division_c(1, 0)except ZeroDivisionError: pass # Expected

argumenty tylko dla słów kluczowych w Pythonie 2

Niestety, Python 2 nie ma jawnej składni do określania argumentów tylko dla słów kluczowych, takich jak Python 3. Ale można osiągnąć to samo zachowanie podnosząc TypeErrors dla nieprawidłowych wywołań funkcji za pomocą operatora * * na listach argumentów. Operator * * jest podobny do operatora * (patrz punkt 18: “Reduce Visual Noise with Variable Positive Arguments”), z tą różnicą, że zamiast przyjmować zmienną liczbę argumentów pozycyjnych, akceptuje dowolną liczbę argumentów słów kluczowych, nawet jeśli nie są one zdefiniowane.

# 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'}

aby safe_division pobierało argumenty tylko dla słów kluczowych w Pythonie 2, masz funkcję accept **kwargs. Następnie wstawiasz argumenty kluczowe, których oczekujesz ze słownika kwargs, używając drugiego argumentu metody pop, aby określić domyślną wartość, gdy brakuje klucza. Na koniec upewnij się, że w kwargs nie ma już argumentów słów kluczowych, aby uniemożliwić wywołującym dostarczanie argumentów, które są nieprawidłowe.

# 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) # ...

teraz możesz wywołać funkcję z argumentami kluczowymi lub bez nich.

safe_division_d(1, 10)safe_division_d(1, 0, ignore_zero_division=True)safe_division_d(1, 10**500, ignore_overflow=True)

próba przekazania argumentów tylko dla słów kluczowych przez pozycję nie zadziała, tak jak w Pythonie 3.

safe_division_d(1, 0, False, True)>>>TypeError: safe_division_d() takes 2 positional arguments but 4 were given

próba przekazania nieoczekiwanych argumentów słów kluczowych również nie zadziała.

safe_division_d(0, 0, unexpected=True)>>>TypeError: Unexpected **kwargs: {'unexpected': True}

rzeczy do zapamiętania

  • argumenty kluczowe sprawiają, że intencja wywołania funkcji jest bardziej jasna.
  • używa argumentów tylko dla słów kluczowych, aby zmusić wywołujących do podania argumentów kluczowych dla potencjalnie mylących funkcji, zwłaszcza tych, które akceptują wiele flag logicznych.
  • Python 3 obsługuje jawną składnię tylko dla słów kluczowych argumentów w funkcjach.
  • Python 2 może emulować argumenty tylko dla słów kluczowych dla funkcji, używając **kwargs i ręcznie podnosząc wyjątki TypeError.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.