Python
Programming
September 2018 1

Интересности и полезности python

From Sandbox
Я уже несколько лет программирую на python, однако, недавно осознал, что множество полезных приёмов и интересных моментов прошли мимо меня, возможно, я не один такой, поэтому решил перечислить их здесь, надеюсь, данные приёмы пригодятся кому-то в работе или побудят познакомиться с этим языком поближе.

Как и во многих языках в python 1 эквивалентно True, а 0 — False, то есть

 1 == True.

Казалось бы, и что в этом такого? Однако, это имеет некоторые побочные эффекты, связанные с тем, что одинаковые объекты обязаны иметь одинаковые хеши, соответственно у вас не получится запихать в один словарь ключ 1 и True.

 >>> a = {1: "one", 0: "zero", True: "true", False: "false"}
# -> {1: 'true', 0: 'false'}

Так же это разрешает следующие операции:

 >>> print(2 * False + True)
# -> 1

В данном примере строки использовались в качестве значений словаря, однако, зачастую хочется их использовать в качестве ключей словаря, меня всегда раздражало, что при создании словаря с помощью фигурных скобок, строки нужно указывать в кавычках, хотелось бы их опустить, это возможно, если создавать словарь через конструктор dict().

 >>> {"one": 1, "two": 2, "three": 3} == dict(one=1, two=2, three=3)
# -> True

Кроме того, с помощью фигурных скобок создаются не только словари, но и множества(set).

 >>> a = {1, 2, 3}

Для объединения двух множеств мне почему-то хочется воспользоваться оператором +, наверно, из-за способа конкатенации строк. Однако, python не поддерживает данный оператор для множеств. Но разумеется, это не значит, что нам всегда придётся пользоваться функциями, создатели подошли к данному вопросу более системно и добавили в язык поддержку основных операций над множествами (а не только объединения) и «повесили» их на логические операторы.


a = {1, 2, 3}
b = {0, 2, 4}
print(a & b)     # -> {2}
print(a | b)     # -> {0, 1, 2, 3, 4}
print(a ^ b)     # -> {0, 1, 3, 4}
print(a - b)     # -> {1, 3}, однако один арифметический
                 # оператор всё же оставили

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

www.python.org/downloads/release/python-370
mail.python.org/pipermail/python-dev/2017-December/151283.html


d = dict(zero='Cero', one='Uno', two='Dos', three='Tres', four='Cuatro',
         five='Cinco', six='Seis', seven='Siete', eight='Ocho', night='Nueve')

for index, (key, value) in enumerate(d.items()):
    print(f"{index} is {key} in England and {value} in Spain")

Обратите внимание на строку вывода, она начинается с префикса f — это особый тип строк, введённый в python 3.6.

Всего в языке три вида строк: обычные, обозначаемые кавычками без префиксов, сырые\не обрабатываемые(raw), в которых спец-символы, вроде, \n не обрабатываются и вставляются как текст и собственно f-строки.

Созданы они были для упрощения вывода, python поддерживает огромное количество способов вывода:


print("result" + str(2))     # Простая конкатенация строк, python не осуществляет
                             # автоматическое приведение всех аргументов к 
                             # строковому типу, это остаётся за программистом
print("result", 2)           # print может принимать несколько аргументов через запятую,
                             # в таком случае они будут выводиться через пробел,
                             # вам не нужны преобразовывать выводимые объекты в строку,
                             # в отличие от предыдущего способа
print("result %d" % 2)                 # %-синтаксис, сделан по аналогии с языком C.
print("result %d %.2f" % (2, 2))       # https://docs.python.org/3.4/library/string.html#formatspec
print("result %(name)s" % {"name": 2}) # также разрешено создавать именованные метки

print("{}".format(2))                  # У класса строки есть метод format()
                                       # он позволяет опускать тип выводимой переменной
print("{0} {1} {0}".format(1, 2)) # так же можно указать номер переменной и таким образом
        			        # вывести её два раза
        			        # нумерация начинается с нуля
# если число переданных переменных меньше использованных в выводе, будет сгенерированно исключение
print("{} {}".format(2))            # -> IndexError: tuple index out of range
print("{0} {0}".format(2, 3))       # -> 2 2 Однако если передано слишком много переменных
                                    # код отработает без ошибок
from math import pi                 # при таком выводе так же поддерживаются строки формата
print("{:.2f}".format(pi))          # -> 3.14

from string import Template         # возможен и такой способ вывода
s = Template("result  $res")        # однако он не получил большого распространения
print(s.substitute(res = [3, 4]))

Теперь добавили ещё и f-строки. В них доступны любые переменные из области видимости, можно вызывать функции, получать элементы по ключу, кроме того, они поддерживают строки формата.


from math import pi
result = 4
name = "user"
print(f"{name:84s} pi= {pi:.2f}, result={result}, {name[2]}")
# -> user                                                                                 pi= 3.14, result=4, e

from datetime import datetime
print(f"{datetime.now():%Y:%m-%d}")

Они быстрее всех остальных способов вывода, так что, если вам доступен python3.6 рекомендуется использовать именно их.

Одна из наикрутейших фишек python — в нём упаковываются и распаковываются не объекты и примитивы, а параметры и коллекции.


def func(*argv, **kwargs)

Однако, есть один архитектурный недостаток в реализации:

  • argv — кортеж, его значения нельзя изменять, нельзя добавлять или удалять значения
  • kwargs — словарь, изменяемый, поэтому кеширование невозможно

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

Множества тоже создаются на основе хеш-таблицы, это значит, что значения должны быть хешируемы, кроме того само множество является изменяемым и не хешируемым типом, есть специальный тип frozenset — не изменяемое множество (не спрашивайте меня, зачем оно нужно).

Обсуждали создание типа frozendict, однако пока его не добавили (хотя как минимум одно применение ему уже есть — в качестве kwargs). За неизменяемый словарь приходится отдуваться namedtuple. А ещё и за записи и простенькие классы.

Кто в студенческие\школьные годы писал циклы для вывода значений массива и бесился из-за запятой в конце, каждый раз решал, забить или переписать, чтобы было красиво, и только на курсе 2-3 узнал о методе join? Или я один такой?

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


a = list(range(5))
print(" ".join(a))                 # -> TypeError: sequence item 0: expected str instance, int found
print(" ".join(str(i) for i in a)) # -> 0 1 2 3 4
print(*a)                          # -> 0 1 2 3 4

Так как строки — тоже коллекции, то их так же можно «джойнить».


print('-'.join("hello"))             # -> h-e-l-l-o

Рассмотрим строку из предыдущего примера.


print(" ".join(str(i) for i in a)) # -> 0 1 2 3 4

Генераторное выражение передано в ф-цию join без каких-либо скобок, круглые скобки можно опускать для упрощения чтения кода. Python заботится о выразительности.


print(sum(i**2 for i in range(10))) # -> 285

Кроме того, круглые скобки можно опускать и при создании кортежей:


article = "python", 2018, "LinearLeopard"            # объявление кортежа
theme, year, author = "python", 2018, "LinearLeopard"# распаковка кортежа
theme, year, _ = "python", 2018, "LinearLeopard"     # слева и справа должно
                                                     # находиться одинаковое количество
                                                     # переменных, можно выделить,
                                                     # что какая-то вам не нужна,
                                                     # обозначив её через
                                                     #  подчёркивание
theme, _, _ = "python", 2018, "LinearLeopard"        # имена могут повторяться
theme, *, author = "python", 2018, "LinearLeopard"    # можно объявить жадный
                                                     # параметр, который съест
                                                     # все неподходящие,
                                                     # разумеется, допустим
                                                     # только один
                                                     # жадный оператор

Звёздочку можно использовать и в объявления функций, таким образом можно создать параметры, которые можно указать только по ключу.


def sortwords(*wordlist, case_sensitive=False):

Можно передавать в ф-цию сколько угодно параметров без боязни, что один из них будет воспринят как значение параметра case_sensitive.

Можно и так.


def func(first, second, *, kwonly):


Внимательнее рассмотрим, чем просто * отличается от *args.


def func(first, second, *, kwonly=True):
    print(first, second, kwonly)


def func2(first, second, *args, kwonly=True):
    print(first, second, *args, kwonly)


func(1)           #-> TypeError: func() missing 1 required positional argument: 'second'
func(1, 2)        #-> 1 2 True
func(1, 2, False) #-> TypeError: func() takes 2 positional arguments but 3 were given
                  # используя * в объявлении вы укажите, что
                  # ваша функция должна быть вызвана с двумя
                  # позиционными параметрами
func(1, 2, kwonly=False) #-> 1 2 False

func2(1, 2, False) #-> 1 2 False True
                   # *args заберёт в себя все позиционные
                   # параметры, то есть вашу функцию может будет
                   # вызывать с неограниченным (>2) числом
                   # параметров


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


def add_to(elem, collection=[]):
    collection.append(elem)
    return collection


a = ["a", "c"]
print(add_to("b", a))           # -> ['a', 'c', 'b']

print(add_to("a"))              # -> ['a']
print(add_to("b"))              # -> ['a', 'b']  Откуда здесь 'a'?

Значения по умолчанию ф-ция хранит в поле __defaults__, можно в любой момент узнать, что там находится.


print(add_to.__defaults__) # -> (['a', 'b'],)

Так как аргумент по умолчанию (пустой список) создался в момент старта программы и не пересоздаётся каждый раз заново, мы получили именно такое поведение.

Исправить подобное поведение можно, если сделать значение по умолчанию неизменяемым типом, а список создавать в теле функции:


def add_to(elem, collection=None):
    collection = collection or []
    collection.append(elem)
    return collection

Обратите внимание на команду


collection = collection or []

это более короткий (и менее понятный, хотя не для всех) аналог


collection = collection if collection else []


Ссылка на следующую часть
+45
29.3k 408
Comments 44
Top of the day