Importowanie w Python


Dowiedz się jak działa import w Python. Poznaj praktyczne zastosowanie __import__ i wdróż dobre praktyki w swoim kodzie.


Importowanie to jedna z kluczowych funkcjonalności jakie musi posiadać język programowania. Pozwala na rozbicie projektu na mniejsze pakiety jak również korzystanie z wewnętrznych bibliotek oraz zewnętrznych modułów.

Zanim przejdziemy do omówienia samego importowania, musimy dowiedzieć się czym jest moduł i pakiet. Moduł to po prostu plik z rozszerzeniem .py . Pakiet natomiast to katalog, który zawiera plik __init__.py i inne pliki, czyli wspomniane wcześniej moduły.

Podstawy

Aby móc korzystać z modułów i bibliotek należy je najpierw zaimportować. Do tego celu służy słowo kluczowe import . Cały system importowania jest bardzo złożonym procesem i prawdopodobnie nie zagłębialiście się jak dokładnie działa. Importować można moduły, pakiety, funkcję i klasy. Lepiej zrozumiemy to na poniższej aplikacji.

app
├── fibb
│   ├── fun.py
│   ├── gen.py
│   ├── __init__.py
│   └── iter.py
├── __init__.py
└── main.py

Mamy tutaj aplikację app, w której jest pakiet fibb. W tym pakiecie znajdują się trzy moduły - fun, gen oraz iter. Każdy z nich implementuje ciąg fibbonaciego w inny sposób. Niżej przedstawione sposoby importowania.

# import pakietu - fibb
import fibb

# import modułu - fun
from fibb import fun

# import funkcji z modułu gen w pakiecie fibb
from fibb.gen import fibb

# import klasy FibIterator z modułu iter w pakiecie fibb
from fibb.iter import FibIterator

Funkcja importująca

Zanim przejdziemy dalej musimy uświadomić sobie, że słowo kluczowe import jest w zasadzie opakowaniem funkcji __import__ . Funkcja importująca jest jednak uproszczona i zwraca moduł, a jak pokazałem wyżej importować można również inne rodzaje obiektów.

Na co dzień korzystamy z zapisu:

import itertools
itertools  # <module 'itertools' (built-in)> 

Który jest niemal tożsamy z zapisem:

itertools = __import__("itertools")
itertools  # <module 'itertools' (built-in)>

Widać od razu, że wszystko co jest importowane jest obiektem. Warto to wiedzieć.

Import z aliasem

Czasem zachodzi potrzeba importowania dwóch modułów o tej samej nazwie. Żeby ich nie nadpisać możemy skorzystać ze słowa kluczowego as .

from fibb.gen import fibb as gen_fibb
from fibb.fun import fibb as fun_fibb

gen_fibb(10)  # <generator object fibb at 0x7f21f89a82b0>
fun_fibb(10)  # [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Praktyczne zastosowanie __import__

No dobra, wiemy że import to opakowanie __import__ ale czy ta wiedza jest dla nas w jakikolwiek sposób potrzebna? Otóż tak. Bardzo rzadko, ale jednak może zajść potrzeba w której będziesz chciał zaimportować moduł, którego nazwy wcześniej nie znałeś.

api_name = get_api_name()
api = __import__(api_name)

Moduł sys

Żeby nieco lepiej zrozumieć mechanizmy jakie zachodzą podczas importowania musimy zapoznać się z grubsza z modułem sys.

Moduł sys zapewnia dostęp do funkcji systemu operacyjnego oraz funkcji związanych z samym Python. Korzystając z tego modułu możemy dowiedzieć się bardzo dużo na temat zasady działania importowania. W prosty sposób możemy pobrać listę wszystkich załadowanych modułów. Wystarczy skorzystać z sys.modules

import sys
sys.modules  #{'builtins': <module 'builtins' (built-in)>, 'sys': <module 'sys' (built-in)>, ... }

Jak widzimy, wynikiem jest zwykłym słownik, a więc bardzo łatwo możemy zobaczyć nazwy załadowanych modułów. Zobaczmy to na przykładzie

import sys
modules = sys.modules.keys()

modules  # dict_keys(['builtins', 'sys', ... ])

# moduły systemowy
'itertools' in modules  # True
'sys' in modules  # True

# moduł lokalny
'fibb' in modules  # False
import fibb
'fibb' in modules  # True

Ścieżki importowania

Moduły przechowywane są w różnych miejscach. Wbudowane pakiety są przechowywane razem z implementacją Python, biblioteki zewnętrzne w przygotowanym miejscu a nasz kod w utworzonym przez nas katalogu. Jeśli korzystamy ze środowisk wirtualnych, to ścieżki te będą inne. Żeby poznać listę ścieżek w których Python będzie szukał modułów wystarczy skorzystać z sys.path

sys.path  # ['', '/usr/lib/python36.zip', '/usr/lib/python3.6', '/usr/lib/python3.6/lib-dynload', '/home/ddeby/.local/lib/python3.6/site-packages', '/usr/local/lib/python3.6/dist-packages', '/usr/lib/python3/dist-packages']

Tę listę można dostosować do swoich potrzeb poprzez dodawanie lub usuwanie ścieżek w zależności od potrzeb, lub przez modyfikację zmiennej środowiskowej PYTHONPATH. Nie sądzę, żebyś musiał korzystać z tych modyfikacji na co dzień. Jedynym praktycznym zastosowaniem wydaje się podmiana ścieżki na przykład na środowisku testowym.

Python przechodzi przez tę listę w celu odszukania odpowiedniego modułu, dlatego kolejność ścieżek jest bardzo ważna z dwóch powodów. Po pierwsze pod względem optymalizacyjnym, bo lista będzie iterowana do pierwszego trafienia. Dlatego odpowiednia kolejność może skrócić czas wyszukiwania. Po drugie może być przyczyną wielu błędów, polegających na przesłanianiu modułów. Katalog roboczy aplikacji jest przeszukiwany jako pierwszy, więc jeśli napiszemy moduł o takiej samej nazwie jak ten wbudowany w python to nadpiszemy jego działanie. Sprawdźmy to nadpisując moduł random. Nowa struktura będzie zatem następująca

app
├── fibb
│   ├── fun.py
│   ├── gen.py
│   ├── __init__.py
│   └── iter.py
├── __init__.py
├── main.py
└── random.py

Moduł może być nawet pusty. Zobaczmy co się stanie jeśli spróbujemy wywołając jakąś funkcję, ze standardowej biblioteki random w Python

import random
random.randint(1, 10)  # AttributeError: module 'random' has no attribute 'randint'

Wbudowane biblioteki

Python oferuję ogromną bibliotekę standardową, które dostarczają wielu praktycznych funkcji. Jeśli pracowałeś z innymi językami, to zapewne przywykłeś do sytuacji w której w przypadku napotkania problemu sam implementowałes rozwiązanie. Zanim zaczniesz pisać takie rozwiązanie w Python, gorąco zachęcam Cię do przeszukania biblioteki standardowej bo istnieje duże prawdopodobieństwo, że znajdziesz w niej rozwiązanie swojego problemu albo chociaż funkcje, które znacznie Ci to uproszczą. W dokumentacji masz listę dostępnych bibliotek. Poniżej przedstawiam szybki przegląd bibliotek, które mogą Ci się przydać:

  • Atexit - pozwala na dodanie dodatkowej logiki podczas wyłączania programu
  • Array - obsługa tablic w Python
  • Argparse - oferuje funkcje do parsowania argumentów
  • Calendar - oferuje ciekawe rozwiązania do obsługi dat
  • Collections - bardzo cenna biblioteka do obsługi różnych typów danych
  • Copy - biblioteka do kopiowania danych
  • Csv - obsługa plików csv
  • Datetime - obsługa dat i godzin, chociaż osobiście bardziej polecam dateutil (zewnętrzna)
  • Heapq - obsługa kopca
  • Itertools - do wygodnego iterowania po obiektach
  • Io - obsługa strumienia wejścia/wyjścia
  • Json - obsługa czytania i zapisywania danych w formacie JSON
  • Logging - wbudowane logowanie informacji i błędów w Python
  • Math - do podstawowych operacji matematycznych
  • Multiprocessing - umożliwia korzystanie z wielu procesów
  • Operator - pozwala na korzystanie ze znanych operatorów w postaci funkcji
  • Os - dostęp do podstawowywch funkcji systemu operacyjnego
  • Pickle - do serializowania obiektów
  • Random - do generowania liczb pseudolosowych
  • Re - do korzystania z wyrażeń regularnych
  • Shutil - wysokopoziomowa obsługa plików
  • Signal - oferuje obsługę zdarzeń asynchronicznych
  • Tempfile - do pracy z plikami tymczasowymi
  • Threading - do obsługi wątków
  • Urllib - do wygodnego parsowania adresów URL
  • Uuid - generowanie UID ze standardem RFC 4122

Jest ich oczywiście znacznie więcej. Wyżej starałem się wypisać te z którymi miałem styczność, lub te które obiły mi się o uszy. Zachęcam gorąco do zapoznania się z pełną listą, bo im mniej czasu spędzisz na przeszukiwanie bibliotek w przyszłości tym więcej będziesz go mieć na pisanie swojego kodu.

Biblioteki zewnętrzne

Wspomniane biblioteki są bardzo rozbudowane jednak nie ma możliwości, żeby rozwiązywały wszystkie problemy. Całe szczęście społeczność Python jest szeroka i chętnie tworzone są zewnętrzne biblioteki rozwiązujące te trudności. Dobrym przykładem może być wspomniane wcześniej dateutil i strefy czasowe. Biblioteki wbudowane mają do siebie to, że są dokładnie przetestowane, czego niestety nie mogą zagwarantować te zewnętrzne. Dlatego korzystając z nich należy wykazać się ostrożnością. Niestety nie mamy pewności, że dana biblioteka będzie dalej rozwijana mimo aktualnie dużej aktywności, zwłaszcza jeśli rozwijana jest przez jedną osobę. Często jednak korzyści korzystania z bibliotek zewnętrznych przewyższają zagrożenia. Nie ma sensu bowiem wykorzystywać koła na nowo. Kierując się wyborem biblioteki polecam zweryfikować ją pod kilkoma względami

  • Zgodność z Python3 - nie musze chyba tłumaczyć jak ważne będzie to w 2020 roku :)
  • Rozwój - na podstawie danych z PyPi lub GitHub możemy podejrzeć czy dana biblioteka jest aktualnie rozwijana
  • Utrzymanie - Każdy kod ma usterki, dlatego ważne jest zweryfikowanie czy błędy w danej bibliotece są poprawiane systematycznie
  • Licencja - korzystając z jakiejkolwiek biblioteki musimy sprawdzić zgodność jej licencji z naszym oprogramowaniem.

From app import *

Jeśli mamy dużo modułów do zaimportowania, lub nie wiemy dokładnie jakie moduły będą nam potrzebne możemy skorzystać z *

Jednak odradzam takie podejście! To wszystko zaciemnia czytelność kodu. Nie wiadomo z jakiego pakietu został zaimportowany dany moduł, czy funkcja, a przez to debugowanie jest znacznie trudniejsze. Do tego istnieje duże ryzyko wystąpienia błędu związanego z przesłonięciem. Łatwiej będzie to zrozumieć na przykładzie. Spróbujmy napisać skrypt, który wypisze ciąg fibbonaciego losowej długości ale zamiast importować to normalnie wykorzystajmy gwiazdkę.

from random import *
from fibb.gen import *


length = randint(1, 10)
length, fibb(length)  # (9, <generator object fibb at 0x7fa62b5c1518>)

Na razie wszystko jest w porządku. Co natomiast jeśli pliku gen dodamy własną funkcję randint ale tylko z jednym parametrem?

def fibb(n):
    [ ... ]


def randint(n):
    [ ... ]    

Otóż wystąpi błąd, widoczny poniżej

from random import *
from fibb.gen import *


length = randint(1, 10)  # TypeError: randint() takes 1 positional argument but 2 were given

Ten przypadek jest akurat optymistyczny, bo od razu widzimy, że wystąpił błąd. Zdarza się, że taki sposób importowania powoduje błąd, który nie jest widoczny od razu i wychodzi w najmniej oczekiwanym momencie.

Dobre praktyki

PEP 8, daje kilka wskazówek odnośnie importowania. Warto się do nich stosować

  1. Importy zawsze powinny być na samej górze pliku
  2. Importy powinny być podzielone na 3 sekcje
    1. Biblioteki Standardowe
    2. Biblioteki zewnętrzne (zainstalowane ale nie będące kodem aplikacji)
    3. Kod z aplikacji (moduły należące do aplikacji)
  3. Poszczególne sekcje powinny być oddzielone pustą linią

Do tego warto, żeby w ramach sekcji sortować importowane moduły alfabetycznie. Zobaczmy to na przykładzie

# import pakietów systemowych z Python
import datetime
import os

# import bibliotek zewnętrznych
from django.views.generic.detail import DetailView

# import kodu lokalnego
from articles.models import Article

Import wielu funkcji

Jeśli pisałeś np w Javie to przywykłeś do tego, że w jednym pliku była jedna klasa. Niemal wszystkie projekty zbudowane w Django odnoszą się do tego zupełnie inaczej. W jednym pliku może być więcej klas. Na przykład w pliku z modelami. Wiemy już, że nie powinniśmy importować przez *. Możemy importować klasy po przecinku ale jak klas będzie więcej, lub ich nazwy będą długie to łatwo przekroczymy 80 znaków. Dlatego standard PEP opisuje również taki przypadek. Zamiast pisać

from articles.models import Article
from articles.models import ArticleBlock
from articles.models import ArticleCollection

Możemy to pogrupować i wypisać w następujący sposób

from articles.models import (
    Article,
    ArticleBlock,
    ArticleCollection
)

Podsumowanie

Ten wpis nie jest wystarczający. Importowanie w Python jest bardzo zaawansowane. Nie opisałem tu wszystkiego, bo nie chciałem żeby wpis się rozrastał. W przyszłości warto również odnieść się do porównania importowania relatywnego i absolutnego oraz własnej klasy importującej, dzięki której można importować rozszerzenia z innymi rozszerzeniami niż .py .
Głównym zadaniem wpisu było pokazanie, że nawet tak oczywista kwestia jak importowanie może być realizowana źle lub przynajmniej niezgodnie ze standardami. Mam nadzieję, że udało mi się Was również przekonać do korzystania z wbudowanych bibliotek, które są potężnym narzędziem w rękach każdego programisty Python.

Wrz 18, 2019

Najnowsze wpisy

Zobacz wszystkie