Чарівний Python: Витонченість і незручність Python. Частина 1 (исходники), Різне, Програмування, статті

У порівнянні з “золотим століттям” популярності Python 1.5.2 – протягом багатьох років стабільної і надійної версії мови – Python придбав безліч нових синтаксичних можливостей і вбудованих функцій і типів. Для кожної зміни окремо було досить вагома підстава, проте в цілому через них сучасний Python – вже не та мова, яка при достатньому досвіді можна вивчити за один вечір. Крім цього, з деякими змінами пов’язані не тільки переваги, а й потенційні неприємності.


У цій статті я розгляну деякі неочевидні можливості останніх версій Python і постараюся визначити, які з них дійсно корисні, а які – просто зайве ускладнення мови. Моя стаття – це спроба вказати на декілька важливих моментів фахівцям, які не використовують Python постійно: від програмістів на інших мовах до вчених, для яких програмування – тільки допоміжний інструмент. При виникненні труднощів я пропоную можливі рішення.


Прокляття упорядкування


При переході з Python 2.0 на Python 2.1 сталася загадкова річ. Порівнянні в минулому об’єкти в новій версії при порівнянні викликали виключення. Зокрема, стало неможливим порівняння комплексних чисел як з іншими комплексними (тип complex), так і з дійсними (типи int, float і long) числами. Насправді ця проблема з’являлася і раніше, при порівнянні обичіних і Unicode-рядків, але тільки в деяких особливих випадках.


На мою думку, це – невдале і просто дуже дивне зміна. У старій добрій версії 1.5.2 я був упевнений, що оператор нерівності поверне яке-небудь значення незалежно від типів порівнюваних об’єктів. Звичайно, часто сенсу в цьому результаті не було (не можна порівнювати рядок з числом), але принаймні це був результат.


Після внесення змін деякі адепти Python завели суперечку про те, що правильно було б накласти заборону на будь-які порівняння об’єктів різних типів – принаймні при відсутності явно певних операторів порівняння. Мені здається, що при наявності користувальницьких класів і множинного успадкування цей варіант викличе сильні труднощі. До того ж було б украй незручно не мати можливості порівнювати один з одним float, int або long (або, скажімо, decimal). Хоча, можливо, розумне рішення існує.


Проте яким би воно не було, з’явилися б сильні відмінності від попередніх версій мови. На даний момент ми маємо абсолютно безладні правила порівняння, при яких не рятує навіть знання типів порівнюваних об’єктів, а відносини нерівності не є транзитивними або замкнутими:


Лістинг 1. Можливість порівняння залежить і від типу, і від значення




>>> map(type, (u1, s1, s2))
[<type “unicode”>, <type “str”>, <type “str”>]
>>> u1 < s1
True
>>> s1 < s2
True
>>> u1 < s2
UnicodeDecodeError: “ascii” codec can”t decode byte 0xf0 in position 0:
ordinal not in range(128)
>>> map(type, (n, j, u1))
[<type “int”>, <type “complex”>, <type “unicode”>]
>>> n < u1
True
>>> j < u1
True
>>> n < j
TypeError: no ordering relation is defined for complex numbers

В якості особливо витонченого знущання, незважаючи на те, що комплексні числа тепер непорівнянні з більшістю інших чисельних типів, оператори нерівності проте повертають цілком певне значення при порівнянні з більшістю нечисельних типів. Я розумію, що “чиста” теорія стверджує, що 1+1j, Наприклад, не менше і не більше 2-3j, Але як тоді варто розуміти це:


Лістинг 2. “Сюрпризи” при порівнянні




>>> 2-3j < “spam”
True
>>> 4+0j < decimal.Decimal(“3.14”)
True
>>> 4+0j < 5+0j
TypeError: no ordering relation is defined for complex numbers

З точки зору “чистої” теорії жодне з цих порівнянь неприпустимо.


Чудеса клоунади: сортування гетерогенних послідовностей


Іноді заходить суперечка про те, чи коректно порівнювати екземпляри непорівнянних типів. Але Python з легкістю виробляє подібні порівняння, і це добре співвідноситься з принципом “duck typing” (судити про об’єкт не по його типу, а з його поведінки). Колекції в Python часто складаються з об’єктів різних типів, в припущенні, що вдасться зробити щось схоже з кожним з об’єктів. Частий приклад такої дії – Кодування декількох абсолютно різних по типу об’єктів для передачі по зовнішніх каналах.


Для більшості подібних дій не потрібно визначати відносини нерівності. Проте є один дуже частий випадок, коли наявність порівнянь виявляється вкрай корисним: сортування, зазвичай для списків (Lists) або аналогічних користувацьких типів. Найчастіше необхідно обробляти колекцію в осмисленому порядку (наприклад, проглядати дані від менших елементів до великих). Іноді ж потрібно просто визначити жорсткий порядок елементів в декількох колекціях (наприклад, щоб визначити відмінності між ними). У таких випадках може знадобитися виконувати одні дії, коли об’єкт міститься в обох списках, і інші, коли він знаходиться тільки в одній з колекцій. Виклик if x in otherlist для кожного елемента привілля до статечному збільшення складності обчислень; паралельний перегляд двох відсортованих списків значно ефективніше. Наприклад:


Лістинг 3. Виконання різних дій в залежності від входження елемента в два списки




list1.sort()
list2.sort()
list2_xtra = []
list2_ndx = 0
for it1 in list1:
it2 = list2[list2_ndx]
while it1 < it2:
list2_ndx += 1
it2 = list2[list2_ndx]
if it1 == it2:
item_in_both(it1)
elif it1 > it2:
item_in_list1(it1)
else:
list2_xtra.appen(it2)
for it2 in list2_xtra:
item_in_list2(it2)

Іноді зручно локально визначити впорядкування навіть при наявності різнорідних елементів (наприклад, обробляти числа з плаваючою комою “по порядку”, хоча не можна сказати, більше вони або менше оброблюваних там же рядків).


Проблеми сортування


Природно, наведений вище алгоритм найчастіше викликає помилки, причому практично випадковим чином. Наприклад, ось невеликий набір списків, які можуть зустрітися в подібній програмі в ролі list1 і list2. Спробуйте здогадатися, які з них відсортують:


Лістинг 4. Вінегрет з сортованих і несортіруемих списків




[“x”,”y”,”z”, 1],
[“x”,”y”,”z”, 1j],
[“x”,”y”,”z”, 1j, 1], # Adding an element makes it unsortable
[0j, 1j, 2j], # An obvious “natural” order
[0j, 1, 2],
[0, 1, 2], # Notice that 0==0j –> True
[chr(120), chr(240)],
[chr(120), chr(240), “x”],
[chr(120), chr(240), u”x”], # Notice u”x”==”x” –> True
[u”a”, “b”, chr(240)],
[chr(240), u”a”, “b”] # Same items, different initial order

Я написав невелику програму, яка намагається відсортувати кожен список:


Лістинг 5. Результати сортування




% python compare.py
(0) [“x”, “y”, “z”, 1] –> [1, “x”, “y”, “z”]
(1) [“x”, “y”, “z”, 1j] –> [1j, “x”, “y”, “z”]
(2) [“x”, “y”, “z”, 1j, 1] –> exceptions.TypeError
(3) [0j, 1j, 2j] –> exceptions.TypeError
(4) [0j, 1, 2] –> exceptions.TypeError
(5) [0, 1, 2] –> [0, 1, 2]
(6) [“x”, “xf0”] –> [“x”, “xf0”]
(7) [“x”, “xf0”, “x”] –> [“x”, “x”, “xf0”]
(8) [“x”, “xf0″, u”x”] –> exceptions.UnicodeDecodeError
(9) [u”a”, “b”, “xf0″] –> [u”a”, “b”, “xf0”]
(10) [“xf0″, u”a”, “b”] –> exceptions.UnicodeDecodeError

Частина отриманих результатів випливає з раніше описаних проблем. Однак зверніть увагу на списки (9) і (10), які містять одні й ті ж елементи в різному порядку: успіх сортування залежить не тільки від типів і значень елементів, але і від деталей конкретної реалізації list.sort()!


Усуваємо проблеми порівняння


Після версії 1.5.2 в Python з’явився дуже корисний тип даних: безлічі (sets), спочатку як стандартний модуль, а згодом і у вбудованому варіанті (хоча деякі додаткові можливості, як і раніше винесені в модуль). У багатьох аналогічних щойно описаним випадках для отримання об’єднання або перетину досить замість того, щоб писати власні програми порівняння, просто використовувати замість списків (lists) безлічі (sets). Наприклад:


Лістинг 6. Множини та операції на них




>>> set1 = set([1j, u”2″, 3, 4.0])
>>> set2 = set([4, 3, 2, 1])
>>> set1 / set2
set([3, 1, 2, 1j, 4.0, u”2″])
>>> set1 & set2
set([3, 4])

Наведений приклад явно позначає одну дивина: операції на множинах використовують відношення рівності, а не еквівалентності. Можливо, в цьому є сенс, але мене особисто дивує, коли в об’єднанні міститься число з плаваючою комою 4.0, А в перетині – ціле 4. Точніше кажучи, те, який конкретно елемент потрапить в результат, залежить від порядку аргументів операції (незважаючи на формальну симетричність математичних операцій об’єднання та перетину):


Лістинг 7. Дивні результати операцій над множинами




>>> set2 & set1
set([3, 4.0])
>>> set([3, 4.0, 4, 4+0j])
set([3, 4.0])

Все ж у першому наближенні безлічі дуже корисні. Проте варто пам’ятати про можливість обійти проблему за допомогою власних функцій порівняння. В Python до версії 2.4 була можливість визначити власну функцію порівняння cmp() і передати її методу list.sort(). Такий спосіб дозволяв визначити порівняння для об’єктів, які інакше порівняти не можна, а проте проблема аргументу cmp() – В тому, що його доводиться викликати при кожному порівнянні: в Python виклик функції – досить дорога операція. Більш того, в кінці кінців виявлялося, що для деяких пар елементів значення функції порівняння обчислюються кілька разів.


Рішенням проблеми неефективності використання cmp може послужити перетворення Шварца (Schwartzian Transform): спочатку створити для кожного елемента сортованому оболонку, відсортувати і прибрати оболонки. На жаль, для цього буде потрібно написання додаткового коду (крім самого виклику list.sort(). ). Python 2.4 пропонує гарне рішення цієї проблеми з використанням нового аргументу key. Його значенням повинна бути функція, що повертає оболонку з об’єктом; таким чином, деталі перетворення Шварца залишаються невидимими для програміста. Пам’ятаючи, що комплексні числа непорівнянні навіть друг з одним, в той час як рядки Unicode викликають помилки лише при порівнянні з деякими звичайними рядками, можна написати:


Лістинг 8. Стабільний і універсальний алгоритм сортування




def stablesort(o):
# Use as: mylist.sort(key=stablesort)
if type(o) is complex:
return (type(o), o.real, o.imag)
else:
return (type(o), o)

Врахуйте, що порядок елементів може виявитися не зовсім таким, яким ви очікували його побачити: він не такий, яким був би при успішній сортування без використання оболонок. Наприклад, елементи різних чисельних типів не перемішуються всередині послідовності, а виявляються в різних частинах відсортованого списку. Але щонайменше забезпечений постійний порядок елементів для практично будь-якого списку (хоча при наявності об’єктів користувацьких типів така процедура все-таки може підвести).


Генератори як “не цілком послідовності”


У своєму розвитку Python значно збільшував орієнтованість на ітератори (laziness). Уже в декількох останніх версіях мови є можливість визначення генераторів за допомогою ключового слова yield у функції. Також при розвитку мови з’явився модуль itertools для операцій над ітераторами. У мові є вбудована функція iter() для отримання итераторов на послідовностях. В Python 2.4 з’явилися висловлювання-генератори (generator expressions), а у версії 2.5 з’явилися розширені генератори, що полегшують написання співпрограми. Багато об’єктів Python стали підтримувати ітерірованіе; наприклад, режим читання файлів, який вимагав виклику .xreadlines() (Раніше модуля xreadlines ), Тепер реалізований за замовчуванням в самому конструкторі open().


Ітерірованіе по dict раніше вимагало використання методу .iterkeys(); Тепер того ж результату можна добитися, просто написавши for key in dct. Функції, подібні xrange(), Незвичайні тим, що, з одного боку, повертають генератор, але, з іншого боку, це не зовсім “правильний” ітератор (немає методу .next()), Але і не повнофункціональний список начебто повертається range() . У той же час enumerate() повертає “справжній” генератор – то, для чого раніше використовувався конструктор xrange(). А itertools.count() – Ще одна функція з відкладеним обчисленням, що робить майже те ж саме, що і xrange(), Але повертає повнофункціональний ітератор.


Python розвивається в напрямку використання відкладених обчислень для створення послідовностей, і це чудово. Такий підхід дозволяє економити оперативну пам’ять і прискорювати дії, особливо при роботі з дуже великими послідовностями.


Проблема полягає в деякій “шизофренічно” Python в тому, що стосується подібностей і відмінностей між “справжніми” послідовностями і ітераторами. Основна складність в тому, що такий підхід порушує принцип “duck typing” – можливість працювати з об’єктом до тих пір, поки він правильно сприймає запити, не накладаючи жодних обмежень на його тип. Багато ітератори (або інші подібні об’єкти) іноді працюють як послідовності, а іноді ні, і навпаки: послідовності можуть вести себе як ітератори, але не завжди. Така поведінка може бути далеко не очевидно для знаходяться поза вузького кола занурених у священні таїнства Python.


Відмінності


Головне загальна властивість і итераторов, і послідовностей – в тому, що кожен з них підтримує власне ітерірованіе по собі – з використанням чи циклу for, Списочних виразів (list comprehensions) або генераторних виразів (generator comprehensions). Далі починаються відмінності. Найбільш важливе з них – те, що послідовності підтримують індексацію, а ітератори – ні. Але індексація – це, напевно, головне, що можна зробити з послідовністю – чому ж ітератори настільки безповоротно відмовляються від неї? Наприклад:


Лістинг 9. Послідовності та ітератори




>>> r = range(10)
>>> i = iter(r)
>>> x = xrange(10)
>>> g = itertools.takewhile(lambda n: n<10, itertools.count())
#…etc…

Для кожного з цих об’єктів можна написати for n in thing. І якщо “конкретизувати” якої-небудь з них за допомогою list(thing), Результат буде однаковий для всіх. Але якщо вам потрібно отримати певний елемент (або декілька), доведеться згадати точний тип об’єкта thing. Наприклад:


Лістинг 10. Успіх і помилки індексації




>>> r[4]
4
>>> i[4]
TypeError: unindexable object

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


Лістинг 11. Емулюючи індексування




>>> thing, temp = itertools.tee(thing)
>>> zip(temp, “.”*5)[-1][0]
4

Попередній виклик itertools.tee() зберігає вихідний ітератор незмінним. Щоб отримати кілька елементів, можна використовувати функцію itertools.islice() і деяка кількість додаткового коду.


Лістинг 12. Дістаємо кілька елементовe




>>> r[4:9:2]
[4, 6, 8]
>>> list(itertools.islice(r,4,9,2)) # works for iterators
[4, 6, 8]

Клас-оболонка


Для зручності наведені процедури можна об’єднати в клас-оболонку:


Лістинг 13. Індексація на ітератора




>>> class Indexable(object):
… def __init__(self, it):
… self.it = it
… def __getitem__(self, x):
… self.it, temp = itertools.tee(self.it)
… if type(x) is slice:
… return list(itertools.islice(self.it, x.start, x.stop, x.step))
… else:
… return zip(temp, range(x+1))[-1][0]
… def __iter__(self):
… self.it, temp = itertools.tee(self.it)
… return temp

>>> integers = Indexable(itertools.count())
>>> integers[4]
4
>>> integers[4:9:2]
[4, 6, 8]

При певному бажанні можна змусити об’єкт працювати і як послідовність, і як ітератор. Але кількість зусиль, яке для цього потрібно, невиправдано велике; індексування мало б “просто” працювати, незалежно від того, чи використовується воно для послідовності або для ітератора.

Слід зауважити, що оболонка Indexable не так хороша, як хотілося б. Основний недолік в тому, що кожен раз доводиться створювати нову копію ітератора. Інший, більш швидкий варіант – кешувати початок послідовності при витяганні блоків і згодом використати його для швидкого доступу до вже переглянутих елементам. Звичайно, слід дотримуватися балансу між обсягом використовуваної пам’яті і витратами на ітерірованіе. Тим не менш було б краще, якби все це вже було реалізовано в самому Python – адепти мови можуть займатися оптимізацією, але среднестатістічесікй програміст не повинен про це думати.


В наступній статті цієї серії я постараюся розглянути концепцію властивостей, тобто виклик методів за допомогою синтаксису атрибутів.


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*