Короткая заметка из серии «Вас предупреждали».

Переход с классических языков программирования на Питон доставляет немало сюрпризов.
Читаем документацию:
Generally speaking, instance variables are for data unique to each instance and class variables are for attributes and methods shared by all instances of the class
Попробуем поиграться

class Vessel:
    #class attribute
    vtype = "boat"

    #instance attribute
    def __init__(self, name):
        self.name = name

    # ислючительно для печати
    def __str__(self):
        res= '>>'
        for a in inspect.getmembers( self):
            if not a[0].startswith("__"): res += f"{a[0]}={a[1]:<14}"
        for a in inspect.getmembers( self.__class__):
            if not a[0].startswith("__"): res += f"__class__.{a[0]}={a[1]:<14}"

        return res


Создаем два объекта проверим значения всех атрибутов:

Iowa = Vessel("Iowa")
Drum=Vessel("Drum")
printAttr(Iowa, Drum)

>>name=Iowa          	vtype=boat          	__class__.vtype=boat          	
>>name=Drum          	vtype=boat          	__class__.vtype=boat 

Пока все как и ожидалось.

Попытаемся изменить vtype: это можно сделать двумя способами, которые по сути просто разный синтаксис одного и того же

Vessel.vtype = "USS boat"
printAttr(Iowa, Drum)
>>name=Iowa          	vtype=USS boat      	__class__.vtype=USS boat      	
>>name=Drum          	vtype=USS boat      	__class__.vtype=USS boat      	

Iowa.__class__.vtype = 'USS WW2 Boat'
printAttr(Iowa, Drum)
>>name=Iowa          	vtype=USS WW2 Boat  	__class__.vtype=USS WW2 Boat  	
>>name=Drum          	vtype=USS WW2 Boat  	__class__.vtype=USS WW2 Boat  	

И снова все в порядке.

Теперь попытаемся сделать тоже самое через атрибут объекта.

Drum.vtype = 'submarine'
printAttr(Iowa, Drum)
>>name=Iowa          	vtype=USS WW2 Boat  	__class__.vtype=USS WW2 Boat  	
>>name=Drum          	vtype=submarine     	__class__.vtype=USS WW2 Boat  	

И вот первая неожиданность: несмотря на то, что vtype это атрибут класса, неожиданно он становится атрибутом объекта.

Проверим:

Vessel.vtype = "NAVY Museum"
>>name=Iowa          	vtype=NAVY Museum   	__class__.vtype=NAVY Museum   	
>>name=Drum          	vtype=submarine     	__class__.vtype=NAVY Museum   	

а что если…

 del Drum.vtype
>>name=Iowa          	vtype=NAVY Museum   	__class__.vtype=NAVY Museum   	
>>name=Drum          	vtype=NAVY Museum   	__class__.vtype=NAVY Museum   

И снова атрибут класса.

Следующее выражение уже не проходит

del Drum.vtype
printAttr(Iowa, Drum)
	del Drum.vtype
	AttributeError: vtype

И последний пример, эмулирующий переопределения класса и удаление атрибута vtype.

Drum.vtype = 'submarine'
del Vessel.vtype
printAttr(Iowa, Drum)

>>name=Iowa          	
>>name=Drum          	vtype=submarine     	

Если начать разбираться с namespace-ами, то подобное поведение становится понятным.
Однако для программистов, кто раньше работал с нормальными языками, это по меньшей мере кажется странным. А если говорить о больших проектах, которые поддерживаются несколькими поколениями разработчиков, это может оказаться провалом сроков и пр.

Принимая во внимание концепцию Питона, что все открыто для всех, почему бы не сделать доступ к «классным» атрибутам только через __class__ или его аналог. На мой взгляд, это бы хоть как-то оградило от сюрпризов и заставило 10 раз подумать прежде чем присваивать что-то классным атрибутам на уровне объектов.

Update: текст PrintAttr

def printAttr(*o):
    for a in o:
        print(a)

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


  1. p45tor
    19.10.2018 18:12

    Неплохо бы указать откуда взялся printAttr, поскольку он не входит в стандартную библиотеку Python.


    1. Komesk Автор
      19.10.2018 18:13

      Согласен, но суть не в ней. Сейчас добавлю


  1. haaji
    19.10.2018 23:10
    +1

    Принимая во внимание концепцию Питона, что все открыто для всех, почему бы не сделать доступ к «классным» атрибутам только через __class__ или его аналог.

    Во-первых, гораздо больше в питоне ценится читаемость и простота, почему для того чтобы получить класс-атрибут я должен:

    1. Точно помнить классовый он или инстансный.
    2. Писать что-то типа
     Drum.__class__. vtype


    Во-вторых, не совсем понятно, чего вы ожидаете, меняя атрибут инстанса в рантайме? Изменение атрибута класса? Эксепшен?

    В-третьих, если уж вы выкладываете код, то хоть немного его приведите в порядок в соответствии с pep-8.

    Iowa = Vessel("Iowa")
    Drum=Vessel("Drum")
    printAttr(Iowa, Drum)


    это

    iowa = Vessel("Iowa")
    drum = Vessel("Drum")
    print_attr(Iowa, Drum)


    1. Komesk Автор
      20.10.2018 01:21
      -1

      1. Вам все равно нужно знать какой это атрибут, чтобы случайно не присвоить ему что-либо, и не попасть в ситуацию мной описанную. И я совсем не против читаемости, но не в ущерб «безопасности»
      2. Я ожидаю того, что описано в документации — изменения значения атрибута у всех инстансов, как это происходит при обращении через __class__


      1. Xalium
        20.10.2018 07:37

        изменения значения атрибута у всех инстансов, как это происходит при обращении через __class__

        А у кого тогда должно обновиться свойство в таких случаях?

        class x():
            prop=1
        
        class y(x):
            pass
        
        inst = y()
        inst.prop = 555
        inst.__class__.prop = 666


        ЗЫ.
        Почти во всех языках (а может и во всех), если можно получить доступ к свойству класса через свойство экземпляра, то присваивание значения через свойство экземпляра присваивает/обновляет именно свойство экземпляра, а не свойство объекта/класса, откуда унаследовалось.


  1. fireSparrow
    19.10.2018 23:13
    +2

    В любом языке можно наткнуться на неприятные сюрпризы, если прийти в него из другого языка, ожидая, что здесь всё будет точно так же. Особенно, если сразу начать писать продакшн-код на новом языке, толком его не освоив. Но нормальные программисты всё-таки сначала учат язык, а потом пишут на нём продакшн-код.
    То, о чём вы пишете в этой статье, рассказывают в любом вводном мануале/ролике по ООП в питоне.


    1. Komesk Автор
      20.10.2018 00:03
      -1

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


  1. Xalium
    19.10.2018 23:59
    +2

    А чего тут особенного? Обычное перекрытие свойств класса свойствами объекта. Точно так же, как и при наследовании и перекрытии свойств одного класса другим классом.
    В javascript кажется так же.


    1. Komesk Автор
      20.10.2018 01:28

      При наследовании атрибуты не меняют сущности — они как были свойствами объекта так и остаются. В данном случае происходит завуалированная подмена, причем не описанная в документации.


      1. Xalium
        20.10.2018 07:27
        +1

        Так я и написал:

        Обычное перекрытие свойств класса свойствами объекта.

        Это не подмена. Просто пока не создано свойство объекта с помощью присваивания ему значения, получаешь доступ на чтение к свойству класса или его суперклассам (что «ближе» к объекту). В JavaScript такое же поведение.
        причем не описанная в документации.

        Описанная. Есть минимум в одном месте:
        Class instances

        A class instance is created by calling a class object (see above). A class instance has a namespace implemented as a dictionary which is the first place in which attribute references are searched. When an attribute is not found there, and the instance’s class has an attribute by that name, the search continues with the class attributes. If a class attribute is found that is a user-defined function object, it is transformed into an instance method object whose __self__ attribute is the instance. Static method and class method objects are also transformed; see above under “Classes”. See section Implementing Descriptors for another way in which attributes of a class retrieved via its instances may differ from the objects actually stored in the class’s __dict__. If no class attribute is found, and the object’s class has a __getattr__() method, that is called to satisfy the lookup.

        Attribute assignments and deletions update the instance’s dictionary, never a class’s dictionary. If the class has a __setattr__() or __delattr__() method, this is called instead of updating the instance dictionary directly.

        Class instances can pretend to be numbers, sequences, or mappings if they have methods with certain special names. See section Special method names.

        Special attributes: __dict__ is the attribute dictionary; __class__ is the instance’s class.

        И поведение это можно менять (инфа из выше):
        If the class has a __setattr__() or __delattr__() method, this is called instead of updating the instance dictionary directly.

        Еще чего то можно мутить через дескрипторы.


        1. Komesk Автор
          21.10.2018 06:18

          Да, если начать разбираться с namespace и dict, то становится примерно понятно как и почему так оно все работает. Хотелось бы чтобы подобное, на мой взгляд нетипичное поведение, было бы описано более четко.


      1. Mingun
        20.10.2018 08:10

        В документации как раз чёрным по белому написано, что любые присваивания атрибутам всегда идут в экземпляр (если это не атрибут-дескриптор, в котором вы можете реализовать любую логику).


        Читаем абзац из раздела 7.2 Инструкции присваивания «Справочного руководства по языку Python» (выделено жирным моё):


        7.2 Инструкции присваивания

        Присваивание объекта одной цели рекурсивно определяется следующим образом.

        • Если целью является ссылка на атрибут: в ссылке вычисляется первичное выражение. Оно должно выдать объект с присваиваемыми атрибутами; если это не так, возбуждается исключение TypeError. Затем этому объекту предлагается присвоить данному атрибуту присваиваемый объект; если он не может выполнить присваивание, он возбуждает исключение (обычно, но не обязательно, AttributeError).

          Примечание: Если объект является экземпляром класса и ссылка на атрибут появляется с обеих сторон оператора присваивания, выражение справа, a.x, может получить доступ к атрибуту экземпляра или (если атрибут экземпляра не существует) к атрибуту класса. Цель слева a.x всегда устанавливается в атрибут экземпляра, при необходимости создавая его. Таким образом, два вхождения a.x не обязательно ссылаются на один и тот же атрибут: если выражение справа ссылается на атрибут класса, выражение слева создаёт новый атрибут экземпляра в качестве цели присваивания:
          class Cls:
          x = 3             # переменная класса
          inst = Cls()
          inst.x = inst.x + 1   # записывает 4 в inst.x, оставляя Cls.x равным 3


          Это описание не обязано применяться к атрибутам-дескрипторам, вроде свойств, созданных с помощью property().


        ...


        1. Komesk Автор
          21.10.2018 06:23

          Вот это да.В этот раздел я точно не заглядывал — спасибо.


  1. stavinsky
    20.10.2018 18:18
    +1

    Если начать разбираться с namespace-ами, то подобное поведение становится понятным.
    Однако для программистов, кто раньше работал с нормальными языками, это по меньшей мере кажется странным.

    Вы не могли бы, пожалуйста, рассказать какие языки нормальные и какие критерии определяют нормальность языка раз уж python с его огромным комьюнити такие «ненормальные»?


    1. Komesk Автор
      21.10.2018 06:31

      Нормальный это безусловно понятие субъективное. Для меня нормальным языком является тот, который не порождает двоякости трактования, и который запрещает делать то, что не разрешено. По своему опыту знаю, что если при разработке системы классов что-то не скрыто — это позже будет использовано и самым извращенным способом.


      1. stavinsky
        22.10.2018 10:33

        Я предложу такое определение нормальности: если языком успешно пользуются тысячи если не миллионы людей — это нормально. Если человек не разобравшись в документации и в поведении о котором знает, надеюсь, любой джун, идет в интернет писать что язык плохой — вот это не нормально. У всех языков есть свои особенности — если слушать Вас, то javascript вообще на костре должен гореть сами знаете где, а .net и java — наше все.
        Интересно какое место в вашей системе координат занимают функциональные языки?


        1. Komesk Автор
          22.10.2018 15:36

          Количество пользователей языка ничего не говорил о нормальности. Анекдот про мух знаете :)
          Нормальность языка можно и нужно оценивать только в контексте поставленных задач.
          И где вы нашли, что я написал, что язык плохой?
          И таки да для разработки больших и сложных систем, где работает пара десятков программистов из разных отделов, я выберу и выбираю .net c джавой, жаль что Delphi помер.