Модули и пакети#

Какво е модул?#

Всеки един Python файл (.py) на практика е модул. Освен това е възможно библиотека, написана на С, и вмъкната динамично също да бъде модул. Третия тип модули са вградените в езика такива.

В тази лекция се фокусираме върху първия тип и модули и как можем да ги създаваме, вмъкваме и боравим с тях.

Как да създам модул?#

Казахме, че всеки Python файл е валиден модул.

Нека създадем един такъв с няколко дефиниции вътре (в папката с тази тетрадка вече би трябвало да се съдържа файл hitchhikers.py).

import#

Имената, функциите и класовете, които създадохме в този файл, не могат да бъдат достъпени директно от друг файл:

compute()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
Cell In[1], line 1
----> 1 compute()

NameError: name 'compute' is not defined

Можем обаче да ги вмъкнем в друг файл (модул) чрез import {името_на_модула} (името на файла преди разширението .py се превръща в име на модула):

import hitchhikers

hitchhikers.compute()
Hm, I'll have to think about that. Return to this place in exactly 7.5 million years...
42

import освен, че интерпретира целия код на модула, добавя имената и дефинициите в един обект от тип модул, имащ името на модула. Затова и ги достъпваме чрез името_на_модула.име_на_обекта.

Какво се съдържа в един модул можем лесно да видим с dir():

dir(hitchhikers)
['ANSWER',
 'TheGreatDeepThought',
 '__builtins__',
 '__cached__',
 '__doc__',
 '__file__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 'compute']
hitchhikers.ANSWER
42
computer = hitchhikers.TheGreatDeepThought()
computer.ask()
Shush! The show is back on.
hitchhikers.__name__
'hitchhikers'
dir()  # by default it shows the contents of the *current* module
['In',
 'Out',
 '_',
 '_2',
 '_3',
 '_4',
 '_6',
 '__',
 '___',
 '__builtin__',
 '__builtins__',
 '__doc__',
 '__loader__',
 '__name__',
 '__package__',
 '__spec__',
 '__vsc_ipynb_file__',
 '_dh',
 '_i',
 '_i1',
 '_i2',
 '_i3',
 '_i4',
 '_i5',
 '_i6',
 '_i7',
 '_ih',
 '_ii',
 '_iii',
 '_oh',
 'computer',
 'exit',
 'get_ipython',
 'hitchhikers',
 'open',
 'os',
 'quit',
 'sys']
__name__
'__main__'

Добре, обаче import къде точно търси?#

  1. Директорията, в която се намира Python скрипта, който се изпълнява (или текущата, ако интерпретаторът е пуснат интерактивно)

  2. Директориите, които са описани в PYTHONPATH променливата на средата

  3. Лист от директории, зададен по време на инсталацията на Python

Този списък от възможни директории може да се види със sys.path:

import sys
sys.path
['/Users/alexander.ignatov/Documents/PythonCourse2022/13 - Modules',
 '/Users/alexander.ignatov/.vscode/extensions/ms-toolsai.jupyter-2022.11.1003412109/pythonFiles',
 '/Users/alexander.ignatov/.vscode/extensions/ms-toolsai.jupyter-2022.11.1003412109/pythonFiles/lib/python',
 '/opt/homebrew/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python310.zip',
 '/opt/homebrew/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python3.10',
 '/opt/homebrew/Cellar/python@3.10/3.10.8/Frameworks/Python.framework/Versions/3.10/lib/python3.10/lib-dynload',
 '',
 '/Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages']

Варианти на import#

С from {module} import {something}, {something_else}, ... можем да импортираме само определени имена от модула, като те биват добавени към съдържанието на текущия (т.е. достъпваме ги без името на оригиналния модул и точка отпред):

from hitchhikers import compute
compute()
Hm, I'll have to think about that. Return to this place in exactly 7.5 million years...
42

С from {module} import {something} as {alias}, {something_else} as {other_alias}, ... можем да прекръстим импортираните имена:

from hitchhikers import ANSWER, TheGreatDeepThought as Computer
comp = Computer()
comp.ask() == ANSWER
Shush! The show is back on.
False
"ANSWER" in dir()
True

Ако искаме абсолютно всички имена на вмъкнем и ползваме в текущия модул по този начин (без тези, започващи с подчертавка _), можем да използваме астериск *:

# изпълни тази клетка ако си изпълнил горните, за да се зачистят import-ите
del hitchhikers, ANSWER, compute, Computer
from hitchhikers import *

compute() == ANSWER
Hm, I'll have to think about that. Return to this place in exactly 7.5 million years...
True

Лимитация на астерикс синтаксиса е, че не може да използва в блок (може само на най-външното ниво на модула):

del compute, ANSWER, TheGreatDeepThought
def obtain_answer():
    from hitchhikers import *  # 💥
    return compute()
  Cell In[16], line 2
    from hitchhikers import *  # 💥
                            ^
SyntaxError: import * only allowed at module level

Както казахме, по подразбиране from {module} import * вмъква абсолютно всички имена от module, които не започват с подчертавка. Имаме всъщност контрол над това, кое може да се вмъкне чрез астерикс, като дефинираме __all__ във въпросния модул. Стойността му е лист от всички имена, които ще бъдат вмъкнати от *.

Пример:

След добавяне на

__all__ = ['compute', 'TheGreatDeepThought']

в hitchhikers.py, следният код, изпълнен в script.py (в същата директория) ще хвърли NameError:

from hitchhikers import *
print(hitchhikers.ANSWER)  # 💥

Пакети#

Пакет e набор от модули. За Python всяка директория, в която има модули, се превръща в пакет (package).

Note: Във версии по-ранни от Python 3.3 трябва задължително в директорията да има файл с име __init__.py.

В директорията на тетрадката би трябвало да има папка game, съдържаща няколко файла и папки:

!tree game
game
├── engine.py
├── level.py
├── player.py
└── players
    ├── ai.py
    ├── input_player.py
    └── mock_player.py

1 directory, 6 files

В горния пример game е пакет, съдържащ модулите engine, level и player. Освен тях, той съдържа и подпакетът players.

Ако искаме да вмъкнем някой модул от пакета, можем да го направим чрез името на пакета (или всички пакети по веригата, разделени с точка), последвано от точка и името на въпросния модул:

import game.level

game.level.EASY
Level(word='SCRIPT', failed_attempts=10)
import game.players.ai

game.players.ai.AI(10)
<game.players.ai.AI at 0x107ab4610>

Вече въведените по-горе синтактични варианти на import също важат:

from game.players.ai import AI
from game.level import EASY as easy, MEDIUM as medium, HARD as hard
from game.engine import *

Освен това, можем и да вмъкнем модули чрез from {package} import {module} [as {alias}], ...:

from game import level, engine

print(level.EASY)
print(engine.GameState)
Level(word='SCRIPT', failed_attempts=10)
<enum 'GameState'>

__init__.py#

На теория можем и да импортнем само пакета. По подразбиране това няма да добави нови модули и имена:

del game.level, game.player, game.engine  # зачисти тетрадката от предните импорти
import game

game.level  # 💥
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[23], line 3
      1 import game
----> 3 game.level  # 💥

AttributeError: module 'game' has no attribute 'level'

Ако искаме да добавим и модули от пакета при импортирането му, можем да ги импортнем в __init__.py, намиращ се в директорията на пакета.

Т.е. ако в game/__init__.py имаме:

import game.engine, game.level, game.player

то можем да импортнем пакета game и да използваме всички модули от него:

# в друг файл, извън пакета `game`:
import game
print(game.level.EASY)  # no error

В __init__.py можем да напишем какъвто искаме инициализационен код, глобален за всички модули в пакета. Съдържанието на скрипта се изпълнява веднага при импортиране на пакета.

Както при модулите, така и тука можем да дефинираме поведението на from {package} import * чрез __all__. По подразбиране, както видяхме за import {package}, това е празен списък, т.е. нищо няма да се вмъкне (за разлика от поведението при модулите, когато се вмъква абсолютно всяко име от модула, което не започва с подчертавка).

Т.е. ако напишем в game/__init__.py:

__all__ = ["engine", "level", "player"]

то ще можем:

# в друг файл, извън пакета `game`:
from game import *
print(level.EASY)  # no error

Релативни импорти#

Дотук разгледахме примерни за абсолютни импорти, т.е. достъпът до даден модул от рамките на пакета или извън него става през пътя от пакета до модула, например game.players.ai достъпва модулът ai от пакета players в пакета game.

import game.players.ai

Със значението на . и .. от Unix файловата система, можем да използваме същите тези символи за релативни импорти в Python. Те се оценяват спрямо локацията на import statement-a.

Например, във файла game/players/input_player.py ни трябва player модула от пакета game. Можем да го направим по абсолютен и релативен начин:

from game import player  # абсолютен импорт
from .. import player  # релативен импорт
  • .. означава “пакетът, намиращ се над текущия”.

  • ..pkg означва модулът/пакетът pkg от пакетът, намиращ се над текущия.

Например:

from ..player import Player

Ще вмъкне името Player от модула player от пакета, намиращ се над текущия.

  • . означава “текущия пакет”.

  • .pkg означава модулът/пакетът pkg от текущия пакет.

Релативните импорти имат недостатъка обаче, че зависят от местоположението на import-a. Освен това в скриптове (т.е. изпълним код, който не е вмъкнат чрез модул) имат различно поведение:

from . import hitchhikers
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[25], line 1
----> 1 from . import hitchhikers

ImportError: attempted relative import with no known parent package

if __name__ == "__main__"#

Както бяхме споменали, при импорт се изпълнява кода на съответния модул. Като пример за това можем да изведем философията на Python, намираща се във вградения модул this:

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

Текущо-изпълнимият файл/модул/скрипт за Python се казва "__main__", т.е. неговия __name__ е "__main__":


__name__
'__main__'

Ако файлът не се изпълнява директно, а бъде импортнат от друг, то в неговия __name__ ще е името на модула. Това означава, че можем да различим дали файлът се изпълнява директно или е импортнат.

Полезно е в случаите, когато искаме да напишем примерно някакви тестове или демонстрации на модула, които да се изпълнят само ако го изпълним директно, и да не се изпълняват при всяко вмъкване. . В такива случаи използваме if __name__ == "__main__": ... (разгледайте например game/engine.py).

Управление на пакети#

Какво е pip?#

Пакетите се създават с цел лесно преизползване. При използване на външни пакети в проекта се появяват конкретни проблеми, които трябва да се решават - как да се инсталират, как да се обновят, как да се изтрият, как да се решават зависимостите и т.н. С това ни помагат различните “package manager”-и, като python-ският такъв е pip (името му е рекурсивен акроним: “PIP Installs Packages”).

Къде е pip?#

Управлението на пакетите е важна част от разработката и затова от Python 3.4 и 2.7.9 насам pip е част от инсталацията на Python 3 и Python 2 респективно.

Можем да проверим дали PIP е инсталиран като се опитаме да видим локацията на pip3 командата като изпълним which pip3 (или where pip3 под Windows):


!which pip3  # linux / macOS
/Users/alexander.ignatov/Documents/PythonCourse2022/venv/bin/pip3

В случай, че няма pip3, е възможно да съществува само pip командата:

!which pip
/Users/alexander.ignatov/Documents/PythonCourse2022/venv/bin/pip

В случай, че имаме и двете команди, можем да ги сраним като видим разликите във версията (ако са еднакви, то няма значение дали използваме pip или pip3. Оттук нататък до края на тетрадката ще ги считаме за едни и същи):

!pip3 --version && pip --version
pip 22.2.2 from /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages/pip (python 3.10)
pip 22.2.2 from /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages/pip (python 3.10)

Как да преинсталирам pip ако нещо не е наред?#

В случай, че pip не може да бъде намерен, има два варианта:

  1. pip е инсталиран, но пътя до него не е в $PATH променливата на средата

  2. pip не е инсталиран. Тогава можем да го сложим по два начина:

    1. Инсталираме pip от get-pip.py скрипта

    2. Инсталираме pip чрез ensurepip модула

!python3 -m ensurepip --upgrade  # за windows е `python` вместо `python3`
Looking in links: /var/folders/q1/7m4c3ff153j93q271xrs9y5r0000gq/T/tmpi5sy7acy
Requirement already satisfied: setuptools in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (65.4.1)
Requirement already satisfied: pip in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (22.2.2)

NOTE: ensurepip не тегли от интернет нищо - директно инсталира версията на pip, която е bundle-ната със съответната версия на Python. В случай, че искаме по-нова от дадената, трябва след това ръчно да актуализираме pip чрез python3 -m pip install --upgrade pip или pip3 install --upgrade pip (респективно само python вместо python3 под Windows).

Какви подкоманди има pip?#

Пакети се инсталират с pip install <package1_name> [<package2_name> ...]:

!pip install requests
Collecting requests
  Using cached requests-2.28.1-py3-none-any.whl (62 kB)
Requirement already satisfied: charset-normalizer<3,>=2 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from requests) (2.1.1)
Requirement already satisfied: idna<4,>=2.5 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from requests) (3.4)
Requirement already satisfied: certifi>=2017.4.17 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from requests) (2022.12.7)
Requirement already satisfied: urllib3<1.27,>=1.21.1 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from requests) (1.26.13)
Installing collected packages: requests
Successfully installed requests-2.28.1

[notice] A new release of pip available: 22.2.2 -> 22.3.1
[notice] To update, run: pip install --upgrade pip

Пакетите по подразбиране се търсят в PyPI (чете се “пай пи ай”, а не “пипи”/”пайпи”/”пайпай”), който е Python Package Index. Това е публичен индекс от пакети, към който всеки потребител може да добавя, допринася, търси и ползва.

Ако искаме pip да търси в друг индекс (примерно такъв с частни репозиторита), можем да го променим чрез -i [index_url] аргумент към pip install. Повече за това тук.

Информация за инсталиран пакет може да изведем с pip show <package_name>:

!pip show requests
Name: requests
Version: 2.28.1
Summary: Python HTTP for Humans.
Home-page: https://requests.readthedocs.io
Author: Kenneth Reitz
Author-email: me@kennethreitz.org
License: Apache 2.0
Location: /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages
Requires: certifi, charset-normalizer, idna, urllib3
Required-by: 

Деинсталирането пък съответно става по същия начин, но този път с подкомандата uninstall:

!pip uninstall requests <<< "y"
Found existing installation: requests 2.28.1
Uninstalling requests-2.28.1:
  Would remove:
    /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages/requests-2.28.1.dist-info/*
    /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages/requests/*
Proceed (Y/n)?   Successfully uninstalled requests-2.28.1

Всички инсталирани пакети и техните версии може да видим с pip list:

!pip list | head
Package            Version
------------------ ---------
appnope            0.1.3
asttokens          2.2.1
backcall           0.2.0
certifi            2022.12.7
charset-normalizer 2.1.1
comm               0.1.2
debugpy            1.6.4
decorator          5.1.1
ERROR: Pipe to stdout was broken

[notice] A new release of pip available: 22.2.2 -> 22.3.1
[notice] To update, run: pip install --upgrade pip

Извеждането на списъка със зависимости (инсталирани пакети + версия) в “requirements” формат може да се извърши с pip freeze:

!pip freeze | head
appnope==0.1.3
asttokens==2.2.1
backcall==0.2.0
certifi==2022.12.7
charset-normalizer==2.1.1
comm==0.1.2
debugpy==1.6.4
decorator==5.1.1
entrypoints==0.4
executing==1.2.0

Обикновено изхода от командата се запазва във файл, наречен requirements.txt. Повече за това по-надолу.

Виртуални среди и venv#

Защо?#

Когато инсталираме пакет, той след това може да бъде използван от всички Python проекти на машината (или на потребителя). Това обаче може да доведе до конфликти. Да предположим, че имаме един проект, който изисква например пакетът А да бъде с версия \( \geq X \), докато друг да е направен да работи с версия на A, която да е по-малка от \( X \) (т.е. ъпдейтването до версия \( X \) би счупило проекта). Инсталацията на пакетът A по познатия начин обаче е глобална и не върши работа в случая - трябва ни някакъв начин, по който да имаме различни инсталации на пакета за различните проекти.

Това е идеята на т.нар. “virtual environments” - създават виртуална среда, която да се отнася само за конкретен проект, в която той да се конфигурира, да се изтеглят пакетите, от които зависи и т.н.

Как?#

С вградената билбиотека venv създаваме virtual environment. Изпълняваме я като модул (с флаг -m) и като параметър указваме името на виртуалната среда, която ще бъде създадена в текущата директория:

!python3 -m venv venv

Note 1: Обикновено се кръщава също venv.

Note 2: Директорията (venv в този случай) на виртуалната среда трябва да бъде игнорирана от Git (т.е. да ѝ се добави името на нов ред в .gitignore файла).

Предната команда ще създаде в текущата директория папката venv, в която се намира всичко необходимо на виртуалната среда, за да работи. Тя обаче още няма да е активирана, като това става чрез изпълняване веднъж на:

!source venv/bin/activate

Note: на Windows ще е venv\Scripts\activate.bat (или venv\Scripts\activate.ps1 на PowerShell)

Това ще пренасочи команди като python/python3 и pip/pip3 към локалните копия, намиращи се под директорията на виртуалната среда. Инсталираните пакети също отиват там. По подразбиране няма такива (освен самите pip и setuptools):

!source venv/bin/activate && pip list  # активираме пак понеже jupyter клетките не запазват bash сесиите
Package    Version
---------- -------
pip        22.2.2
setuptools 65.4.1

[notice] A new release of pip available: 22.2.2 -> 22.3.1
[notice] To update, run: pip install --upgrade pip

Работата във виртуалната среда приключва, когато приключи текущата конзолна сесия или когато бъде изпълнена командата deactivate (venv\Scripts\deactivate.bat под Windows).

requirements.txt#

Ако свалим даден проект локално и се опитаме да го изпълним, то ако той зависи от някакви third-party модули/пакети/билбиотеки, ще ни хвърли ImportError, понеже няма да ги намери. Трудно и излишно времеемко е обаче ръчно да проверим кои зависимости ги няма и да ги свалим. Затова ни служи requirements.txt - файл, в който всички dependency-та са описани (име на пакет и версии).

Създава се чрез изхода на pip freeze:

!pip freeze > requirements.txt

Съдържанието е във формат {има на пакет}{знак за сравнение}{версия}:

!cat requirements.txt | head
appnope==0.1.3
asttokens==2.2.1
backcall==0.2.0
certifi==2022.12.7
charset-normalizer==2.1.1
comm==0.1.2
debugpy==1.6.4
decorator==5.1.1
entrypoints==0.4
executing==1.2.0

Използването на файла (т.е. изтеглянето на всички правилни версии на описаните пакети) става чрез аргумента -r на pip install:

!pip install -r requirements.txt
Requirement already satisfied: appnope==0.1.3 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 1)) (0.1.3)
Requirement already satisfied: asttokens==2.2.1 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 2)) (2.2.1)
Requirement already satisfied: backcall==0.2.0 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 3)) (0.2.0)
Requirement already satisfied: certifi==2022.12.7 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 4)) (2022.12.7)
Requirement already satisfied: charset-normalizer==2.1.1 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 5)) (2.1.1)
Requirement already satisfied: comm==0.1.2 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 6)) (0.1.2)
Requirement already satisfied: debugpy==1.6.4 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 7)) (1.6.4)
Requirement already satisfied: decorator==5.1.1 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 8)) (5.1.1)
Requirement already satisfied: entrypoints==0.4 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 9)) (0.4)
Requirement already satisfied: executing==1.2.0 in /Users/alexander.ignatov/Documents/PythonCourse2022/venv/lib/python3.10/site-packages (from -r requirements.txt (line 10)) (1.2.0)

[notice] A new release of pip available: 22.2.2 -> 22.3.1
[notice] To update, run: pip install --upgrade pip

Да обобщим: setup на проект for dummies#

  1. Създаване на виртуална среда:

    python3 -m venv venv

    Важно: при използване на система за контрол на версиите (напр. Git) трябва новосъздадената директория да бъде игнорирана от нея (>> .gitignore).

  2. Активиране на средата:

    source venv/bin/activate (unix) или venv\Scripts\activate.bat (windows)

  3. Подсигуряване на това, че pip е последна версия:

    pip install --upgrade pip

  4. Работата по инсталиране на пакети, пускане на кода и т.н. трябва задължително да става докато е активирана виртуалната среда

  5. След инсталиране на всеки пакет трябва да се обновява списъкът със зависимости:

    pip freeze > requirements.txt