Shanraq.org Shanraq.org
Переменные и типы: почему «260» и 260 — разные вещи
IT

Python: от данных до своей сводки Урок 4 из 56

Переменные и типы: почему «260» и 260 — разные вещи

Четвёртый урок курса по Python. Данные из чужого файла приходят текстом, и пока текст не стал числом, складывать его бессмысленно: «260» + «260» даёт 260260. Измерено и главное денежное правило: 0.1 + 0.2 не равно 0.3, а Decimal — равно. Плюс None, истинность строки «0» и разница между == и is.

Зачем это нужно

В первом уроке мы взяли числа у Всемирного банка, и они пришли готовыми. В жизни так бывает редко: чаще всего цифры приезжают строкой — из CSV, из формы, из чужого отчёта. И тут выясняется, что "260" и 260 для Python — разные вещи, а сложить их одинаково не получится.

Второе, ради чего написан урок: деньги нельзя считать так, как учат в школе. Почему — увидите в третьем блоке вывода.

Сразу целиком

Файл tipter.py. Запуск: python tipter.py из окружения.

Программа длинная — шестьдесят строк. Держать в голове нужно первую половину: строка в число, деньги в Decimal. Остальное (None, истинность, is) — то, что достаточно узнать в лицо и вернуться, когда встретится.

"""Урок 4: переменные и типы — на числах, которые пришли строкой.

Из CSV, из формы и из обычного текстового файла значения чаще всего приходят
текстом. Пока текст не превращён в число, складывать его бессмысленно:
сложение строк даёт не то, что ожидают.
"""

from decimal import Decimal

# Переменная — это имя для значения. Имя пишут маленькими буквами, слова
# разделяют подчёркиванием: так принято во всём Python.
price_text = "260"
count = 2

print("== одно и то же на вид, разное по сути")
print("строка:", price_text, type(price_text))
print("число: ", 260, type(260))
print("строка + строка:", price_text + price_text)
print("число + число: ", 260 + 260)

print()
print("== превращаем текст в число")
price = int(price_text)
print("int(\"260\"):", price, type(price))
print("с пробелами тоже:", int("  260  "))
print("дробное:", float("260.5"), type(float("260.5")))
print("обратно в текст:", str(price) + " тенге")
print("сумма:", price * count, "тенге")

print()
print("== а вот это не превратится")
try:
    int("260,5")
except ValueError as err:
    print("int(\"260,5\") →", type(err).__name__, "—", err)

print()
print("== почему деньги не считают в float")
print("0.1 + 0.2 =", 0.1 + 0.2)
print("0.1 + 0.2 == 0.3 →", 0.1 + 0.2 == 0.3)
print("округлили до тиына:", round(0.1 + 0.2, 2))
print("с Decimal:", Decimal("0.1") + Decimal("0.2"))
print("Decimal сравнивается честно →", Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))

print()
print("== да, нет и пусто")
print("bool(0):     ", bool(0))
print("bool(260):   ", bool(260))
print('bool(""):    ', bool(""))
print('bool("0"):   ', bool("0"), "← строка непустая, значит истина")
print("bool(None):  ", bool(None))
print("None — это не ноль и не пустая строка:", None == 0, None == "")

print()
print("== сравнения и логика")
budget = 1000
print("хватает на две:", price * count <= budget)
print("хватает и не дороже 300 за штуку:", price * count <= budget and price <= 300)
print("или дёшево, или мало:", price <= 100 or count <= 2)
print("не дороже 300:", not price > 300)

print()
print("== == сравнивает значения, is — один ли это объект")
first = [260, 2]
second = [260, 2]
print("first == second:", first == second)
print("first is second:", first is second)
print("first is first:  ", first is first)
nothing = None
print("правильная проверка на пусто: nothing is None →", nothing is None)

Вывод:

== одно и то же на вид, разное по сути
строка: 260 <class 'str'>
число:  260 <class 'int'>
строка + строка: 260260
число + число:  520

== превращаем текст в число
int("260"): 260 <class 'int'>
с пробелами тоже: 260
дробное: 260.5 <class 'float'>
обратно в текст: 260 тенге
сумма: 520 тенге

== а вот это не превратится
int("260,5") → ValueError — invalid literal for int() with base 10: '260,5'

== почему деньги не считают в float
0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3 → False
округлили до тиына: 0.3
с Decimal: 0.3
Decimal сравнивается честно → True

== да, нет и пусто
bool(0):      False
bool(260):    True
bool(""):     False
bool("0"):    True ← строка непустая, значит истина
bool(None):   False
None — это не ноль и не пустая строка: False False

== сравнения и логика
хватает на две: True
хватает и не дороже 300 за штуку: True
или дёшево, или мало: True
не дороже 300: True

== == сравнивает значения, is — один ли это объект
first == second: True
first is second: False
first is first:   True
правильная проверка на пусто: nothing is None → True

Разбор

Переменная — имя для значения

price_text = "260"
count = 2

Знак = не «равно», а «присвоить»: справа значение, слева имя, под которым оно теперь известно. Имена пишут маленькими буквами, слова разделяют подчёркиванием — price_text, а не PriceText. Это не каприз: так пишет весь Python, и чужой код будет читаться привычно.

Тип — это то, что значение умеет

type() показывает, чем значение является:

строка: 260 <class 'str'>
число:  260 <class 'int'>

На экране они выглядят одинаково. А ведут себя по-разному: "260" + "260" даёт 260260 — строки склеились; 260 + 260 даёт 520.

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

Четыре типа, которых хватит надолго: int — целое, float — с плавающей точкой, str — строка, bool — истина или ложь. Пятое — None, и о нём ниже.

Превращения и их границы

int("260") даёт число. int(" 260 ") — тоже: пробелы по краям он терпит. А вот int("260,5") не сможет:

int("260,5") → ValueError — invalid literal for int() with base 10: '260,5'

Запятая в качестве десятичного разделителя — обычное дело в казахстанских и российских выгрузках, и Python об этом не знает. Такую строку сначала чинят (replace(",", ".")), потом превращают в float.

Обратное превращение — str(260) — понадобится, когда число надо приклеить к тексту. Хотя чаще для этого берут f-строку из прошлого урока.

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

Почему деньги не считают в float

Самое важное в уроке:

0.1 + 0.2 = 0.30000000000000004
0.1 + 0.2 == 0.3 → False

Это не ошибка Python и не его особенность. float хранит число в двоичной системе, а 0.1 в двоичной — бесконечная дробь, как 1/3 в десятичной. Приходится округлять, и на третьем знаке после запятой вылезает хвост.

Для роста цен на графике это неважно. Для денег — важно: тиыны накапливаются, и сумма чека не сходится с итогом в кассе.

Два выхода, оба в выводе:

  • округлять при показе: round(0.1 + 0.2, 2) даёт 0.3. Годится, когда считаете для человека, а не для бухгалтерии;
  • считать в Decimal: Decimal("0.1") + Decimal("0.2") даёт ровно 0.3, и сравнение возвращает True. Обратите внимание: Decimal берут из строки, а не из float, — иначе хвост попадёт внутрь ещё до сложения.

True, False и None

bool — это два значения: True и False. Но истинность есть у всего, и вот здесь ловушка:

bool(0):      False
bool(""):     False
bool("0"):    True ← строка непустая, значит истина
bool(None):   False

Строка "0" — истина, потому что она не пуста. Данные, пришедшие текстом, лгут здесь особенно охотно: "0" из CSV в проверке if value: окажется истиной, хотя означает ноль.

None — отдельное значение, означающее «ничего нет». Это не ноль и не пустая строка: None == 0 даёт False. Оно понадобится, когда в данных пропуск — а пропуски в реальных таблицах есть всегда.

and, or, not

Три слова вместо значков — читается как фраза:

print("хватает и не дороже 300 за штуку:", price * count <= budget and price <= 300)

and — истина, когда истинны оба. or — когда хотя бы одно. not — переворачивает. Скобки ставьте, когда сомневаетесь: они бесплатны, а разбираться потом дорого.

== сравнивает значения, is — объекты

first == second: True
first is second: False

Два разных списка с одинаковым содержимым равны, но это не один и тот же объект. == спрашивает «одинаковое ли внутри», is — «это одна и та же вещь в памяти».

Практическое правило: is используют почти исключительно с Noneif value is None:. Для всего остального ==. Если написать if value == None, оно чаще всего сработает, но так не пишут.

Карта урока

Карта урока: строка, число и истина

Скажите своими словами

Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.

  1. Почему "260" + "260" не ошибка, хотя результат — не то, что нужно?
  2. Что не так с 0.1 + 0.2 и как считать деньги?
  3. Почему bool("0") — истина?

Разминка

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

1. Предскажите. Что напечатает эта строка? Обратите внимание, что + делает в двух случаях разное.

print(int("8") + 1, "8" + "1")

2. Заполните пропуск. Вместо ... подготовьте строку так, чтобы float её принял.

text = " 1,15 "
clean = ...
print(float(clean))

3. Почините. Программа говорит, что значения нет, хотя оно есть и равно нулю. Замените проверку на правильную.

value = 0.0
print("значения нет:", not value)

Задание

Обязательное. Дано:

first = " 1,15 "
second = "0,25"
third = None

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

Ожидаемый вывод:

первый: 1.15 кг, тип float
второй: 0.25 кг, тип float
третий пуст: True
а ноль — это значение: False
всего 1.40 кг, в среднем 0.700 кг

Готово, когда: вывод совпадает построчно; пропуск проверяется через is None, а не через «пусто»; ни одна строка не превращается в число без strip и замены запятой.

На своих данных. Возьмите из чека три веса — их пишут дробными, и на них видно то, ради чего урок написан: "1,15", " 0,25 ", "0,2" (килограммы). Превратите каждый в число (двум потребуется починка), сложите и напечатайте сумму f-строкой с двумя знаками. Затем посчитайте ту же сумму через Decimal и сравните оба результата с помощью ==.

Напечатанные строки совпадут — обе покажут 1.60, — а == вернёт False. Это и есть ответ на вопрос, зачем деньги считают в Decimal.

По желанию.

  • Проверьте: 0.1 + 0.2 + 0.3 == 0.6? А round(0.1 + 0.2 + 0.3, 2) == 0.6?
  • Напечатайте type() для 5, 5.0, "5", True, None — пять разных ответов.
  • Выясните на своём примере, чему равно bool(" ") — строка из одного пробела.

Куда это встанет в проекте

Сводка получает первое правило работы с чужими данными: пришло текстом — приведи к числу и проверь, что получилось. Дальше мы соберём эти значения в список, а потом в таблицу.

Долги. Мы починили одну строку руками, но в файле их будут тысячи — это циклы. Мы ничего не сохранили — это списки и словари. И мы пока не умеем сказать «если не получилось превратить — пропусти строку»: это урок про исключения.

Ответы

Показать ответы

На вопросы

  1. Потому что у сложения строк есть свой смысл — склейка, — и Python им воспользовался. Он не угадывает намерение; следить за типами должен тот, кто пишет.
  2. float хранит числа в двоичной системе, где 0.1 — бесконечная дробь, поэтому появляется хвост 0.30000000000000004. Для показа человеку хватает round, для денег берут Decimal, создавая его из строки. И сравнивать float с Decimal через == — плохой договор: либо округляйте оба до нужного знака, либо считайте в Decimal с первого символа до последнего.
  3. Потому что истинность строки определяется её длиной, а не содержимым: "0" не пуста, значит истина. Пустая строка "" — ложь.

К разминке

  1. 9 81. Слева int("8") даёт число, и + складывает. Справа обе части — строки, и тот же + их склеивает. Знак один, действий два, и разницу задаёт тип.
9 81
  1. text.strip().replace(",", "."). Пробелы по краям float ещё простит, а запятую — нет: float("1,15") падает с ValueError.
text = " 1,15 "
clean = text.strip().replace(",", ".")
print(float(clean))
1.15
  1. not value спрашивает «пусто ли», а пустыми Python считает и ноль, и пустую строку, и пустой список. Отсутствие значения — это None, и спрашивают о нём прямо:
value = 0.0
print("значения нет:", value is None)
значения нет: False

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

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

Чтобы проверить, нужно войти. Войти

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

Пока нет комментариев. Будьте первым.