Рубрика: Программирование

Посты, связанные с разработкой ПО.

А вы знали про hash(-1)?

(Речь идет о реализации CPython)

Встроенная функция hash возвращает целое число – хэш-сумму, которое используется при сравнении ключей словаря во время поиска, например. Для пользовательских классов hash вызывает магический метод класса  __hash__ , а для примитивных типов уже есть встроенная реализация на Си. 

Примечательно, что для чисел hash обычно возвращает само же значение числа-аргумента, кроме нескольких случаев. Запустим этот код:

def print_hash(x):
    print(f'hash({x}) = {hash(x)}')
for i in range(2, -4, -1):
    print_hash(i)

Вывод:

hash(2) = 2
hash(1) = 1
hash(0) = 0
hash(-1) = -2  <-- что?
hash(-2) = -2
hash(-3) = -3

Оказывается hash не возвращает -1, а конвертирует его явно в -2. Я изучил исходный код на Си и нашел это место. «Легенда гласит», что в CPython число -1 зарезервировано внутренне для индикации ошибок при выполнении этой функции.

Еще интереснее для рациональных чисел. От hash от NAN – ноль. Плюс еще пасхалка: hash от бесконечности возращает первые несколько цифр числа π. 

print_hash(-1.0)  # -2
print_hash(float('nan'))  # 0
print_hash(float('+inf'))  # 314159
print_hash(float('-inf'))  # -314159

🧙 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Тонкости try

Что вернет функция foo()?
def foo():
    try:
        return 'try'
    finally:
        return 'finally'

foo()

Правильный ответ будет ‘finally’:

Дело в том, что функция возвращает результат последнего выполненного return. А, учитывая, что блок finally всегда выполняется, то будет выполнено два return, последний из них будет return ‘finally’.

Что будет при вложенных блоках finally?
# вспомогательная функция, чтобы считать return-ы
def returner(s):
    print(f'  return {s}')
    return s

def foo():
    try:
        return returner('try')
    finally:
        return returner('finally')

print('Result: ', foo())

print('-' * 50)

def baz():
    try:
        try:
            return returner('try')
        finally:
            return returner('finally inner')
    finally:
        return returner('finally outer')

print('Result: ', baz())

Вывод:

  return try
  return finally
Result:  finally
--------------------------------------------------
  return try
  return finally inner
  return finally outer
Result:  finally outer

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

Еще один коварный вопрос про try и finally.

Что будет при выполнении кода?

for i in range(10):
    try:
        print(1 / i)
    finally:
        print('finally')
        break

На первой итерации цикла произойдет исключение из-за деления на 0. Блока except нет. Но тем не менее исключение все равно будет подавлено, потому что в блоке finally есть break. Вот такая особенность языка. В будущих версиях (3.8+) тоже самое должно работать и с конструкцией continue.

🧙 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Склеиваем пути правильно

Так делать плохо:

my_path = root + '/' + user + '/' + filename

Потому что:

  • В разных ОС – разные разделители пути: ‘/’ для nix-подобных и macOS, ‘\\’ для Windows
  • В компонентах могут быть или не быть слеши – легко допустить ошибку
  • Набирать это даже не удобно (имхо)

Самый простой способ правильного склеивания путей – os.path.join выберет нужный разделитель и расставит его как надо:

my_path = os.path.join(root, user, filename)

Есть еще более современный и удобный способ, который также поставляется в стандартной библиотеке Python – модуль pathlib. Это библиотека для работы с путями и файлами в стиле ООП. Примечательно, что объект Path поддерживает оператор /, который собственно и склеивает пути:

my_path = Path(root) / user / filename

У класса Path есть куча методов для получения путей в разных форматах, извлечения компонент пути, получении инфо о файлах и папках и много другое. Вот лишь некоторые из них:

>>> Path('~').expanduser()
PosixPath('/Users/bob')
>>> Path('~/../../usr').expanduser().resolve()
PosixPath('/usr')

>>> Path.cwd()
PosixPath('/Users/bob')

>>> Path('/usr/bin/foo').parts
('/', 'usr', 'bin', 'foo')

>>> Path('my/library.tar.gar').suffixes
['.tar', '.gar']

>>> Path('my/library.tar.gar').parent
PosixPath('my')

>>> str(Path('/usr/bin/foo'))
'/usr/bin/foo'

>>> sorted(Path('Projects/playground_python').glob('*.py'))
[PosixPath('Projects/playground_python/btc_gen.py'), PosixPath('Projects/playground_python/getattr.py'), ...]

>>> Path('test.txt').touch()
>>> Path('test.txt').exists()
True
>>> Path('test.txt').is_file()
True
>>> Path('test.txt').is_dir()
False
>>> Path('test.txt').is_symlink()
False

>>> Path('temp/1/foo').mkdir(parents=True, exist_ok=True)
>>> Path('temp/1/foo').resolve().as_uri()
'file:///Users/bob/temp/1/foo'
>>> Path('temp/1/foo').rmdir()

И еще очень много всего!

🤩 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway! 👈

Счастливой отладки

Человек чинит наклоненный на 45 градусов автомобиль

Python не запрещает создавать переменные и функции с именами, идентичными встроенным. Шутки ради переопределим print:

import sys

# счастилвой отладки!
def print(*values, sep=' ', end='\n', file=sys.stdout, flush=False):
    # шутка
    def joke(value):
        if type(value) is str:
            value = value[::-1]
        elif type(value) is int:
            value += 1
        return value

    # отсюда достанем исходную версию print
    import builtins
    return builtins.print(*map(joke, reversed(values)),
                          sep=sep, end=end, file=file, flush=flush)


print('Hello, world!', '2 x 2 =', 4)  # 5 = 2 x 2 !dlrow ,olleH

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

🤩 Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway!👈

Слабые ссылки

Недавно в заметке про управление памятью в Python мы упоминали слабые ссылки. По опросу на моем канале лишь 1 человек из 4 знал про слабые ссылки в Python, и лишь 6% читателей их применяли. Что же это такое? Слабые ссылки позволяют получать доступ к объекту, как и обычные, однако, так сказать, они не учитываются в механизме подсчета ссылок. Другими словами, слабые ссылки не могут поддерживать объект живым, если на него не осталось больше сильных ссылок.

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

Встроенный модуль weakref отвечает за функциональность слабых ссылок.

📎 Пример. Создаем класс Foo, сильную ссылку на его экземпляр, затем слабую ссылку, проверяем:

import weakref

class Foo: ...

strong_foo = Foo()
weak_foo = weakref.ref(strong_foo)
print(weak_foo())   # вызов слабой ссылки - доступ к исходному объекту
print(weak_foo() is strong_foo)  # True

del strong_foo  # это была последняя сильная ссылка
print(weak_foo())  # None

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

Слабые ссылки можно создавать на пользовательские классы, на set и на подклассы от dict и list, но не на сами dict и list. Встроенные типы tuple, int и подобные не поддерживают слабые ссылки (да и зачем они им?).  

В weakref.ref вторым аргументом можно передать функцию, которая будет вызвана при финализации объекта слабой ссылки:

weak_foo = weakref.ref(strong_foo, lambda r: print(f'finalizing {r}'))

weakref.getweakrefcount(object) и weakref.getweakrefs(object) позволяют получить количество слабых ссылок на объект и их сами.

weakref.proxy(object[, callback]) – создает слабый прокси-объект к объекту. Т.е. с ним можно обращаться также как и исходным, пока он не удалится. Попытка использовать прокси к уничтоженному объекту вызовет ReferenceError.

Наконец, к предназначению слабых ссылок: организацию кэшей. Есть типы:

weakref.WeakSet – как set, но элементы хранятся по слабым ссылкам и удаляются, если на них больше нет сильных ссылок

weakref.WeakKeyDictionary – как dict, но КЛЮЧИ по слабым ссылкам.

weakref.WeakValueDictionary – как dict, но ЗНАЧЕНИЯ по слабым ссылкам.

📎 Пример:

import weakref

class Foo: ...
f1, f2 = Foo(), Foo()

weak_dict = weakref.WeakValueDictionary()
weak_dict["f1"] = f1
weak_dict["f2"] = f2

def print_weak_dict(wd):
    print('weak_dict: ', *wd.items())

print_weak_dict(weak_dict)  # оба в словаре

del f2
print_weak_dict(weak_dict)  # один ушел

del f1
print_weak_dict(weak_dict)  # ничего не осталось

📎 Наконец, можно следить за тем, когда объект будет удален:

f = Foo()
# просто установим обработчик (finalize сам никого не удаляет)
weakref.finalize(f, print, "object dead or program exit")
del f  # а вот тут print вызовется

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway!