#ТаковПуть
Структурная типизация и причём тут утки. ч.2 🦆🦆🦆
Если в прошлый раз мы работали с функциями, тут поработаем с классами.
Пусть будет класс Order, реализация в нем нам неважна, поэтому оставим его пустым.
Теперь реализуем класс OrderStorage, у которого будет метод save
Создадим 2 класса, которые будут реализовывать структуру класса OrderStorage
Создадим функцию, которая будет принимать на вход Order и класс, который будет реализовывать структуру класс OrderStorage
За счет того, что DBStorage и FileStorage реализуют структуру OrderStorage, то функция save_order прекрасно обрабатывает эти классы.
Полный код:
Ниже представлен аналогичный пример, но в другой реализации:
Строка
нужна, чтоб код запустить в одном файле.
Структурная типизация и причём тут утки. ч.2 🦆🦆🦆
Если в прошлый раз мы работали с функциями, тут поработаем с классами.
Пусть будет класс Order, реализация в нем нам неважна, поэтому оставим его пустым.
class Order:
pass
order = Order()
Теперь реализуем класс OrderStorage, у которого будет метод save
from typing import Protocol
class OrderStorage(Protocol):
def save(self, order: Order) -> None:
pass
Создадим 2 класса, которые будут реализовывать структуру класса OrderStorage
class DBStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в БД")
class FileStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в файл")
db_storage = DBStorage()
file_storage = FileStorage()
Создадим функцию, которая будет принимать на вход Order и класс, который будет реализовывать структуру класс OrderStorage
save_order(order, db_storage)
save_order(order, file_storage)
За счет того, что DBStorage и FileStorage реализуют структуру OrderStorage, то функция save_order прекрасно обрабатывает эти классы.
Полный код:
from typing import Protocol
class Order:
pass
order = Order()
class OrderStorage(Protocol):
def save(self, order: Order) -> None:
pass
class DBStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в БД")
class FileStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в файл")
db_storage = DBStorage()
file_storage = FileStorage()
save_order(order, db_storage)
save_order(order, file_storage)
Ниже представлен аналогичный пример, но в другой реализации:
from __future__ import annotations
from typing import Protocol
class OrderStorage(Protocol):
def save(self, order: Order) -> None:
pass
class Order:
def __init__(self, *storages: OrderStorage) -> None:
self.storages = storages
def save_order(self) -> None:
for storage in self.storages:
storage.save(self)
class DBStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в БД")
class FileStorage:
def save(self, order: Order) -> None:
print("Заказ сохранили в файл")
db_storage = DBStorage()
file_storage = FileStorage()
order = Order(db_storage, file_storage)
order.save_order()
Строка
from __future__ import annotations
нужна, чтоб код запустить в одном файле.
#ЗаЧаем
Цитата из книги "Вы, конечно, шутите, мистер Фейнман!", посвящена участнику проекта Манхеттен
В ответ на эту цитату, цитата преподавателя МГУ Столярова Андрея Викторовича из книги "Программирование. Введение в профессию"
Цитата из книги "Вы, конечно, шутите, мистер Фейнман!", посвящена участнику проекта Манхеттен
А что касается мистера Френкеля, который затеял всю эту деятельность, то он начал страдать от компьютерной болезни — о ней сегодня знает каждый, кто работал с компьютерами. Это очень серьезная болезнь, и работать при ней невозможно. Беда с компьютерами состоит в том, что ты с ними играешь. Они так прекрасны, столько возможностей — если четное число, делаешь это, если нечетное, делаешь то, и очень скоро на одной-единственной машине можно делать все более и более изощренные вещи, если только ты достаточно умен.
В ответ на эту цитату, цитата преподавателя МГУ Столярова Андрея Викторовича из книги "Программирование. Введение в профессию"
Как ни странно, именно эта “болезнь” превращает человека в программиста. Если хотите стать программистом — постарайтесь подхватить болезнь, описанную Фейнманом.
#ТаковПуть
Типизация.
Можно написать простой метод
А можно написать аналогичный метод с типизацией
Типизация призвана повысить читаемость кода.
Также типизация существенно ускоряет программу
Типизация.
Можно написать простой метод
def print_phone(phone = None):
if phone:
print(f"Phone number is {phone}")
else:
print("No phone number")
print_phone() #No phone number
А можно написать аналогичный метод с типизацией
def print_phone(phone: str | None = None) -> None:
if phone:
print(f"Phone number is {phone}")
else:
print("No phone number")
print_phone() #No phone number
Типизация призвана повысить читаемость кода.
Также типизация существенно ускоряет программу
#ТаковПуть
В Python есть прекрасные функции: all() и any(), которые делают код чище и эффективнее.
Классический пример, который показывает базовую работу этих функций:
any() находит первый подходящий элемент, all() проверяет, что все элементы удовлетворяют условию
Пример ниже показывает вызов функций в выражении, что удобно для комплексных проверок, т.е. когда много проверок внутри одной проверки.
Примером ниже показываю работу с генераторами:
На самом деле, последний yield даже не вызвался. Это связано с тем, что функции all() и any() - ленивые.
Как работают ленивые функции показываю в примере ниже:
🎯 Наглядная демонстрация принципа "короткого замыкания". any() выполнила только первую проверку, all() остановилась на третьей, не проверяя четвертую. Это важно для снижения вычислительной нагрузки и ускорения кода.
В Python есть прекрасные функции: all() и any(), которые делают код чище и эффективнее.
Классический пример, который показывает базовую работу этих функций:
nums = [2, 4, 5, 6]
any_1 = any(n % 2 == 0 for n in nums)
all_1 = all(n % 2 == 0 for n in nums)
print(any_1) #True
print(all_1) #False
any() находит первый подходящий элемент, all() проверяет, что все элементы удовлетворяют условию
Пример ниже показывает вызов функций в выражении, что удобно для комплексных проверок, т.е. когда много проверок внутри одной проверки.
def check_1():
return True
def check_2():
return True
def check_3():
return False
def check_4():
return True
checks_2 = [check_1, check_2, check_3, check_4]
any_2 = any(check() for check in checks_2)
all_2 = all(check() for check in checks_2)
print(any_2) #True
print(all_2) #False
Примером ниже показываю работу с генераторами:
def checks_3():
yield True
yield True
yield False
yield True
any_3 = any(check for check in checks_3())
all_3 = all(check for check in checks_3())
print(any_3) #True
print(all_3) #False
На самом деле, последний yield даже не вызвался. Это связано с тем, что функции all() и any() - ленивые.
Как работают ленивые функции показываю в примере ниже:
def checks_4():
print("#1") # any() остановится здесь и не пойдет дальше
yield True
print("#2")
yield True
print("#3") # all() остановится здесь и не пойдет дальше
yield False
print("#4")
yield True
print("any")
any(check for check in checks_4())
print("all")
all(check for check in checks_4())
# Output:
# any
# #1
# all
# #1
# #2
# #3
🎯 Наглядная демонстрация принципа "короткого замыкания". any() выполнила только первую проверку, all() остановилась на третьей, не проверяя четвертую. Это важно для снижения вычислительной нагрузки и ускорения кода.
👍1
#ТаковПуть
Кратко о bool
С удивлением обнаружил, что нельзя наследоваться от bool.
Как понимаю, причина в том, что разработчики языка сознательно идут на это ограничение ради надежности языка.
Несмотря на невозможность наследоваться от bool, можно использовать магические методы для превращения своего класса в bool. 🪄🧙♂
Жизненный пример, где в классе магический метод bool не будет лишним
Есть еще другой вариант написания кода, но он считается небезопасным, т.к. объект неожиданно становится bool.
Я не знаю, где такое может пригодится, но если очень хочется, чтоб вызов класса возвращал bool, то есть техническая возможность такое сделать.
Кратко о bool
С удивлением обнаружил, что нельзя наследоваться от bool.
Как понимаю, причина в том, что разработчики языка сознательно идут на это ограничение ради надежности языка.
Несмотря на невозможность наследоваться от bool, можно использовать магические методы для превращения своего класса в bool. 🪄🧙♂
class IsEmpty:
def __init__(self, sequence):
self._sequence = sequence
def __bool__(self):
return not bool(self._sequence)
# Примеры разных последовательностей
sec_1 = [1, 2, 3]
sec_2 = {"a":1, "b":2}
sec_3 = {}
# три разных варианта получить bool
result_1 = IsEmpty(sec_1).__bool__()
result_2 = IsEmpty(sec_2)
result_3 = IsEmpty(sec_3)
print(result_1) # False
print(bool(result_2)) # False
if result_3: # автоматом вызовется __bool__
print("Пустой") # вызовет этот вариант
else:
print("Не пустой")
Жизненный пример, где в классе магический метод bool не будет лишним
class DatabaseConnection:
def __bool__(self):
return self.is_connected()
...
# такой вариант неидеоматичен
with DatabaseConnection(...) as conn:
if conn.is_connected():
...
# такой вариант предпочтительнее
with DatabaseConnection(...) as conn:
if conn:
...
Есть еще другой вариант написания кода, но он считается небезопасным, т.к. объект неожиданно становится bool.
def bool_class(cls):
def wrapper(*args):
result = cls(*args)
return bool(result)
return wrapper
@bool_class
class IsEmpty:
def __init__(self, sequence):
self._sequence = sequence
def __bool__(self):
return not bool(self._sequence)
# Примеры разных последовательностей
sec_1 = [1, 2, 3]
sec_2 = {"a":1, "b":2}
sec_3 = {}
result_1 = IsEmpty(sec_1)
result_2 = IsEmpty(sec_2)
result_3 = IsEmpty(sec_3)
print(result_1) # False
print(result_2) # False
print(result_3) # True
# причем принтуются именно булевые значения
print(result_3 is True) # True
Я не знаю, где такое может пригодится, но если очень хочется, чтоб вызов класса возвращал bool, то есть техническая возможность такое сделать.
#ТаковПуть
Практикуемся с типами
Начнем с примера:
В примере создаётся тип-алиас Order, который является просто псевдонимом для str
Это делается для того, чтобы не просто у некой переменной был тип, а чтобы он был осмысленный.
Продолжим. Сделаем не просто алиас, а еще и произведем проверку на наполнение.
Теперь создадим новый тип Order, который наследуется от str
Создали новый тип, а не просто алиас и при этом сохранили всю функциональность str
Расширим пример. Добавим метод для нашего типа
Т.е. был создан пользовательский тип данных со встроенным методом!
Практикуемся с типами
Начнем с примера:
type Order = str
order: Order = "123"
print(type(order)) # <class 'str'>
В примере создаётся тип-алиас Order, который является просто псевдонимом для str
Это делается для того, чтобы не просто у некой переменной был тип, а чтобы он был осмысленный.
Продолжим. Сделаем не просто алиас, а еще и произведем проверку на наполнение.
from typing import NewType
Order = NewType('Order', str)
def validate_order(order_str: str) -> Order:
if not order_str.isdigit():
raise ValueError("Ожидается число")
return Order(order_str)
order: Order = validate_order("123")
print(type(order)) # <class 'str'>, но тип аннотирован как Order
Теперь создадим новый тип Order, который наследуется от str
Order = type('Order', (str,), {})
order: Order = Order("123")
print(type(order)) # <class '__main__.Order'>
print(isinstance(order, str)) # TrueСоздали новый тип, а не просто алиас и при этом сохранили всю функциональность str
Расширим пример. Добавим метод для нашего типа
def order_format(self):
return f"Order: {self}"
Order = type('Order', (str,), {'format': order_format})
order = Order("123")
print(order.format()) # "Order: 123"
Т.е. был создан пользовательский тип данных со встроенным методом!
👍1
#ТаковПуть
Фишки работы с Enum, ч.1
Enum используется для создания групп связанных констант и повышения читабельности кода.
Можно сравнить 2 примера, чтоб понять, зачем применяется Enum
Второй пример читабельней. Т.к. в первом не понятно, что означает цифра 1.
Для работы с Enum необходимо создать класс, который будет наследоваться от Enum
Реализуем класс со статусами заказа
Интересное в таком классе то, что объекты в нем итерируются и его объекты являются экземплярами этого же класса
С итерируемостью всё просто:
Теперь давайте создадим две похожие функции.
Обе будут принимать на вход экземпляр класса OrderStatus.
Правда, возвращать они будут разные вещи: значение объекта enum, другая сам объект enum
Как видим, мы можем подать OrderStatus.DELIVERED в функцию
, т.к. OrderStatus.DELIVERED - является экземпляром класса OrderStatus
Что визуально непривычно.
Также видно, когда мы сравниваем объект со строкой, то ожидаемо получаем False
Однако. Мы можем переделать наш класс. И отнаследоваться не только от Enum, но и от str
Проверим, как изменились результаты сравнения:
Как видим, теперь сравнивание строки с объектом привело к True. Это связано с тем, что объект отнаследован от str.
Также отпадает необходимость в наличии функции get_order_status_value.
Фишки работы с Enum, ч.1
Enum используется для создания групп связанных констант и повышения читабельности кода.
Можно сравнить 2 примера, чтоб понять, зачем применяется Enum
result == 1
result == Results.SUCCESS
Второй пример читабельней. Т.к. в первом не понятно, что означает цифра 1.
Для работы с Enum необходимо создать класс, который будет наследоваться от Enum
Реализуем класс со статусами заказа
from enum import Enum
class OrderStatus(Enum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
Интересное в таком классе то, что объекты в нем итерируются и его объекты являются экземплярами этого же класса
С итерируемостью всё просто:
for status in OrderStatus:
print(status) # возвращает объект enum
print(status.value) # возвращает значение объекта enum
Теперь давайте создадим две похожие функции.
Обе будут принимать на вход экземпляр класса OrderStatus.
Правда, возвращать они будут разные вещи: значение объекта enum, другая сам объект enum
def get_order_status_value(order_status: OrderStatus) -> str:
return order_status.value
def get_order_status_enum(order_status: OrderStatus) -> OrderStatus:
return order_status
print("delivered" == get_order_status_value(OrderStatus.DELIVERED))
print("delivered" == get_order_status_enum(OrderStatus.DELIVERED))
# Output:
# True
# False
Как видим, мы можем подать OrderStatus.DELIVERED в функцию
, т.к. OrderStatus.DELIVERED - является экземпляром класса OrderStatus
Что визуально непривычно.
Также видно, когда мы сравниваем объект со строкой, то ожидаемо получаем False
Однако. Мы можем переделать наш класс. И отнаследоваться не только от Enum, но и от str
class OrderStatus(str, Enum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
Проверим, как изменились результаты сравнения:
print("delivered" == get_order_status_value(OrderStatus.DELIVERED))
print("delivered" == get_order_status_enum(OrderStatus.DELIVERED))
# Output:
# True
# TrueКак видим, теперь сравнивание строки с объектом привело к True. Это связано с тем, что объект отнаследован от str.
Также отпадает необходимость в наличии функции get_order_status_value.
#ТаковПуть
Фишки работы с Enum, ч.2
Тут создадим класс, который не просто будет наследоваться от str и Enum, но и содержать различные методы.
Пояснения будут в самом коде.
Фактически тут представлены все те вещи, которые описывались в ч. 1, но представлены в более интересном виде.
Фишки работы с Enum, ч.2
Тут создадим класс, который не просто будет наследоваться от str и Enum, но и содержать различные методы.
Пояснения будут в самом коде.
from enum import Enum
class OrderStatus(str, Enum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
@classmethod
def get_next_status( cls, current_status: 'OrderStatus' ) -> 'OrderStatus':
status_flow = {
cls.CREATED: cls.PROCESSING,
cls.PROCESSING: cls.SHIPPED,
cls.SHIPPED: cls.DELIVERED
}
return status_flow.get(current_status, current_status)
def is_final( self ) -> bool:
return self in {self.DELIVERED, self.CANCELLED}
@classmethod
def from_string( cls, value: str ) -> 'OrderStatus':
try:
return cls(value.lower())
except ValueError:
raise ValueError(f"Invalid status: {value}. Valid statuses are: {', '.join([s.value for s in cls])}")
# Пусть будет некий заказ, кроторый будет представлен в виде dict, и этот dict будет содержать текущий статус заказа
order = {"status": "created"}
# Проверяем статус заказа, не является ли он CREATED
print(order["status"] == OrderStatus.CREATED) # True
# Зная текущий статус, можем получить следующий
next_status = OrderStatus.get_next_status(OrderStatus.CREATED)
order["status"] = next_status.value
print(order["status"]) # "processing"
# Пробуем найти экземпляр класса OrderStatus по строке
status_enum = OrderStatus.from_string(order["status"])
print(status_enum) # OrderStatus.PROCESSING
# Проверка конечного статуса
print(OrderStatus.DELIVERED.is_final()) # True
Фактически тут представлены все те вещи, которые описывались в ч. 1, но представлены в более интересном виде.
🔥1
#ТаковПуть
Цепочка сравнений
Классический пример, где используется союз and для соединения результатов проверок:
Можно упростить конструкцию без изменения результата
Т.е. python позволяет нам писать меньше кода и дописывает конструкцию за нас (под капотом)
Но разница все же есть:
1) вторая конструкция немного медленнее (разница ничтожно мала, можно игнорировать этот факт)
2) лично для меня, первая конструкция смотрится читабельнее.
Цепочка сравнений
Классический пример, где используется союз and для соединения результатов проверок:
print(1 < 5 and 5 >= 3)
# Output: True
Можно упростить конструкцию без изменения результата
print(1 < 5 >= 3)
# Output: True
Т.е. python позволяет нам писать меньше кода и дописывает конструкцию за нас (под капотом)
Но разница все же есть:
1) вторая конструкция немного медленнее (разница ничтожно мала, можно игнорировать этот факт)
2) лично для меня, первая конструкция смотрится читабельнее.
#ТаковПуть
Работа с dataclass
Читал книгу "Типизированный Python".
Там было сказано, что dataclass не распаковывается и если нужна распаковка, то лучше использовать NamedTuple
Решил показать реализацию распаковки dataclass:
Распаковкой является строка:
Это достигается реализацией метода _ _iter_ _. Плюс решил реализовать метод coordinates в виде свойства, чтоб обращаться к нему без скобок, т.е. не coordinates(), а coordinates.
Работа с dataclass
Читал книгу "Типизированный Python".
Там было сказано, что dataclass не распаковывается и если нужна распаковка, то лучше использовать NamedTuple
Решил показать реализацию распаковки dataclass:
from dataclasses import dataclass
@dataclass(slots=True, frozen=True)
class Coordinates:
longitude: float
latitude: float
def __iter__(self):
return iter((self.longitude, self.latitude))
@property
def coordinates(self):
return self
c = Coordinates(longitude=10, latitude=20)
lon,lat =c.coordinates
print(f"lon:{lon}")
print(f"lat:{lat}")
print(f"longitude:{c.longitude}")
# output:
# lon:10
# lat:20
# longitude:10
Распаковкой является строка:
lon,lat =c.coordinates
Это достигается реализацией метода _ _iter_ _. Плюс решил реализовать метод coordinates в виде свойства, чтоб обращаться к нему без скобок, т.е. не coordinates(), а coordinates.
#ТаковПуть
Фишки работы с Enum, ч.3
Вообще считается, что множественное наследование – это не очень хорошо. Так как при реализации таких классов могут быть конфликты между базовыми классами.
Начиная с версии 3.11 появилась возможность наследоваться от специального класса StrEnum.
Пример тот же. Множественное наследование убрано, а функционал сохранен.
Было бы скучно каждый раз писать один и тот же код. Поэтому для разнообразия пример ниже показывает, как реализовать поддержку данного кода на разных версиях python
Фишки работы с Enum, ч.3
Вообще считается, что множественное наследование – это не очень хорошо. Так как при реализации таких классов могут быть конфликты между базовыми классами.
Начиная с версии 3.11 появилась возможность наследоваться от специального класса StrEnum.
from enum import StrEnum
class OrderStatus(StrEnum):
CREATED = "created"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
#добавил, чтоб вывод от print был идентичен. На сами данные это не влияет
def __str__(self):
return f"{self.__class__.__name__}.{self.name}"
@classmethod
def get_next_status(cls, current_status: 'OrderStatus') -> 'OrderStatus':
status_flow = {
cls.CREATED: cls.PROCESSING,
cls.PROCESSING: cls.SHIPPED,
cls.SHIPPED: cls.DELIVERED
}
return status_flow.get(current_status, current_status)
def is_final(self) -> bool:
return self in {self.DELIVERED, self.CANCELLED}
@classmethod
def from_string(cls, value: str) -> 'OrderStatus':
try:
return cls(value.lower())
except ValueError:
raise ValueError(f"Invalid status: {value}. Valid statuses are: {', '.join([s.value for s in cls])}")
# Пусть будет некий заказ, который будет представлен в виде dict, и этот dict будет содержать текущий статус заказа
order = {"status": "created"}
# Проверяем статус заказа (работает напрямую со строками!)
print(order["status"] == OrderStatus.CREATED) # True
# Зная текущий статус, можем получить следующий
next_status = OrderStatus.get_next_status(OrderStatus.CREATED)
order["status"] = next_status.value
print(order["status"]) # "processing"
# Пробуем найти экземпляр класса OrderStatus по строке
status_enum = OrderStatus.from_string(order["status"])
print(status_enum) # OrderStatus.PROCESSING
# Проверка конечного статуса
print(OrderStatus.DELIVERED.is_final()) # True
Пример тот же. Множественное наследование убрано, а функционал сохранен.
Было бы скучно каждый раз писать один и тот же код. Поэтому для разнообразия пример ниже показывает, как реализовать поддержку данного кода на разных версиях python
from enum import Enum
import sys
if sys.version_info >= (3, 11):
from enum import StrEnum
BaseEnum = StrEnum
else:
class BaseEnum(str, Enum):
pass
class OrderStatus(BaseEnum):
# остальной код без изменений берем из примера выше
#ТаковПуть
Знакомимся ближе с кортежами.
Как известно, кортежи неизменяемый тип данных.
Код ниже приведет к ошибке:
Давайте изменим неизменяемый meal!
Попробуем еще раз изменить meal!
Ой! meal не изменился! А fruits изменился?
fruits пустой! Но как же так, если изначально meal = (fruits, vegetables)?
Все дело в том, что fruits в начале и в конце это разные объекты.
Знакомимся ближе с кортежами.
fruits = ['яблоко','мандарин']
vegetables = ['лук', 'помидор']
berries = ['strawberry']
meal = (fruits, vegetables)
Как известно, кортежи неизменяемый тип данных.
Код ниже приведет к ошибке:
meal += berries
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can only concatenate tuple (not "list") to tuple
Давайте изменим неизменяемый meal!
fruits.append('апельсин')
print(meal)(['яблоко', 'мандарин', 'апельсин'], ['лук', 'помидор'])
Попробуем еще раз изменить meal!
fruits = []
print(meal)
(['яблоко', 'мандарин', 'апельсин'], ['лук', 'помидор'])
Ой! meal не изменился! А fruits изменился?
print(fruits)
[]
fruits пустой! Но как же так, если изначально meal = (fruits, vegetables)?
Все дело в том, что fruits в начале и в конце это разные объекты.
fruits = ['яблоко','мандарин']
print(id(fruits))
fruits = []
print(id(fruits))
1140598442176 < — это id fruits = ['яблоко','мандарин']
1140594170112 < — это id fruits = []
#ТаковПуть
Удивительные кортежи: запятая против скобок
Попробуем строку (str) поместить в квадратные и круглые скобки.
Ожидаем, что l будет списком (list), а t — кортежем (tuple)
Но почему t стал строкой? Ожидался же кортеж!
Запись t = (a) интерпретатор видит так:
Результат— обычная строка.
Волшебный секрет Python: кортеж создаёт не скобка, а запятая! 🤯
Скобки для кортежей часто служат для удобочитаемости. Суть — в запятой.
Вот так тоже можно:
P.S. Важное исключение:
Пустой кортеж создается ТОЛЬКО скобками:
Удивительные кортежи: запятая против скобок
Попробуем строку (str) поместить в квадратные и круглые скобки.
Ожидаем, что l будет списком (list), а t — кортежем (tuple)
a = 'a'
l = [a] # Всё ок, это list
t = (a) # А это... что?
print(type(l)) # <class 'list'>
print(type(t)) # <class 'str'> 🤯
Но почему t стал строкой? Ожидался же кортеж!
Запись t = (a) интерпретатор видит так:
«Возьми то, что лежит в переменной a, и просто убери скобки и запиши в t».
Результат— обычная строка.
Волшебный секрет Python: кортеж создаёт не скобка, а запятая! 🤯
a = 'a'
l = [a] # Для списка запятая не нужна
t = (a,) # А для кортежа — ОБЯЗАТЕЛЬНА!
print(type(l)) # <class 'list'>
print(type(t)) # <class 'tuple'>
Скобки для кортежей часто служат для удобочитаемости. Суть — в запятой.
Вот так тоже можно:
t1 = ('a',) # Со скобками
t2 = 'a', # Без скобок (запятая есть!)
t3 = tuple(['a']) # Явное созданиеP.S. Важное исключение:
Пустой кортеж создается ТОЛЬКО скобками:
empty_tuple = () # Кортеж
empty_list = [] # Список
#ТаковПуть
Опасные значения по-умолчанию
Продолжаем говорить о волшебных секретах python.
Давайте попробуем указать в функции значением по-умолчанию пустой список
Почему вечерние задачи добавились к утренним? Мы же создали новый список!
Волшебный секрет Python: изменяемые объекты (list, set, dict) создаются в момент определения функции, а не каждого вызова! 🤯
Аргумент tasks=[] создает ОДИН общий список на все вызовы функции. Все, кто вызывает функцию без аргумента, получают ссылку на один и тот же объект.
А как же тогда сделать правильно? 🤔
Правильный путь: использовать неизменяемое значение по умолчанию (None)
Это правило работает для всех изменяемых типов:
· Списки - list
· Словари - dict
· Множества - set
На этом всё? А вот и нет.
Поработаем с классом:
Как видно, указав экземпляр класса в виде дефолтного значения, получили проблему с затиранием данных.
Затирание получилось в этом примере, на другом получилось бы дублирование.
Экземпляры классов - такие же ИЗМЕНЯЕМЫЕ объекты, как и списки!
Правило то же самое: используем None.
Опасные значения по-умолчанию
Продолжаем говорить о волшебных секретах python.
Давайте попробуем указать в функции значением по-умолчанию пустой список
def add_task(task, tasks=[]):
tasks.append(task)
return tasks
morning_tasks = add_task('позавтракать')
print(morning_tasks) # ['позавтракать']
evening_tasks = add_task('поужинать')
print(evening_tasks) # ['позавтракать', 'поужинать'] 🤯
Почему вечерние задачи добавились к утренним? Мы же создали новый список!
Волшебный секрет Python: изменяемые объекты (list, set, dict) создаются в момент определения функции, а не каждого вызова! 🤯
Аргумент tasks=[] создает ОДИН общий список на все вызовы функции. Все, кто вызывает функцию без аргумента, получают ссылку на один и тот же объект.
А как же тогда сделать правильно? 🤔
Правильный путь: использовать неизменяемое значение по умолчанию (None)
def add_task(task, tasks=None):
if tasks is None:
tasks = []
tasks.append(task)
return tasks
morning_tasks = add_task('позавтракать')
print(morning_tasks) # ['позавтракать']
evening_tasks = add_task('поужинать')
print(evening_tasks) # ['поужинать']
Это правило работает для всех изменяемых типов:
· Списки - list
· Словари - dict
· Множества - set
На этом всё? А вот и нет.
Поработаем с классом:
class Task:
def __init__(self, description):
self.description = description
def create_task(desc, task_storage=Task("Дефолтная задача")):
task_storage.description = desc
return task_storage
task1 = create_task("купить хлеб")
print(task1.description) # купить хлеб
task2 = create_task("полить цветы")
print(task2.description) # полить цветы
print(task1.description) # полить цветы 🤯
Как видно, указав экземпляр класса в виде дефолтного значения, получили проблему с затиранием данных.
Затирание получилось в этом примере, на другом получилось бы дублирование.
Экземпляры классов - такие же ИЗМЕНЯЕМЫЕ объекты, как и списки!
Правило то же самое: используем None.
#ТаковПуть #ЧистыйКот
Секрет импортов (часть 1)
Обычное дело, когда одни и те же импорты используются в разных файлах проекта.
Логично, что если какая-то библиотека нужна в файле, просто её импортируем.
Но существует волшебный секрет 🤫
1. В каком-нибудь файле проекта указываем некий import
2. Находится и загружается библиотека в программу
3. Библиотека добавляется в глобальный кеш sys.modules
4. При последующих импортах в ЛЮБОМ файле просто возвращается готовый объект из кеша!!!
В программировании не рекомендуется дублировать код. В Python это относится и к импортам.
Казалось бы, проблема решена — но появляется новая!
Тут важно вспомнить дзен принцип:
Тут имеется в виду, что такой код, как указан ниже, тяжелее читать из-за неявного использования os.
Встает вопрос. Что делать?
Принято в каждом модуле (дублировать) указывать явно те импорты, которые в этом модуле используются.
Т.е. корректно делать так:
module_a.py
module_b.py
Секрет импортов (часть 1)
Обычное дело, когда одни и те же импорты используются в разных файлах проекта.
file1.pyimport requests
import json
from typing import Dict
import pandas as pd
file2.pyimport requests
import json
from typing import List
import pandas as pd
file3.pyimport requests
import json
import pandas as pd
Логично, что если какая-то библиотека нужна в файле, просто её импортируем.
Но существует волшебный секрет 🤫
Логика такая:Импортированный модуль становится доступен во ВСЕМ проекте через кеш sys.modules!
1. В каком-нибудь файле проекта указываем некий import
2. Находится и загружается библиотека в программу
3. Библиотека добавляется в глобальный кеш sys.modules
4. При последующих импортах в ЛЮБОМ файле просто возвращается готовый объект из кеша!!!
В программировании не рекомендуется дублировать код. В Python это относится и к импортам.
Казалось бы, проблема решена — но появляется новая!
Тут важно вспомнить дзен принцип:
The Zen of Python: 'Explicit is better than implicit'
Тут имеется в виду, что такой код, как указан ниже, тяжелее читать из-за неявного использования os.
module_a.pyimport os
...
module_b.pyos.getcwd() # Откуда взялся os?
Встает вопрос. Что делать?
Принято в каждом модуле (дублировать) указывать явно те импорты, которые в этом модуле используются.
Т.е. корректно делать так:
module_a.py
import os
...
module_b.py
import os
os.getcwd()
#ТаковПуть
Секреты импортов (часть 2)
До недавнего времени было проблемой, когда хочешь написать типизированный код на python, получается, что половина импортов – это импорты типов.
Волшебный секрет python:
Начиная с версии 3.13 типы стали полноценной частью языка!
Больше никаких импортов:
· ❌ from typing import List, Dict, Tuple
· ✅ list[int], dict[str, float], tuple[int, str]
Union и Optional ушли в историю:
· ❌ Union[str, int], Optional[str]
· ✅ str | int, str | None
Literal теперь просто значения:
· ❌ Literal['read', 'write']
· ✅ 'read' | 'write'
Self теперь не требует импорта:
· ❌ from typing import Self
· ✅ импорт не требуется
Секреты импортов (часть 2)
До недавнего времени было проблемой, когда хочешь написать типизированный код на python, получается, что половина импортов – это импорты типов.
Волшебный секрет python:
Начиная с версии 3.13 типы стали полноценной частью языка!
Больше никаких импортов:
· ❌ from typing import List, Dict, Tuple
· ✅ list[int], dict[str, float], tuple[int, str]
Union и Optional ушли в историю:
· ❌ Union[str, int], Optional[str]
· ✅ str | int, str | None
Literal теперь просто значения:
· ❌ Literal['read', 'write']
· ✅ 'read' | 'write'
Self теперь не требует импорта:
· ❌ from typing import Self
· ✅ импорт не требуется
# Раньше
from typing import Self
class User:
def clone(self) -> Self:
return User()
#Теперь
class User:
def clone(self) -> Self:
return User()
👍1
#ЗаЧаем
Давно ничего не публиковал. С головой ушел в разработку программы, которая принимает на вход XML файл, а дальше:
1) предоставляет удобный интерфейс для работы с данными файла
2) генерирует отчеты по заданным полям. Потом на этот функционал надо сделать визуальную часть на Django или на чем-то подобном и сделать дружелюбный пользователю генератор отчетов. Пока все на yaml конфиге держится.
3) ради чего я это все затеял. На работе надо массово XML файлы одной структуры в XML файлы другой структуры переделывать. Можно, конечно, xslt использовать, но там есть сложные логики, какие в xslt не запихнуть.
Проект можно посмотреть тут:
https://gitverse.ru/DemonssTano/XMLProcessorAPI
Уже много чего работает, но пока прога сырая. Если есть желание поработать над реальной программой, пишите, организую :)
Давно ничего не публиковал. С головой ушел в разработку программы, которая принимает на вход XML файл, а дальше:
1) предоставляет удобный интерфейс для работы с данными файла
2) генерирует отчеты по заданным полям. Потом на этот функционал надо сделать визуальную часть на Django или на чем-то подобном и сделать дружелюбный пользователю генератор отчетов. Пока все на yaml конфиге держится.
3) ради чего я это все затеял. На работе надо массово XML файлы одной структуры в XML файлы другой структуры переделывать. Можно, конечно, xslt использовать, но там есть сложные логики, какие в xslt не запихнуть.
Проект можно посмотреть тут:
https://gitverse.ru/DemonssTano/XMLProcessorAPI
Уже много чего работает, но пока прога сырая. Если есть желание поработать над реальной программой, пишите, организую :)
#ТаковПуть
Загадочные лямбды
Данную статью можно использовать как пример для собеседования.
Создадим список функций. Заранее подадим значения в функции, а потом вызовем их одну за другой.
Что-то пошло не так...
Ожидали:a, b, c
Получили: c, c, c
Давайте усложним пример:
Теперь вообще магия! Кажется, что "x" внутри цикла и после него - разные переменные. Проверим эту теорию с помощью id():
Если выполнить этот код, можно убедиться - объекты x действительно разные, но результат все равно странный.
Волшебный секрет🤫
1. Лямбды "ленивые" - они вычисляются в момент вызова, а не создания
2. Замыкания (closure) в Python запоминают название переменной, а не ее значение
3. Область видимости - все лямбды ссылаются на одну и ту же переменную x
В нашем примере, при проходе циклом по items, мы перезаписываем переменную x. Когда лямбды finally вызываются, они используют текущее значение x.
Интересный поворот:
Предлагаю самостоятельно подумать, почему изменился результат в примере выше
Самый простой способ исправить код: передать значение x как параметр по умолчанию:
Почему это работает:
1. Лямбды сохраняют ссылку на переменную x из внешней области видимости, а не её значение на момент создания
2. Поскольку цикл for не создаёт новую область видимости для каждой итерации, все лямбды ссылаются на одну и ту же переменную x
3. К моменту вызова лямбд значение x равно последнему элементу списка
4. Параметр по умолчанию вычисляется в момент создания функции, "замораживая" текущее значение
P.S. Эта особенность есть не только в Python — похожее поведение встречается в JavaScript и других языках с замыканиями!
Загадочные лямбды
Данную статью можно использовать как пример для собеседования.
Создадим список функций. Заранее подадим значения в функции, а потом вызовем их одну за другой.
funcs = []
items = ["a", "b", "c"]
for x in items:
funcs.append(lambda: print(x))
for func in funcs:
func()
# output:
# c
# c
# c
Что-то пошло не так...
Ожидали:a, b, c
Получили: c, c, c
Давайте усложним пример:
funcs = []
items = ["a", "b", "c"]
for x in items:
funcs.append(lambda: print(x))
x = 'd' # Изменяем значение x
for func in funcs:
func()
# output:
# d
# d
# d
Теперь вообще магия! Кажется, что "x" внутри цикла и после него - разные переменные. Проверим эту теорию с помощью id():
funcs = []
items = ["a", "b", "c"]
for x in items:
print(id(x)) # проверка id
funcs.append(lambda: print(x))
x = 'd'
print(id(x)) # проверка id
for func in funcs:
func()
Если выполнить этот код, можно убедиться - объекты x действительно разные, но результат все равно странный.
Волшебный секрет🤫
1. Лямбды "ленивые" - они вычисляются в момент вызова, а не создания
2. Замыкания (closure) в Python запоминают название переменной, а не ее значение
3. Область видимости - все лямбды ссылаются на одну и ту же переменную x
В нашем примере, при проходе циклом по items, мы перезаписываем переменную x. Когда лямбды finally вызываются, они используют текущее значение x.
Интересный поворот:
funcs = []
items = ["a", "b", "c"]
def set_funcs(items, funcs):
for x in items:
funcs.append(lambda: print(x))
set_funcs(items, funcs)
x = 'd'
for func in funcs:
func()
# output:
# c
# c
# c
Предлагаю самостоятельно подумать, почему изменился результат в примере выше
Самый простой способ исправить код: передать значение x как параметр по умолчанию:
for x in items:
funcs.append(lambda x=x: print(x))
for func in funcs:
func()
# output:
# a
# b
# c
Почему это работает:
1. Лямбды сохраняют ссылку на переменную x из внешней области видимости, а не её значение на момент создания
2. Поскольку цикл for не создаёт новую область видимости для каждой итерации, все лямбды ссылаются на одну и ту же переменную x
3. К моменту вызова лямбд значение x равно последнему элементу списка
4. Параметр по умолчанию вычисляется в момент создания функции, "замораживая" текущее значение
P.S. Эта особенность есть не только в Python — похожее поведение встречается в JavaScript и других языках с замыканиями!
#ТаковПуть
Секрет отладочного вывода f-строк
Привычная запись f-строк
Начиная с python 3.8 можно сократить запись до:
Самое интересное, что на этом функционал не заканчивается! У нас никуда не пропали возможности взаимодействия с объектами
Но надо помнить:
Эти фишки замедляют программу, поэтому для нас это только отладочный механизм.
Секрет отладочного вывода f-строк
Привычная запись f-строк
result = "hello"
print(f"result='{result}'")
# output: result='hello'
Начиная с python 3.8 можно сократить запись до:
result = "hello"
print(f"{result=}")
# output: result='hello'
Самое интересное, что на этом функционал не заканчивается! У нас никуда не пропали возможности взаимодействия с объектами
name = "Alice"
age = 30
score = 95.5
items = [1, 2, 3]
print(f"{name=} {age=} {score=:.1f}")
# name='Alice' age=30 score=95.5
print(f"{name.upper()=}")
# name.upper()='ALICE'
print(f"{age * 2=}")
# age * 2=60
print(f"{items=}")
# items=[1, 2, 3]
print(f"{len(items)=}")
# len(items)=3
Но надо помнить:
Эти фишки замедляют программу, поэтому для нас это только отладочный механизм.
#ТаковПуть
Отладка без IDE
Я придерживаюсь принципа, что программист должен уметь работать без IDE.
Без IDE достаточно сложно отлаживаться, особенно в проектах с серверами, например, django.
Однако в Python есть встроенная функция для отладки, которая не требует установки IDE!
Когда код дойдет до breakpoint(), выполнение остановится и откроется интерактивная консоль отладки.
В консоли отладки можно:
· n (next) - выполнить следующую строку
· c (continue) - продолжить выполнение
· s (step) - войти в функцию
· p переменная - напечатать значение переменной
· l (list) - показать код вокруг текущей строки
· q (quit) - выйти из отладчика
С примером выше мы можем сделать так:
После чего отладчик завершит работу, т.к. брейкпоинтов больше нет.
Отладка без IDE
Я придерживаюсь принципа, что программист должен уметь работать без IDE.
Без IDE достаточно сложно отлаживаться, особенно в проектах с серверами, например, django.
Однако в Python есть встроенная функция для отладки, которая не требует установки IDE!
def func(x, y):
result = x + y
breakpoint() # 👈 остановка
return result * 2
func(5, 3)
Когда код дойдет до breakpoint(), выполнение остановится и откроется интерактивная консоль отладки.
В консоли отладки можно:
· n (next) - выполнить следующую строку
· c (continue) - продолжить выполнение
· s (step) - войти в функцию
· p переменная - напечатать значение переменной
· l (list) - показать код вокруг текущей строки
· q (quit) - выйти из отладчика
С примером выше мы можем сделать так:
> python func.py
> p result
8
> c
После чего отладчик завершит работу, т.к. брейкпоинтов больше нет.
👍1