питон

Скрытые жемчужины Python: малоизвестные советы и хитрости — часть 1

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

Иначе после цикла for

Знаете ли вы, что вы можете использовать оператор else после цикла for? Оператор else выполняется, если цикл не прерывается и, следовательно, выполняется до завершения. То есть:

for i in range(10):
    if i == 5:
        print("Break loop, no else!")
        break
else:
    print("This won't be called.")

for i in range(10):
    if i == 10:
        print("This won't be called")
        break
else:
    print("The loop did not break!")

Выходы:

Break loop, no else!
The loop did not break!

Хорошо, но почему? Идея состоит в том, чтобы упростить следующий тип процедуры:

found = None
for v in some_list:
    if some_search_condition():
        found = v
        break

if found is None:
    found = some_default_value

со следующим:

for v in some_list:
    if some_search_condition():
        found = v
        break
else:
  found = some_default_value

Возможно, для этого есть лучшее название, чем «другое», но это то, что есть.

| оператор в питоне

|, часто называемый «конвейером», — это оператор в Python, который может кратко определять различные вычисления. Давайте посмотрим на некоторые из них:

Побитовое или

Классическое использование | заключается в выполнении побитового или, и это происходит во многих языках, таких как Java, C, C++ и JavaScript, и это лишь некоторые из них. Точно так же он также используется для этой цели в Python при выполнении между числами. Например:

a = 4 # in binary 0100 = 0*2^3 + 1*2^2 + 0*2^1 + 0*2^0 = 4
b = 8 # in binary 1000 = 1*2^3 + 0*2^2 + 0*2^1 + 0*2^0 = 8
# bitwise or: 
#    0100
#    1000
# => 1100 = 12
print(a | b) 
# => 12

& аналогичным образом используется для побитового и.

Объединение словарей

Начиная с Python 3.9 оператор | можно использовать для объединения словарей:

a = { "a": 5, "b": 6 }
b = { "c": 7 }
print(a | b)
# => {'a': 5, 'b': 6, 'c': 7}

Обратите внимание, что если словари имеют общие ключи, значение в последнем словаре в операции будет сохранено:

a = { "a": 5, "b": 6 }
b = { "a": 7 }
print(a | b)
# => {'a': 7, 'b': 6}

Вы также можете связать операцию для объединения нескольких словарей:

a = { "a": 5, "b": 6 }
b = { "a": 7 }
c = { "a": 8, "c": 10 }
print(a | b | c)
# => {'a': 8, 'b': 6, 'c': 10}

Союз наборов

set в Python — это неупорядоченный набор элементов без дубликатов. Он обеспечивает быструю проверку членства и множество полезных операций, таких как объединение наборов. Объединение множеств — это объединенные элементы всех из них (без дубликатов), и здесь можно использовать оператор |:

a = set([1, 2, 3])
b = set([2, 3, 4])
# union:
print(a | b)
# or equivalently
print(a.union(b))

Его также можно связать:

a = set([1, 2, 3])
b = set([2, 3, 4])
c = set([3, 4, 5])
print(a | b | c)
# => {1, 2, 3, 4, 5}

Если вы хотите узнать больше о наборе в Python, я написал подробное руководство по этому поводу здесь.

Поэлементное логическое значение или в NumPy и Pandas

Как в NumPy, так и в Pandas | имеет важную цель выполнения поэлементного логического или. Я подчеркиваю здесь важность, потому что это обеспечивает значительное ускорение по сравнению, например, с использованием .apply() в Pandas. Проиллюстрируем это на примере:

from time import time
import numpy as np
import pandas as pd

a = pd.Series(np.arange(1000000))
# using .apply()
start_time = time()
b = a.apply(lambda x: x < 300000 or x > 700000) 
print(time() - start_time)
# => 0.1980600357055664

start_time = time()
b = (a < 300000) | (a > 700000)
print(time() - start_time)
# => 0.0020973682403564453

При запуске это поэлементно или было в 94 раза быстрее! Точно так же вы можете использовать & для выполнения поэлементного и. Когда важна скорость, эти операторы незаменимы.

Союз в подсказках типа

При указании подсказки типа для переменной, которой разрешено иметь несколько типов, можно использовать тип Union:

from typing import Union

def some_method(
    # a can be of type str or int
    a: Union[str, int]
):
    ...

Но вы также можете использовать | и написать:

def some_method(
    # a can be of type str or int
    a: str | int
):
    ...

Ваши собственные операции

Наконец, вы можете сами определить использование | для своих классов, выполнив перегрузку оператора. Для этого вам просто нужно определить метод внутри класса с именем __or__. Давайте создадим забавный пример, где методы объединены в цепочку:

class Callback:
    def __init__(self, func):
        self.func = func

    # implementing this method will enable
    # the | operator for this class:
    # a | b
    # where a will be self, and b will be
    # the first argument in __or__
    def __or__(self, other):
        # chain the methods
        def combined_func(input):
            # self(...) can be used since
            # __call__ is defined below
            tmp = self(input)
            return other(tmp)
        return Callback(combined_func)

    def __call__(self, input):
        return self.func(input)

Перегрузив __or__-оператор, обратные вызовы теперь могут быть объединены в цепочку для создания нового обратного вызова:

method = (
    Callback(lambda x: x + 1) | 
    Callback(lambda x: x * 2) |
    Callback(lambda x: x ** 2)
)

print(method(3)) # ((3+1) * 2) ** 2 = 64
# => 64
print(method(4)) # ((4+1) * 2) ** 2 = 100
# => 100

Многоточие

... или многоточие — это константа в Python, которая используется в различных ситуациях, часто сигнализируя о том же значении, что и в обычном тексте, т. е. «и так далее» или «что-то должно быть здесь". Обратите внимание, что ввод ... и Ellipsis эквивалентен:

assert Ellipsis == ...

Давайте посмотрим на некоторые из его применений.

В NumPy

При работе с несколькими измерениями в NumPy многоточие можно использовать для обозначения «остальных измерений», то есть:

import numpy as np

tensor = np.random.uniform(size=(10, 10, 10, 10, 10))

print(tensor[0, ...].shape)
# corresponds to tensor[0, :, :, :, :]
# => (10, 10, 10, 10)
print(tensor[0, 0, ...].shape)
# corresponds to tensor[0, 0, :, :, :]
# => (10, 10, 10)
print(tensor[0, 0, ..., 0].shape)
# corresponds to tensor[0, 0, :, :, 0]
# => (10, 10)
print(tensor[0, 0, ..., 0, 0].shape)
# corresponds to tensor[0, 0, :, 0, 0]
# => (10,)

Чтобы узнать больше о NumPy-трюках, ознакомьтесь с этой статьей.

В качестве заполнителя

Альтернативой использованию pass для обозначения того, что функция ничего не делает или что она должна быть завершена позже, является использование Ellipsis:

def my_method():
    ...

Вы также можете использовать его для занятий:

class MyClass: ...

Если вы похожи на меня и пишете «…» для контента, который вы собираетесь заполнить позже при написании текста, этот подход должен быть естественным.

Ввод

Многоточие можно использовать внутри подсказок типа в Python в разных ситуациях.

Кортежи

При добавлении подсказки типа для кортежа вы можете определить его фиксированную или переменную длину. В последнем случае на сцену выходит многоточие. Давайте посмотрим на несколько примеров:

from typing import Tuple

def some_method(
    # ex: (1, "hello", 2)
    a: Tuple[int, str, int],
    # ex: ()
    b: Tuple[()],
    # variable length! 
    # ex: (1, 2, 3, 4) or (1, 2)
    c: Tuple[int, ...],
): ...

Подсказка типа для переменной c говорит, что кортеж может содержать любое количество целых чисел. Итак, как бы вы определили кортеж переменной длины любоготипа? Есть два варианта:

from typing import Tuple, Any
def some_other_method(
    # option 1
    a: Tuple[Any, ...],
    # option 2 (equivalent to option 1)
    b: Tuple
): ...

Доступно

Объект типа Callable — это то, что можно назвать, например, методом. Прежде чем понять роль многоточия здесь, давайте разберемся, как обычно определяется подсказка типа Callable:

from typing import Callable

Callable[[TypeOfFirstArg, TypeOfSecondArg, and so on...], ReturnType]

Например:

from typing import Callable

def some_method(
    # takes zero arguments, returns nothing/None
    # ex: a()
    a: Callable[[], None], 
    # takes two arguments, an int and a string, returns an int
    # ex: b(1, "hello")
    b: Callable[[int, str], int],
):
    a()
    b(1, "hello")

Но что, если Callable должен иметь возможность принимать любое количество аргументов любого типа? Вот тут-то и появляется многоточие. Просто определите его следующим образом:

def some_other_method(
    # takes any number of arguments of any type, returns an int
    # ex: c(1, 2, 3, 4) or c()
    c: Callable[..., int],
):
    c()
    c(1,2,3)

Также обратите внимание, что простое указание:

d: Callable подразумевает:

d: Callable[..., Any]

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

Кстати, начиная с Python 3.10, многоточие также имеет собственный тип: types.EllipsisType.

Ваши собственные функции и классы

Как и None, Ellipsis является одноэлементным, что означает, что в программе есть только один экземпляр объекта, и любая ссылка на Ellipsis будет указывать на один и тот же объект в памяти. Таким образом, всякий раз, когда многоточие имеет смысл семантически и/или вам нужен еще один синглтон, кроме None, его можно использовать в ваших классах или методах.

Pydantic — популярная библиотека для проверки данных, о которой я недавно написал статью, посвященную ее использованию для обработки переменных среды. Там многоточие можно использовать в качестве первого аргумента так называемой Field-функции, чтобы указать, что атрибут требуется. Например:

class Model(BaseModel):
    # a list of strings that is required and needs to have at least 5 items
    a_required_attr: List[str] = Field(..., min_items=5)

Если вам понравилась эта статья:

  • 👏 Хлоп, это поможет мне понять, что нравится моим читателям и чего они хотят больше.
  • 🙏 Подписывайтесь или подписывайтесь, если хотите читать мои будущие статьи, новые каждую неделю!
  • 📚 Если вы ищете больше контента, посмотрите мои списки для чтения в разделах ИИ, Питон или Наука о данных.

Спасибо за чтение и хорошего дня.