company_banner

Полезные советы по Python, которых вы ещё не встречали. Часть 2

Автор оригинала: Martin Heinz
  • Перевод
Недавно мы опубликовали перевод материала, в котором были приведены полезные советы для Python-программистов. У того материала есть продолжение, которое мы представляем вашему вниманию сегодня.



Именование среза с использованием функции slice


Работа с множеством значений, которые заданы индексами, может быстро обернуться беспорядком — как в плане поддержки, так и в плане читабельности кода. Один из вариантов улучшения ситуации заключается в использовании констант для значений, задаваемых индексами. Но есть и более удачный способ написания качественного кода:

#              ID    First Name     Last Name
line_record = "2        John         Smith"

ID = slice(0, 8)
FIRST_NAME = slice(9, 21)
LAST_NAME = slice(22, 27)

name = f"{line_record[FIRST_NAME].strip()} {line_record[LAST_NAME].strip()}"
# name == "John Smith"

В этом примере можно видеть, что, дав срезам имена с помощью функции slice, и использовав эти имена при получении фрагментов строки, мы смогли избавиться от запутанных индексов. Узнать подробности об объекте slice можно с помощью его атрибутов .start, .stop и .step.

Запрос пароля у пользователя во время выполнения программы


Множеству инструментов командной строки или скриптов для работы требуется имя пользователя и пароль. Если вам придётся писать подобную программу — вы, возможно, сочтёте полезным модуль getpass:

import getpass

user = getpass.getuser()
password = getpass.getpass()
# Выполнить некие действия...

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

Нахождение близких соответствий в строках


Теперь поговорим о немного более таинственной возможности стандартной библиотеки Python. Предположим, вы попали в ситуацию, когда вам понадобилось, задействуя концепцию наподобие расстояния Левенштейна, найти в списке слова, похожие на некую входную строку. Решить эту задачу можно с помощью модуля difflib.

import difflib
difflib.get_close_matches('appel', ['ape', 'apple', 'peach', 'puppy'], n=2)
# returns ['apple', 'ape']

Метод difflib.get_close_matches ищет наилучшие, «достаточно хорошие» совпадения. Первый аргумент этого метода задаёт искомую строку, второй аргумент задаёт список, в котором выполняется поиск. Этому методу можно передать необязательный аргумент n, который задаёт максимальное число возвращаемых совпадений. Ещё этот метод поддерживает необязательный именованный аргумент cutoff (по умолчанию он установлен в значение 0.6), который позволяет задавать пороговое значение для оценки совпадений.

Работа с IP-адресами


Если вам приходится писать на Python программы для работы с сетью — это значит, что вам может очень пригодиться модуль ipaddress. Одним из вариантов его использование является генерирование списка IP-адресов из диапазона адресов, заданных в формате CIDR (Classless Inter-Domain Routing, бесклассовая адресация).

import ipaddress
net = ipaddress.ip_network('74.125.227.0/29')  # Подходит и для работы с IPv6
# IPv4Network('74.125.227.0/29')

for addr in net:
    print(addr)

# 74.125.227.0
# 74.125.227.1
# 74.125.227.2
# 74.125.227.3
# ...

Ещё одна полезная возможность этого модуля — проверка IP-адреса на предмет принадлежности его к некоей сети:

ip = ipaddress.ip_address("74.125.227.3")

ip in net
# True

ip = ipaddress.ip_address("74.125.227.12")
ip in net
# False

У модуля ipaddress есть и много других интересных возможностей, о которых я тут не рассказываю. Почитать подробности о нём можно здесь. Правда, пользуясь этим модулем, учитывайте ограничения, касающиеся его совместной работы с другими модулями, имеющими отношение к сетевому программированию. Например, нельзя использовать экземпляры IPv4Network в виде строк адреса. Подобные объекты для этого сначала надо конвертировать в строки с помощью str.

Отладка программы в командной строке


Если вы — из тех, кто не хочет пользоваться IDE и пишет код в Vim или Emacs, тогда вы, возможно, попадали в ситуацию, когда вам пригодился бы отладчик, вроде тех, что есть в IDE. И знаете что? У вас такой отладчик уже есть. Для того чтобы им воспользоваться, достаточно запустить программу с помощью конструкции вида python3.8 -i. Флаг -i позволяет, после завершения программы, запустить интерактивную оболочку. С её помощью можно исследовать переменные и вызывать функции. Это интересная возможность, но как насчёт настоящего отладчика (pdb)? Давайте поэкспериментируем со следующей простой программой, код которой находится в файле script.py:

def func():
    return 0 / 0

func()

Запустим её командой python3.8 -i script.py и получим следующее:

# Скрипт дал сбой...

Traceback (most recent call last):
  File "script.py", line 4, in <module>
    func()
  File "script.py", line 2, in func
    return 0 / 0
ZeroDivisionError: division by zero
>>> import pdb
>>> pdb.pm()  # Запускаем отладчик после завершения программы
> script.py(2)func()
-> return 0 / 0
(Pdb)

Мы видим место программы, в котором произошёл сбой. Зададим точку останова:

def func():
    breakpoint()  # import pdb; pdb.set_trace()
    return 0 / 0

func()

Снова запустим скрипт.

script.py(3)func()
-> return 0 / 0
(Pdb)  # начинаем здесь
(Pdb) step
ZeroDivisionError: division by zero
> script.py(3)func()
-> return 0 / 0
(Pdb)

В большинстве ситуаций для отладки скриптов достаточно команды print и результатов трассировки, но иногда для того, чтобы разобраться со сложным сбоем, нужно покопаться в программе и вникнуть в суть происходящего. В подобных случаях в коде задают точки останова и исследуют программу. Например — смотрят аргументы функций, вычисляют выражения, проверяют значения переменных, или, как показано выше, просто занимаются пошаговым выполнением кода. Pdb представляет собой полнофункциональную Python-оболочку. В этой оболочке можно выполнить практически всё, что угодно. В ходе работы вам пригодятся некоторые специфические команды отладчика, справку по которым можно найти здесь.

Объявление нескольких конструкторов в классе


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

import datetime

class Date:
    def __init__(self, year, month, day):
        self.year = year
        self.month = month
        self.day = day

    @classmethod
    def today(cls):
        t = datetime.datetime.now()
        return cls(t.year, t.month, t.day)

d = Date.today()
print(f"{d.day}/{d.month}/{d.year}")
# 14/9/2019

В подобной ситуации вы, вместо использования методов класса, можете склониться к тому, чтобы поместить всю логику альтернативных конструкторов в __init__ и решить задачу с использованием *args, **kwargs и множества выражений if. В результате может получиться рабочий код, но этот код будет тяжело читать и поддерживать. Тут я порекомендовал бы поместить минимум логики в __init__ и выполнить все операции в отдельных методах/конструкторах. При таком подходе в нашем распоряжении окажется чистый код, с которым удобно будет работать и автору этого кода, и тому, кто этим кодом будет пользоваться.

Кэширование результатов вызова функций с помощью декоратора


Доводилось ли вам писать функции, которые выполняли какие-нибудь длительные операции чтения-записи, или достаточно медленные рекурсивные вычисления? Думали ли вы при этом о том, что таким функциям не повредило бы кэширование результатов? Кэшировать (мемоизировать) результаты вызова функции можно с помощью декоратора lru_cache из модуля functools:

from functools import lru_cache
import requests

@lru_cache(maxsize=32)
def get_with_cache(url):
    try:
        r = requests.get(url)
        return r.text
    except:
        return "Not Found"


for url in ["https://google.com/",
            "https://martinheinz.dev/",
            "https://reddit.com/",
            "https://google.com/",
            "https://dev.to/martinheinz",
            "https://google.com/"]:
    get_with_cache(url)

print(get_with_cache.cache_info())
# CacheInfo(hits=2, misses=4, maxsize=32, currsize=4)

В этом примере мы выполняем GET-запросы, результаты которых кэшируются (кэшировано может быть до 32 результатов). Тут можно увидеть и то, что мы получаем сведения о кэше функции, воспользовавшись методом cache_info. Декоратор, кроме того, даёт в наше распоряжение метод clear_cache, применяемый для инвалидации кэша. Тут мне ещё хотелось бы отметить то, что кэширование нельзя использовать с функциями, у которых есть побочные эффекты, или с функциями, создающими мутабельные объекты при каждом вызове.

Нахождение элементов, которые встречаются в итерируемом объекте чаще всего


Нахождение в списке таких элементов, которые встречаются в нём чаще других, это весьма распространённая задача. Решить её можно, например, воспользовавшись циклом for и словарём, в котором будут собраны сведения о количестве одинаковых элементов. Но такой подход — это пустая трата времени. Дело в том, что решать подобные задачи можно с помощью класса Counter из модуля collections:

from collections import Counter

cheese = ["gouda", "brie", "feta", "cream cheese", "feta", "cheddar",
          "parmesan", "parmesan", "cheddar", "mozzarella", "cheddar", "gouda",
          "parmesan", "camembert", "emmental", "camembert", "parmesan"]

cheese_count = Counter(cheese)
print(cheese_count.most_common(3))
# Вывод: [('parmesan', 4), ('cheddar', 3), ('gouda', 2)]

Внутренние механизмы класса Counter основаны на словаре, хранящем соответствия элементов и количества их вхождений в список. Поэтому соответствующий объект можно использовать как обычный объект dict:

print(cheese_count["mozzarella"])
# Вывод: 1

cheese_count["mozzarella"] += 1

print(cheese_count["mozzarella"])
# Вывод: 2

Кроме того, при работе с Counter в нашем распоряжении оказывается метод update(more_words), используемый для добавления к счётчику новых элементов. Ещё одна полезная возможность Counter заключается в том, что он позволяет использовать математические операции (сложение и вычитание) при работе с экземплярами этого класса.

Итоги


Думаю, что большая часть из приведённых сегодня советов вполне может использоваться теми, кто пишет на Python, практически ежедневно. Надеюсь, вы нашли среди них что-то такое, что пригодится именно вам.

Уважаемые читатели! Знаете какие-нибудь интересные приёмы Python-программирования? Если так — просим ими поделиться.

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Похожие публикации

Комментарии 24

    +2
    Есть интересный и простой прием с заменой индексов на имена при работе с упорядоченными коллекциями:
    users = [('Sam', 18, 'male'), ('Lisa', 25, 'female')]
    name, age, sex = 0, 1, 2
    for user in users:
        print(user[name], '-', user[age])
    
      0
      name, age, sex = range(3)
        +9

        А не проще ли


        for (name, age, sex) in users:
            print(name, '-', age)

        ?

          +1
          Подозреваю, что способ, описанный DaneSoul может быть полезным в случае, когда интересующие элементы кортежей расположены не в позициях 0, 1 и 2, а, скажем, в позициях 3, 7 и 16.
          Хотя я бы сначала задумался о корректности выбранного подхода, если он заставляет работать с такими кортежами.
            +1

            Лишние переменные можно заменить подчеркиваниями, например:


            for (name, _, _, _, email) in users:
                print(name, '-', email)

            А кортежи часто бывают при чтении из файла.

              0

              Если нужны только первый и последний элементы — есть ещё такой вариант (*_ собирает всё остальное в список):


              In [1]: for first, *_, last in (range(10), range(42)): 
                 ...:     print(f'{first=}, {last=}, {len(_)=}') 
                 ...:                                                                                                                                               
              first=0, last=9, len(_)=8
              first=0, last=41, len(_)=40
                0
                Тогда может так?
                for (name, *_, email) in users:
                    print(name, '-', email)
              0
              А зачем в этом цикле нужна переменная пол (sex), хватит и nil )

              for (name, age, nil) in users:
                  print(name, '-', age)


                +5

                Ну у кого-то sex, а у кого-то nil

                  +1
                  Так nil в Вашем примере это такая же переменная (как по-сути и подчеркивание в примере выше), в этом легко убедится распечатав их значения в print()
                  for name, age, nil in users:
                      print(name, '-', age, nil)
                  for name, _, _ in users:
                      print(name, _)
                  

                  А явно указать там в присвоении None нельзя:
                  for name, age, None in users:
                  SyntaxError: can't assign to keyword
                  Кстати, кортеж, когда он используется для получения значений не обязательно заключать в скобки.
                    0
                    Согласен. Чтобы сократить лишнее, можно ограничить список только 2мя нужными элементами. Например так

                    users = [('Sam', 18, 'male'), ('Lisa', 25, 'female')]
                    name, age = 0, 1
                    for (name, age, _) in users:
                        print(name, '-', age)
                    0

                    Так плохо, ненужные переменные затеняют подчеркиванием.

                +1
                не все системы поддерживают скрытие паролей
                ищет наилучшие, «достаточно хорошие» совпадения
                принадлежности его к некоей сети
                в коде задают точки останова и исследуют программу
                [...]

                Такое ощущение, что текст заметки тоже написан на питоне: чем меньше строгости в определениях — тем лучше.

                  –3

                  Похоже автор наворовал советы и скомпилировал статью.
                  Других причин зачем он делает столько лишних действий с pdb представить тяжело, ведь даже в доке https://docs.python.org/3/library/pdb.html написано что править и лишний раз прогонять скрипт не обязательно, можно вызвать python -m pdb script.py и pdb командами читать листинг, ставить/убирать брейкпойнты и продолжать выполнение.

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

                    В примере функция с побочным эффектом.
                      0
                      Ну, приводить в 100500-й раз пример с вычислением чисел Фибоначчи было бы как-то скучно, наверное :) А так то вы правы, конечно.
                        0
                        Более того, в документации:
                        @functools.lru_cache(maxsize=128, typed=False)
                        Decorator to wrap a function with a memoizing callable that saves up to the maxsize most recent calls. It can save time when an expensive or I/O bound function is periodically called with the same arguments.


                        0
                        В чём побочный эффект?
                          0

                          А вдруг сайт не работает? В прошлый раз вернул 200, а щас уже Not Found. Любое IO по сути делает функцию не чистой.

                            0

                            А в чем побочный эффект-то? Так-то и содержимое сайта может измениться, понятно, что кэшированный результат об этом не узнаёт.

                              0
                              Так в том и эффект, что содержимое сайта может измениться. По-хорошему так не делается, мемоизировать надо детерминированные функции. Но в качестве примера и для каких-то даже рабочих целей — почему бы и нет.
                                0
                                Ну любой, кто использует кэш для интернет-запросов, должен осознавать, какие риски с этим приходят, это же не повод не использовать кэширование потому что «по-хорошему так не делается». Просто мне субъективно кажется, что область, где применяется кэширование, на 90% состоит из IO.
                                  –1
                                  Ну, не обязательно. Я, например, применял кэширование для NLP задач, вызовы библиотек для работы с текстами часто бывают довольно медленными и в то же время там бывают вызовы функций, которые часто повторяются с одинаковыми параметрами, они очень хорошо кэшируются (у меня было ускорение до 10 раз).
                        0

                        Именованные слайсы вообще почти никогда не нужны. Если вы только не работаете с ужасно отформатированными строками.

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое