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

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


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


У деяких мовах (у Java, наприклад) на цьому відмінності все і закінчується: атрибути є атрибути, методи є методи. В Java особливе значення надається інкапсуляції та ізоляції даних; таким чином заохочується використання методів на кшталт “getX” – “setX” для доступу до закритих даними класу. У психології Java використання явних викликів методів відразу ж робить можливим випадок, в якому при доступі до даних або їх зміну можуть знадобитися додаткові розрахунки чи будь-які інші дії. Звичайно, результатом Java-підходу стає велика подробиця коду і іноді здаються дивними правила: замість foo.bar треба писати foo.getBar() , А замість foo.bar=value доводиться говорити foo.setBar(value).


У зв’язку з цим варто відзначити досить незвичайний підхід, реалізований в Ruby. В Ruby вимоги по приховану даних ще сильніше, ніж в Java: всі атрибути обов’язково закриті; прямий доступ до даних об’єкта неможливий. У той же час в Ruby є деякі синтаксичні можливості, завдяки яким виклики методів виглядають як доступ до атрибутів в інших мовах. По-перше, в Ruby дужки при виклику методу необов’язкові, по-друге, назви методів можуть містити символи, які в більшості мов є операторами. Так що на Ruby foo.bar – Це просто скорочення для foo.bar();, А запис foo.bar=value виявляється викликом foo.bar=(value). В результаті весь доступ являє собою виклики методів.


Python – значно більш гнучкий мову, ніж Java або Ruby, але це виявляється проблемою в тій же мірі, в якій і гідністю. В Python доступ foo.bar або присвоювання foo.bar=value може і бути й просто зверненням до даних, і викликом будь-якої функції. При цьому в другому випадку є добрих півдюжини способів викликати виконання такого коду, з трохи різним поведінкою і запаморочливими тонкощами і нюансами використання в кожному окремому випадку. Така кількість можливостей вносить безлад в ідеологію мови і робить його більш складним в розумінні для неспеціалістів (і навіть для фахівців). Я розумію, як це сталося: можливості об’єктно-орієнтованого програмування з’являлися в Python в кілька стадій. Але мені не подобається той безлад, який ми маємо на сьогоднішній день.


Старомодний спосіб


З давніх часів (ще до Python 2.1) в мові був магічний метод .__getattr__() , Що дозволяв класу робити обчислення при доступі до даних об’єкта. Відповідно методи .__setattr__() і .__delattr__() могли ініціювати виклик коду при установці і видаленні таких “атрибутів”. Проблема полягає в тому, що не можна заздалегідь передбачити, чи буде цей код дійсно викликатися – це залежить від того, є Чи атрибут з замовленим вами ім’ям в obj.__dict__. Можна б було спробувати створити керуючі доступом методи .__setattr__() і .__delattr__() , Але це все одно не завадило б прямому доступу до obj.__dict__ . І зміна дерев спадкування, і передача об’єктів зовнішніх функцій найчастіше роблять дуже неочевидним відповідь на питання, буде чи не буде деякий метод реально запускатися при роботі з об’єктом. Наприклад:


Лістинг 1. Чи відбудеться виклик методу?





>>> class Foo(object):
… def __getattr__(self, name):
… return “Value of %s” % name
>>> foo = Foo()
>>> foo.just_this = “Some value”
>>> foo.just_this
“Some value”
>>> foo.something_else
“Value of something_else”

Доступ до foo.just_this не викликає виконання коду, тоді як до foo.something_else – Викликає; якби даний фрагмент не був таким коротким, вловити цю різницю було б дуже важко. Очевидне рішення – виклик hasattr() – Дає невірну відповідь:


Лістинг 2. Проблеми hasattr ()





>>> hasattr(foo,”never_mentioned”)
True
>>> foo2.__dict__.has_key(“never_mentioned”) # this works
False
>>> foo2.__dict__.has_key(“just_this”)
True

Використання __ slots__


В Python 2.2 з’явився новий механізм створення “захищених” класів. Ніде не сказано, для чого насправді призначається атрибут _slots_ класів нового типу. Здебільшого в документації по Python радять використовувати .__slots__ для збільшення продуктивності класів з дуже великою кількістю примірників, а не як спосіб оголошення атрибутів. Проте атрибут __ slots__ робить саме це: створює клас без атрибута .__dict__ і тільки із заздалегідь вказаними атрибутами (хоча методи оголошуються як в звичайному визначенні класу). Таке рішення досить специфічно, але воно дає гарантію, що метод __ getattr__ буде викликаний при доступі до атрибуту:


Лістинг 3. __slots__ як гарантія виклику методу





>>> class Foo2(object):
… __slots__ = (“just_this”)
… def __getattr__(self, name):
… return “Value of %s” % name
>>> foo2 = Foo2()
>>> foo2.just_this = “I”m slotted”
>>> foo2.just_this
“I”m slotted”
>>> foo2.something_else = “I”m not slotted”
AttributeError: “Foo” object has no attribute “something_else”
>>> foo2.something_else
“Value of something_else”

Оголошення .__slots__ гарантує, що прямий доступ може бути зроблений тільки до заданих атрибутам; все інше буде здійснюватися через метод .__getattr__() . Якщо ж ви ще й створите метод .__setattr__() , Можна змусити присвоювання не викликати виключення AttributeError , А робити щось інше (проте слід подбати про те, щоб присвоювання атрибуту з __ slots__ проходило без змін). Наприклад:


Лістинг 4. Використання. __setattr__ Разом зі. __slots__





>>> class Foo3(object):
… __slots__ = (“x”)
… def __setattr__(self, name, val):
… if name in Foo.__slots__:
… object.__setattr__(self, name, val)
… def __getattr__(self, name):
… return “Value of %s” % name

>>> foo3 = Foo3()
>>> foo3.x
“Value of x”
>>> foo3.x = “x”
>>> foo3.x
“x”
>>> foo3.y
“Value of y”
>>> foo3.y = “y” # Doesn”t do anything, but doesn”t raise exception
>>> foo3.y
“Value of y”

Метод. __getattribute__ ()


В Python починаючи з версії 2.2 є можливість використовувати метод .__getattribute__() замість схоже названого старого .__getattr__(). Точніше, вона є при використанні класів нового типу (new-style classes) – а зазвичай користуються саме ними. Метод .__getattribute__() потужніше свого “молодшого брата” в тому, що він перехоплює весь доступ до атрибутів незалежно від того, чи внесений атрибут в obj.__dict__ або obj.__slots__. Проблема методу .__getattribute__() в тому, що весь доступ здійснюється з його використанням. Якщо ви користуєтеся цією можливістю, то для того, щоб отримати “справжнє” значення атрибута, доведеться трохи постаратися: як правило, знадобиться викликати .__getattribute__() для класу-батька (зазвичай object). Наприклад:


Лістинг 5. Повертаємо “справжнє” значення атрибута





>>> class Foo4(object):
… def __getattribute__(self, name):
… try:
… return object.__getattribute__(self, name)
… except:
… return “Value of %s” % name

>>> foo4 = Foo4()
>>> foo4.x = “x”
>>> foo4.x
“x”
>>> foo4.y
“Value of y”

У всіх версіях Python .__setattr__() і .__delattr__() також перехоплюють доступ на запис і видалення для всіх атрибутів, а не тільки для відсутніх в obj.__dict__.


Дескриптори


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


Проте в більшості випадків потрібно забезпечити спеціальне поведінка лише декількох атрибутів, в той час як операції над іншими повинні працювати як звичайно. При доступі до звичайних атрибутів не потрібен ні виклик додаткового коду, ні затримки, пов’язані з таким викликом. У таких випадках можна використовувати дескриптори атрибутів або тісно пов’язані з ними властивості. Насправді властивості і дескриптори використовують один і той же внутрішній механізм, але способи їх визначення сильно розрізняються. І, як легко здогадатися, у кожного способу оголошення є свої переваги і недоліки.


Спочатку розглянемо дескриптори. Основна ідея полягає в тому, що атрибуту класу призначається примірник іншого класу спеціального виду. Цей спеціальний клас – клас дескриптора – це клас нового типу, що має методи .__get__(), .__set__() і __delete__() (Або принаймні деякі з них). Якщо клас дескриптора реалізує щонайменше перші два методу, він називається змінним дескриптором (data descriptor), якщо ж реалізований тільки перший метод, він називається незмінним дескриптором (non-data descriptor).


Як правило, незмінні дескриптори повертають викликаються об’єкти (callable objects). Насправді “незмінний дескриптор” – це найчастіше просто “красиве” назву методу, а проте метод, який реально буде викликаний, може визначатися під час виконання програми. Тут ми робимо крок в моторошний світ метакласи і декораторів, про який я вже писав у цій рубриці. Звичайно, звичайний метод теж може визначати реально виконуваний код в залежності від будь-яких умов, так що введення невиконувані дескрипторів не виробляє ніяких корінних змін в концепції методу.


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


Лістинг 6. Приклад змінюваного дескриптора





>>> class ErrWriter(object):
… def __get__(self, obj, type=None):
… print >> sys.stderr, “get”, self, obj, type
… return self.data
… def __set__(self, obj, value):
… print >> sys.stderr, “set”, self, obj, value
… self.data = value
… def __delete__(self, obj):
… print >> sys.stderr, “delete”, self, obj
… del self.data
>>> class Foo(object):
… this = ErrWriter()
… that = ErrWriter()
… other = 4
>>> foo = Foo()
>>> foo.this = 5
set <__main__.ErrWriter object at 0x5cec90>
<__main__.Foo object at 0x5cebf0> 5
>>> print foo.this
get <__main__.ErrWriter object at 0x5cec90>
<__main__.Foo object at 0x5cebf0> <class “__main__.Foo”>
5
>>> print foo.other
4
>>> foo.other = 6
>>> print foo.other
6

Клас Foo визначає this і that як дескриптори – екземпляри класу ErrWriter . Атрибут other ж – простий атрибут класу. Насправді в даній реалізації є невелика похибка. При першому доступі до foo.other відбувається читання атрибута класу; після присвоювання операція читання звертається вже до атрибуту екземпляра. Атрибут класу залишається на місці, хоча і в прихованому вигляді:


Лістинг 7. Атрибут класу і атрибут екземпляра





>>> foo.other
6
>>> foo.__class__.other
4

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


Лістинг 8. Унікальність дескриптора





>>> foo2 = Foo()
>>> foo2.this
get <__main__.ErrWriter object at 0x5cec90>
<__main__.Foo object at 0x5cebf0> <class “__main__.Foo”>
5

Щоб зберігати різну поведінку для різних екземплярів класу, доводиться використовувати аргумент obj , Переданий методам класу ErrWriter . Значення obj являє собою екземпляр об’єкта з дескриптором. Так що неунікальний (non-singleton) дескриптор може виглядати подібно такому:


Лістинг 9. Неунікальний дескриптор





class ErrWriter(object):
def __init__(self):
self.inst = {}
def __get__(self, obj, type=None):
return self.inst[obj]
def __set__(self, obj, value):
self.inst[obj] = value
def __delete__(self, obj):
del self.inst[obj]

Властивості


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


Як не дивно, ідея властивостей знову повертає нас до даного мною на початку статті короткому опису роботи мови Ruby. Властивість виглядає при використанні як звичайний атрибут, але визначається через функції установки значення, його одержання і так далі. При бажанні можна було б реалізувати в Python правила Ruby і взагалі не надавати доступу до “справжнім” атрибутам класу. Проте швидше за все ви захочете використовувати обидва підходи. Ось як працюють властивості:


Лістинг 10. Як працюють властивості





class FooP(object):
def getX(self): return self.__x
def setX(self, value): self.__x = value
def delX(self): del self.__x
x = property(getX, setX, delX, “I”m the “x” property.”)

Імена функцій отримання / зміни / видалення значення можуть бути будь-якими. Зазвичай розумно використовувати осмислені імена на кшталт перелічених вище. Дійсний код цих функцій може бути будь-яким, але має сенс використовувати імена атрибутів з подвійним підкресленням спереду. Ці атрибути прив’язуються до примірника за звичайними правилами “напів-приховання” імен Python. При цьому самі методи теж можна використовувати:


Лістинг 11. Використання методів





>>> foop = FooP()
>>> foop.x = “FooP x”
>>> foop.getX()
“FooP x”
>>> foop._FooP__x
“FooP x”
>>> foop.x
“FooP x”

Прав, анархія


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


До того ж у мене були смутні думки про інших, набагато більш заплутаних і похмурих, способах використання метакласи, фабрик класів і декораторів, які можуть бути використані програмістом для отримання схожих результатів (хоча я і не згадав їх у цій статті). Ці ідеї завели б мене в найтемніші місця метапрограмування в Python.


Було б здорово, якби описані мною способи були доступні для використання, але їх модифікації були б просто параметризовані, а не використовували б абсолютно різні принципи і синтаксис. Одна з головних цілей Python 3000 – спростити всю цю структуру, але до сьогоднішнього дня я не побачив жодних конкретних пропозицій щодо впорядкування можливостей реалізації принципу “атрибути як методи”. Можна було б, наприклад, зробити в Python декоратори для класів (як зараз для функцій і методів) і ввести стандартний модуль декораторів для найбільш часто використовуваних моделей таких от “чарівних атрибутів”. Звичайно, це просто припущення, і я точно не уявляю собі, як це могло б працювати, але мені здається, що така уловка могла б приховати всі ці складності від тих 95% програмістів на Python, які дійсно не хочуть лізти в нетрі внутрішнього устрою мови.


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


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

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

Ваш отзыв

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

*

*