В питоне аттрибуты класса можно сколько угодно модифицировать во время работы, и изменения видны всем объектам этого класса и других подклассов. Под катом — одно полезное применение этого факта.


Вообще говоря, мало что в питоне нельзя изменить на ходу простым присваиванием. Есть небольшой список зарезервированных имён, которым действительно нельзя ничего присваивать под страхом SyntaxError, но он реально небольшой:


>>> import keyword
>>> keyword.kwlist
['False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield']

Всё остальное, включая пространство имён builtins — просто переменные, в основном стандартных типов, которые можно в любой момент переназначить. Очень удобно, потому что позволяет без труда подсунуть интерпретатору нужную функцию или значение взамен встроенной.


То же самое относится (с оговоркой, см. ниже) и к аттрибутам классов, за счёт чего можно передавать данные сразу куче объектов без громоздкой системы сообщений. Допустим, у нас есть некий синглетон и куча объектов, которые к нему обращаются. Синглетонов этого типа, вообще говоря, может быть несколько; не одновременно, конечно, но рано или позно может оказаться, что текущий синглетон нужно выкинуть и подставить на его место новый. Ситуация вполне реальная: в моём случае это была игра, в которой все сущности на экране иногда передают информацию объекту, который следит за статистикой игры в целом. И да, я знаю про очередь событий и прочая, но в случае прототипа quick-and-dirty решение (и скорость работы, кстати) иногда лучше, чем правильная архитектура.


class A():
    some_variable = None
    # The rest of the class

class B(A):
    # Class B code

b = B()
A.some_variable = 'foo'
assert b.some_variable == 'foo'

Этот нехитрый тест проходится независимо от того, сколько уровней наследования между A и B (при условии, что some_variable не переопределена каким-то из наследников А) и по скольки файлам раскиданы классы. Конечно, у такого метода есть подводные камни. Основной состоит в том, что классы не умирают практически никогда. То есть если some_variable — увесистый объект с кучей данных, то даже после удаления всех объектов А и его подклассов сборщик мусора к нему не притронется. Ответственность за удаление A.some_variable лежит исключительно на программисте. Проверить, что присваивается именно объект подходящего типа, тоже довольно нетривиально. Да и вообще такой нестандартный хак требует подробной документации, потому что объекты А() вроде ничего ниоткуда явно не получают, а тем не менее откуда-то в курсе.


Со встроенными классами так поступить, увы, нельзя. И понятно, почему: если бы такой хак был возможен, половина модулей на pip содержала бы в каком-нибудь неожиданном месте какую-нибудь атаку вот в таком духе:


>>> str.format = send_all_your_data_to_me
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'str'

Подменить значение переменной __builtins__.str на свой класс таки можно, но это коснётся только тех случаев, когда конструктор строки вызывается явно:


>>> __builtins__.str = None
>>> type('')
<class 'str'>
>>> str(123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'NoneType' object is not callable
>>> a = lambda x: str(x)
>>> a(123)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <lambda>
TypeError: 'NoneType' object is not callable
Поделиться с друзьями
-->

Комментарии (6)


  1. griganton
    09.05.2017 13:23
    +2

    А где примеры хулиганства, рассказ хотя бы про monkey-patching и возможности, которые он открывает? Рассказать про то, как это все используется в тестировании и так далее? Как сделать пользовательский класс, которому нельзя назначить атрибут?

    Вся статья ужимается до фразы «В питоне можно назначать атрибуты, но не всегда»


    1. Shtucer
      09.05.2017 13:27

      Или даже так: "В питоне всё — объекты"


  1. Shtucer
    09.05.2017 13:26
    +1

    Да и вообще такой нестандартный хак требует подробной документации, потому что объекты А() вроде ничего ниоткуда явно не получают, а тем не менее откуда-то в курсе.

    Магия, ага. Так вы и не создаете объекта А(). А вот объект A создается сразу после определения и сразу с атрибутом класса some_variable. А вот конструктор А() вызывается при создании b = B(). Не понимаю почему это "нестандартный хак".
    Вас тут спасает, что объект B() не имеет атрибута self.some_variable, поэтому при доступе через b.some_variable возвращается B.some_variable, унаследованный от A.
    А вот если сделать b.some_variable = 'bar' проверка не пройдёт, но assert B.some_variable == A.some_variable все еще будет работать. Но и это решается через доступ непосредственно к атрибутам класса. assert b.class.somevariable == A.some_variable Это задокументированное поведение объектов и классов, а не "хак".


    Но, да, все это метапрограммирование в питоне требует внимательного изучения и аккуратного применения.


    1. griganton
      09.05.2017 13:32

      Да, на самом деле всё запоминается легко: питон ищет атрибут у самого объекта, потом у класса, а потом идет вверх по иеархии в соответствии с mro.


  1. iroln
    09.05.2017 16:04

    О чём статья, вообще не понял. Выглядит недописанной, обрывается как-то внезапно.


    Напомнило. Когда-то мне надо было сделать свойства (property) для классов (не для экземпляров).


    class BarProperties(type):
        _foo = 0
    
        @property
        def foo(cls):
            print('get foo')
            return cls._foo
    
        @foo.setter
        def foo(cls, value):
            print('set foo')
            cls._foo = value
    
    class Bar(metaclass=BarProperties):
        pass
    
    >>>Bar.foo
    get foo
    0
    >>>Bar.foo = 1
    set foo
    >>>Bar().foo
    AttributeError: 'Bar' object has no attribute 'foo'

    Такая вот дикость… Но лучше так никогда не делать :)


  1. aquamakc
    10.05.2017 09:52

    рано или позно может оказаться, что текущий синглетон нужно выкинуть и подставить на его место новый


    На лицо ошибка в архитектуре ПО. И статья о том, как подпереть падающую конструкцию костылём.