Czy importowanie wewnątrz funkcji jest pythoniczne?


126

PEP 8 mówi:

  • Importy są zawsze umieszczane na początku pliku, zaraz po komentarzach do modułów i dokumentach oraz przed wartościami globalnymi i stałymi modułu.

Czasami naruszam PEP 8. Czasami importuję rzeczy wewnątrz funkcji. Generalnie robię to, jeśli istnieje import, który jest używany tylko w ramach jednej funkcji.

Jakieś opinie?

EDYTUJ (powód, dla którego uważam, że importowanie w funkcjach może być dobrym pomysłem):

Główny powód: może uczynić kod bardziej przejrzystym.

  • Patrząc na kod funkcji, mogę zadać sobie pytanie: „Co to jest funkcja / klasa xxx?” (xxx jest używany wewnątrz funkcji). Jeśli mam wszystkie moje importy na górze modułu, muszę tam zajrzeć, aby określić, co to jest xxx. Jest to bardziej problem podczas używania from m import xxx. Widzenie m.xxxw funkcji pewnie mówi mi więcej. W zależności od tego, co to mjest: Czy jest to dobrze znany moduł / pakiet ( import m) najwyższego poziomu ? Czy jest to podmoduł / pakiet ( from a.b.c import m)?
  • W niektórych przypadkach posiadanie tych dodatkowych informacji („Co to jest xxx?”) W pobliżu miejsca, w którym używane jest xxx, może ułatwić zrozumienie funkcji.

2
i robisz to dla wydajności?
Macarse

4
Wydaje mi się, że w niektórych przypadkach sprawia to, że kod jest bardziej przejrzysty. Domyślam się, że surowa wydajność spada podczas importowania w funkcji (ponieważ instrukcja import zostanie wykonana za każdym razem, gdy funkcja zostanie wywołana).
codeape

Możesz odpowiedzieć „Co to jest funkcja / klasa xxx?” używając składni import xyz zamiast składni abc import xyz
Tom Leys,

1
Jeśli jasność jest jedynym czynnikiem, U może równie dobrze zamieścić stosowny komentarz w tej sprawie. ;)
Lakshman Prasad

5
@becomingGuru: Jasne, ale komentarze mogą nie być zsynchronizowane z rzeczywistością ...
codeape

Odpowiedzi:


88

Myślę, że na dłuższą metę docenisz umieszczenie większości importowanych plików na początku pliku, dzięki czemu możesz od razu stwierdzić, jak skomplikowany jest Twój moduł, biorąc pod uwagę to, co musi zaimportować.

Jeśli dodaję nowy kod do istniejącego pliku, zwykle wykonuję import tam, gdzie jest to potrzebne, a jeśli kod pozostanie, sprawię, że wszystko będzie bardziej trwałe, przenosząc wiersz importu na początek pliku.

Jeszcze jedna kwestia, wolę uzyskać ImportErrorwyjątek przed uruchomieniem jakiegokolwiek kodu - jako sprawdzenie poprawności, więc to kolejny powód, aby importować u góry.

Używam pyCheckerdo sprawdzenia nieużywanych modułów.


47

Istnieją dwa przypadki, w których naruszam PEP 8 w tym zakresie:

  • Importy cykliczne: moduł A importuje moduł B, ale coś w module B wymaga modułu A (choć często jest to znak, że muszę refaktoryzować moduły, aby wyeliminować zależność cykliczną)
  • Wstawianie punktu przerwania pdb: import pdb; pdb.set_trace()Jest to przydatne, b / c Nie chcę umieszczać import pdbna górze każdego modułu, który chciałbym debugować, i łatwo zapamiętać, aby usunąć import, gdy usuwam punkt przerwania.

Poza tymi dwoma przypadkami dobrze jest umieścić wszystko na górze. Sprawia, że ​​zależności są wyraźniejsze.


7
Zgadzam się, że dzięki temu zależności są bardziej przejrzyste w odniesieniu do modułu jako całości. Uważam jednak, że importowanie wszystkiego na górze może sprawić, że kod na poziomie funkcji będzie mniej przejrzysty. Patrząc na kod funkcji, możesz zadać sobie pytanie: „Co to jest funkcja / klasa xxx?” (xxx jest używane wewnątrz funkcji). I musisz spojrzeć na samą górę pliku, aby zobaczyć, skąd pochodzi xxx. Jest to bardziej problem, gdy używasz from m import xxx. Widok m.xxx mówi więcej - przynajmniej jeśli nie ma wątpliwości, czym jest m.
codeape

20

Oto cztery przypadki użycia importu, których używamy

  1. import(i from x import yi import x as y) u góry

  2. Opcje importu. Na górze.

    import settings
    if setting.something:
        import this as foo
    else:
        import that as foo
  3. Import warunkowy. Używany z bibliotekami JSON, XML i tym podobnymi. Na górze.

    try:
        import this as foo
    except ImportError:
        import that as foo
  4. Dynamiczny import. Jak dotąd mamy tylko jeden przykład.

    import settings
    module_stuff = {}
    module= __import__( settings.some_module, module_stuff )
    x = module_stuff['x']

    Zauważ, że ten dynamiczny import nie wprowadza kodu, ale wprowadza złożone struktury danych napisane w Pythonie. To trochę jak wytrawiony fragment danych, z wyjątkiem tego, że wytrawiliśmy go ręcznie.

    Jest to również mniej więcej u góry modułu


Oto, co robimy, aby kod był bardziej przejrzysty:

  • Zadbaj o to, aby moduły były krótkie.

  • Jeśli mam wszystkie moje importy na górze modułu, muszę tam zajrzeć, aby ustalić, jaka jest nazwa. Jeśli moduł jest krótki, łatwo to zrobić.

  • W niektórych przypadkach posiadanie tych dodatkowych informacji blisko miejsca, w którym jest używana nazwa, może ułatwić zrozumienie funkcji. Jeśli moduł jest krótki, łatwo to zrobić.


Skracanie modułów jest oczywiście bardzo dobrym pomysłem. Aby jednak zawsze mieć „informacje o imporcie” dla dostępnych funkcji, maksymalna długość modułu musiałaby wynosić jeden ekran (prawdopodobnie maksymalnie 100 linii). W większości przypadków byłoby to prawdopodobnie zbyt krótkie, aby było praktyczne.
codeape

Przypuszczam, że można to doprowadzić do logicznej skrajności. Myślę, że może istnieć punkt równowagi, w którym twój moduł jest „na tyle mały”, że nie potrzebujesz wyszukanych technik importu do zarządzania złożonością. Nasz średni rozmiar modułu to - tak się składa - około 100 linii.
S.Lott

8

Należy pamiętać o jednej rzeczy: niepotrzebny import może powodować problemy z wydajnością. Jeśli więc jest to funkcja, która będzie często wywoływana, lepiej po prostu umieścić import na górze. Oczywiście jest to optymalizacja, więc jeśli istnieje uzasadniony argument, że importowanie do funkcji jest bardziej przejrzyste niż importowanie na początku pliku, w większości przypadków jest to ważniejsze od wydajności.

Jeśli robisz IronPython, powiedziano mi, że lepiej jest importować funkcje wewnętrzne (ponieważ kompilacja kodu w IronPythonie może być powolna). W ten sposób możesz wtedy znaleźć sposób na importowanie funkcji wewnętrznych. Ale poza tym twierdzę, że po prostu nie warto walczyć z konwencjami.

Generalnie robię to, jeśli istnieje import, który jest używany tylko w ramach jednej funkcji.

Inną kwestią, którą chciałbym poruszyć, jest to, że może to być potencjalny problem z konserwacją. Co się stanie, jeśli dodasz funkcję korzystającą z modułu, który był wcześniej używany tylko przez jedną funkcję? Czy będziesz pamiętać o dodaniu importu na początku pliku? A może zamierzasz przeskanować każdą funkcję pod kątem importu?

FWIW, są przypadki, w których ma sens importowanie do funkcji. Na przykład, jeśli chcesz ustawić język w cx_Oracle, musisz ustawić _zmienną środowiskową NLS LANG przed jej zaimportowaniem. Dlatego możesz zobaczyć taki kod:

import os

oracle = None

def InitializeOracle(lang):
    global oracle
    os.environ['NLS_LANG'] = lang
    import cx_Oracle
    oracle = cx_Oracle

2
Zgadzam się z twoim problemem konserwacji. Refaktoryzacja kodu może być nieco problematyczna. Jeśli dodam drugą funkcję, która korzysta z modułu używanego wcześniej tylko przez jedną funkcję - albo przenoszę import na górę, albo łamię własną ogólną zasadę, importując moduł również w drugiej funkcji.
codeape

2
Myślę, że argument dotyczący wydajności może też iść w drugą stronę. Importowanie modułu może być czasochłonne. W rozproszonych systemach plików, takich jak superkomputery, importowanie dużego modułu, takiego jak numpy, może zająć kilka sekund. Jeśli moduł jest potrzebny tylko do pojedynczej, rzadko używanej funkcji, importowanie do funkcji znacznie przyspieszy typowy przypadek.
amaurea

6

Wcześniej złamałem tę zasadę dla modułów, które są autotestowane. Oznacza to, że są zwykle używane tylko do wsparcia, ale definiuję dla nich główne, więc jeśli uruchomisz je samodzielnie, możesz przetestować ich funkcjonalność. W takim przypadku czasami importuję getopti cmdtylko w zasadzie, ponieważ chcę, aby ktoś czytający kod wiedział, że te moduły nie mają nic wspólnego z normalnym działaniem modułu i są uwzględniane tylko do testów.


5

Wychodząc z pytania o dwukrotne załadowanie modułu - dlaczego nie oba?

Import w górnej części skryptu wskaże zależności, a kolejny import w funkcji sprawi, że ta funkcja będzie bardziej atomowa, ale pozornie nie spowoduje pogorszenia wydajności, ponieważ import ciągły jest tani.


3

O ile tak jest importi nie from x import *, należy je umieścić na górze. Dodaje tylko jedną nazwę do globalnej przestrzeni nazw, a ty trzymasz się PEP 8. Ponadto, jeśli później będziesz jej potrzebować gdzie indziej, nie musisz niczego przenosić.

To nic wielkiego, ale ponieważ nie ma prawie żadnej różnicy, proponuję zrobić to, co mówi PEP 8.


3
Właściwie umieszczenie from x import *wewnątrz funkcji wygeneruje SyntaxWarning, przynajmniej w 2.5.
Rick Copeland

3

Spójrz na alternatywne podejście, które jest używane w sqlalchemy: wstrzykiwanie zależności:

@util.dependencies("sqlalchemy.orm.query")
def merge_result(query, *args):
    #...
    query.Query(...)

Zwróć uwagę, jak zaimportowana biblioteka jest zadeklarowana w dekoratorze i przekazana jako argument do funkcji !

Takie podejście sprawia, że ​​kod jest czystszy, a także działa 4,5 razy szybciej niż importinstrukcja!

Benchmark: https://gist.github.com/kolypto/589e84fbcfb6312532658df2fabdb796


2

W modułach, które są zarówno „normalnymi” modułami i mogą być wykonywane (tj. Mają if __name__ == '__main__':sekcję), zwykle importuję moduły, które są używane tylko podczas wykonywania modułu w sekcji głównej.

Przykład:

def really_useful_function(data):
    ...


def main():
    from pathlib import Path
    from argparse import ArgumentParser
    from dataloader import load_data_from_directory

    parser = ArgumentParser()
    parser.add_argument('directory')
    args = parser.parse_args()
    data = load_data_from_directory(Path(args.directory))
    print(really_useful_function(data)


if __name__ == '__main__':
    main()

1

Jest jeszcze jeden przypadek (prawdopodobnie „narożnik”), w którym może być korzystne korzystanie z importrzadko używanych funkcji: skrócić czas uruchamiania.

Kiedyś trafiłem w tę ścianę dość złożonym programem działającym na małym serwerze IoT, akceptującym polecenia z linii szeregowej i wykonującym operacje, być może bardzo złożone operacje.

Umieszczanie importinstrukcji na górze plików, co ma na celu przetworzenie wszystkich importów przed uruchomieniem serwera; od importwykazu zawartego jinja2, lxml,signxml i inne ciężary „ciężki” (SoC i nie był bardzo silny) oznaczało minut przed pierwszą instrukcją faktycznie wykonywane.

OTOH umieszczając większość importów w funkcjach, w ciągu kilku sekund udało mi się „ożywić” serwer na linii szeregowej. Oczywiście, gdy moduły były faktycznie potrzebne, musiałem zapłacić cenę (uwaga: można to również złagodzić, uruchamiając zadanie w tle, które wykonuje je importw czasie bezczynności).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.