Привет! Мы – часть команды разработки «Рамблер/Медиа» (портал «Рамблер»). На протяжении трех лет мы поддерживаем и развиваем несколько больших python-приложений. Чуть больше года назад перед нами встала задача написать еще одно большое приложение – API к основному хранилищу новостей, и мы сделали это на Rust.
В статье мы расскажем о том, что заставило нас отойти от привычного стека технологий, и покажем, какие плюсы по сравнению с Python есть у Rust.
Мы не ответим на вопрос, почему выбор пал именно на Rust, а не Go, например, или на какой-либо другой язык. Также мы не будем сравнивать производительность Python- и Rust-приложений – эти темы достойны отдельного обсуждения.
Этот материал написали cbmw и AndreyErmilov
Содержание:
- Первая часть (типы, пользовательские типы и полиморфизм, перечисления, Option и Result, паттерн-матчинг, трейты и протоколы, обобщенное программирование)
- Вторая часть (многопоточность, асинхронность, функциональная парадигма и заключение – «Зачем же питонисту Rust») – готовится к публикации и выйдет чуть позже
Если не хочется читать эту статью или невтерпеж ждать второй части материала, можно посмотреть видео нашего выступления.
Типы
Первое различие, с которым сталкиваются разработчики, Rust – язык со статической типизацией.
Можно по-разному смотреть на динамическую и статическую типизацию, но, на наш взгляд, основное отличие демонстрирует изображение ниже:
В случае с Python множество ошибок типизации мы видим уже на проде – в интерфейсе Sentry. В Rust такие ошибки отлавливаются еще на этапе сборки и это просходит, как правило, локально или в CI.
Учитывая, что ошибки, связанные с несоответствием типов, в наших приложениях составляют подавляющее большинство, статическая типизация Rust выглядит как достаточно весомый плюс. Можно было бы тут и остановиться, но многие, думаю, слышали, что в последнее время в Python активно развивается опциональная статическая типизация. Почему бы не попробовать проверить такие проблемы еще до их попадания в прод?
Тут на сцену выходит mypy, как самое зрелое решение в этой области. Сам создатель языка Python активно принимает участие в разработке mypy. И это замечательный инструмент, позволяющий проанализировать код и найти те самые проблемы с типизацией. Давайте рассмотрим его детально.
Начнем с крайне простого примера:
from typing import List
def last(items: List[int]) -> int:
return items.pop()
Этот код делает тривиальную штуку – забирает крайний правый элемент из списка и передает его в качестве возвращаемого значения функции.
С точки зрения mypy и нотации типов этот код является вполне корректным:
? mypy --strict types-01.py
Success: no issues found in 1 source file
А теперь давайте рассмотрим аналогичный код в Rust:
fn last(mut items: Vec<i32>) -> i32 {
items.pop()
}
И посмотрим, к чему приведет попытка его скомпилировать:
? types git:(master) ? cargo run
error[E0308]: mismatched types
|
1 | fn last(mut items: Vec<i32>) -> i32 {
| items.pop()
| ^^^^^^^^^^^ expected `i32`,
| found enum `std::option::Option`
|
= note: expected type `i32`
found enum `std::option::Option<i32>`
Ошибка компиляции явно говорит о том, что метод .pop()
в каких-то случаях может вернуть None. И действительно, если мы в качестве аргумента передадим пустой вектор, так и произойдет.
Но почему mypy не предупредил нас о потенциальной ошибке? Дело в том, что в Python при пустом списке произойдёт Exception, который никак не учитывается и не отражается в нотации типов. Это кажется достаточно большой проблемой, которая не позволяет использовать возможности статической типизации в полной мере. В целом существование исключений и их игнорирование в системе нотации типов перекладывает ответственность за корректность кода на разработчика.
Отлично, давайте перепишем Python-код по аналогии с Rust, не вызывая исключения:
from typing import List, Optional
def last(array: List[int]) -> Optional[int]:
if len(array) == 0:
return None
return array.pop()
Такой код будет корректным и не вызовет исключений, однако многочисленные проверки очень сильно увеличивают кодовую базу и сводят на нет всю простоту и лаконичность, которой славится Python. Кроме того, идея отказа от исключений в Python выглядит инородно, поскольку это одна из концептуальных составляющих языка.
Да, безусловно, есть попытки осуществить это. Хороший пример – библиотека returns.
В целом она выглядит как хорошая попытка реализовать использующийся в Rust подход путем отказа от вызовов исключений. Это, в свою очередь, позволяет более безопасно с точки зрения типов описывать какую-то изолированную или бизнес-логику, что само по себе уже является огромным плюсом.
Пользовательские типы и полиморфизм
Типы являются не только способом избежать ошибок, но и удобными строительными блоками, которые помогают писать красивый и понятный код. Давайте посмотрим, как это работает в Rust.
Рассмотрим задачу. У нас есть разные сущности – расстояние, которое измеряется в километрах и метрах, и время, которое измеряется в часах и секундах. Мы хотим уметь получать скорость. Опишем структуры:
/// Distance, km
struct Kilometer(f64);
/// Distance, m
struct Meter(f64);
/// Time, h
struct Hour(f64);
/// Time, s
struct Second(f64);
/// Speed, km/h
struct KmPerHour(f64);
/// Speed, km/s
struct KmPerSecond(f64);
/// Speed, m/h
struct MeterPerHour(f64);
/// Speed, m/s
struct MeterPerSecond(f64);
Реализуем операцию деления для километров и метров и в каждом случае будем получать свой тип:
/// Speed, km/h
impl Div<Hour> for Kilometer {
type Output = KmPerHour;
fn div(self, rhs: Hour) -> Self::Output {
KmPerHour(self.0 / rhs.0)
}
}
/// Speed, km/s
impl Div<Second> for Kilometer {
type Output = KmPerSecond;
fn div(self, rhs: Second) -> Self::Output {
KmPerSecond(self.0 / rhs.0)
}
}
/// Speed, m/h
impl Div<Hour> for Meter {
type Output = MeterPerHour;
fn div(self, rhs: Hour) -> Self::Output {
MeterPerHour(self.0 / rhs.0)
}
}
/// Speed, m/s
impl Div<Second> for Meter {
type Output = MeterPerSecond;
fn div(self, rhs: Second) -> Self::Output {
MeterPerSecond(self.0 / rhs.0)
}
}
Проверим, что наш код работает. Rust в зависимости от типов, которые мы делим и на которые мы делим, определит, какого типа будет скорость.
fn main() {
let distance = Meter(100.);
let duration = Second(50.);
let speed = distance / duration; // MeterPerSecond
assert_eq!(speed.0, 2.);
let distance = Kilometer(180.);
let duration = Hour(3.);
let speed = distance / duration; // KmPerHour
assert_eq!(speed.0, 60.);
}
Реализуем тоже самое на Python.
Опишем структуры:
@dataclass
class Hour:
"""Time, h."""
value: float
@dataclass
class Second:
"""Second, s."""
value: float
@dataclass
class KmPerHour:
"""Speed, km/h."""
value: float
@dataclass
class KmPerSecond:
"""Speed, km/s."""
value: float
@dataclass
class MeterPerHour:
"""Speed, m/h."""
value: float
@dataclass
class MeterPerSecond:
"""Speed, m/s."""
value: float
Сделаем реализацию деления только для километров и представим, что сделали так же и для метров. Нам нужно использовать overload
чтобы показать, как в зависимости от типа входного параметра меняется тип результата:
from typing import overload
@dataclass
class Kilometer:
value: float
@overload
def __truediv__(self, other: Hour) -> KmPerHour: ...
@overload
def __truediv__(self, other: Second) -> KmPerSecond: ...
def __truediv__(self,
other: Union[Hour, Second]
) -> Union[KmPerHour, KmPerSecond]:
if isinstance(other, Hour):
return KmPerHour(self.value / other.value)
elif isinstance(other, Second):
return KmPerSecond(self.value / other.value)
...
Проверим код, используя mypy:
? 01-types poetry run mypy --strict typing-02-2.py
Success: no issues found in 1 source file
А теперь случайно ошибемся в возвращаемом типе: при делении на секунды будем возвращать километры в час:
if isinstance(other, Hour):
return KmPerHour(self.value / other.value)
elif isinstance(other, Second):
return KmPerHour(self.value / other.value)
Запустим mypy:
? 01-types poetry run mypy --strict typing-02-2.py
Success: no issues found in 1 source file
Mypy не видит в коде с ошибкой никакой проблемы, потому что мы по-прежнему возвращаем одно из корректных значений, описанных в Union[KmPerHour, KmPerSecond]
.
Явно укажем, что ожидаем получить при делении на секунды именно км/с, и снова запустим mypy.
speed: KmPerSecond = Kilometer(1.0) / Second(1.0)
assert isinstance(speed, KmPerHour)
? 01-types poetry run mypy --strict typing-02-2.py
Success: no issues found in 1 source file
Понятно, почему это происходит, но не понятно, как избежать подобных ошибок с mypy.
Перечисления
Перечисления существуют во многих языках. Посмотрим, как в Python и Rust происходит работа с ними.
Создадим перечисление, описывающее возможные состояния пользователя:
from enum import Enum, auto
class UserStatus(Enum):
PENDING = auto()
ACTIVE = auto()
INACTIVE = auto()
DELETED = auto()
Сделаем тоже самое в Rust:
enum UserStatus {
Pending,
Active,
Inactive,
Deleted,
}
В это простом примере оба варианта выглядят одинаково. Но в Rust мы можем связать статус пользователя с дополнительной информацией.
enum UserStatus {
Pending(DateTime<Utc>),
Active(i32),
Inactive(i32),
Deleted,
}
В примере для статуса Pending
мы храним информацию о том, как долго мы ожидаем подтверждения от пользователя; для активного и неактивного пользователей храним их идентификаторы.
Доставать находящиеся внутри перечисления типы мы можем с помощью паттерн-матчинга, про который поговорим чуть позже.
Возможность внутри вариантов перечислений хранить значения сильно влияет на то, как Rust-разработчики пишут код – перечисления являются одним из наиболее часто используемых возвращаемых типов. На их основе возникли типы Option
и Result
, про которые мы сейчас поговорим.
Option и Result
Мы уже встречались с типом Option
, когда доставали из вектора с числами крайне правый элемент. Result
похож на Option
, но может содержать в себе два типа, а не один: успешный результат выполнения операции или ошибку.
pub enum Option<T> {
None,
Some(T),
}
pub enum Result<T, E> {
Ok(T),
Err(E),
}
Давайте на примере разберем, как использование Option
влияет на корректность работы приложения.
let mut vec = vec![1, 2, 3];
let last_element = vec.pop();
assert_eq!(last_element, Some(3));
Когда мы достали из вектора правый элемент, то получили не число, а значение типа Option
, содержащее в варианте Some
нужное число. Мы не сможем его сложить с другим числом, т.к. в этом случае мы потерям информацию о возможном варианте None
.
let mut vec = vec![1, 2, 3];
let last_element = vec.pop();
assert_eq!(last_element, Some(3));
let four = last_element + 1;
// Cannot add `std::option::Option<{integer}>` to `{integer}`
Чтобы использовать полученное из вектора число мы можем прибегнуть к паттерн-матчингу, который мы рассмотрим еще ниже. А сейчас проверим, как аналогичный код работает в Python. Мы используем написанную нами функцию last()
, чтобы возвращаемый тип был Optional
.
from typing import List, Optional
def last(array: List[int]) -> Optional[int]:
if len(array) == 0:
return None
return array.pop()
numbers = [1, 2, 3]
last_element = last(numbers)
four = last_element + 1
? 01-types poetry run mypy --strict typing-04-1.py
typing-04-1.py:12: error: Unsupported operand types for + ("None" and "int")
typing-04-1.py:12: note: Left operand is of type "Optional[int]"
Mypy, как и комплиятор Rust, не позволит нам сложить опциональное значение с числом. Но для этого программисту нужно будет самостоятельно указать, что возвращаемое значение Optional
.
Паттерн-матчинг
Раз уж мы упомянули pattern-matching, давайте, наконец, раскроем эту концепцию чуть подробнее.
Для начала рассмотрим следующий Python-код:
class UserStatus(Enum):
PENDING = auto()
ACTIVE = auto()
INACTIVE = auto()
DELETED = auto()
def serialize(user_status: UserStatus) -> str:
if user_status == UserStatus.PENDING:
return 'Pending'
elif user_status == UserStatus.ACTIVE:
return 'Active'
elif user_status == UserStatus.INACTIVE:
return 'Inactive'
elif user_status == UserStatus.DELETED:
return 'Deleted'
Все, что этот код делает, – преобразует элементы перечисления UserStatus в строковое представление. Выглядит это достаточно просто.
А теперь рассмотрим аналогичный вариант на Rust:
enum UserStatus {
Pending,
Active,
Inactive,
Deleted,
}
fn serialize(user_status: UserStatus) -> &'static str {
match user_status {
UserStatus::Pending => "Pending",
UserStatus::Active => "Active",
UserStatus::Inactive => "Inactive",
UserStatus::Deleted => "Deleted",
}
}
Разница в том, что в случае, когда разработчик по какой-то причине (например, если добавляется новый статус пользователя при рефакторинге) не опишет один из исходных вариантов перечисления в функции serialize, Rust ему об этом скажет:
fn serialize(user_status: UserStatus) -> &'static str {
match user_status {
UserStatus::Pending => "Pending",
UserStatus::Active => "Active",
}
}
// Error: non-exhaustive patterns: `Inactive` and `Deleted` not covered
Это и есть одно из отличительных свойств pattern-matching в Rust. При его использовании в коде компилятор заставляет рассмотреть все варианты.
И возвращаясь к функции last
, которую мы приводили в начале: при обработке Option, являющегося результатом вызова функции, компилятор не даст забыть обработать ситуацию, при которой результатом выполнения станет None.
Соответственно, аналогичное правило касается и типа Result
:
let number = "5";
let parsed: Result<i32, ParseIntError> = number.parse();
let message = match parsed {
Ok(value) => format!("Number parsed successfully: {}", value),
Err(error) => format!("Can't parse a number. Error: {}", error),
};
assert_eq!(message, "Number parsed successfully: 5");
В случае если нам нужно определить некоторое дефолтное поведение, Rust предоставляет следующую конструкцию:
fn fibonacci(n: u32) -> u32 {
match n {
0 => 1,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
В этом примере мы видим, что описаны только два конкретных значения, а для всех остальных рекурсивно вызывается функция fibbonacci
.
Трейты и протоколы
В этом разделе мы сравним возможности недавно появившихся в Python протоколов и трейтов Rust. Возможно, не все используют протоколы, и чтобы сравнение было полезным, сделаем краткий обзор основных идей протоколов.
Представим, что нам нужно написать функцию-валидатор, которая принимает список экземпляров класса Image
и возвращает список из булевых значений. True
будет обозначать, что изображение валидное и весит не больше, чем MAX_SIZE
, False
– невалидное. Напишем код:
from typing import List
MAX_SIZE = 512_000
class Image:
def __init__(self, image: bytes) -> None:
self.image = image
def validate(images: List[Image]) -> List[bool]:
return [len(image) <= MAX_SIZE for image in images]
Если мы запустим mypy, то увидим следующую ошибку:
? 01-types poetry run mypy --strict p-01-2.py
p-01-2.py:8: error: Argument 1 to "len" has incompatible type "Image"; expected "Sized"
Found 1 error in 1 file (checked 1 source file)
Mypy сообщает, что ожидается класс типа Sized
, а мы вместо этого передали Image
. Из документации становится понятно: все, что реализует магический метод __len__
, является Sized
.
В Python мы давно привыкли к утиной типизации, и требование реализовать метод __len__
кажется вполне понятным. Сделаем это.
from typing import List
MAX_SIZE = 512_000
class Image:
def __init__(self, image: bytes) -> None:
self.image = image
def __len__(self) -> int:
return len(self.image)
def validate(images: List[Image]) -> List[bool]:
return [len(image) <= MAX_SIZE for image in images]
После добавления __len__
mypy определит код как корректный.
Итого – Sized
это и есть протокол, а про наш класс Image
можно сказать, что он реализует протокол Sized
.
Но давайте рассмотрим тему протоколов немного подробнее и усложним задачу – будем валидировать различные документы по их статусу – были ли они проверены и можно ли их публиковать. Функция validate
будет возвращать только те документы, которые прошли проверку.
from abc import abstractmethod
from typing import List, Protocol
class SupportsReview(Protocol):
@abstractmethod
def approved(self) -> bool: ...
class Article(SupportsReview):
def approved(self) -> bool:
return True
class PhotoGallery(SupportsReview):
def approved(self) -> bool:
return True
class Test(SupportsReview):
def approved(self) -> bool:
return True
def validate(documents: List[SupportsReview]) -> List[SupportsReview]:
return [
document for document in documents
if document.approved()
]
documents = [Article(), PhotoGallery(), Test()]
approved_documents = validate(documents)
assert len(approved_documents) == 3
В этом коде мы описываем протокол SupportsReview
и валидатор работает со всеми классами, реализующими этот протокол. Если бы один из классов не поддерживал SupportsReview
, то mypy сообщил бы, что в documents
у нас есть значение неподходящего типа.
Сравнивая протоколы в Python с трейтами в Rust, мы увидим, что они очень похожи. Давайте напишем тоже самое на Rust.
Начнем с создания трейта Review
:
trait Review {
fn approved(&self) -> bool;
}
Создадим структуры и реализуем для них трейт Review
:
struct Article;
impl Review for Article {
fn approved(&self) -> bool {
true
}
}
struct PhotoGallery;
impl Review for PhotoGallery {
fn approved(&self) -> bool {
true
}
}
struct Test;
impl Review for Test {
fn approved(&self) -> bool {
true
}
}
Опишем функцию validate
и запустим код:
fn validate(documents: Vec<Box<dyn Review>>) -> Vec<Box<dyn Review>> {
documents
.into_iter()
.filter(|document| document.approved())
.collect::<Vec<_>>()
}
fn main() {
let documents: Vec<Box<dyn Review>> = vec![
Box::new(Article),
Box::new(PhotoGallery),
Box::new(Test),
];
let approved = validate(documents);
assert_eq!(approved.len(), 3);
}
Код на Rust выглядит менее понятно, чем код на Python за счет появления типов Box
и описания поддержки трейта Review
, как dyn Review
. Это важный момент – за все приходится платить, и это плата за статическую типизацию.
Обобщенное программирование
Мы обсудили протоколы и выяснили, что с их помощью мы можем накладывать ограничения на типы, с которым работаем. Но что делать, если нам нужно описать для типа более одного ограничения и указать, что при этом везде должен быть один и тот же тип? На помощь нам приходят дженерики. Рассмотрим, как строится работа с ними в Python и сравним с Rust.
Реализуем узел бинарного дерева поиска:
from typing import Generic, TypeVar, Optional
T = TypeVar('T')
class Node(Generic[T]):
def __init__(self, value: T,
left: Optional['Node'[T]] = None,
right: Optional['Node'[T]] = None,
) -> None:
self.value = value
self.left = left
self.right = right
if __name__ == '__main__':
root = Node(2)
root.left = Node(1)
root.right = Node(3)
Мы описали обобщенный тип T
, который может храниться внутри узла. Запустим mypy и убедимся, что все корректно описано.
? 01-types poetry run mypy --strict generics-01-1.py
Success: no issues found in 1 source file
Ошибемся в одном значении внутри узла и посмотрим, как mypy отловит эту ошибку:
root = Node(2)
root.left = Node(1)
root.right = Node('Hello!') # Тут ошибка
При создании корня дерева mypy определил тип T
как int
и не должен позволить нам создать другой узел с типом str
.
generics-01-1.py:18: error: Argument 1 to "Node" has incompatible type "str"; expected "int"
Found 1 error in 1 file (checked 1 source file)
Mypy верно поймал ошибку.
Но достаточно ли нам для описания узла дерева текущего определения? На данный момент мы наложили только одно ограничение – все типы внутри дерева должны быть одинаковыми. Но чтобы реализовать бинарное дерево поиска, необходимо уметь сравнивать значения внутри. Например, сейчас мы можем в узлы положить None
и при этом код будет определяться, как корректный.
Давайте наложим на тип T
дополнительное ограничение – T
должен реализовывать протокол сравнения. Поищем протокол Comparable
.
К сожалению, разговоры про этот протокол шли еще в 2015 году, но он так и не появился. Реализуем его самостоятельно:
C = TypeVar('C')
class Comparable(Protocol):
def __lt__(self: C, other: C) -> bool: ...
def __gt__(self: C, other: C) -> bool: ...
def __le__(self: C, other: C) -> bool: ...
def __ge__(self: C, other: C) -> bool: ...
И добавим в бинарное дерево поиска:
...
T = TypeVar('T', bound=Comparable)
class Node(Generic[T]):
def __init__(self, value: T,
left: Optional['Node'[T]] = None,
right: Optional['Node'[T]] = None,
) -> None:
self.value = value
self.left = left
self.right = right
def add(self, node: 'Node'[T]) -> None:
if node.value <= self.value:
self.left = node
else:
self.right = node
if __name__ == '__main__':
root = Node(2)
root.add(Node(1))
root.add(Node(3))
Mypy проверяет код и подтверждает, что все корретно. Попробуем ошибиться и проверим, как mypy отловит ошибку:
root = Node(None)
root.add(Node(None))
root.add(Node(None))
? 01-types poetry run mypy --strict generics-01-4.py
generics-01-4.py:35: error: Value of type variable "T" of "Node" cannot be "None"
generics-01-4.py:36: error: Value of type variable "T" of "Node" cannot be "None"
generics-01-4.py:37: error: Value of type variable "T" of "Node" cannot be "None"
Found 3 errors in 1 file (checked 1 source file)
Ошибка поймана, все работает.
Теперь реализуем тоже самое на Rust.
struct Node<T>
where T: Ord
{
pub value: T,
pub left: Option<Box<Node<T>>>,
pub right: Option<Box<Node<T>>>,
}
impl<T> Node<T>
where T: Ord
{
pub fn add(&mut self, node: Node<T>) {
if node.value <= self.value {
self.left = Some(Box::new(node))
} else {
self.right = Some(Box::new(node))
}
}
}
fn main() {
let mut root = Node { value: 2, left: None, right: None };
let node_1 = Node { value: 1, left: None, right: None };
let node_3 = Node { value: 3, left: None, right: None };
root.add(node_1);
root.add(node_3);
}
Код похож на тот, который мы делали в Python, но трейт сравнения нам не нужно писать самостоятельно. Он уже есть, и мы просто описываем его where T: Ord
.
Это отличие не кажется принципиальным, и можно сделать вывод, что протоколы и дженерики в Python не уступают Rust.
К сожалению, это не так.
from typing import TypeVar, Generic, Sized, Hashable
T = TypeVar('T', Hashable, Sized)
class Base(Generic[T]):
def __init__(self, bar: T):
self.bar: T = bar
class Child(Base[T]):
def __init__(self, bar: T):
super().__init__(bar)
На этот код mypy выведет:
? 01-types poetry run mypy --strict generics-01-6.py
generics-01-6.py:13: error: Argument 1 to "__init__"
of "Base" has incompatible type "Hashable"; expected "T"
generics-01-6.py:13: error: Argument 1 to "__init__"
of "Base" has incompatible type "Sized"; expected "T"
Found 2 errors in 1 file (checked 1 source file)
Этот пример скопирован из issue mypy на гитхабе и висит там уже достаточно давно.
Mypy – прекрасный проект, и работа, которая ведется над ним, достойна уважения и восхищения. Но пока опциональная статическая типизация в Python выглядит недостаточно мощным инструментом, позволяющим избавиться от всех ошибок, связанных с несоответствием типов. Rust же позволяет сделать это.
Продолжение следует ?
menstenebris
enum в Python все же более гибкие чем показано в статье
vladkorotnev
Разве это не присвоит просто каждому члену перечисления по строке?
В Rust подразумевается, что именно к каждому члену можно приделать по пачке полей. Т.е. превратить каждого члена в самостоятельную структуру, являющуюся при этом частью конечного перечисления.
yakimka8
Так? docs.python.org/3/library/enum.html#planet
vladkorotnev
Да, вот этот пример, вроде бы, то что надо
freecoder_xx
В вариантах перечисления может быть разный набор полей? В Rust можно создать такое перечисление:
cbmw
Да, может
mayorovp
Будет ли в такой схеме
Pending("Foo")
инстансомUserStatus
?cbmw
А почему он должен им быть? Вопрос был в том, можно ли в варианты перечисления засунуть разные структуры (наборы полей).
mayorovp
Потому что в статье было упомянуто, что в Rust можно "связать статус пользователя с дополнительной информацией", а тут утверждается что и в Питоне можно. Но я не вижу этого связывания (в том смысле этого слова, который был в цитате) в ваших примерах.
cbmw
А почему вы решили что связывание в данном контексте обязательно наследование, а не, например, композиция?
mayorovp
Можете вместо наследования использовать что угодно, лишь бы
Pending("Foo")
имело хоть какое-нибудь отношение кUserStatus
.Связывание в данном контексте означает возможность связать значение перечисления с произвольном значением, а не с константой. То есть
Pending("Pending")
,Pending("Foo")
иPending("Bar")
должны быть полностью равноправны как значения перечисления, чего в коде выше не наблюдается.mayorovp
Комбинируя увиденное в других комментариях, могу предположить как это должно выглядеть на Питоне на самом деле:
Если только я не допустил какой-нибудь глупой ошибки — это и есть аналог перечислению из Rust. А вовсе не та ерунда, которую пытались написать cbmw и menstenebris...
ardraeiss
Не то чтоб ошибки, но последняя строка скорее бессмысленная, потому что пользоваться ею неудобно будет.
Вот так-то так она более применима
и обозначает что у поля SomeClass.message ожидаемые(!) типы значений — это перечисленная четвёрка.
Но это вовсе не мешает присвоить туда что угодно
А полного прямого аналога предложенного исходно перечисления я в Питоне вот так и не назову. Всё-таки "подсказка по типам" Питона принципиально отличается от жёсткого задания типов Раста, у них даже задачи разные.
cbmw
Всё так, Union не делает проверок в runtime.
Если совсем придираться, на мой взгляд, должно быть как-то так:
ardraeiss
Да, как вариант.
mayorovp
А что именно там неудобно будет?
Э-э-э, а None-то тут зачем и откуда?
andreymal
Вот для вас это «Э-э-э», а например для разработчиков Pydantic всё нормально)
ardraeiss
Да просто чтобы выставить значение-по-умолчанию "значение не выставлено". Считать ли это антипаттёрном — вопрос сильно дискуссионный, но именно в таком значение None часто используется.
menstenebris
В python я могу перечислению присвоить не только строку, но и dataclass с полями. Таким образом каждый член перечисления становится структурой с полям.
Проблема в другом. В python перечисление это костыль над системой классов, им даже пришлось отдельную реализацию писать чтобы он не жрал столько памяти. В Rust же перечисление это абстракция нулевой стоимости, можно использовать по поводу и без. Но из за этого кое какие вещи становятся недоступны, например в diesel не могут нормально скрутить вместе enum в rust и enum в postgres.
freecoder_xx
Могут ли варианты перечисления иметь данные разных типов?
mayorovp
Будет ли в такой схеме
Status("Foo")
инстансомUserStatus
?ardraeiss
Простите, а зачем? То есть "для какой цели/задачи"
Потому что я не уверен, что понимаю суть Вашего вопроса. По структуре классов Status является базовым классом для UserStatus и не может быть инстансом унаследованного класса, тем более через множественное наследование.
И мне не совсем понятно зачем там вообще использовано то множественное наследование, enum.Enum этого не требует. С тем же успехом работать будет и "class UserStatus(Enum):"
mayorovp
Потому что перечисления в Rust работают именно так, а тут вроде как пытаются построить их аналог.
ardraeiss
Хмм.
В этом случае сама идея "сделать из питона раст" видится мне наибольшая ошибка — ибо конечно же результат не будет столько же удобен/гибок/удобочитаем, как исходный Rust! Из раста если питон делать тоже будет непойми что.
cbmw
Никто аналоги тут не строит, вся эта ветка обсуждения началась с того что мы в статье действительно показали python enum немного однобоко (не такими гибкими).
А к вопросу о том как работают перечисления в расте, вот вам небольшой контрпример:
Какое отношение имеет структура
Move
к вариантуMessage::Move
?mayorovp
Она является типом первого и единственного поля Message::Move, а что?
Bruce_Robertson
Неужто все забыли и как ни в чем не бывало плюсуют посту в блоге той самой гоп-компании, которая буквально год назад, организовала уголовное преследование своего бывшего сотрудника, пытаясь отжать у него, его же детище. А потом, когда это не удалось, для отмыва репутации передала требования другой, подконтрольной компании, которая продолжила судебную травлю, но уже в международном формате. Неужели вы все это забыли и эта гоп-контора продолжит тут пиариться, как будто ничего и не было?
cbmw AndreyErmilov коллеги, вы считаете нормальным, работать, писать посты для компании, которая спустя 15 лет (!) организует уголовное преследование своего сотрудника, позарившись на плоды его труда? Либо вам плевать на такое и лишь бы деньги платили?
Заранее прошу извинить за выделение жирным, просто иначе это воззвание потеряется тут.
embden
А ещё не забывайте, что вы сидите на ресурсе, который через пару-тройку месяцев после начала этой истории в самый её разгар сам активно сотрудничал с рамблер групп для "марафона удалёнки".
Inspector-Due
Конечно, Рамблер делала ужасные вещи, но разве мы должны ставить минусы под постами, которые вообще никакого отношения к тому инциденту? Ведь бизнес хотели отжать одни люди, а писали статью другие. Так значит мы должны принижать труд вторых?
Bruce_Robertson
swelf
А еще хабр корпоративный блог не закрыл. Границы ответсвенности то должны быть.
Dair_Targ
Какое дело все эти рассуждения имеют к содержанию и качеству статьи, за которые собственно плюсы и получаются?
stokker
Гитлер_хороший_художник?
SadOcean
Ну тащемта да.
Возможно, музыкант был грубияном, наркоманом и мерзким чуваком, но если музыка у него хорошая — разве нельзя признать, что она хорошая?
Рамблер большой, он делал плохие вещи, делал хорошие вещи, в нем работает куча людей.
Какой уровень взаимодействия с Рабмлером делает тебя соучастником наезда на автора NGinx?
Bruce_Robertson
Я вроде бы понятно пояснил в комменте выше. Проще уже некуда. Публикация в блоге набирает плюсцов — поднимается в топе сама компания, для блога которой статья написана. Т.е ты своими действиями пиаришь компанию, которая позволила себе немыслимое. Если автор уберет упоминание о RG из текста, уберет публикацию из их блога, я с радостью поблагодарю его за труд.
Всем солидарным — жму руку. 20 плюсов моему комменту и всего 4 минуса как бы намекают, что не все потеряно и люди не забывают такие подлые дела.
IvanElenskiyBogolepov
Тогда вам стоит пройти по всем площадкам, где публикуются работы Google, Microsoft и прочих гигантов тоже с подобными заявлениями — и не забудьте заминусить ВСЕ их проекты и статьи, а то воротят бог весть что. Минусы под статьи ставят к статье, а не к людям или компаниям — иначе нарушается объективное восприятие информации, которая была в статье подана.
Только наивный будет считать, что бизнес будет всегда чистеньким, особенно больших размеров — везде есть люди со своими интересами, которые не всегда благие для остальных. Но судить по паре десятков людей, решивших попиариться или что-то хуже сделать о тысячах людей, которые любят свою работу и/или просто пишут тут статьи — как воспринимать мир черно-белым. Принцип коллективной отвественности не стоит тут навязывать, да и никто не предложит ВСЕМ этим тысячам сотрудников достойное место чисто из солидарности, а у кого-то ипотека, дети…
Удобно быть моралистом, когда лично тебя это никак не касается — можно встать в атакующую позу и не считаться с людьми, потому что вы правы в своем праведном гневе) И далеко не все сотрудники Рамблера были в восторге — поищите публичные письма их сотрудников, вроде были как раз тут, на хабре.
stokker
Так никто не говорит людям «уходите из Рамблера» или «не печатайте статьи на Хабре»! Достаточно просто не печатать в их корпоративном блоге. Или за это там тоже наказывают? ))
IvanElenskiyBogolepov
Ну так давайте не будем смотреть конференции и прочие мероприятия Google?) Они ведь пиарятся, хотя давно убрали «Don't be evil» из своего устава, да и сколько было скандалов (что неизбежно при увеличении размеров фирмы).
Все равно мы будем пользоваться Android, писать на Go и прочее-прочее).
Имидж компании состоит не только из негативных поступков, но и из положительных. Лично я не одобряю заведение уголовного дела, но и продолжать бессмысленное макание людей (которых причисляют косвенно к виновникам) в грязь не вижу смысла. Имиджевые потери для Рамблера и так были достаточно высокие — пусть отрабатывают, как могут. Даже если это простые статьи тут, которые кому-то пригодятся.
И если капнуть чуть глубже в те события: подавляющее в тот момент было именно пожелание «уходите из Рамблера».
И давайте размышлять абстрактно — кто-то обрушил своим недальновидным поведением имидж фирмы, причем осудил другого человека. Что будет дальше? Фирма должна развалиться из-за того, что теперь ее имидж плохой? А чем это отличается от «уходите из Рамблера» по причине того, что он перестанет существовать?)) Вариант «не чинить репутацию» равен «уйти из Рамблера» на длинной дистанции, просто не по своему желанию)
stokker
Да, ради этих слов стоило зарегиться!
Разработчику важно работать на свою репутацию, тогда ничего не страшно. К тому же на длинной дистанции все уходят.IvanElenskiyBogolepov
Давно сидел тут без учетки, но после N-ого раза на тему злых корпораций прорвало)
Согласен про работу на свою репутацию. Про уход — все хотят уходить из компаний с нормальной репутацией, а не оставаться мечеными. Пускай исправляются полезными для общества делами.
Осуждать программиста за его статью по вине какого-то менеджера (которого он и никогда не видел/слышал скорее всего в наших реалиях) есть дело не самое адекватное. Какой срок давности у данного преступления? Или Рамблер уже никогда не отмоется?))
IvanElenskiyBogolepov
Всегда интересно, как это на хабре (все же адекватные, уважают чужое мнение) ставят минус в карму без явной аргументации в комментариях — узнал)
ardraeiss
Минус и за просто вопросы или цитирование исторических фактов могут поставить. Молча ибо "сам догадайся чем ты мимопроходившему сообществу не мил". Особенно когда кроме минуса ответить то и нечего ибо документальный факт, а чью-то мозоль отдавил или картину мира пошатнул.
Так что — остаётся относиться к хабру как к просто месту общения просто случайных людей. Ничем, по факту, айтишники не отличаются от кого угодно стереотипно другого. И хабр тут не исключение.
Bruce_Robertson
А вот и сотруднички Рамблера подвалили. Ваня, вас сюда начальство прислало с заданием, или вы сами пришли выслужиться?
Причем тут гугл и майкрософт? Они были замечены в рейдерстве с возбуждением уголовного дела? Речь сейчас не про них, а конкретно про Рамблер Груп.Вы же вроде миддл, но построить простейшую логическую цепочку видимо не в состоянии. А потом еще удивляетесь «а чего это мне в карму минусят».
Ага, еще напишите «не мы такие, жизнь такая». Вас кто-то просит увольняться? Ну не можете это сделать и ладно, всякое может быть. Прост не афишируйте хотя бы причастность к этой структуре, не пишите для нее посты, тем самым пиаря ее. Берите пример с парней, работающих в Роскомнадзоре.
Да плевать на ваши публичные филькины грамоты, если потом вы в комменты приходите отмазываться и пишете посты пиарящие блог этой галеры.
Осуждают не за саму статью, а за ее размещение в блоге.
Я даже не знаю что нужно сделать, чтобы от такого отмыться. Статьями на Хабре — точно нет. У таких дел нет срока давности, увы.
IvanElenskiyBogolepov
Тут запрет на личное мнение по данной ситуации?)
Попытка дискредитации через сопричастность к фирме (а не к решению по данной проблеме) — это «сильный» аргумент. Напишешь хоть слово — сразу враг?) С подобной нетерпимостью даже диалог не выстроишь.
Я не просил поддерживать Рамблер. Не писал о том, что невиновата организация — только люди, которые управляют (которые уже почти все свалили или будут заменены). Взваливать ответственность на сотрудников — как вспоминать немцев из ВОВ и вешать их вину на современников; как запрещать олимпийцам выступать под флагом страны, в которой они родились и любят (страну, а не государство). У всех должен быть шанс исправиться, да и не отвечают сыны за отцов.
И если мы перешли на личности — у вас у самого карма далеко внизу, что намекает на не самый адекватный способ ведения диалога самим обществом. Да и классификация про клоунов заочно в профиле — это вряд ли нормально. С чужим мнением принято считаться, даже если оно вас не устраивает — априори. Вы сами об этом просите в профиле) Более того — вы пытаетесь задавить не конкретные действия, а чувства других людей: кому было все равно, тот уже ушел на волне «как ты можешь там работать».
Bruce_Robertson
Ваш пример изначально некорректный. Взваливать ответственность на сотрудников — это разделение ответственности за преступления нацистов, солдатами вермахта наравне с их генералами. У них тоже была такая отмазка «мы же ничего не решаем, мы просто выполняли приказы». Не прокатило.
Моя карма свидетельствует лишь о том, что я не стесняюсь в выражениях.
Не у всех, а только у тех кто признал ошибку. Вы читали чем кончилось дело? Дело передали в компанию, которая продолжила судебное преследование уже в международном формате. Заслуживает ли права на исправление тот, кто только делает вид что исправился? На мой взгляд однозначно нет.
IvanElenskiyBogolepov
Я не стал скрывать свою личность, хотя и мог.
Потому что вы не уловили суть примера — сейчас никто из рядовых сотрудников не совершает преступление. Да и не принимали рядовые сотрудники решение (а уж тем более какое-либо действие) в отношении данного дела, в отличие от тех же рядовых нацистов.
И зря — слово режет сильнее ножа. Для себя вы просите корректности в выборе слов.
Где вы увидели НЕ признание ошибки?) Ошибка есть — это факт, о чем мы уже писали ранее.
Если вы настолько в курсе, то стоит углубиться еще больше в последние новости — Мамут больше не у руля, виновник самоудалился (при этом попортив кровь Сберу, который ни сном-ни духом об этом был). Да, разбирательство продолжается, и всем очевидно, что оно закончится в пользу автора — проще в суде заткнуть одну российскую компанию по неправомерной претензии, чем заставить весь мир платить за софт с оглядкой на 15 лет использования.
stokker
IvanElenskiyBogolepov
Ни в коем случае. Вы ведь сами отвечаете на свой вопрос цитированием меня.
В итоге недальновидной и жадной (и несколько жалкой на фоне сделки F5 Networks по Nginx) попытки нагреться высшего руководства перед уходом сильно пострадал имидж фирмы — а также был причинен ущерб автору. О какой выплате по авторским правам может идти речь, если все понимают, что доказательств нет?
Dair_Targ
https://ru.wikipedia.org/wiki/Reductio_ad_Hitlerum