Size: a a a

PyWay – гуру Python 🐉

2020 September 02
PyWay – гуру Python 🐉
Часть 4 статьи по разработке GUI на Kivy для сетевой игры в «Дурака» на Python готова! Каюсь, что так долго, но обстоятельства. В процессе разработки я столкнулся с некоторым число проблем и загвоздок, которые появляются, когда вы уходите за рамки Hello-world-ов и пытаетесь создать реальное полноценное приложение или игру. Попытался коснуться их в этой части статьи и привести решения, которые смог подобрать к ним. Кому-то решения покажутся кустарными и неумелыми, но что ж? Это мой первый проект на Kivy, вы в праве поправить меня, если что не так, дать совет, а еще лучше – форкайте код на GitHub и вносите изменения на свой вкус. Приятного чтения!
источник
2020 September 15
PyWay – гуру Python 🐉
Пускай имеется такая задача: дан список с численными элементами. Требуется найти и вернуть первый отрицательный элемент. Казалось бы, должна быть какая-нибудь встроенная функция для этого, но нет. Придется писать ее самим. Решение в лоб:

items = [1, 3, 5, -17, 20, 3, -6]

for x in items:
   if x < 0:
       print(x)
       break
else:
   print('not found')


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

list(filter(lambda x: x < 0, items))[0]

По-моему, стало гораздо сложнее, хоть и в одну строку. А может лучше так:

[x for x in items if x < 0][0]

Что ж, теперь лаконичнее, но все равно не идеал. Какая самая большая ошибка здесь? В обоих вариантах кода идет перебор всего итератора до конца, а потом отбрасываются все лишние значения, кроме нулевого индекса. Тогда как изначальный код останавливается, найдя нужно значение, экономя и время, и память.

Лучше использовать встроенную функцию next – она возвращает следующий элемент из итератора, а в качестве итератора мы напишем генераторное выражение с if. Вот так:

next(x for x in items if x < 0)

Вот это коротко, экономно и очень по-питоновски. Остается одна проблемка: если элемент не найден, что будет брошено исключение StopIteration. Чтобы подавить его, достаточно вторым аргументом в next передать значение по-умолчанию, которое безшумно возвращается, если не найдено ни одного подходящего элемента. И не забыть обернуть генераторное выражение в скобки:

items = [1, 2, 4]
result = next((x for x in items if x < 0), 'not found')
print(result)  # not found


С произвольной функцией, задающей критерий поиска (ее еще называют предикат – predicate) это выглядит так:

def is_odd(x):
   return x % 2 != 0

next(x for x in items if is_odd(x))

# или еще лучше
next(filter(is_odd, items))


Так как в Python 3 filter работает лениво, как и генератор, она не "обналичивает" весь исходный список через фильтр, а лишь идет до первого удачно выполненного условия. Любите итераторы! ✌️
источник
2020 September 30
PyWay – гуру Python 🐉
Немного комбинаторики

Если вы не знали, то в модуле itertools в Python есть ряд комбинаторных функций. Туда входят:

product() – прямое (Декартово) произведение одной или нескольких последовательностей.
permutations() – перестановки и размещения элементов последовательности.
combinations() – уникальные комбинации из элементов последовательности.
combinations_with_replacement() – комбинации с замещениями.

Например:

>>> print(*product([1, 2, 3], 'XY'))
(1, 'X') (1, 'Y') (2, 'X') (2, 'Y') (3, 'X') (3, 'Y')

>>> print(*combinations([1, 2, 3], 2))
(1, 2) (1, 3) (2, 3)

>>> print(*permutations([1, 2, 3], 2))
(1, 2) (1, 3) (2, 1) (2, 3) (3, 1) (3, 2)


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

В конце текста вас ждет бонус: функция по подбору паролей, основанная на combinations_with_replacement(). Не зря же они ее туда добавили? :)
источник
2020 October 15
PyWay – гуру Python 🐉
MRO – это method resolution order, то есть порядок разрешения методов.
Написал небольшую заметку, из которой вы узнаете:

‣ Что такое MRO и как его считать?
‣ По каким правилам работает алгоритм для вычисления MRO?
‣ Когда он не работает?
‣ Как задать произвольный MRO?

Код реализации алгоритма линеаризации я разместил на repl.it, можно поиграться прямо в браузере. https://t.me/iv?url=https://tirinox.ru/mro-python/&rhash=56b30beec7290d
источник
2020 October 28
PyWay – гуру Python 🐉
Как правильно бросить исключение без параметров?
Анонимная викторина
37%
raise MyException()
34%
raise MyException
29%
Оба варианта прокатят
Проголосовало: 260
источник
2020 October 29
PyWay – гуру Python 🐉
После того, как мы узнали, что такое MRO, можно поговорить про функцию super().
Важно знать, что super() возвращает не сам объект-родитель, а прокси-объект (встроенного класса super), который перенаправляет вызовы к методам родительских классов. Функция super() работает в соответствии с порядком MRO, а значит, что в некоторых случаях она будет обращаться не только к непосредственным родителям класса, но и к собратьям класса. Также, функцию super() можно вызывать вне определения классов, но при этом необходимо указать хотя бы один параметр.
Читайте далее:
источник
2020 November 23
PyWay – гуру Python 🐉
Когда вы определяете функцию внутри другой функции и используете локальные переменные внешней функции во вложенной, вы создаете замыкание. Время жизни этих переменных "продляется" в особой области видимости enclosing даже после завершения работы внешней функции. Пример: make_adder возвращает функцию-прибавлятор. Объект из переменной a будет жить и работать даже после выхода из make_adder:

def make_adder(a):
   def adder(x):
       return a + x
   return adder

plus_5 = make_adder(5)
print(plus_5(3))  # 8


Здесь я хочу коснуться одной популярной проблемы. Дело в том, что если мы создадим несколько функций внутри одного контекста, то они будут разделять одну область видимости enclosing. Рассмотрим пример создания трех функций в цикле:

def make_adders():
   adders = []
   for a in range(3):
       def adder(x):
           return a + x
       adders.append(adder)
   return adders

adders = make_adders()
for adder in adders:
   print(adder(2))  # 4 4 4


Вместо функций прибавляющих разные числа от 0 до 2, мы получили 3 одинаковых функции, потому что внутри себя они поддерживают ссылку на одну и ту же переменную a, значение которой останется равным 2 после выполнения всего цикла целиком.

Есть простой прием, помогающий "зафиксировать" значения переменной в моменте: достаточно добавить во вложенную функцию дополнительный аргумент со значением по умолчанию, равным нужной переменной a=a:

def make_adders():
   adders = []
   for a in range(3):
       def adder(x, a=a):  # FIX!
           return a + x
       adders.append(adder)
   return adders

adders = make_adders()
for adder in adders:
   print(adder(2))  # 2 3 4


Еще лучше переименовать аргумент, чтобы избежать конфликтов имен и замечаний IDE, например, так:

def adder(x, that_a=a):  # FIX!
   return that_a + x
источник
2020 November 30
PyWay – гуру Python 🐉
Пока писал предыдущий пост, я наткнулся на одну обманку. Люблю оформлять функции, возвращающие коллекции, как генераторы с ключевым словом yield. Вот так:

def make_adders():
   for a in range(3):
       def adder(x):
           return a + x
       yield adder


Видите, тут нет фикса a=a из поста выше! Казалось бы, что код должен также содержать в себе баг и выводить "4 4 4", но он работает, как задумано изначально:

adders = make_adders()
for adder in adders:
   print(adder(2))  # 2 3 4


Однако, если мы применим list к генератору, извлекая все значения разом, то баг вернется:

adders = list(make_adders())
for adder in adders:
   print(adder(2))  # 4 4 4


Разгадка. В первом случае происходят следующие действия:
a = 0
yield функцию (a + x), make_adders становится на паузу
⋅ печать adder(2) = 0 + 2 = 2
make_adders запускается
a = 1
yield функцию (a + x), пауза
⋅ печать adder(2) = 1 + 2 = 2
И так далее. То есть мы запускаем adder только один раз в тот момент, пока переменная a еще равна нужному значению.

Во втором код list прокручивает make_adders до конца, оставляя a = 2, и все функции выдают одинаковый результат.

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

Еще кое-что.

adders = make_adders()
for adder in adders:
   print(adder(2))  # 2 3 4


После исполнения цикла в коде выше, генератор adders будет исчерпан. В нем больше не останется значений, и если еще раз запустить цикл по adders, то он пройдет ровно 0 итераций.

next(adders)  # StopIteration

Генератор – вещь одноразовая.
источник
2020 December 21
PyWay – гуру Python 🐉
Сегодня настроение немного поиграть. Вот сделал для вас и для собственного развлечения небольшую демку бродилки в стиле Wolfenstein 3D. Да, так делали 3D графику лет 40 назад, ну и что? Зато весело и просто!
источник
2020 December 25
PyWay – гуру Python 🐉
Асинхронный код – это красиво и эффективно. Обычно можно найти готовую асинхронную версию нужной библиотеки, но все еще часто приходится иметь дело со старым синхронным кодом, который вызывается через loop.run_in_executor.

С помощью предложенного декоратора можно превратить обычную синхронную функцию в асинхронную:

import asyncio
from functools import wraps, partial

def async_wrap(func):
   @wraps(func)
   async def run(*args, loop=None, executor=None, **kwargs):
       if loop is None:
           loop = asyncio.get_event_loop()
       pfunc = partial(func, *args, **kwargs)
       return await loop.run_in_executor(executor, pfunc)
   return run


Примеры использования:

# превращаем обычный sleep в асинхронный
import time
async_sleep = async_wrap(time.sleep)  

# асинхронное удаление файлов
import os
async_remove = async_wrap(os.remove)

# своя функция
@async_wrap
def my_func(duration):
   print("my_func start")
   time.sleep(1)
   print("my_func end")


# применение
async def main():
   await asyncio.gather(async_sleep(3), my_func(2))
   await my_func(1)

asyncio.run(main())


Такой код перестанет быть блокирующим и не будет мешать выполнению других асинхронных корутин.

Если требуется, можете использовать свои loop и executor, передав их именованными аргументами:

await my_func(1, loop=myloop)

Помните, что код "бывших" синхронных функций будет выполняться в другом потоке или даже в другом процессе (если используется ProcessPoolExecutor), что может повлечь за собой неожиданные эффекты. Например, matplotlib на macOS часто падает, если построение графика совершается через executor.
источник
2021 January 01
PyWay – гуру Python 🐉
🎅​ Поздравляю всех с 2021 годом!
Пожалуй, не буду вспоминать, был ли этот год тяжелым или легким. Для каждого из нас он был каким-то своим уникальным.
Хочу лишь пожелать каждому из вас, чтобы будущий год приносил только хорошее, а забирал лишь плохое. Чтобы мы были окружены хорошими людьми, приятным общением и позитивными эмоциями. Чтобы мы занимались больше тем, чем нам нравится, а остальным пусть занимаются роботы, которых мы запрограммируем. ☃️️​ 
Желаю, чтобы мы больше развивались, путешествовали, творили, заводили новые знакомства и улучшали свое благосостояние.
А главное, чтобы не болели. Не важно чем, и как это назовут в новостях. 
Берегите себя и спасибо, что вы здесь! 🎄​ 👨‍👩‍👧‍👦​ 🎇​​​

Код для генерации картинки на Python, как всегда, прилагается.
источник
2021 January 19
PyWay – гуру Python 🐉
Этот небольшой постик должен быть целиком в канале, но из-за непримиримой борьбы парсера Телеграма с символом подчеркивания, мне пришлось вынести текст на сайт. 😤 Просто Телеграм считает двойное подчеркивание (dunder = double underscore) сигналом для курсивного текста, даже если явно указано, что этот текст – код. Пробовал экранировать обратным слэшем, иногда получается, иногда нет. Так что придется нажать на кнопку, чтобы прочесть 👇.
источник
2021 February 02
PyWay – гуру Python 🐉
Именованные кортежи или namedtuple

Тип namedtuple определен в стандартном модуле collections. Этот класс расширяет понятие кортежа, добавляя ему и его полям имена и, таким образом, наделяя их смысловой нагрузкой. Он позволяет писать более читаемый и самодокументируемый код. nametuple можно использовать и как обычный кортеж, или получать доступ к полям по именам, а не только индексам.

Вывоз конструктора namedtuple по сути вернет новый тип с определенными настройками. Пример:

from collections import namedtuple

# новый класс User с полями name и phone
User = namedtuple('User', ['name', 'phone'])

# конкретный юзер - экземпляр
user1 = User('John', phone='+79991002030')
print(user1)  # User(name='John', phone='+79991002030')


Первый аргумент namedtuple – название класса, а второй – список параметров, который также может быть и строкой типа 'name, phone' или даже 'name phone'. Имена полей любые, кроме зарезервированных слов и имен, начинающихся с подчеркивания.

Работа с namedtuple:

>> Point = namedtuple('Point', ['x', 'y'])
>> p = Point(x=11, y=22)  # создание
>>> p[0] + p[1]    # доступ по индексам
33
>>> p.x + p.y   # доступ по именам
33
>>> x, y = p   # распаковка, как обычный кортеж
>>> x, y
(11, 22)
>>> p    # читабельное repr
Point(x=11, y=22)
>>> Point._make((22, 33))  # создание из обычного кортежа или другого итерируемого объекта
Point(x=22, y=33)
>>> p._asdict()  # представление в форме словаря
{'x': 11, 'y': 22}


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

Кроме описанных выше возможностей, namedtuple позволяет также:
1. Задавать значения поле по умолчанию
2. Добавлять док-стринги (подсказки) к самому классу и отдельным полям
3. Создавать копии кортежа с заменой полей
4. Расширять функциональность путем наследования

Об этим возможностях расскажу в следующем посте.
источник
2021 February 03
PyWay – гуру Python 🐉
Значения по умолчанию для namedtuple (Python 3.7+)

from collections import namedtuple
User = namedtuple('User', ['name', 'phone'])
user = User(name='Jack')  # ошибка! не указан phone!


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

User = namedtuple('User', ['name', 'phone'], defaults=('NoName', 'NoPhone'))
user = User(name='Jack')
>> user
User(name='Jack', phone='NoPhone')
>>> User()  # вообще без параметров
User(name='NoName', phone='NoPhone')


Чтобы устранить исключение о недостающих полях в конструкторе, достаточно передать defaults как кортеж из None:

fields = ('val', 'left', 'right')
Node = namedtuple('Node', fields, defaults=(None,) * len(fields))

>>> Node()
Node(val=None, left=None, right=None)


🤸Вообще говоря, defaults могут быть и короче, чем список полей. В таком случае значения по умолчанию применяются только к самым правым полям. Чтобы было понятно, вот пример:

Node = namedtuple('Node', ('val', 'left', 'right'), defaults=(100, 200))

Так как defaults из двух элементов, а полей – три, то они применятся только к последним полям 'left' и 'right', а 'val' останется без дефолтного значения, вынуждая нас всегда его указывать:

>>> Node()
TypeError: __new__() missing 1 required positional argument: 'val'
>>> Node(13)
Node(val=13, left=100, right=200)
>>> Node(val=42)
Node(val=42, left=100, right=200)


💣 Важно! namedtuple, как и обычный tuple является неизменяемым типом (immutable), однако, вам не запрещено в качестве поля использовать изменяемый тип, к примеру, list или dict, так как кортеж содержит лишь ссылку на объект, а за содержимое самого объекта он не отвечает. Поэтому, не рекомендуется делать значением по умолчанию списки, сеты и словари, а также пользовательские изменяемые объекты.

Посмотрите, здесь все экземпляры Group будут разделять один и тот же список по умолчанию для поля 'users':

Group = namedtuple('Node', ('name', 'users'), defaults=('5B', []))  # плохо!
g = Group()
g.users.append('Vanya')  # повлияет на g2 тоже!
g2 = Group()
print(g2.users)  # ['Vanya']


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

def new_empty_group(name='5B'):
   return Group(name=name, users=[])
источник
2021 February 04
PyWay – гуру Python 🐉
Замена поля в namedtuple

namedtuple, будучи кортежем, является неизменяемым типом. Однако, метод _replace возвращает новый объект, в котором отредактированы выбраные поля, а все остальные равны значениям из предыдущего кортежа.

from collections import namedtuple
Book = namedtuple('Book', ['id', 'title', 'authors'])
book1 = Book(1, 'Игрок', 'Достоевский Ф.М.')
book2 = book1._replace(id=2, title='Преступление и наказание')
>>> book1
Book(id=1, title='Игрок', authors='Достоевский Ф.М.')
>>> book2
Book(id=2, title='Преступление и наказание', authors='Достоевский Ф.М.')


⚠️ Метод replace делает поверхностную копию данных, то есть копирует ссылки. Если со строчками и числами все будет в порядке (они скопируются), то ссылка на список будет разделяться между обоими объектами.

Документация namedtuple

Имеется возможность снабдить сам класс и его поля документацией (doc-strings):

# сам класс
Book.__doc__ += ': Hardcover book in active collection'
# его поля
Book.id.__doc__ = '13-digit ISBN'
Book.title.__doc__ = 'Title of first printing'
Book.authors.__doc__ = 'List of authors sorted by last name'


Наберите в интерпретаторе help(Book) и увидите, что у Book есть теперь описание класса и всех полей. А также по умолчанию namedtuple добавляет кучу стандартной документации по методам класса.
источник
2021 February 10
PyWay – гуру Python 🐉
Можно расширить функциональность namedtuple, создав класс-наследник с целью добавлять в него новые методы и свойства, не теряя всех преимуществ namedtuple. Например, для точки можно определить метод hypot, рассчитывающий расстояние от начала координат.

class Point(namedtuple('Point', ['x', 'y'])):
   _ _ slots = ()
   @property
   def hypot(self):
       return (self.x  2 + self.y  2) ** 0.5
   def __str__(self):
       return f'Point: x={self.x}  y={self.y}  hypot={self.hypot}'

>>> print(Point(1, 2))
Point: x=1  y=2  hypot=2.23606797749979


Однако, вы не можете менять значения полей внутри методов, так как кортеж – неизменяемый тип данных.

Существует еще один экзотический способ создать класс от namedtuple, причем с "типизированными" полями:

from typing import NamedTuple

class Employee(NamedTuple):
   name: str
   id: int


Это эквивалент такой записи:

Employee = namedtuple('Employee', ['name', 'id'])

Однако, первый вариант смотрится выразительнее и помогает IDE анализировать код.

По сравнению с обычными классами namedtuple также неплохо экономит оперативную память. Вот посмотрите этот пример. Обычный класс с двумя атрибутами занимает целых 328, когда как схожий namedtuple – всего 120.

Строка _ _ slots = () здесь играет особую роль. Я еще не рассказывал про слоты, но если вкратце, то таким образом мы предотвращаем создание внутреннего словаря для данного класса, что экономит еще 8 байт.

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

Вывод: namedtuple позволяет быстро и удобно создавать небольшие неизменяемые классы для хранения данных. Такие классы эффективны по объему используемой памяти и уже содержат большое количество вспомогательных функций, таких как инициализация, сравнение, хэширование, представление данных, преобразование в словарь и так далее. Также, namedtuple наследует поведение обычного кортежа tuple, в том плане, что можно обратиться к полям по индексам или распаковать их в отдельные переменные.

Но не спешите переделывать все ваши классы на namedtuple, потому что:
1. namedtuple не изменяем
2. без танцев с бубнами невозможно наследование
3. много прочих проблем с ООП

Если вам нужен изменяемый аналог namedtuple, то советую присмотреться к dataclass. О нем я расскажу в будущих постах.
источник
2021 February 24
PyWay – гуру Python 🐉
@dataclass@dataclass

Классы данных появились в Python с версии 3.7. Изначально они описаны в PEP-0557.
Главным образом, dataclass предназначены для структурированного хранения данных. Они облегчают работу программиста путем автоматической генерации рутинного кода, а именно инициализации, сравнения, представления repr и прочего.

Короче, если обычно мы пишем так:

class Computer:
   def __init__(self, cores, ram=2048):
       self.cores = cores
       self.ram = ram

   def __repr__(self):
       return f'Computer({self.cores}, {self.ram})'

   # прочие методы, eq


Это можно сократить до:

from dataclasses import dataclass

@dataclass
class Computer:
   cores: int
   ram: int = 2048

print(Computer(2))  # Computer(cores=2, ram=2048)

Как видите, очень лаконично и читабельно. Здесь типы (аннотации) – обязательная вещь. Без них поле не будет считаться атрибутом экземпляра класса, то есть для него не будет сгенерирован код!

@dataclass
class Number:
   value: int  # поле, сгенерируется код
   foo = 10  # просто переменная уровня класса, будет проигнорирована

print(Number(10, foo=20))  # ошибка!


Существует еще более короткий способ определения класса данных:

from dataclasses import make_dataclass
Computer = make_dataclass('Computer', ('ip', 'cores', 'ram'))


Как видите, dataclass очень похожи на namedtuple из прошлых постов, однако они НЕ являются кортежами и вообще внутренне устроены иначе, словно обычные класс, в которых кто-то за нас определил init и прочие полезные методы. По умолчанию dataclass является изменяемым типом. То есть его методы могут менять значения полей.

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

Напоследок, вот так легко можно превратить dataclass в dict, например, для последующего сохранения в JSON:

from dataclasses import asdict
asdict(Computer(cores=8, ram=2**15))  # {'cores': 8, 'ram': 32768}


Официальная документация dataclass на английском.

Темы далее:

1. Настройка dataclasses
2. Наследование
3. Пост-инициализация
4. Хитрости с полями list, dict
источник
2021 March 19
PyWay – гуру Python 🐉
Настройка dataclass

В предыдущем посте мы использовали dataclass без параметров, однако у него есть возможности включить или выключить разные генерации методов:

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
  ...

Краткое объяснение каждого параметра (в скобках – значения по умолчанию):

1. init (вкл) – генерация метода инициализации. Игнорируется, если у класса уже есть свой init.
2. repr (вкл) – генерации метода repr для класса. Дает удобное представление класса при печати его в консоль или в логи.
3. eq (вкл) – генерировать ли метод равенства ==? Сравниваются все поля по очереди (похоже на tuple).
4. order (выкл) – добавить ли методы сравнения (>, ≥, ≤, <)? Принцип сравнения такой же, как у tuple – поля сравниваются по порядку сверху вниз.
5. frozen (выкл) – заморозка полей. При попытке присвоения к полям класса возникнет исключение. Таким образом класс становится только для чтения (чем-то похож на namedtuple).
6. unsafe_hash (выкл) – отвечает за генерацию метода hash. Почему unsafe (то есть небезопасный)? Потому что ваш класс может быть мутабельным (изменяемым), поэтому вы сами берете на себя ответственность за соответствия содержимого класса и его хэша. Ситуация, когда сначала от экземпляра берут хэш, чтобы, например, сохранить его во множестве, а потом поля класса меняются, приводит к наличию двух одинаковых элементов во множестве. Это противоречит его смыслу, но возможно. У меня был пост об этом.
Когда у вас frozen=True и eq=True, то класс считается уже неизменяемым, и к нему автоматически генерируется hash-функция, поэтому не требуется дополнительно ставить unsafe_hash=True.

По поводу сравнения на больше-меньше, чтобы понимали, даю пример:

from dataclasses import dataclass

@dataclass(order=True)
class Player:
   name: str
   score: int

p1 = Player('Maksim', 10)
p2 = Player('Ivan', 30)
p3 = Player('Ivan', 40)

print(f'{p1} > {p2} = {p1 > p2}')  # True: Maksim > Ivan
print(f'{p2} > {p3} = {p2 > p3}')  # False: Ivan == Ivan, но 30 < 40!


Сначала сравниваются имена (как строки). Строки сравниваются посимвольно: можете себе представить, что ищите слово в бумажном словаре, так вот там слово Maksim будет стоять всегда после слова Ivan. Зная, что Maksim > Ivan, получается что p1 > p2. Сравнение даже не смотрит на поле score (счет).

Во втором случае, оба игрока имеют одинаковые имена Ivan, и метод сравнения переходит уже к полю score. Следовательно, p2 > p3 == False, потому что p2.score < p3.score.

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

В сравнениях и равенствах участвуют только те поля, у которых вы пометили тип через аннотации (или задали через функцию field).
Функция field позволяет настраивать каждое поле класса индивидуально. Это тема для отдельного поста.
источник
2021 March 23
PyWay – гуру Python 🐉
from dataclasses import dataclass, field

Функция field позволяет настроить каждое поле dataclass индивидуально. Можно создавать поля и без field, задав просто тип и при необходимости значение по умолчанию. Однако, есть случаи, когда без field не обойтись. Давайте рассмотрим по очереди все его настройки. Итак, полный прототип функции выглядит так:

field(*, default=MISSING, default_factory=MISSING, repr=True, hash=None, init=True, compare=True, metadata=None)

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

1) default – значение поля по умолчанию. То же самое, что идет после равно, если мы не используем field.

@dataclass
class Foo:
   x: int = field(default=10)  # x: int = 10


Этот способ указания значения по умолчанию подходит для неизменяемых типов данных: чисел (int, float, ...), строк (str), кортежей (tuple).

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

2) default_factory – это второй способ задать значение по умолчанию. Здесь вы предоставляете фабрику – функцию без аргументов, которая будет вызываться КАЖДЫЙ раз при создании экземпляра класса, а результат ее работы будет записан в данное поле.
Такой способ обязателен для изменяемых (мутабельных) типов данных, таких как списки (list), словари (dict), множества (set) и пользовательские классы с изменяемыми полями.

Пример. Пусть одно из полей – список, тогда нельзя просто взять и написать для него = []. В последних версиях Python здесь будет исключение уже на этапе определения класса! Иначе бы все экземпляры Class разделяли бы один и тот же объект списка в поле students:

@dataclass
class Class:
   number: int
   students: list = []  # Ошибка!


Здесь следует задать field с указанием default_factory.

students: list = field(default_factory=lambda: [])
# или еще короче и правильнее вот так:
students: list = field(default_factory=list)


Функция list без аргументов каждый раз возвращает НОВЫЙ пустой список. Аналогично можно писать default_factory=set или default_factory=dict или даже default_factory=MyClass, если у класса MyClass есть конструктор без обязательных аргументов.

3) repr – этот параметр позволяет исключить поле из строкового представления класса. Например, если поле – пароль, вы можете пожелать, чтобы оно не отображалось в логах:

@dataclass
class User:
   name: str
   password: str = field(repr=False)

admin = User('admin', '1234')
print(admin)  # User(name='admin')


4) init – включать или нет поле в код инициализации init. Возможно, вы хотите вычислить это поле во время пост-инициализации и не требовать его в конструкторе класса, тогда init=False – ваш выбор.

5) compare – включать ли поле в методы сравнения (равно, больше, меньше)? Если конкретное поле не должно влиять на результат сравнения или равенства двух экземпляров класса, просто выключите его через compare=False:

@dataclass(order=True)
class User:
   name: str = field(compare=False)
   age: int

ken = User('Ken', 20)
linda = User('Linda', 18)
print(ken > linda)  # True


В примере сравнивается только age. Попробуйте убрать compare=False и увидите, что результат сравнения изменится.

6) hash – может быть True, False или None. Определяет, участвует ли данное поле в хэш-функции. Если None (это так по умолчанию), то решение принимается на базе compare: если поле участвует в сравнениях, то и учитывается при вычислении хэша от класса. Если True – то всегда участвует в хэш-функции, если False – то всегда исключается.

7) metadata – словарь для метаданных, иначе говоря для каких-то дополнительных свойств этого поля, которые разработчик придумывает сам. Напрямую это обычно не используется, а встречается, как правило, в коде сторонних библиотек. Например, туда можно прикрепить название колонки в SQL-таблице или ключ для сериализации.
источник
2021 April 26
PyWay – гуру Python 🐉
😷 Друзья, прошу прощения, что не писал ничего вот уже месяц. Злобные вирусы нас никак не отпускают,
а оплачиваемых больничных у вольных разработчиков не бывает. Сегодня будет коротенький, но полезный постик.
источник