Dlaczego Python zezwala na wywołania funkcji z niewłaściwą liczbą argumentów?

80

Python to mój pierwszy dynamiczny język. Niedawno zakodowałem wywołanie funkcji, które niepoprawnie podaje nieprawidłową liczbę argumentów. Nie udało się to z wyjątkiem w momencie wywołania tej funkcji. Spodziewałem się, że nawet w języku dynamicznym tego rodzaju błąd można wykryć podczas analizowania pliku źródłowego.

Rozumiem, że typ rzeczywistych argumentów nie jest znany do momentu wywołania funkcji, ponieważ ta sama zmienna może zawierać wartości dowolnego typu w różnym czasie. Ale liczba argumentów jest znana, gdy tylko plik źródłowy jest analizowany. Nie zmieni się podczas działania programu.

Więc to nie jest kwestia filozoficzna

Aby zachować to w zakresie Stack Overflow, pozwólcie, że sformułuję pytanie w ten sposób. Czy jest jakaś funkcja oferowana przez Python, która wymaga od niego opóźnienia sprawdzenia liczby argumentów w wywołaniu funkcji do czasu rzeczywistego wykonania kodu?

dzień dobry
źródło
17
A jeśli nazwa odnosi się do innej funkcji całkowicie w czasie wykonywania?
jonrsharpe
cóż, jako kontrargument, liczba argumentów do wywołania f(*args)nie będzie znana podczas fazy analizowania.
Łukasz Rogalski
3
Należy zauważyć, że w teorii typów typ wartości obejmuje w przypadku funkcji liczbę argumentów.
PyRulez,
9
Skąd Python wie, którą funkcję chcesz wywołać, dopóki nie spróbujesz jej wywołać? To nie tak, że funkcje w Pythonie mają nazwy. (To nie jest sarkazm)
user253751
1
@immibis: Cóż, nie. Mają imiona. Nie są jednak szczególnie przywiązani do tych imion. Funkcja zdefiniowana jako def foo(): whateverma foo.__name__ == 'foo', ale to nie powstrzyma Cię przed wykonaniem foo = some_other_functionlub bar = foo.
user2357112 obsługuje Monikę

Odpowiedzi:

146

Python nie może z góry wiedzieć, jaki obiekt zostanie wywołany, ponieważ będąc dynamicznym, możesz zamienić obiekt funkcji . Kiedykolwiek. Każdy z tych obiektów może mieć inną liczbę argumentów.

Oto skrajny przykład:

import random

def foo(): pass
def bar(arg1): pass
def baz(arg1, arg2): pass

the_function = random.choice([foo, bar, baz])
print(the_function())

Powyższy kod ma 2 na 3 szanse na zgłoszenie wyjątku. Ale Python nie może wiedzieć a-priori, czy tak będzie, czy nie!

I nawet nie zacząłem od dynamicznego importu modułów, dynamicznego generowania funkcji, innych wywoływalnych obiektów ( __call__można wywołać dowolny obiekt z metodą) lub argumentów typu catch-all ( *argsi **kwargs).

Aby jednak było to bardziej jasne, w swoim pytaniu stawiasz:

Nie zmieni się podczas działania programu.

Tak nie jest, nie w Pythonie, po załadowaniu modułu możesz usunąć, dodać lub zamienić dowolny obiekt w przestrzeni nazw modułu, w tym obiekty funkcji.

Martijn Pieters
źródło
Widziałem tabel funkcyjnych przed, więc nie może być zupełnie tak zła. Och, czekaj, funkcje w tabeli nie używają *argumentów… a to jest Python, więc… tak, dość ekstremalne.
Ray Toal
3
Teraz to koszulka. (Jeśli nie możesz dowiedzieć się, jak dodać atrybucję. Jeśli chcesz, powiedz mi, a prawdopodobnie będę mógł to
rozgryźć
@PyRulez: Moja koszulka z kopią posta z analizą wyrażeń regularnych w kształcie jednorożca korzysta z adresu URL. Możesz po prostu dodać https://stackoverflow.com/a/34567789i skończyć z tym.
Martijn Pieters
4
@PyRulez: ale tak naprawdę to tylko lista wysyłkowa. Punkt jest pokazanie, że można używać funkcji jako obiekty, i nie można znać się przód, który z nich jest tzw. W przeciwnym razie jest to dość powszechna technika .
Martijn Pieters
Może za pomoc użytkownikowi w rozwiązaniu pierwotnego problemu: można by wspomnieć o wykryciu błędu przed uruchomieniem kodu, o użyciu narzędzia do weryfikacji statycznej.
Xavier Combelle,
35

Znana jest liczba przekazywanych argumentów, ale nie jest to faktycznie wywołana funkcja. Zobacz ten przykład:

def foo():
    print("I take no arguments.")

def bar():
    print("I call foo")
    foo()

Może się to wydawać oczywiste, ale umieśćmy je w pliku o nazwie „fubar.py”. Teraz w interaktywnej sesji Pythona wykonaj następujące czynności:

>>> import fubar
>>> fubar.foo()
I take no arguments.
>>> fubar.bar()
I call foo
I take no arguments.

To było oczywiste. A teraz zabawna część. Zdefiniujemy funkcję, która wymaga niezerowej ilości argumentów:

>>> def notfoo(a):
...    print("I take arguments!")
...

Teraz robimy coś, co nazywa się małpą łataniem . Możemy faktycznie zastąpić funkcję foow fubarmodule:

>>> fubar.foo = notfoo

Teraz, kiedy wołamy bar, TypeErrorzostanie podniesiona wola; nazwa fooodnosi się teraz do funkcji, którą zdefiniowaliśmy powyżej, zamiast do pierwotnej funkcji znanej wcześniej jako foo.

>>> fubar.bar()
I call foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/horazont/tmp/fubar.py", line 6, in bar
    foo()
TypeError: notfoo() missing 1 required positional argument: 'a'

Więc nawet w takiej sytuacji, w której może wydawać się bardzo oczywiste, że wywoływana funkcja foonie przyjmuje żadnych argumentów, Python może wiedzieć, że jest to faktycznie foofunkcja, która jest wywoływana, gdy wykonuje tę linię źródłową.

Jest to właściwość Pythona, która sprawia, że ​​jest on potężny, ale powoduje też pewne powolne działanie. W rzeczywistości, uczynienie modułów tylko do odczytu w celu poprawy wydajności było omawiane na liście mailingowej python-ideas jakiś czas temu, ale nie zyskało żadnego realnego wsparcia.

Jonas Schäfer
źródło