Я уже несколько лет программирую на 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 []

Комментарии (35)


  1. vasily-v-ryabov
    01.09.2018 17:16

    Хороший обзор, скину коллегам почитать. Из мелочей: вроде frozenset через z, и вот этот код можно было бы детальнее прокомментировать:
    def func(first, second, *, kwonly):


    1. LinearLeopard Автор
      01.09.2018 19:30

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

      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) числом
                         # параметров
      


    1. saatanaperkele
      02.09.2018 13:33
      +1

      У Дена Бейдера есть книжонка Python Tricks, коллегам лучше сразу ее читать. Она маленькая и в ней как раз собраны всякие такие приколы.


      1. LinearLeopard Автор
        02.09.2018 13:35

        Спасибо за наводку, большинство интересных фишек я подсмотрел в выступлениях Raymond Hettinger, курсе python от CSC и официальной документации, разумеется. В принципе там таких приёмов на ещё ни одну статью. Чем и собираюсь заняться в ближайшее время.


      1. vasily-v-ryabov
        03.09.2018 13:45

        Уже куплена неделю назад. :) И коллегам нравится. На Хабре как раз месяц назад статья была с отрывком из неё, так и нашёл.


  1. worldmind
    01.09.2018 18:19

    > collection = collection or []

    если правильно помню if else вводили потому что такой вариант в некоторых случаях работает не так как ожидается, но деталей не помню, они должны быть тут


    1. LinearLeopard Автор
      01.09.2018 19:35

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

      collection |= []
      # TypeError: unsupported operand type(s) for |=: 'NoneType' and 'list' 

      не работает


      1. kalininmr
        02.09.2018 12:34
        +1

        и не должно, or и | — разные операторы


        1. LinearLeopard Автор
          02.09.2018 13:36

          Да, я к тому, что в других языках это можно сделать, но в python решили так не делать.


          1. kalininmr
            02.09.2018 13:39
            +2

            это в каких таких?
            логическое или и битовое или — вроде везде разные опрераторы


            1. LinearLeopard Автор
              02.09.2018 15:43

              Упс, перепутал |= и ||=.


    1. EgZvor
      03.09.2018 15:12

      Судя по PEP, ввели не вариант, а новый механизм, потому что используя только or and можно было не всё сделать. Кроме того, что этими механизмами можно записывать условия в выражениях, они больше ничем не связаны. Собственно, варианты с or и and работают только потому, что эти бинарные операции работают лениво, и возвращают для or: первый операнд, если он сводится к True, иначе — второй; для and: первый операнд, если он — False, иначе второй.


    1. trapwalker
      03.09.2018 19:10

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

      item = (
          items and item[0]
          or can_use_others and other_items and other_items[0]
          or default
      )
      

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


  1. phenik
    01.09.2018 18:19

    Простая проверка на NaN
    if x != x:
    работает при обработке данных таблиц экселя, в других случаях не проверял.


    1. barker
      01.09.2018 20:32

      всё-таки лучше math.isnan, т.к. можно при желании сломать эту проверку для произвольных x


    1. worldmind
      03.09.2018 21:22

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


  1. DaneSoul
    01.09.2018 21:47

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

    На самом деле такая формулировка не точна.
    Они равны (и поэтому функция хеширования дает одинаковый результат и Ваш пример со словарем корректен), но они не эквивалентны (то есть это разные объекты с разными id).

    Проверка на проверка на равенство делается оператором ==
    Проверка на эквивалентность делается оператором is

    print(1 == True)    # True
    print(1 is True)    # False
    
    print(id(1))        # 10943008
    print(id(True))     # 10262944


    1. LinearLeopard Автор
      01.09.2018 22:10

      Да, извиняюсь, я имел в виду логическое равенство, а не равенство id.
      Просто использовать термин «равно», мне показалось тоже не хорошей идеей. Надо было выбрать другой термин.


    1. kalininmr
      02.09.2018 20:16

      так у них даже типы разные.
      bool — дочерний к int


    1. mayorovp
      02.09.2018 21:35
      +2

      Это называется «идентичность», а не «эквивалентность». Эквивалентность — это как раз ==.


  1. justhabrauser
    01.09.2018 22:02
    -2

    «Как и во многих языках в python 1 эквивалентно True»
    Ой всё (tm)

    Если человек не различает _эквивалентно_ и приведение типов в операции сравнения…

    С днем знаний, чо


    1. LinearLeopard Автор
      02.09.2018 13:54
      +3

      Никаких преобразований типов не производится. Класс bool наследуется от класса int

      print(int.__subclasses__()) # -> [<class 'bool'>]
      


  1. id_potassium_chloride
    02.09.2018 01:05

    Значения по умолчанию ф-ция хранит в поле __defaults__, можно в любой момент узнать, что там находится.
    Похожий механизм есть и объектов. Сегодня буквально пишу
    class Example:
        arr=[]
        __init__(self,...
    И с удивлением обнаруживаю, что arr ведёт себя как static поле. От неожиданности в конструкторе прописал self.arr=[].copy. Вроде надёжно, хы :D


    1. Cykooz
      02.09.2018 02:59

      Переменная, которую вы указали на «уровне» класса — это поле класса (класс ведь тоже объект в питоне), а не экземпляра этого класса. Экземпляр класса может иметь свою «версию» этой переменной, которая появится после первого же присвоения (self.arr = []) в любом методе класса (рекомендуется это делать в методе __init__). Если у экземпляра класса нет своей «версии» переменной, то при попытке «прочитать» её, будет использована «версия» из класса.
      Поэтому для «полей» класса действует такая же рекомендация, что и для аргументов функций со значением по умолчанию — не надо использовать изменяемые типы.


    1. trapwalker
      03.09.2018 14:24

      self.arr=[].copy

      Судя по «трюкам» в этой статье не все поймут вашего сарказма в этом месте.
      Поясню. Не стоило в конструкторе делать копирование. Литералы [], {}, () и т.д. идентичны соответствующим выражениям: list(), dict(), tuple().
      Тело объявления класса выполняется один раз при первом импорте модуля или при запуске программы, если это __main__. Если вы употребите литерал [] или вызов list() в конструкторе, то список будет инициализироваться при каждом вызове конструктора. Кстати, __init__ — это, строго говоря, не конструктор. В питоне конструктор — это __new__. Это я так… на всякий случай.

      Кстати о списках и прочих mutable объектах в классовых переменных. Это бывает полезно, если мы хотим без использования метаклассов реализовать класс, который будет «знать» все свои инстансы. Он просто будет сохранять их в свою коллекцию на классовом уровне.
      class MyClass:
          all_instances = []
          def __init__(self):
              self.all_instances.append(self)
      
      instance1 = MyClass()
      instance2 = MyClass()
      
      print(MyClass.all_instances == [instance1, instance2])  # True
      


      1. mayorovp
        03.09.2018 15:14

        Кстати, __init__ — это, строго говоря, не конструктор. В питоне конструктор — это __new__.

        Неправда ваша. Метод __init__ полностью аналогичен конструкторам в других популярных языках (кроме Delphi который пошел своим путем).


  1. anthonio
    02.09.2018 12:27
    -1

    SEX = 'Female', 'Male'
    sex = SEX[True] # Male
    sex = SEX[False] # Female


  1. alexdesyatnik
    02.09.2018 13:56

    Хорошая статья. За что люблю Питон: вроде несложный язык, а полно приятных и не очень мелочей, постоянно узнаешь что-то новое (в данном случае про создание словарей со строковыми ключами через dict). Небольшой вопрос: а почему бы не использовать map в примере про join? И читаемость получше, и печатать меньше. (Хотя с читаемостью это может быть субъективно, я в молодости был укушен Хаскеллем и прочей функциональшиной.)


    1. Mingun
      02.09.2018 23:12

      Справедливости ради, про создание словарей с простыми строковыми ключами через конструктор dict() упоминается в официальном учебнике (последний абзац):


      Словари

      5.5 Словари


      Другим полезным встроенным в Python типом данных является словарь (смотрите раздел Типы сопоставлений — dict). В других языках словари иногда называются «ассоциативной памятью» или «ассоциативными массивами». В отличие от последовательностей, индексируемых диапазоном чисел, словари индексируются ключами, которые могут быть любым неизменяемым типом; строки и числа ключами могут быть всегда. Кортежи могут использоваться в качестве ключей, только если они содержат строки, числа или кортежи; если кортёж прямо или косвенно содержит любой изменяемый объект, он не может использоваться в качестве ключа. Вы не можете использовать в качестве ключей списки, поскольку они могут быть изменены по месту с помощью присваиваний по индексам, срезам или такими методами, как append() или extend().


      Лучше всего думать о словаре как о наборе пар ключ: значение, с тем требованием, что ключи должны быть уникальными (в пределах одного словаря). Пара фигурных скобок создаёт пустой словарь: {}. Размещение в фигурных скобках списка разделённых запятыми пар ключ: значение добавляет начальные пары ключ: значение в словарь; также таким способом словари пишутся при их выводе на печать.


      Основными операциями над словарём являются сохранение значения по некоторому ключу и извлечение значения по указанному ключу. Также с помощью инструкции del пару ключ: значение можно удалить. Если вы сохраняете значение по ключу, который уже используется, старое связанное с этим ключом значение будет забыто. При попытке извлечения значения по несуществующему ключу возбуждается ошибка.


      Выполнение на словаре выражения list(d) вернёт список всех используемых в словаре ключей в порядке их вставки (если вы хотите, чтобы он был отсортирован, просто используйте вместо него sorted(d)). Чтобы проверить, находится ли в словаре один ключ, используйте ключевое слово in.


      Ниже приведён небольшой пример использования словаря:


      >>> tel = {'jack': 4098, 'sape': 4139}
      >>> tel['guido'] = 4127
      >>> tel
      {'jack': 4098, 'sape': 4139, 'guido': 4127}
      >>> tel['jack']
      4098
      >>> del tel['sape']
      >>> tel['irv'] = 4127
      >>> tel
      {'jack': 4098, 'guido': 4127, 'irv': 4127}
      >>> list(tel)
      ['jack', 'guido', 'irv']
      >>> sorted(tel)
      ['guido', 'irv', 'jack']
      >>> 'guido' in tel
      True
      >>> 'jack' not in tel
      False

      Конструктор dict() строит словари непосредственно из последовательностей пар ключ-значение:


      >>> dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
      {'sape': 4139, 'guido': 4127, 'jack': 4098}

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


      >>> {x: x**2 for x in (2, 4, 6)}
      {2: 4, 4: 16, 6: 36}

      Если ключи являются простыми строками, иногда проще указывать пары с помощью именованных аргументов:


      >>> dict(sape=4139, guido=4127, jack=4098)
      {'sape': 4139, 'guido': 4127, 'jack': 4098}


      1. alexdesyatnik
        03.09.2018 08:34

        В официальном учебнике и про остальные вещи из этой и других подобных статей всё расписано. Но, мне кажется, чтобы прочитать его от корки до корки и запомнить все нюансы, память нужно иметь поистине слоновью. Для менее продвинутых (или, может быть, менее профессиональных) пользователей Питона статьи вида «10 интересных приёмов, про которые вы всегда хотели узнать» весьма полезны для заполнения пробелов или восстановления подзабытого.


  1. Pichenug
    02.09.2018 13:56
    +1

    Большинство из того, что вы описали есть во многих стартовых учебниках по языку типа учебника Лутца(хотя воды там ой как много).
    Это я к тому, что изучая особенности языка только на ходу очень много базовых, но полезных «штук», проходит мимо. Имхо, лучше систематически подходить к такому вопросу и начать с учебника, тогда, например, распаковка кортежей и создание функций с возможностью передачи аргументов только по ключу не будет сюрпризом.


    1. LinearLeopard Автор
      02.09.2018 14:01
      +2

      Я учил python, наверно, языком 8. Просто гуглив синтаксические конструкции, согласен, лучше учить язык по хорошим учебникам или записям лекций, но иногда тебе просто надо сдать курсач через неделю\ты изменил место работы\перекинули на другой проект\решили, написать вспомогательную часть текущего на python.
      Собственно статья для тех, у кого нет недели на чтение книги, но есть время на чтение статейки другой.
      Но материала ещё много, надеюсь в последующих статьях вы тоже узнаете что-то новое.


  1. unabl4
    03.09.2018 01:39

    Статически вычисляемое дефолтовое значение параметра функции — это как раз то, чем я себе ногу отстрелил.
    Я поставил стандартное значение равным пустому массиву, который я в теле функции заполнял значениями и как-то использовал. Причём функция была ещё и в добавок рекурсивная. В общем, это было крайне неприятной находкой.


  1. EgZvor
    03.09.2018 16:29

    С параметрами по умолчанию связана одна интересная особенность: они вычисляются на этапе компиляции модуля в байт-код

    Это не так. Определения функций (и классов) "вычисляются" на этапе выполнения. Хотя я не уверен, что имеется в виду под этапом компиляции в байт-код. Насколько я понимаю такого "этапа" нет.


    class HelloPrinter(list):
        def __init__(self):
            print('Hello')
            super().__init__()
    
    def func(el, collection=HelloPrinter()):
        collection.append(el)
        print(collection)
    
    print('Plain print')
    
    func('a')
    func('b')
    
    def func(el, collection=HelloPrinter()):
        collection.append(el)
        print(collection)
    
    func('c')
    func('d')

    ? ~ python3 test_default_func.py   
    Hello
    Plain print
    ['a']
    ['a', 'b']
    Hello
    ['c']
    ['c', 'd']


    1. worldmind
      03.09.2018 21:27

      Значения по умолчанию вычисляются один раз при определении функции.