В сети много подобных статей, но я просто подумал, что могу добавить свое видение темы и сделать список «хитростей» более полным. Используемые здесь фрагменты кода каким-то образом имеют решающее значение для моего рабочего процесса, и я использую их снова и снова.

Наборы

Часто разработчики забывают, что в Python установлен тип данных, и пытаются использовать списки для всего. Что установлено? Ну короче:

Набор - это неупорядоченная коллекция без повторяющихся элементов.

Если вы познакомитесь с наборами и их логикой, это может решить за вас множество проблем. Например - как получить все уникальные буквы, используемые в слове?

myword = "NanananaBatman"
set(myword)
{'N', 'm', 'n', 'B', 'a', 't'}

Бум. Проблема решена, но, честно говоря, это из официальной документации Python, так что ничего удивительного.

Как насчет этого? Получить список элементов без повторения элементов?

# first you can easily change set to list and other way around
mylist = ["a", "b", "c", "c"]
# let's make a set out of it
myset = set(mylist)
# myset will be:
{'a', 'b', 'c'}
# and, it's already iterable so you can do:
for element in myset:
    print(element)
# but you can also convert it to list again:
mynewlist = list(myset)
# and mynewlist will be:
['a', 'b', 'c']

Как видите, повторяющаяся буква «c» больше не актуальна. Вы должны знать только одно: порядок элементов между mylist и mynewlist может быть разным:

mylist = ["c", "c", "a", "b"]
mynewlist = list(set(mylist))
# mynewlist is:
['a', 'b', 'c']
# as you can see it's different order;

Но! Мы можем пойти глубже.

Представьте себе случай, когда у вас есть отношение «один ко многим» между некоторыми объектами, чтобы сделать его более конкретным - пользователь и разрешения; обычно бывает так, что один пользователь может иметь несколько разрешений. Теперь представьте, что кто-то хочет изменить несколько разрешений - добавить некоторые и удалить некоторые одновременно, как решить эту проблему?

# this is the set of permissions before change;
original_permission_set = {"is_admin", "can_post_entry", "can_edit_entry", "can_view_settings"}
# this is new set of permissions;
new_permission_set = {"can_edit_settings", "is_member", "can_view_entry", "can_edit_entry"}
# now permissions to add will be:
new_permission_set.difference(original_permission_set)
# which will result:
{'can_edit_settings', 'can_view_entry', 'is_member'}
# As you can see can_edit_entry is in both sets; so we do not need 
# to worry about handling it
# now permissions to remove will be:
original_permission_set.difference(new_permission_set)
# which will result:
{'is_admin', 'can_view_settings', 'can_post_entry'}
# and basically it's also true; we switched admin to member, and add
# more permission on settings; and removed the post_entry permission

Короче говоря - не бойтесь наборов, поскольку они могут решить вам много проблем, подробнее о наборах вы можете найти в официальных документах Python.

Календарь веселья

Очень часто, когда вы разрабатываете что-то, что сильно зависит от дат и периодов времени, таких как месяцы или недели, вас интересует некоторая информация, например, какой последний день месяца в данном году. Это кажется простым, но поверьте мне, что правильная обработка даты и времени - чрезвычайно сложная тема, и я хотел бы сказать, что реализация календаря имеет очень плохую подготовку, и это кошмар, являющийся причиной множества крайних случаев.

Итак - как найти последний день месяца?

import calendar
calendar.monthrange(2020, 12)
# will result:
(1, 31)
# BUT! you need to be careful here, why? Let's read the documentation:
help(calendar.monthrange)
# Help on function monthrange in module calendar:
# monthrange(year, month)
#     Return weekday (0-6 ~ Mon-Sun) and number of days (28-31) for
#    year, month.
# As you can see the first value returned in tuple is a weekday, 
# not the number of the first day for a given month; let's try 
# to get the same for 2021
calendar.monthrange(2021, 12)
(2, 31)
# So this basically means that the first day of December 2021 is Wed
# and the last day of December 2021 is 31 (which is obvious, cause
# December always has 31 days)
# let's play with February
calendar.monthrange(2021, 2)
(0, 28)
calendar.monthrange(2022, 2)
(1, 28)
calendar.monthrange(2023, 2)
(2, 28)
calendar.monthrange(2024, 2)
(3, 29)
calendar.monthrange(2025, 2)
(5, 28)
# as you can see it handled nicely the leap year;

Что касается начала месяца, это действительно просто - он всегда начинается с 1-го :)

Как вы можете использовать информацию о том, что месяц начинается в определенный день недели? Вы легко можете определить день недели для любого дня:

calendar.monthrange(2024, 2)
(3, 29)
# means that February 2024 starts on Thursday
# let's define simple helper:
weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
# now we can do something like:
weekdays[3]
# will result in:
'Thursday'
# now simple math to tell what day is 15th of February 2020:
offset = 3  # it's the first value from monthrange
for day in range(1, 29):
    print(day, weekdays[(day + offset - 1) % 7])
1 Thursday
2 Friday
3 Saturday
4 Sunday
...
18 Sunday
19 Monday
20 Tuesday
21 Wednesday
22 Thursday
23 Friday
24 Saturday
...
28 Wednesday
29 Thursday
# which basically makes sense;

Возможно, это не супер-готовый пример, поскольку будний день можно легко найти с помощью модуля datetime:

from datetime import datetime
mydate = datetime(2024, 2, 15)
datetime.weekday(mydate)
# will result:
3
# or:
datetime.strftime(mydate, "%A")
'Thursday'

В любом случае, в модуле календаря определенно есть развлечения, о чем полезно знать:

# checking if year is leap:
calendar.isleap(2021)  # False
calendar.isleap(2024)  # True
# or checking how many days will be leap days for given year span:
calendar.leapdays(2021, 2026)  # 1
calendar.leapdays(2020, 2026)  # 2
# read the help here, as range is: [y1, y2), meaning that second
# year is not included;
calendar.leapdays(2020, 2024)  # 1

Enumerate имеет второй аргумент

Да, есть второй аргумент. Забавно, но некоторые действительно опытные разработчики не знают; Вы должны проверить примеры:

mylist = ['a', 'b', 'd', 'c', 'g', 'e']
for i, item in enumerate(mylist):
    print(i, item)
# Will give:
0 a
1 b
2 d
3 c
4 g
5 e
# but, you can add a start for enumeration:
for i, item in enumerate(mylist, 16):
    print(i, item)
# and now you will get:
16 a
17 b
18 d
19 c
20 g
21 e

Это простая начальная точка, с которой должно начинаться перечисление. Это может помочь вам в ситуациях, когда вы работаете над какой-то логикой смещений.

Обработка логики if-else

Часто бывает, что вам нужно обрабатывать много разной логики в зависимости от условия, неопытный разработчик кончает примерно так:

OPEN = 1
IN_PROGRESS = 2
CLOSED = 3
def handle_open_status():
    print('Handling open status')
def handle_in_progress_status():
    print('Handling in progress status')
def handle_closed_status():
    print('Handling closed status')
def handle_status_change(status):
    if status == OPEN:
        handle_open_status()
    elif status == IN_PROGRESS:
        handle_in_progress_status()
    elif status == CLOSED:
        handle_closed_status()
handle_status_change(1)  # Handling open status
handle_status_change(2)  # Handling in progress status
handle_status_change(3)  # Handling closed status

Выглядит неплохо, но я видел кодовые базы с 20 и более условиями, подобными этому.

Как тогда поступать?

from enum import IntEnum
class StatusE(IntEnum):
    OPEN = 1
    IN_PROGRESS = 2
    CLOSED = 3
def handle_open_status():
    print('Handling open status')
def handle_in_progress_status():
    print('Handling in progress status')
def handle_closed_status():
    print('Handling closed status')
handlers = {
    StatusE.OPEN.value: handle_open_status,
    StatusE.IN_PROGRESS.value: handle_in_progress_status,
    StatusE.CLOSED.value: handle_closed_status
}
def handle_status_change(status):
    if status not in handlers:
         raise Exception(f'No handler found for status: {status}')
    handler = handlers[status]
    handler()
handle_status_change(StatusE.OPEN.value)  # Handling open status
handle_status_change(StatusE.IN_PROGRESS.value)  # Handling in progress status
handle_status_change(StatusE.CLOSED.value)  # Handling closed status
handle_status_change(4)  # Will raise the exception

Это общий шаблон, который можно использовать в python, как правило, он делает код немного чище - особенно там, где ваш основной метод обработки огромен и требует обработки множества условий.

Модуль Enum

Я немного поцарапал тему в предыдущем абзаце, давайте копнем глубже.

Модуль enum предоставляет утилиты для обработки перечислений, самые интересные из них: Enum, IntEnum - давайте немного покопаемся:

from enum import Enum, IntEnum, Flag, IntFlag
class MyEnum(Enum):
    FIRST = "first"
    SECOND = "second"
    THIRD = "third"

class MyIntEnum(IntEnum):
    ONE = 1
    TWO = 2
    THREE = 3
# Now we can do things like:
MyEnum.FIRST  # <MyEnum.FIRST: 'first'>
# it has value and name attributes, which are handy:
MyEnum.FIRST.value  # 'first'
MyEnum.FIRST.name  # 'FIRST'
# additionally we can do things like:
MyEnum('first')  # <MyEnum.FIRST: 'first'>, get enum by value
MyEnum['FIRST']  # <MyEnum.FIRST: 'first'>, get enum by name

С IntEnum он похож, но есть некоторые отличия:

MyEnum.FIRST == "first"  # False
# but
MyIntEnum.ONE == 1  # True
# to make first example to work:
MyEnum.FIRST.value == "first"  # True

В кодовых базах среднего размера enum модуль чрезвычайно полезен для управления вашими константами в проекте.

Локализация enum может быть немного сложной, но это выполнимо, позвольте мне быстро показать вам, как я справляюсь с этим в django:

from enum import Enum
from django.utils.translation import gettext_lazy as _
class MyEnum(Enum):
    FIRST = "first"
    SECOND = "second"
    THIRD = "third"
    @classmethod
    def choices(cls):
        return [
             (cls.FIRST.value, _('first')),
             (cls.SECOND.value, _('second')),
             (cls.THIRD.value, _('third'))
         ]
# And later in eg. model definiton:
some_field = models.CharField(max_length=10, choices=MyEnum.choices())

IPython FTW

ipython означает интерактивный питон, и это командная оболочка для интерактивных вычислений, это как интерпретатор питона, но с включенными батареями или по-другому: на стероидах.

Для использования ipython вам необходимо установить его:

pip install ipython

А позже вместо обычного использования команды ofpython для входа в интерпретатор используйте вместо этого ipython

# you should see something like this after you start:
Python 3.8.5 (default, Jul 28 2020, 12:59:40) 
Type 'copyright', 'credits' or 'license' for more information
IPython 7.18.1 -- An enhanced Interactive Python. Type '?' for help.
In [1]:

Он поддерживает системные команды, например: ls или cat клавиша tab будет показывать подсказки, что делает сеанс интерактивного программирования намного более приятным. Вы также можете использовать стрелки вверх / вниз для поиска последних команд, как правило, вы можете делать много чего - обратитесь к официальной документации ipython. Особенно рекомендую ознакомиться с волшебными функциями, с их помощью можно сохранять, делиться кодом, который вы использовали, измерять время выполнения и многое другое.

На данный момент все. Надеюсь, вы найдете здесь что-нибудь интересное.

Уровень кодирования

Спасибо, что стали частью нашего сообщества! Подпишитесь на наш канал YouTube или присоединитесь к Интервью по программированию Skilled.dev.