Список типов данных в python
Содержание:
- Введение
- Реализация/протокол типа Iterator:
- Расширенная реализация/протокол типа generator.
- Файл
- Основы
- Standard Interpreter Types¶
- Interactions with enums and exhaustiveness checks
- Числовые типы
- Interactions with narrowing
- Python Numbers
- Операции сравнения
- Dynamic Type Creation¶
- Методы целых чисел
- Протоколы
- Понимание утиной типизации в Python.
Введение
Автор иллюстрации — Magdalena Tomczyk
Python — язык с динамической типизацией и позволяет нам довольно вольно оперировать переменными разных типов. Однако при написании кода мы так или иначе предполагаем переменные каких типов будут использоваться (это может быть вызвано ограничением алгоритма или бизнес логики)
И для корректной работы программы нам важно как можно раньше найти ошибки, связанные с передачей данных неверного типа
Сохраняя идею динамической утиной типизации в современных версиях Python (3.6+) поддерживает аннотации типов переменных, полей класса, аргументов и возвращаемых значений функций:
- PEP 3107 — Function Annotations
- PEP 484 — Type Hints
- PEP 526 — Syntax for Variable Annotations
Аннотации типов просто считываются интерпретатором Python и никак более не обрабатываются, но доступны для использования из стороннего кода и в первую очередь рассчитаны для использования статическими анализаторами.
Меня зовут Тихонов Андрей и я занимаюсь backend-разработкой в Lamoda.
В этой статье я хочу объяснить основы использования аннотаций типов и рассмотреть типичные примеры, реализуемые аннотациями из пакета .
Реализация/протокол типа Iterator:
Объекты типа должны поддерживать следующие два метода, которые вместе образуют протокол итератора :
Метод вернет объект итератора. Это необходимо для того, чтобы разрешить использование контейнеров и итераторов с операторами и . Метод соответствует слоту структуры типа для объектов Python в API-интерфейсе. — необязательный указатель на функцию, которая возвращает итератор для объекта. Его присутствие обычно сигнализирует о том, что экземпляры этого типа являются итеративными, хотя последовательности могут быть итерируемыми без этой функции.
Метод должен возвращать следующий элемент из контейнера, а если элементы в последовательности закончились, то метод должен бросить исключение .
Python определяет несколько объектов итератора для поддержки итерации по общим и конкретным типам последовательностей, словарям и другим более специализированным формам. Конкретные типы не важны за пределами их реализации протокола итератора.
Как только итератора вызывает , он должен продолжать вызывать его и при последующих вызовах. Реализации, которые не подчиняются этому свойству, считаются нарушенными.
Для создания объекта типа можно воспользоваться встроенной функцией . По итератору можно двигаться с помощью функции .
Расширенная реализация/протокол типа generator.
Обратите внимание, что вызов любого из методов генератора, когда генератор уже выполняется, вызывает исключение. Запускает выполнение функции генератора или возобновляет его при последнем выполненном выражении
Когда функция генератора возобновляется с помощью метода , текущее выражение всегда возвращает как. Затем выполнение продолжается до следующего выражения , где генератор снова приостанавливается, а значение возвращается объекту вызвавшему. Если генератор завершает работу без получения другого значения, возникает исключение
Запускает выполнение функции генератора или возобновляет его при последнем выполненном выражении . Когда функция генератора возобновляется с помощью метода , текущее выражение всегда возвращает как . Затем выполнение продолжается до следующего выражения , где генератор снова приостанавливается, а значение возвращается объекту вызвавшему . Если генератор завершает работу без получения другого значения, возникает исключение .
Этот метод обычно вызывается неявно, например, с помощью цикла или встроенной функции.
Метод возобновляет выполнение и “отправляет » значение в функцию генератора. Аргумент становится результатом текущего выражения . Метод возвращает следующее значение, полученное генератором, или вызывает , если генератор завершает работу без получения другого значения. Когда вызывается для запуска генератора, он должен быть вызван с аргументом , поскольку нет выражения , которое могло бы получить значение.
Метод может использоваться, например, чтобы реализовать генератор, который можно будет завершать из вызывающей программы или переустанавливать в нем текущую позицию в последовательности результатов.
Метод вызывает исключение типа в точке, где генератор был приостановлен и возвращает следующее значение, полученное функцией генератора. Если генератор завершает работу без получения другого значения, возникает исключение . Если функция генератора не ловит переданное исключение или вызывает другое исключение, то это исключение распространяется на вызывающий объект.
Метод вызывает исключение в точке, где функция генератора была приостановлена. Если функция генератора затем завершает работу корректно, уже закрыта или вызывает (не улавливая исключение), close возвращается к вызывающему объекту. Если генератор выдает значение, то возникает ошибка . Если генератор вызывает любое другое исключение, оно передается вызывающему объекту. Метод ничего не делает, если генератор уже вышел из-за исключения или нормального выхода..
Файл
Работа с файлами, хранящимися где-то на внешнем носителе, в Python реализована в виде объектов-файлов. Они относятся к объектам базового типа, но обладают весьма характерной чертой: нельзя создать экземпляр объекта-файла при помощи литералов.
Чтобы начать работу с файлами, нужно вызвать функцию и передать ей в качестве аргументов имя файла из внешнего источника и строку, описывающую режим работы функции:
Операции с файлами могут быть разными, а, следовательно, разными могут быть и режимы работы с ними:
- – выбирается по умолчанию, означает открытие файла для чтения;
- – файл открывается для записи (если не существует, то создаётся новый);
- – файл открывается для записи (если не существует, то генерируется исключение);
- – режим записи, при котором информация добавляется в конец файла, а не затирает уже имеющуюся;
- – открытие файла в двоичном режиме;
- – ещё одно значение по умолчанию, означающее открытие файла в текстовом режиме;
- – читаем и записываем.
Основы
В простейшем случае аннотация содержит непосредственно ожидаемый тип. Более сложные кейсы будут рассмотрены ниже. Если в качестве аннотации указан базовый класс, допустимо передача экземпляров его наследников в качестве значений. Однако использовать можно только те возможности, что реализованы в базовом классе.
Аннотации для переменных пишут через двоеточие после идентификатора. После этого может идти инициализация значения. Например,
Параметры функции аннотируются так же как переменные, а возвращаемое значение указывается после стрелки и до завершающего двоеточия. Например,
Для полей класса аннотации должны быть указаны явно при определении класса. Однако анализаторы могут выводить автоматически их на основе метода, но в этом случае они не будут доступны во время выполнения программы. Подробнее про работу с аннотациями в рантайме во второй части статьи
Кстати, при использовании dataclass типы полей необходимо указывать именно в классе. Подробнее про dataclass
О функции print мы уже немного
говорили на предыдущем занятии, здесь рассмотрим подробнее различные
возможности ее использования. Например, эту функцию можно записывать в таких
вариациях:
print(1) print(1, 2) print(1, 2, 3)
И так далее,
число аргументов может быть произвольным. Соответственно все эти значения в
строчку будут выведены в консоли. Причем, значения разделяются между собой
пробелом. Это разделитель, который используется по умолчанию. Если нужно
изменить значение этого разделителя, то для этого используется специальный
именованный аргумент sep:
print(1, 2, 3, sep=",") print(1, 2, 3, sep="-") print(1, 2, 3, sep="***")
то есть, здесь можно прописывать самые разные строки-разделители.
Далее, вы уже
заметили, что каждый вызов функции print делает перевод
строки. Этот символ автоматически добавляет в конец выводимых данных. Но, мы
также можем его изменить. Для этого используется именованный аргумент end:
print(1, 2, 3, sep=",", end=':') print(1, 2, 3, sep="-", end='--end--\n') print(1, 2, 3, sep="***")
Смотрите, теперь
у нас после первой строчки нет перевода строки, а поставлено двоеточие с
пробелом, которые мы указали в аргументе end. После второго
вывода в конце была добавлена строчка и указан символ ‘\n’ перевода
строки.
В качестве
примера все это можно использовать для более гибкого вывода значений с помощью print:
name = "Федор" print("Имя", name, sep=":")
Но это не самый
удобный вывод значений. Функция print позволяет делать довольно гибкий
форматированный вывод данных с применением спецификаторов. Например:
name = "Федор"; age = 18 print("Имя %s, возраст %d"%(name, age))
В результате,
вместо спецификатора %s будет подставлена первая переменная,
указанная в скобках, в виде строки, а вместо %d – вторая
переменная age в виде целого
числа. То есть, для каждого типа данных существует свой спецификатор. Наиболее
употребительные, следующие:
-
%d, %i, %u – для вывода целочисленных
значений; -
%f – для вывода
вещественных значений; -
%s
– для
вывода строк; -
%%
— для
вывода символа %
Вот основные
возможности функций input и print в Python.
Видео по теме
Python 3 #1: установка и запуск интерпретатора языка
Python 3 #2: переменные, оператор присваивания, типы данных
Python 3 #3: функции input и print ввода/вывода
Python 3 #4: арифметические операторы: сложение, вычитание, умножение, деление, степень
Python 3 #5: условный оператор if, составные условия с and, or, not
Python 3 #6: операторы циклов while и for, операторы break и continue
Python 3 #7: строки — сравнения, срезы строк, базовые функции str, len, ord, in
Python 3 #8: методы строк — upper, split, join, find, strip, isalpha, isdigit и другие
Python 3 #9: списки list и функции len, min, max, sum, sorted
Python 3 #10: списки — срезы и методы: append, insert, pop, sort, index, count, reverse, clear
Python 3 #11: списки — инструмент list comprehensions, сортировка методом выбора
Python 3 #12: словарь, методы словарей: len, clear, get, setdefault, pop
Python 3 #13: кортежи (tuple) и операции с ними: len, del, count, index
Python 3 #14: функции (def) — объявление и вызов
Python 3 #15: делаем «Сапер», проектирование программ «сверху-вниз»
Python 3 #16: рекурсивные и лямбда-функции, функции с произвольным числом аргументов
Python 3 #17: алгоритм Евклида, принцип тестирования программ
Python 3 #18: области видимости переменных — global, nonlocal
Python 3 #19: множества (set) и операции над ними: вычитание, пересечение, объединение, сравнение
Python 3 #20: итераторы, выражения-генераторы, функции-генераторы, оператор yield
Python 3 #21: функции map, filter, zip
Python 3 #22: сортировка sort() и sorted(), сортировка по ключам
Python 3 #23: обработка исключений: try, except, finally, else
Python 3 #24: файлы — чтение и запись: open, read, write, seek, readline, dump, load, pickle
Python 3 #25: форматирование строк: метод format и F-строки
Python 3 #26: создание и импорт модулей — import, from, as, dir, reload
Python 3 #27: пакеты (package) — создание, импорт, установка (менеджер pip)
Python 3 #28: декораторы функций и замыкания
Python 3 #29: установка и порядок работы в PyCharm
Python 3 #30: функция enumerate, примеры использования
Standard Interpreter Types¶
This module provides names for many of the types that are required to
implement a Python interpreter. It deliberately avoids including some of
the types that arise only incidentally during processing such as the
type.
Typical use of these names is for or
checks.
If you instantiate any of these types, note that signatures may vary between Python versions.
Standard names are defined for the following types:
-
The type of user-defined functions and functions created by
expressions.Raises an with argument .
The audit event only occurs for direct instantiation of function objects,
and is not raised for normal compilation.
-
The type of -iterator objects, created by
generator functions.
-
The type of objects, created by
functions.New in version 3.5.
-
The type of -iterator objects, created by
asynchronous generator functions.New in version 3.6.
- class (**kwargs)
-
The type for code objects such as returned by .
Raises an with arguments , , , , , , , , .
Note that the audited arguments may not match the names or positions
required by the initializer. The audit event only occurs for direct
instantiation of code objects, and is not raised for normal compilation.- (**kwargs)
-
Return a copy of the code object with new values for the specified fields.
New in version 3.8.
-
The type for cell objects: such objects are used as containers for
a function’s free variables.New in version 3.8.
-
The type of methods of user-defined class instances.
-
The type of built-in functions like or , and
methods of built-in classes. (Here, the term “built-in” means “written in
C”.)
-
The type of methods of some built-in data types and base classes such as
or .New in version 3.7.
-
The type of bound methods of some built-in data types and base classes.
For example it is the type of .New in version 3.7.
-
The type of methods of some built-in data types such as .
New in version 3.7.
-
The type of unbound class methods of some built-in data types such as
.New in version 3.7.
- class (name, doc=None)
-
The type of . Constructor takes the name of the
module to be created and optionally its .Note
Use to create a new module if you
wish to set the various import-controlled attributes.-
The of the module. Defaults to .
-
The which loaded the module. Defaults to .
Changed in version 3.4: Defaults to . Previously the attribute was optional.
-
The name of the module.
-
Which a module belongs to. If the module is top-level
(i.e. not a part of any specific package) then the attribute should be set
to , else it should be set to the name of the package (which can be
if the module is a package itself). Defaults to .Changed in version 3.4: Defaults to . Previously the attribute was optional.
-
- class (t_origin, t_args)
-
The type of such as
.should be a non-parameterized generic class, such as ,
or . should be a (possibly of
length 1) of types which parameterize :>>> from types import GenericAlias >>> listint == GenericAlias(list, (int,)) True >>> dictstr, int == GenericAlias(dict, (str, int)) True
New in version 3.9.
- class (tb_next, tb_frame, tb_lasti, tb_lineno)
-
The type of traceback objects such as found in .
See for details of the
available attributes and operations, and guidance on creating tracebacks
dynamically.
-
The type of frame objects such as found in if is a
traceback object.See for details of the
available attributes and operations.
-
The type of objects defined in extension modules with , such
as or . This type is used as
descriptor for object attributes; it has the same purpose as the
type, but for classes defined in extension modules.
-
The type of objects defined in extension modules with , such
as . This type is used as descriptor for simple C
data members which use standard conversion functions; it has the same purpose
as the type, but for classes defined in extension modules.CPython implementation detail: In other implementations of Python, this type may be identical to
.
Interactions with enums and exhaustiveness checks
Type checkers should be capable of performing exhaustiveness checks when
working Literal types that have a closed number of variants, such as
enums. For example, the type checker should be capable of inferring that
the final else statement must be of type str, since all three
values of the Status enum have already been exhausted:
class Status(Enum): SUCCESS = 0 INVALID_DATA = 1 FATAL_ERROR = 2 def parse_status(s: Union) -> None: if s is Status.SUCCESS: print("Success!") elif s is Status.INVALID_DATA: print("The given data is invalid because...") elif s is Status.FATAL_ERROR: print("Unexpected fatal error...") else: # 's' must be of type 'str' since all other options are exhausted print("Got custom status: " + s)
The interaction described above is not new: it’s already
. However, many type
checkers (such as mypy) do not yet implement this due to the expected
complexity of the implementation work.
Some of this complexity will be alleviated once Literal types are introduced:
rather than entirely special-casing enums, we can instead treat them as being
approximately equivalent to the union of their values and take advantage of any
existing logic regarding unions, exhaustibility, type narrowing, reachability,
and so forth the type checker might have already implemented.
Числовые типы
«Все сущее есть Число» – сказал однажды мудрый грек по имени Пифагор. Числа – важнейший и фундаментальнейший из всех типов данных для всех языков программирования. В Python для их представления служит числовой тип данных.
int (целое число)
Концепция целых чисел проста и естественна. Это числа без дробной части, которые, говоря математическим языком, являются расширением натурального ряда, дополненного нулём и отрицательными числами.
Там, где есть числа, есть и математика. Поэтому резонно, что целые числа используются для исчисления всевозможных математических выражений. Также применяется в качестве описаний количественных свойств какого-либо объекта.
float (число с плавающей точкой)
Действительные или вещественные числа придуманы для измерения непрерывных величин. В отличие от математического контекста, ни один из языков программирования не способен реализовать бесконечные или иррациональные числа, поэтому всегда есть место приближению с определенной точностью, из-за чего возможны такие ситуации:
В плане записи, ничем не отличаются от
В плане использования – тоже, разве что в любых мало-мальски серьёзных вычислениях без никуда.
complex (комплексное число)
Привет высшей математике! Как вещественный ряд расширяет множество рациональных чисел, так и ряд комплексных чисел расширяет множество вещественных. Показательной особенностью комплексного ряда является возможность извлечения корня из отрицательных чисел.
В Python комплексные числа задаются с помощью функции
Помните, что операция сравнения для комплексных чисел не определена:
Комплексные числа широко применяются, например, для решения дифференциальных уравнений.
bool (логический тип данных)
В каком-то смысле наиболее простой и самый понятный из всех типов данных. У bool есть всего два значения:
- Истина (True);
- Ложь (False).
Однако за этой простотой кроется колоссальный пласт теории в виде булевой алгебры.
Переменные логического типа нужны для реализации ветвлений, они применяются для установки флажков, фиксирующих состояния программы, а также используются в качестве возвращаемых значений для функций, названия которых, зачастую, начинаются на «is» (isPrime, isEqual, isDigit). То есть тех, которые, на человеческом языке, отвечали бы на вопрос одним словом «Да» или «Нет».
Interactions with narrowing
Type checkers may optionally perform additional analysis for both enum and
non-enum Literal types beyond what is described in the section above.
For example, it may be useful to perform narrowing based on things like
containment or equality checks:
def parse_status(status: str) -> None: if status in ("MALFORMED", "ABORTED"): # Type checker could narrow 'status' to type # Literal here. return expects_bad_status(status) # Similarly, type checker could narrow 'status' to Literal if status == "PENDING": expects_pending_status(status)
It may also be useful to perform narrowing taking into account expressions
involving Literal bools. For example, we can combine Literal,
Literal, and overloads to construct «custom type guards»:
Python Numbers
Integers, floating point numbers and complex numbers fall under Python numbers category. They are defined as , and classes in Python.
We can use the function to know which class a variable or a value belongs to. Similarly, the function is used to check if an object belongs to a particular class.
Output
5 is of type <class 'int'> 2.0 is of type <class 'float'> (1+2j) is complex number? True
Integers can be of any length, it is only limited by the memory available.
A floating-point number is accurate up to 15 decimal places. Integer and floating points are separated by decimal points. 1 is an integer, 1.0 is a floating-point number.
Complex numbers are written in the form, , where x is the real part and y is the imaginary part. Here are some examples.
Notice that the variable b got truncated.
Операции сравнения
Для сравнения чисел, доступно \(8\) операций сравнения, причем все они имеют одинаковый приоритет:
№ | Операция | Результат | Замечание |
---|---|---|---|
1 | True если x меньше y, иначе False | ||
2 | True если x меньше или равно y, иначе False | ||
3 | True если x больше y, иначе False | ||
4 | True если x больше или равно y, иначе False | ||
5 | True если x равно y, иначе False | ||
6 | True если x не равно y, иначе False | ||
7 | True если x и y это один и тот же объект, иначе False | ||
8 | True если x и y это не один и тот же объект, иначе False |
Важно: приоритет операций сравнения ниже математических и побитовых операций.
Наряду с оператором сравнения значений чисел и , в Python имеются операторы и , которые позволяют выяснить, являются сравниваемые значения одним и тем же объектом или нет. Например:
Не смотря на то, что значения a и b равны, в памяти компьютера они хранятся как разные объекты:
Однако, придумать для данного оператора сколь-нибудь полезное практическое применение, относительно математических операций я не могу.
В Python сравнение является эквивалентным т.е. сравнения связаные оператором в произвольные цепочки могут быть записаны в более компактной форме. Выполнение таких выражений начинается слева направо и останавливается как только будет получено первое значение False. Это означает, что если в выражении сравнение вернет False то сравнение выполняться не будет.
Dynamic Type Creation¶
- (name, bases=(), kwds=None, exec_body=None)
-
Creates a class object dynamically using the appropriate metaclass.
The first three arguments are the components that make up a class
definition header: the class name, the base classes (in order), the
keyword arguments (such as ).The exec_body argument is a callback that is used to populate the
freshly created class namespace. It should accept the class namespace
as its sole argument and update the namespace directly with the class
contents. If no callback is provided, it has the same effect as passing
in .New in version 3.3.
- (name, bases=(), kwds=None)
-
Calculates the appropriate metaclass and creates the class namespace.
The arguments are the components that make up a class definition header:
the class name, the base classes (in order) and the keyword arguments
(such as ).The return value is a 3-tuple:
metaclass is the appropriate metaclass, namespace is the
prepared class namespace and kwds is an updated copy of the passed
in kwds argument with any entry removed. If no kwds
argument is passed in, this will be an empty dict.New in version 3.3.
Changed in version 3.6: The default value for the element of the returned
tuple has changed. Now an insertion-order-preserving mapping is
used when the metaclass does not have a method.
See also
-
Full details of the class creation process supported by these functions
- PEP 3115 — Metaclasses in Python 3000
-
Introduced the namespace hook
- (bases)
-
Resolve MRO entries dynamically as specified by PEP 560.
This function looks for items in bases that are not instances of
, and returns a tuple where each such object that has
an method is replaced with an unpacked result of
calling this method. If a bases item is an instance of ,
or it doesn’t have an method, then it is included in
the return tuple unchanged.New in version 3.7.
Методы целых чисел
Целые числа – это объекты, которые обладают следующими методами:
- int.bit_length()
- возвращает количество бит, которое необходимо для представления числа в памяти, без учета знака и незначащих нулей:
- int.to_bytes(length, byteorder, *, signed=False)
- возвращает массив байтов, который соответствует данному числу:
Параметр задает необходимое количество байтов, а определяет в каком порядке возвращать байты: значение – от старшего к младшему, – от младшего к старшему. Оба параметра являются обязательными:
Если указанных байтов недостаточно для представления числа, то будет вызвано исключение OverflowError. А что бы узнать и (или) использовать порядок байтов который использует машина, выполняющая код используйте .
Параметр позволяет установить использование дополнительного кода для отрицательных целых чисел:
Если , а число является отрицательным, то будет вызвано исключение OverflowError.
- classmethod int.from_bytes(bytes, byteorder, *, signed=False)
- возвращает целое число, которое соответствует указанному массиву байтов.
Параметры и являются обязательными. — должен быть байто-подобным объектом (строки байтов, массивы байтов, array.array и т.д.). Описание параметров и смотрите в :
Протоколы
Такие термины как «протокол итератора» или «протокол дескрипторов» уже привычны и используются давно.
Теперь можно описывать протоколы в виде кода и проверять их соответствие на этапе статического анализа.
Стоит отметить, что начиная с Python 3.6 в модуль typing уже входят несколько стандартных протоколов.
Например, (требующего наличие метода ), (требует ) и некоторых других.
Описание протокола
Протокол описывается как обычный класс, наследующийся от Protocol. Он может иметь методы (в том числе с реализацией) и поля.
Реальные классы, реализующие протокол могут наследоваться от него, но это не обязательно.
Мы можете комбинировать протоколы с помощью наследования, создавая новые.
Однако в этом случае вы так же должны явно указать Protocol как родительский класс
Дженерики, self-typed, callable
Протоколы как и обычные классы могут быть Дженериками. Вместо указания в качестве родителей и можно просто указать
Кроме того, протоколы могут использоваться в тех случаях, когда синтаксиса аннотации недостаточно.
Просто опишите протокол с методом нужной сигнатуры
Проверки в рантайме
Хотя протоколы и рассчитаны в первую очередь на использование статическими анализаторами, иногда бывает нужно проверить принадлежность класса нужному протоколу.
Чтобы это было возможно, примените к протоколу декоратор и / проверки начнут проверять соответствие протоколу
Однако такая возможность имеет ряд ограничений на использование. В частности, не поддерживаются дженерики
Понимание утиной типизации в Python.
Утиная типизация заключается в том, что вместо проверки типа чего-либо в Python мы склонны проверять, какое поведение оно поддерживает, зачастую пытаясь использовать это поведение и перехватывая исключение, если оно не работает.
Например, мы можем проверить, является ли что-то целым, пытаясь преобразовать его в целое число:
try x = int(input("Введите целое число: ")) except ValueError print("Это не целое число. Попробуй еще раз.") else print(f"Отлично: {x}")
Python программисты говорят «если это похоже на утку и крякает как утка, то это утка». Не нужно проверять ДНК утки, чтобы понять утка ли это, нужно просто посмотреть на ее поведение.
Утиная типизация «DuckTyping» настолько глубоко заложена и распространена в Python, что она действительно повсюду, как вода для рыбы: мы даже не думаем об этом. В Python очень часто проще предположить поведение объектов, вместо проверки их типов.
В Python программисты постоянно используют такие слова, как последовательность , итерируемый , вызываемый , отображение , чтобы описать поведение объекта, а не описание его типа. Тип означает класс объекта, который можно получить, используя встроенную функцию .
Слова, ориентированные на поведение, важны: нас не волнует, что такое объект, нам важно, что он может сделать
- — последовательность,
- — итерируемый,
- — вызываемый,
- — отображение,
- — файлоподобный,
- — менеджер контекста,
- — методы утиной типизации,
- ,
- «Утиную типизацию»?
- Если «DuckTyping» везде, то ?