Импорт в Python и организация проекта
Когда вы работаете с модулями в Python, импорт играет важную роль, и понимание его работы может значительно улучшить вашу разработку. В этом посте мы рассмотрим, как работает оператор import в Python и как организовать структуру вашего проекта для более эффективного использования импорта.
При импорте модуля Python не учитывает, в каком файле находится этот импорт. Единственное, что влияет на импорт, - это то, как был запущен код. Если модуль не был загружен ранее, Python пытается найти его в нескольких каталогах, которые определены в переменной sys.path.
По умолчанию sys.path содержит следующие каталоги:
- Каталог, из которого был запущен скрипт
- Каталоги, указанные в переменной окружения PYTHONPATH
- Каталог текущего активного виртуального окружения
- Каталог установки Python
- Если вы запускаете свой скрипт командой python scriptname.py, то первым в списке будет каталог, содержащий запускаемый скрипт. Текущий рабочий каталог не имеет значения.
- Если вы запускаете код командой python -m packagename, то первым в списке будет текущий рабочий каталог. При этом Python попытается найти и импортировать packagename в соответствии с общими правилами.
- Если вы используете инструменты, такие как pytest, для запуска кода, они также могут добавлять что-то в sys.path.
В большинстве случаев вам не потребуется вручную изменять sys.path, так как стандартный алгоритм заполнения его значений является стандартным и привычным для всех. Если по какой-то причине этот алгоритм не подходит вам, возможно, у вас неправильно организована структура проекта.
Так как поиск модулей для импорта происходит сначала в каталоге "проекта", важно аккуратно называть ваши файлы и каталоги. Если вы случайно назовете свой модуль так же, как встроенный или сторонний модуль, при любом импорте такого модуля будет загружаться именно ваш модуль, что может нарушить работу кода.
Иногда используемые нами фреймворки могут поддерживать только определенную структуру проекта, которая не всегда является оптимальной. В остальных случаях я могу предложить два подхода для организации вашего проекта:
Вынесите запускаемые скрипты на верхний уровень, а остальной код упакуйте в пакет.
Упаковка вашего кода в пакет с уникальным именем позволит избежать конфликтов имен. При этом вынесение всех запускаемых файлов на один уровень делает состав sys.path предсказуемым.
Примерная структура проекта будет выглядеть следующим образом:
Copy code
├── appname
│ ├── __init__.py
│ ├── other_module.py
│ └── some_module.py
├── cli_module.py
└── requirements.txt
Создайте распространяемый пакет (рекомендуется).
В этом случае вы упаковываете весь ваш код в пакет, что помогает избежать конфликтов имен. Для запуска команд вы можете использовать синтаксис python -m appname.cli_module или заполнить секцию entry_points в файле с описанием вашего проекта (например, setup.cfg или pyproject.toml). После этого вы сможете запускать ваш код, находясь в любом каталоге, без необходимости указывать полные пути к файлам.
Для удобства разработки с таким подходом удобно устанавливать пакет в режиме редактирования с помощью команды pip install -e ..
Примерная структура проекта для создания распространяемого пакета будет выглядеть следующим образом:
├── pyproject.toml
└── src
└── appname
├── __init__.py
├── cli_module.py
├── other_module.py
└── some_module.py
Дополнительные материалы для изучения:
Дополнительные материалы:
* https://packaging.python.org/en/latest/
* https://docs.python.org/3/reference/import.html
* https://docs.python.org/3/library/sys.html#sys.path
* https://ru.wikipedia.org/wiki/Рабочий_каталог
Когда вы работаете с модулями в Python, импорт играет важную роль, и понимание его работы может значительно улучшить вашу разработку. В этом посте мы рассмотрим, как работает оператор import в Python и как организовать структуру вашего проекта для более эффективного использования импорта.
При импорте модуля Python не учитывает, в каком файле находится этот импорт. Единственное, что влияет на импорт, - это то, как был запущен код. Если модуль не был загружен ранее, Python пытается найти его в нескольких каталогах, которые определены в переменной sys.path.
По умолчанию sys.path содержит следующие каталоги:
- Каталог, из которого был запущен скрипт
- Каталоги, указанные в переменной окружения PYTHONPATH
- Каталог текущего активного виртуального окружения
- Каталог установки Python
- Если вы запускаете свой скрипт командой python scriptname.py, то первым в списке будет каталог, содержащий запускаемый скрипт. Текущий рабочий каталог не имеет значения.
- Если вы запускаете код командой python -m packagename, то первым в списке будет текущий рабочий каталог. При этом Python попытается найти и импортировать packagename в соответствии с общими правилами.
- Если вы используете инструменты, такие как pytest, для запуска кода, они также могут добавлять что-то в sys.path.
В большинстве случаев вам не потребуется вручную изменять sys.path, так как стандартный алгоритм заполнения его значений является стандартным и привычным для всех. Если по какой-то причине этот алгоритм не подходит вам, возможно, у вас неправильно организована структура проекта.
Так как поиск модулей для импорта происходит сначала в каталоге "проекта", важно аккуратно называть ваши файлы и каталоги. Если вы случайно назовете свой модуль так же, как встроенный или сторонний модуль, при любом импорте такого модуля будет загружаться именно ваш модуль, что может нарушить работу кода.
Иногда используемые нами фреймворки могут поддерживать только определенную структуру проекта, которая не всегда является оптимальной. В остальных случаях я могу предложить два подхода для организации вашего проекта:
Вынесите запускаемые скрипты на верхний уровень, а остальной код упакуйте в пакет.
Упаковка вашего кода в пакет с уникальным именем позволит избежать конфликтов имен. При этом вынесение всех запускаемых файлов на один уровень делает состав sys.path предсказуемым.
Примерная структура проекта будет выглядеть следующим образом:
Copy code
├── appname
│ ├── __init__.py
│ ├── other_module.py
│ └── some_module.py
├── cli_module.py
└── requirements.txt
Создайте распространяемый пакет (рекомендуется).
В этом случае вы упаковываете весь ваш код в пакет, что помогает избежать конфликтов имен. Для запуска команд вы можете использовать синтаксис python -m appname.cli_module или заполнить секцию entry_points в файле с описанием вашего проекта (например, setup.cfg или pyproject.toml). После этого вы сможете запускать ваш код, находясь в любом каталоге, без необходимости указывать полные пути к файлам.
Для удобства разработки с таким подходом удобно устанавливать пакет в режиме редактирования с помощью команды pip install -e ..
Примерная структура проекта для создания распространяемого пакета будет выглядеть следующим образом:
├── pyproject.toml
└── src
└── appname
├── __init__.py
├── cli_module.py
├── other_module.py
└── some_module.py
Дополнительные материалы для изучения:
Дополнительные материалы:
* https://packaging.python.org/en/latest/
* https://docs.python.org/3/reference/import.html
* https://docs.python.org/3/library/sys.html#sys.path
* https://ru.wikipedia.org/wiki/Рабочий_каталог
packaging.python.org
Python Packaging User Guide
The Python Packaging User Guide (PyPUG) is a collection of tutorials and guides for packaging Python software.
/ и * в определнии функции
Видели когда-нибудь вот такой синтаксис?
def foo(first, /, second, *, third):
print(first, second, third)
Выглядит странно?
На самом деле / и * навязывают положение ключевых и позиционных аргументов.
Попытаемся, например, вызвать foo() с неправильным набором параметров:
>>> foo("bar", "qux")
...
TypeError: foo() missing 1 required keyword-only argument: 'third'
Нужен keyword аргумент, окей, попробуем еще раз:
>>> foo("bar", third="qux")
...
TypeError: foo() missing 1 required positional argument: 'second'
Снова нет. Укажем все параметры:
>>> foo("bar", "baz", third="qux")
bar baz qux
Получилось!
Оба символа опциональны. И, в моем опыте, если ставят, то обычно только звездочку.
Источник: https://docs.python.org/3/tutorial/controlflow.html#special-parameters
Видели когда-нибудь вот такой синтаксис?
def foo(first, /, second, *, third):
print(first, second, third)
Выглядит странно?
На самом деле / и * навязывают положение ключевых и позиционных аргументов.
Попытаемся, например, вызвать foo() с неправильным набором параметров:
>>> foo("bar", "qux")
...
TypeError: foo() missing 1 required keyword-only argument: 'third'
Нужен keyword аргумент, окей, попробуем еще раз:
>>> foo("bar", third="qux")
...
TypeError: foo() missing 1 required positional argument: 'second'
Снова нет. Укажем все параметры:
>>> foo("bar", "baz", third="qux")
bar baz qux
Получилось!
Оба символа опциональны. И, в моем опыте, если ставят, то обычно только звездочку.
Источник: https://docs.python.org/3/tutorial/controlflow.html#special-parameters
Python documentation
4. More Control Flow Tools
As well as the while statement just introduced, Python uses a few more that we will encounter in this chapter. if Statements: Perhaps the most well-known statement type is the if statement. For exa...
Пара фактов о численных типах
(которые вы, возможно, не знали)
Факт 1
В Python есть три встроенных численных типа. Кроме int и float, которыми мы обычно пользуемся, есть еще complex — комплексные числа.
Комплексные числа много используют в математике (например, с их помощью можно брать некоторые забористые интегралы, которые обычным способом не берутся), и в физике (особенно в расчетах, связанных с электричеством и магнетизмом).
Сконструировать комплексное число в Python можно так:
a = complex(2, 1)
или вот так:
a = 2 + 1j
Получится одно и то же.
Факт 2
Все численные типы в Python унаследованы от класса Number. Проверить это можно так:
from numbers import Number
isinstance(1984, Number) #True
isinstance(3.1415926, Number) #True
isinstance(1j, Number) #True
Кстати, сюрприз: bool тоже унаследован от Number:
isinstance(False, Number) #True
Факт 3
Под капотом логический тип — те же числа, только bool имеет всего два значения: 0 и 1. Это обеспечивает нам легкое приведение True к единице, а False к нулю.
Это же, впрочем, дает ни разу не интуитивное поведение в некоторых случаях:
1/False # ZeroDivisionError: division by zero
True * 1 # 1
False * 1 # 0
complex(True, 3.9) # (1+3.9j)
my_list = [1, 2, 3, 4]
my_list[False] # 1
"False"[True] # a
А, и да
Факт 4
Complex не является составным типом. Это просто объект, который принимает до двух параметров при инициализации.
(которые вы, возможно, не знали)
Факт 1
В Python есть три встроенных численных типа. Кроме int и float, которыми мы обычно пользуемся, есть еще complex — комплексные числа.
Комплексные числа много используют в математике (например, с их помощью можно брать некоторые забористые интегралы, которые обычным способом не берутся), и в физике (особенно в расчетах, связанных с электричеством и магнетизмом).
Сконструировать комплексное число в Python можно так:
a = complex(2, 1)
или вот так:
a = 2 + 1j
Получится одно и то же.
Факт 2
Все численные типы в Python унаследованы от класса Number. Проверить это можно так:
from numbers import Number
isinstance(1984, Number) #True
isinstance(3.1415926, Number) #True
isinstance(1j, Number) #True
Кстати, сюрприз: bool тоже унаследован от Number:
isinstance(False, Number) #True
Факт 3
Под капотом логический тип — те же числа, только bool имеет всего два значения: 0 и 1. Это обеспечивает нам легкое приведение True к единице, а False к нулю.
Это же, впрочем, дает ни разу не интуитивное поведение в некоторых случаях:
1/False # ZeroDivisionError: division by zero
True * 1 # 1
False * 1 # 0
complex(True, 3.9) # (1+3.9j)
my_list = [1, 2, 3, 4]
my_list[False] # 1
"False"[True] # a
А, и да
Факт 4
Complex не является составным типом. Это просто объект, который принимает до двух параметров при инициализации.
Про культуру коммитов
Работаю над куском кода, который опирается на json файл. Файл этот длиной 7к+ строк (только не спрашивайте, как так вышло). И кусок этот одновременно
- сложный
- используется большим количеством пользователей.
И тут из мастера в этот json прилетают изменения. В которых во всем файле пробелы заменены на табы. То есть, дифф слишком большой и отсмотреть глазами не получается. Поправлю, конечно, но досадно -- и небезопасно.
В связи с чем напоминаю, как коммитить (вот, например, хорошая статья от другого автора)
Для меня главное в этом тексте:
Один коммит = одна атомарная задача
И от себя бы еще добавила:
Дифф удобно смотреть ривьюеру
Чтобы этого добиться, перед каждым коммитом нужно отсматривать изменения в каждом файле, которые хотите залить в репо: с помощью git diff или в IDE.
И еще я никогда не делаю
git add .
чтобы даже не привыкать добавлять всю работу, которая у меня есть локально: в ней бывают мусор и наброски кода. Если себя приучить к этому, то гораздо сложнее случайно залить то, что лить не хотели.
Работаю над куском кода, который опирается на json файл. Файл этот длиной 7к+ строк (только не спрашивайте, как так вышло). И кусок этот одновременно
- сложный
- используется большим количеством пользователей.
И тут из мастера в этот json прилетают изменения. В которых во всем файле пробелы заменены на табы. То есть, дифф слишком большой и отсмотреть глазами не получается. Поправлю, конечно, но досадно -- и небезопасно.
В связи с чем напоминаю, как коммитить (вот, например, хорошая статья от другого автора)
Для меня главное в этом тексте:
Один коммит = одна атомарная задача
И от себя бы еще добавила:
Дифф удобно смотреть ривьюеру
Чтобы этого добиться, перед каждым коммитом нужно отсматривать изменения в каждом файле, которые хотите залить в репо: с помощью git diff или в IDE.
И еще я никогда не делаю
git add .
чтобы даже не привыкать добавлять всю работу, которая у меня есть локально: в ней бывают мусор и наброски кода. Если себя приучить к этому, то гораздо сложнее случайно залить то, что лить не хотели.
Одна особенность filter
Синтаксис встроенной функции filter такой:
filter(function, iterable).
Эта функция фильтрует значения переданной последовательности с помощью функции function. Если function получает очередной элемент последовательности и возвращает True, то элемент попадает в результат работы filter, иначе нет.
Например, таким способом можно отфильтровать только строки, состоящие из чисел:
>>> strings = ['two', 'list', '', 'dict', '100', '1', '50']
>>> list(filter(str.isdigit, strings))
['100', '1', '50']
Или только четные значения:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
list(filter(lambda x: x % 2 == 0, numbers))
Часто в качестве фильтров используют лямбда-функции или член-функции классов.
А еще (внезапно) вместо функции можно использовать None:
>>> random = [1, 'a', 0, False, True, '0', '']
>>> list(filter(None, random))
[1, 'a', True, '0']
И тогда filter вернет только truthy значения.
Синтаксис встроенной функции filter такой:
filter(function, iterable).
Эта функция фильтрует значения переданной последовательности с помощью функции function. Если function получает очередной элемент последовательности и возвращает True, то элемент попадает в результат работы filter, иначе нет.
Например, таким способом можно отфильтровать только строки, состоящие из чисел:
>>> strings = ['two', 'list', '', 'dict', '100', '1', '50']
>>> list(filter(str.isdigit, strings))
['100', '1', '50']
Или только четные значения:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
list(filter(lambda x: x % 2 == 0, numbers))
Часто в качестве фильтров используют лямбда-функции или член-функции классов.
А еще (внезапно) вместо функции можно использовать None:
>>> random = [1, 'a', 0, False, True, '0', '']
>>> list(filter(None, random))
[1, 'a', True, '0']
И тогда filter вернет только truthy значения.
👍3
Чем мутабельные объекты отличаются от иммутабельных?
В python объекты бывают мутабельные и иммутабельные. Значние иммутабельного объектра нельзя изменить после того, как он был создан, а значение мутабельного можно.
Рассмотрим на примере. В Python есть встроенная функция
>>>beatles[0] = “Pete”
>>>beatles
[“Pete”, “Paul”, “John”, “George”]
>>>id(beatles)
140181376779336
>>>bass = "Paul"
>>>id(bass)
140068833595776
>>>bass += " McCartney"
>>>bass
Paul McCartney
>>>id(bass)
140068833600432
Почему так? Строка иммутабельна, то есть, после того, как она уже создана, нельзя изменить ее содержимое. Поэтому интерпретатор перезаписывает ее по новому адресу с новыми символами. А список мутабелен, поэтому его адрес не меняется оттого, что мы заменили один элемент.
Вот какие встроенные типы есть в python:
Класс Описание Мутабелен?
---------------------------------
bool булевый тип ☹️
int целочисленный тип ☹️
float число с плавающей запятой ☹️
list список ☺️
tuple кортеж ☹️
str строка ☹️
set множество ☺️
dict словарь ☺️
Важно отличать мутабельность от иммутабельности, так как некоторые типы данных, например, dict или set, реализованы как хэш-таблицы. Когда вы добавляете элемент в множество, интерпретатор берет хэш от этого элемента и размещает значение по адресу, который совпадает с хэшом. Если мы попытаемся положить список в множество, то получим TypeError, потому что хэш бы изменился, когда изменился бы один из элементов списка:
🐍
В python объекты бывают мутабельные и иммутабельные. Значние иммутабельного объектра нельзя изменить после того, как он был создан, а значение мутабельного можно.
Рассмотрим на примере. В Python есть встроенная функция
id(). В реализации CPython она возвращает адрес, по которому объект находится в памяти. Создадим список и посмотрим, в ячейке с каким номером он окажется. beatles = [“Ringo”, “Paul”, “John”, “George”]
id(beatles)
140181376779336
Видим, что список лежит по адресу 140181376779336. Теперь изменим какой-нибудь элемент списка и посмотрим, изменился ли адрес.>>>beatles[0] = “Pete”
>>>beatles
[“Pete”, “Paul”, “John”, “George”]
>>>id(beatles)
140181376779336
Видим, что адрес списка не меняется, если мы изменяем элементы, которые в него входят. Теперь создадим в памяти строку >>>bass = "Paul"
>>>id(bass)
140068833595776
и добавим в нее что-нибудь>>>bass += " McCartney"
>>>bass
Paul McCartney
>>>id(bass)
140068833600432
Видим, что адрес строки изменился. Почему так? Строка иммутабельна, то есть, после того, как она уже создана, нельзя изменить ее содержимое. Поэтому интерпретатор перезаписывает ее по новому адресу с новыми символами. А список мутабелен, поэтому его адрес не меняется оттого, что мы заменили один элемент.
Вот какие встроенные типы есть в python:
Класс Описание Мутабелен?
---------------------------------
bool булевый тип ☹️
int целочисленный тип ☹️
float число с плавающей запятой ☹️
list список ☺️
tuple кортеж ☹️
str строка ☹️
set множество ☺️
dict словарь ☺️
Количество встроенных иммутабельных типов в python больше, но если вы захотите создать свой класс, он будет мутабельным. Важно отличать мутабельность от иммутабельности, так как некоторые типы данных, например, dict или set, реализованы как хэш-таблицы. Когда вы добавляете элемент в множество, интерпретатор берет хэш от этого элемента и размещает значение по адресу, который совпадает с хэшом. Если мы попытаемся положить список в множество, то получим TypeError, потому что хэш бы изменился, когда изменился бы один из элементов списка:
TypeError: unhashable type: 'list'
Поэтому мутабельные объекты, такие как словари или списки, не могут быть ключами в словаре или элементами множества. А вот строки или числа могут. И это на самом деле очень естественноPlease open Telegram to view this post
VIEW IN TELEGRAM
Параметры map
А вы знали, что у функции map три параметра?
Функция map используется для того, чтобы делать однотипные операции над наборами данных. Например, с ее помощью удобно приводить типы данных:
>>> chr_nums = ['1', '2', '3', '4', '5']
>>> list(map(int, chr_nums))
[1, 2, 3, 4, 5]
или округлять значения:
>>> floats = [2.2865, 3.6420, 6.6418, 8.7231, 3.1528]
>>> list(map(round, floats))
[2, 4, 7, 9, 3]
Но у map есть еще третий параметр, который используют, чтобы передать аргументы в обрабатывающую функцию.
Например, если мы хотим возвести все числа в квадрат, то двойки в аргументы можно передать как список:
>>> list(map(pow, range(10), [2]*10))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
То же с округлением:
>>> floats = [2.2865, 3.6420, 6.6418, 8.7231, 3.1528]
>>> list(map(round, floats, [3]*len(floats)))
[2.287, 3.642, 6.642, 8.723, 3.153]
Либо так:
>>> from itertools import repeat
>>> list(map(round, floats, repeat(1)))
[2.3, 3.6, 6.6, 8.7, 3.2]
repeat бесконечно повторяет нужную константу, а map ориентируется на самую короткую из введенных последовательностей. Получается ровно столько параметров для map, сколько нужно.
Больше в доке: map, repeat.
А вы знали, что у функции map три параметра?
Функция map используется для того, чтобы делать однотипные операции над наборами данных. Например, с ее помощью удобно приводить типы данных:
>>> chr_nums = ['1', '2', '3', '4', '5']
>>> list(map(int, chr_nums))
[1, 2, 3, 4, 5]
или округлять значения:
>>> floats = [2.2865, 3.6420, 6.6418, 8.7231, 3.1528]
>>> list(map(round, floats))
[2, 4, 7, 9, 3]
Но у map есть еще третий параметр, который используют, чтобы передать аргументы в обрабатывающую функцию.
Например, если мы хотим возвести все числа в квадрат, то двойки в аргументы можно передать как список:
>>> list(map(pow, range(10), [2]*10))
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
То же с округлением:
>>> floats = [2.2865, 3.6420, 6.6418, 8.7231, 3.1528]
>>> list(map(round, floats, [3]*len(floats)))
[2.287, 3.642, 6.642, 8.723, 3.153]
Либо так:
>>> from itertools import repeat
>>> list(map(round, floats, repeat(1)))
[2.3, 3.6, 6.6, 8.7, 3.2]
repeat бесконечно повторяет нужную константу, а map ориентируется на самую короткую из введенных последовательностей. Получается ровно столько параметров для map, сколько нужно.
Больше в доке: map, repeat.
Гид по множественному присваиванию
Уверена, что множественное присваивание в Python видели все. Выглядит оно вот так:
В общем, множественное присваивание можно использовать не только с кортежами, но и списками, строками и любыми iterable типами. Его плюс в том, что оно позволяет не использовать индексы, а значит, уменьшает склонность к ошибкам и делает код читаемее. А еще оно используется в <<args, kwargs>> синтаксисе, который часто смущает начинающих, но об этом в следующий раз. 🐠
Уверена, что множественное присваивание в Python видели все. Выглядит оно вот так:
>>> x, y, z = 1, 2, 3
>>> x
1
>>> z
3
На самом деле на низком уровне создается tuple и в цикле инициализируется значениями 1, 2, 3. Поэтому с тем же успехом можно не опускать скобки и написать вот так:
>>> (x, y, z) = (1, 2, 3)
>>> x
1
>>> z
3
И это тоже сработает. Поэтому множественное присваивание еще называют tuple unpacking. Да и чаще всего его, кажется, используют именно с кортежами. Но вообще так можно писать с любыми iterable типами:
>>> x, y = [1, 2]
>>> y
2
И со строками тоже:
>>> x, y = 'hi'
>>> x
'h'
>>> y
'i'
Множественное присваивание, если применять его с оператором *, позволяет делать еще вот такие крутые штуки:
>>> numbers = [1, 2, 3, 4, 5, 6]
>>> first, *rest = numbers
>>> first
1
>>> rest
[2, 3, 4, 5, 6]
По сути * здесь заменяет слайсы. С тем же успехом можно было написать:
>>> first, last = numbers[0], numbers[1:]
>>> first
1
>>> last
[2, 3, 4, 5, 6]
Но через распаковку получается читаемее. Можно даже <<вырезать>> середину:
>>> first, *middle, last = numbers
И если вам не нужны все промежуточные индексы, то хороший тон это вообще использовать нижнее подчеркивание:
>>> first, *_, last = [1, 2, 3, 4, 5, 6]
Кстати, если речь о вложенных объектах, то обратите внимание, что копия будет глубокой:
>>> color, point = ('blue', (0, 0, 255))
>>> color
'blue'
>>> point
(0, 0, 255)
В некоторых случаях это очень удобно. В общем, множественное присваивание можно использовать не только с кортежами, но и списками, строками и любыми iterable типами. Его плюс в том, что оно позволяет не использовать индексы, а значит, уменьшает склонность к ошибкам и делает код читаемее. А еще оно используется в <<args, kwargs>> синтаксисе, который часто смущает начинающих, но об этом в следующий раз. 🐠
🥰2
Какова сложность операции добавления элемента в список?
Чтобы ответить на этот вопрос, нужно разобраться, как списки устроены на низком уровне. Определение списка в исходниках CPython выглядит так:
Во-первых, отсюда сразу видно, что получить длину массива можно очень быстро, за O(1), потому что не нужно пересчитывать все элементы.
В общем случае, сложность добавления элемента в список зависит от того, в середину массива мы добавляем элемент или в конец. Добавить элемент в начало или середину списка это O(n), но операция
Чтобы ответить на этот вопрос, нужно разобраться, как списки устроены на низком уровне. Определение списка в исходниках CPython выглядит так:
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item;
Py_ssize_t allocated;
} PyListObject;
Здесь ob_item -- это непрерывный массив указателей на элементы списка, а allocated содержит длину массива. Вот и все, что нужно знать, чтобы отвечать на вопросы о сложности операций со списками. Во-первых, отсюда сразу видно, что получить длину массива можно очень быстро, за O(1), потому что не нужно пересчитывать все элементы.
len(a) # O(1)
Ещё сразу понятно, что при такой реализации стоимость индексирования не зависит от длины списка или значения индекса, так как изменить значение по известному номеру ячейки тоже можно тоже за O(1). a[10] = 5 # O(1)
А вот чтобы найти значение в списке, придется обходить каждую ссылку и проверять содержимое на равенство. Поэтому проверить вхождение -- операция дорогая, в худшем случае O(n). 5 in a # O(n)
Добавление одиночных элементов в конец списка (метод append()) в большинстве случаев работает за O(1). Но здесь есть одна деталь: место в памяти для массива указателей аллоцируется заранее. И когда занятый объем памяти истощается, интерпретатор выделяет ещё примерно 15% от объема массива. Эта периодическая экспансия время от времени несколько замедляет добавление новых элементов в список, поэтому об O(1) можно говорить условно. a.append(5) # ~O(1)
Еще одна распространенная потребность это конкатенировать списки, например, с помощью оператора +. Сложность конкатенации линейно зависит от длины присоединяемого списка: если его длина 100 элементов, то нужно 100 раз добавить в конец новый элемент. a + b # ~O(k)
Чтобы получить доступ к элементам слайса a[i:j], нужно итерироваться между индексами i и j. Поэтому сложность такой операции O(k), где k -- длина слайса. А вот удалить слайс это O(n), из-за необходимости сдвинуть все следующие за удаленным участком элементы на новые места, здесь n -- длина всего массива.a[i:j] # ~O(k)
del a[i:j] # ~O(n)
Метод, который получает и удаляет последний элемента списка pop(), имеет сложность O(1), в то время как pop(i) элемента из середины списка требует O(n). Почему так? Потому что после того, как один указатель был удален из середины или начала, нужно передвинуть все следующие за ним на 1 позицию вперёд. А когда элемент был удален из конца массива, этого делать не нужно. По этой же причине вычленение по индексу(pop) или вставка элемента в середину списка тоже дорогая операция и требует O(n) операций.
a.pop() #O(1)
a.pop(i) #O(n)
a.insert(i, item) #O(n)
Обобщим:Операция Сложность
----------------------
len(a) O(1)
a[i] O(1)
a[10] = 5 O(1)
5 in a O(n)
a.append(5) ~O(1)
a + b O(k)
a[i:j] O(k)
del a[i:j] O(n)
a.pop() O(1)
a.pop(i) O(n)
a.insert(i, item) O(n)
В общем, чтобы отвечать на вопросы о сложности операций со списками, нужно понимать, как они устроены на низком уровне. Самые дорогие операции -- те, которые требуют обхода части или всего списка, например, поиск значения в списке, конкатенация, слайсинг, удаление или добавление элементов в начало списка. Самые экономные операции -- получение длины списка, операции с элементами в конце списка и со значениями по известному индексу. В общем случае, сложность добавления элемента в список зависит от того, в середину массива мы добавляем элемент или в конец. Добавить элемент в начало или середину списка это O(n), но операция
append() требует только O(1)🐍👍1🔥1
Что такое временная сложность алгоритма?
Часть 1.
Когда в детстве меня учили умножать числа, мне говорили, что смысл умножения в том, чтобы короче записать сумму. Например, 4 * 3 это то же, что 4 + 4 + 4.
Сведение умножения к сумме -- самый простой, наивный алгоритм умножения. А теперь я возьму мой рабочий ноут и попробую перемножить этим способом какие-нибудь большие числа, например, 4 * 10000000000:
Как понять, быстро ли работает алгоритм? Можно попробовать измерить время напрямую:
Чтобы уйти от физических свойств вычислителя, используют понятие сложности. Асимптотическая сложность алгоритма -- это то, как изменяется время исполнения в зависимости от объема входных данных. В этом определении намеренно не учитывается то, на каком устройте выполняется алгоритм: это может быть компьютер, калькулятор или даже человек. Такой абстрактный вычислитель называют машиной Тьюринга.
При этом разделяют сложность алгоритма в худшем и лучшем случае. Представьте, что мы хотим проверить, есть ли в списке число 42. Мы проверяем первый элемент на равенство 42, второй, третий… В лучшем случае первый элемент окажется искомым. В худшем -- последний. Поэтому обычно рассматривают сложность алгоритма в наилучшем, наихудшем случае, и в среднем. Как правило, пессимистическая оценка самая интересная из всех, потому что позволяет планировать объем ресурсов.
Временную сложность алгоритма в худшем случае выражают с использованием нотации «O» большое. В этом случае из рассмотрения исключают члены меньшего порядка и говорят об изменении используемых ресурсов при стремлении размера входа к бесконечности. Например, если время, которое нужно алгоритму для выполнения работы, для всех входов длины n не превосходит 5n^3 + 3n, то асимптотическая временная сложность равна O(n^3). 🤓
Часть 1.
Когда в детстве меня учили умножать числа, мне говорили, что смысл умножения в том, чтобы короче записать сумму. Например, 4 * 3 это то же, что 4 + 4 + 4.
Сведение умножения к сумме -- самый простой, наивный алгоритм умножения. А теперь я возьму мой рабочий ноут и попробую перемножить этим способом какие-нибудь большие числа, например, 4 * 10000000000:
sum = 0
for i in range(0, 10000000000):
sum += 4
print(sum)
Если я попробую посчитать то же самое на калькуляторе, то замечу, что лаптоп отрабатывает заметно медленнее, несмотря на то, что он мощнее. Почему? Потому что для умножения чисел существует несколько алгоритмов, а мы выбрали самый неэффективный из них. Ещё есть, например, метод Карацубы, или алгоритм на основе преобразования Фурье. В калькуляторах обычно используется второй, и он требует значительно меньше операций, чем наивный.Как понять, быстро ли работает алгоритм? Можно попробовать измерить время напрямую:
import time
start = time.time()
my_awesome_alg()
end = time.time()
print(end - start)
Но мы обнаружим, что скорость зависит от производительности машины, от ее архитектуры и загруженности в момент выполнения программы. Поэтому физическое время никак не поможет нам оценить эффективность алгоритма.Чтобы уйти от физических свойств вычислителя, используют понятие сложности. Асимптотическая сложность алгоритма -- это то, как изменяется время исполнения в зависимости от объема входных данных. В этом определении намеренно не учитывается то, на каком устройте выполняется алгоритм: это может быть компьютер, калькулятор или даже человек. Такой абстрактный вычислитель называют машиной Тьюринга.
При этом разделяют сложность алгоритма в худшем и лучшем случае. Представьте, что мы хотим проверить, есть ли в списке число 42. Мы проверяем первый элемент на равенство 42, второй, третий… В лучшем случае первый элемент окажется искомым. В худшем -- последний. Поэтому обычно рассматривают сложность алгоритма в наилучшем, наихудшем случае, и в среднем. Как правило, пессимистическая оценка самая интересная из всех, потому что позволяет планировать объем ресурсов.
Временную сложность алгоритма в худшем случае выражают с использованием нотации «O» большое. В этом случае из рассмотрения исключают члены меньшего порядка и говорят об изменении используемых ресурсов при стремлении размера входа к бесконечности. Например, если время, которое нужно алгоритму для выполнения работы, для всех входов длины n не превосходит 5n^3 + 3n, то асимптотическая временная сложность равна O(n^3). 🤓
👍2🔥1
Что такое временная сложность алгоритма?
Часть 2.
Рассмотрим несколько примеров.
O(1)
Время, которое не зависит от объема входных данных, или постоянное время. За постоянное время можно получить элемент массива по индексу или добавить элемент в связный список.
O(n)
Алгоритм, в котором мы идём по списку, чтобы узнать, есть ли в нем значение 42, в худшем случае требует O(n) операций, где n -- длина списка. Такую же сложность имеет сравнение строк, потому что по сути оно тоже требуют обхода всей строки.
O(log n)
Проверить, есть ли значение 42 в списке, можно быстрее, чем за O(n), если список отсортирован. Тогда можно проверить на равенство элемент из середины списка. Если он равен 42, то останавливаемся. Если больше -- значит, слева можно не искать, там значения только меньше. Будем продолжать проверку, выбирая каждый раз элемент из середины оставшегося отрезка. Этот алгоритм называют бинарным поиском и он имеет логарифмическую сложность, потому что количество вариантов уменьшается каждый раз на 2, как функция, обратная степенной (она же логарифм).
O(n log n)
Можно показать, что большинство алгоритмов сортировки имеют сложность n log(n). За время n log(n) работают сортировка слиянием, кучей, и быстрая сортировка. Еще есть теорема, которая говорит, что если сортировка основана на сравнении элементов, то быстрее, чем за n log(n) отсортировать элементы не получится.
O(n^2)
За время n^2 работает обход двумерного массива, это можно представить себе как обход таблицы n * n. И ещё за это же время работают некоторые не очень эффективные по времени алгоритмы сортировки, например, сортировка пузырьком
Для разработчика важно иметь интуицию насчет временной сложности алгоритмов, потому что за счет неэффективности вычислений можно загубить производительность самого мощного железа, как это случилось, когда калькулятор побил мой мощный ноутбук. Поэтому этой науке уделяется большое внимание в разработке. 95%, что на собеседовании вам предложат алгоритмическую задачу и попросят оценить время выполнения вашего решения.
Если вы хотите углубиться в теорию алгоритмов, то я советую специализацию Алгоритмы и структуры данных на Курсере. Теория здесь изложена доступно и в курсе есть задачи, чтобы набить руку. А еще есть Стенфордский учебник по теории сложности, он написан замечательным языком и в нем прекрасные примеры. 🎓
Часть 2.
Рассмотрим несколько примеров.
O(1)
Время, которое не зависит от объема входных данных, или постоянное время. За постоянное время можно получить элемент массива по индексу или добавить элемент в связный список.
O(n)
Алгоритм, в котором мы идём по списку, чтобы узнать, есть ли в нем значение 42, в худшем случае требует O(n) операций, где n -- длина списка. Такую же сложность имеет сравнение строк, потому что по сути оно тоже требуют обхода всей строки.
O(log n)
Проверить, есть ли значение 42 в списке, можно быстрее, чем за O(n), если список отсортирован. Тогда можно проверить на равенство элемент из середины списка. Если он равен 42, то останавливаемся. Если больше -- значит, слева можно не искать, там значения только меньше. Будем продолжать проверку, выбирая каждый раз элемент из середины оставшегося отрезка. Этот алгоритм называют бинарным поиском и он имеет логарифмическую сложность, потому что количество вариантов уменьшается каждый раз на 2, как функция, обратная степенной (она же логарифм).
O(n log n)
Можно показать, что большинство алгоритмов сортировки имеют сложность n log(n). За время n log(n) работают сортировка слиянием, кучей, и быстрая сортировка. Еще есть теорема, которая говорит, что если сортировка основана на сравнении элементов, то быстрее, чем за n log(n) отсортировать элементы не получится.
O(n^2)
За время n^2 работает обход двумерного массива, это можно представить себе как обход таблицы n * n. И ещё за это же время работают некоторые не очень эффективные по времени алгоритмы сортировки, например, сортировка пузырьком
Для разработчика важно иметь интуицию насчет временной сложности алгоритмов, потому что за счет неэффективности вычислений можно загубить производительность самого мощного железа, как это случилось, когда калькулятор побил мой мощный ноутбук. Поэтому этой науке уделяется большое внимание в разработке. 95%, что на собеседовании вам предложат алгоритмическую задачу и попросят оценить время выполнения вашего решения.
Если вы хотите углубиться в теорию алгоритмов, то я советую специализацию Алгоритмы и структуры данных на Курсере. Теория здесь изложена доступно и в курсе есть задачи, чтобы набить руку. А еще есть Стенфордский учебник по теории сложности, он написан замечательным языком и в нем прекрасные примеры. 🎓
Coursera
Data Structures and Algorithms
Offered by University of California San Diego. Master ... Enroll for free.
👍2🔥1
Чем == отличается от is?
Рассмотрим на примере. Создадим словарь и скопируем ссылку на него:
Кстати, чтобы проверить, есть ли вообще у переменной значение, обычно пользуются оператором
выстрелить себе в ногу получить интересные результаты. Поэтому в PEP8 и рекомендуют проверять на значение
== проверяет, одинаковые ли значения у переменных.is проверяет, указывают ли переменные на один и тот же объект.Рассмотрим на примере. Создадим словарь и скопируем ссылку на него:
a = [1, 2, 3]Теперь создадим новый словарь с помощью копирования:
b = a
b is a # True
b == a # True
b = a[:]Но из-за того, что мы скопировали в новый объект все значения старого, то содержимое двух словарей все равно совпадает:
b is a # False
>>> b == a # TrueТо есть,
a is b по сути то же, что id(a) == id(b). Поэтому если a is b вернет True, это будет значить, что и содержимое у них одинаковое. Просто потому что это один и тот же объект.Кстати, чтобы проверить, есть ли вообще у переменной значение, обычно пользуются оператором
is, а не ==:a = 42Дело в том, что класс
a is None # False
None реализован как синглтон. Это значит, что экземпляр этого класса существует в интерпретаторе в единственном числе и все неопределенные значения указывают на него. a = NoneДля некоторых классов оператор
b = None
a is b # True
== тоже сработает, но так лучше не делать, потому что в пользовательских классах этот оператор можно переопределить и None именно с помощью is. 🐍👍3🔥1
Пять способов создать словарь
Словарь -- структура, которая позволяет хранить данные в формате ключ-значение. Словари удобны для того, чтобы передавать информацию внутри программы, они быстрые, легко конвертируются в формат JSON, который используется в http запросах, и поэтому являются одним из главных инструментов разработчика.
Я знаю пять (!) способов создать словарь в Python. С помощью литералов словаря:
И последний, который я использую, когда мне надо взять ключи из одного контейнера, а значения из другого:
Словарь -- структура, которая позволяет хранить данные в формате ключ-значение. Словари удобны для того, чтобы передавать информацию внутри программы, они быстрые, легко конвертируются в формат JSON, который используется в http запросах, и поэтому являются одним из главных инструментов разработчика.
Я знаю пять (!) способов создать словарь в Python. С помощью литералов словаря:
fish = {
"move": "water",
"eat": "insects",
"say": None
}
Используя конструктор явно: snail = dict(
eat=”leaves”,
move=”ground”,
say=None
)
Или инициализируя его кортежами: cow = dict([
(“move”, “ground”),
(“eat”, “grass”),
(“say, “moo”)
])
Четвертый, с помощью генераторных выражений (версия интерпретатора 3.5 и выше):>>> animals = [“snail”, “fish”, “cow”]
>>> {animal: it for it, animal in enumerate(animals)}
{'snail': 0, 'fish': 1, 'cow': 2}
Этот трюк еще бывает полезен, если нужно поменять местами ключи и значения:>>> {v: k for k, v in animals.items()}
{1: 'snail', 2: 'fish', 3: 'cow’}
Только будьте осторожны, потому что ключи в словаре должны быть уникальны. Если какие-то значения исходного словаря совпадали, то, когда они станут ключами нового, дубликаты исчезнут. И последний, который я использую, когда мне надо взять ключи из одного контейнера, а значения из другого:
>>> animals = ["frog", "snail", "bird"]
>>> numbers = [1, 2, 3]
>>> dict(zip(animals, numbers))
{'snail': 2, 'frog': 1, 'bird': 3}
Почему так много? Потому что каждый удобен под свой случай.🐍🔥2👍1
Грамотно ставим запятые
Допустим, у нас в коде есть список:
Плюсы запятой в конце коллекции:
🤖один и тот же формат всех строк
🤓меньше ошибок по невнимательности
🐙удобно ставить под контроль версий
Допустим, у нас в коде есть список:
>>> capitals = ['Beijing', 'New Delhi']
Добавим в него ещё город и сделаем diff двух файлов перед коммитом. >>> capitals = ['Beijing', 'New Delhi', 'Mexico City']
Инструмент подсветит строчку и будет сложнее понять, какой именно элемент добавили, особенно если список длинный. Поэтому лучше писать: >>> capitals = [
'Beijing',
'New Delhi'
'Mexico City'
]
Так на diff'е будет видно, какой именно элемент появился, и делать ревью тоже будет проще. Кстати, вы заметили, что я забыла запятую? Это частая ошибка, и довольно неприятная, потому что при этом не только не будет брошено исключение, но и объединятся две последние строки: >>> capitals
['Beijing', 'New DelhiMexico City']
Упс😕. Строки, не разделенные запятой, в Python конкатенируются. Это удобно для того, чтобы размещать в коде длинные тексты без переноса строки: >>> why_so = ('Это очень очень '
'очень длинная строка или '
'документ на вашем сайте.')
>>> why_so
'Это очень очень очень длинная строка или документ на вашем сайте.'
Но иногда это играет с разработчиками злую шутку. Поэтому, чтобы не забывать добавлять запятую в предпоследнем элементе, хорошей практикой считается всегда ставить запятую в конце коллекции:>>> capitals = [
'Beijing',
'New Delhi',
'Mexico City',
]
Она не повлияет на выполнение программы, зато уменьшит вероятность ошибки программиста. При этом diff будет читаться лучше, потому что утилита подсветит только новую строку. Плюсы запятой в конце коллекции:
🤖один и тот же формат всех строк
🤓меньше ошибок по невнимательности
🐙удобно ставить под контроль версий
👍2🔥2❤🔥1