Открытые классы в ruby, заметки для питонистов

    По своему опыту знаю, что открытые классы в ruby раздражают и вызывают непонимание в среде питонистов. Ну в самом деле, что за странность открыть класс String и переопределить там size?


    А можно рассмотреть вопрос в другой стороны, попробуем разобрать небольшой пример. Все знают как в python разные типы проверяются на истинность — всё что пусто, то ложь. В ruby это не так, здесь ложь это nil и false. Иногда было бы удобнее как в python.

    Как это можно решить например в rust, включая поддержку для встроенных типов:

    trait Existable {
      fn exist(&self) -> bool;
    }
    
    impl Existable for int {
      fn exist(&self) -> bool { if *self == 0 { false } else { true } }
    }
    
    fn main () {
      assert!(5.exist() == true)
      assert!(0.exist() == false)
    }
    


    Представим что в python схема не такая, и мы хотим ее реализовать. В python нельзя определить новые методы для сторонних классов, поэтому можно сделать что-то вроде:

    class Trait(object):
        def __init__(self):
            self.registry = {}
    
        def impl(self, typ):
            def reg(func):
                self.registry[typ] = func
                return func
            return reg
    
        def __call__(self, arg):
            return self.registry[arg.__class__](arg)
    
    Existable = Trait()
    
    
    @Existable.impl(int)
    def int_exists(i):
        return False if i == 0 else True
    
    if __name__ == '__main__':
        assert Existable(0) == False
        assert Existable(5) == True
    

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

    Как в ruby реализовали подобную штуку?

    class Object
    # File activesupport/lib/active_support/core_ext/object/blank.rb, line 13
      def blank?
        respond_to?(:empty?) ? empty? : !self
      end
    end
    


    Метод `empty?` определен на классах `Array` и `Hash` (как минимум). Определите на своем классе и метод `blank?` будет корректно работать. То есть здесь мы имеем trait в ruby — объекты отвечающие на `empty?` реализуют trait проверки на пустоту, тот самый `Existable`.

    Это простейший корректный пример использования открытых классов в ruby, а есть и много других замечательных штук из ActiveSupport, одной из частей Ruby on Rails.

    Надеюсь благодаря такому простому примеру, вы теперь понимаете, что открытые классы в ruby вовсе не зло, а полезный инструмент, который в прямых руках позволяет реализовывать расширения среды изящно.

    Если было не совсем понятно, что я имею в виду — открытые классы в ruby позволяют доопределять поведение любых, встроенных в том числе, классов, также как и traits в rust и других языках.

    PS Я не утверждаю что в python нельзя реализовать подобный механизм — я простенький пример даже привел в самой статье. Еще раз — речь о правильном использовании открытых классов в ruby, а не о преимуществах какого-либо языка перед другим.
    Метки:
    Поделиться публикацией
    Похожие публикации
    Комментарии 39
    • 0
      разве MixIn не из этой области?

      • 0
        Mixin это всего лишь набор методов, так скажем. Вопрос в том как его включить для работы в нужные классы. В ruby вы можете его подключить во встроенные классы, благодаря их открытости.
        • 0
          Всю жизнь знаю, что mixin — это способ внесения изменений в классы, без наследования.

          Не точно, но кажется нельзя подмешивать что-либо в классы int и str (и прочие builtin-классы, которые немного не такие), но во все остальные — никаких проблем.
          • 0
            А можно пример? Я что-то ни в одном проекте такого не встречал. Вы имеете в виду игры с __mro__?
            • 0
              Если под «игры с __mro__» подразумевались вариации на тему someClass.__bases__ += ( mixinClass, ) — то да.

              Но я слабо представляю, когда такого рода штуки реально нужны/полезны/красивы.
      • +1
        Эм, почему «В python нельзя определить новые методы для сторонних классов»?

        class A(object):
            pass
        
        a = A()
        
        def new_method(self):
            print "It's working!"
        
        setattr(A, 'new_method', new_method)
        
        a.new_method()
        


        А вообще, ковыряться во внутренностях того, с чем надо взаимодействовать как с «чёрным ящиком» — плохая идея.
        • 0
          Я не говорил о принципиальной невозможности, просто так не делается. Да и setattr здесь лишний, можете просто присвоить.

          >>> A.kuku = new_method
          >>> a.kuku()
          It's working!
          


          И потом я об открытых классах ruby, а не о системе типов python.
          • 0
            Так на этом утверждении о невозможности Вы весь питоновский пример строите, а статью начинаете с «переопределить там size».

            Если говорим о переопределении, то и пример надо бы с переопределением.

            Если говорим перегрузке функций в зависимости от типов, то у нас для этого PEP 443 есть.
            • 0
              Типу int вы ничего не присвоите. Нечто похожее на PEP443 есть прямо в тексте. Я не говорю о переопределении, я говорю о расширении среды.
              • 0
                «переопределить там size» это стандартная питоновская страшилка про ruby с открытыми классами, которую статья призвана развеять.
              • 0
                setattr не лишний, он абстрактнее простого присвоения.
            • +3
              Помню, переносил как-то Ruby-библиотеку с одной версии языка на другую. Тем, что с новым Ruby всё вдруг поламалось, наверное, никого не удивишь. Но вот зато какое «удовольствие» было искать, где же всё-таки определён один такой перегруженный метод! Нужно знать не только, в каком файле он прописан, но и в какой последовательности загружаются модули. А это значит, что чтобы понять какую-то маленькую деталь, нужно сначала осознать поток контроля во всём приложении, что довольно сложно без понимания вот таких деталей.

              А пример неудачный. В Python вы для каждого типа определяете свою функцию проверки, а в Ruby обходитесь обобщённым методом. Никто не мешает в Питоне точно также определить одну единственную обобщённую функцию и просто вызывать её как `blank(obj)`, а не `obj.blank()`.
              • 0
                Вполне удачный, покажите мне реализацию этой функции.
                • 0
                  Если дословно, то так:

                  def blank(obj):
                      if hasattr(obj, '__iter__'):  # или любая аналогичная проверка для нужных типов
                          return len(obj)
                     else: 
                         return not bool(obj)
                  

                  В зависимости от нужного вам поведения проверять можно на конкретные типы, другие атрибуты или что угодно ещё. Возможно, в Ruby есть какие-то фишки, недоступные в Python, но написать внешнюю функцию, заменающую внутренний метод, можно всегда.
                  • 0
                    len(obj) != 0
                    

                    Ну, вы поняли.
                    • 0
                      Да неважно, что в ruby есть некий механизм, которого нет в python. Важно что открытые классы это не зло.
                      • 0
                        Дело вкуса. Я говорил про пример, что он не слишком удачный.
                  • 0
                    По поводу порядка сборки среди исполнения, а в ruby она именно что собирается из всего дерева исходников, это в принципе меня никогда не подлавливало в реальных проектах. Есть правила как именно надо раскладывать свой код, и чего делать не стоит. Может вам просто попалась плохая библиотека.
                  • +1
                    Механизм-то сам по себе замечательный, но приводит к тотальному манкипатчингу при полном непонимании как и когда его правильно использовать… Впрочем такая проблема есть и с паттернами из Банды Четырех.
                    • 0
                      Да ни к чему он не приводит, я не понимаю о чем вы таком говорите — в моих проектах и в библиотеках которые я использую, такого нет. А хреновый код пишется хреновыми программистами, вне связи с языками.
                      • 0
                        Я именно про это и написал :\
                        • 0
                          Впрочем такая проблема есть и с

                          Вы пишете про некую проблему, которой нет ;) это мне напоминает войны java vs python на предмет private и protected.
                          • 0
                            Инкапсуляция не нужна! Проблема существует, если вы работаете с фрилансерами/удаленщиками и прочими джуниорами, которых вы, или ваши адекватные коллеги, не взращивали на правильных ценностях. И хоть «Банда Четырех» к таковым ценностям относится — чрезмерное использование паттернов в ООП ни к месту приводит к аду.
                            • 0
                              Ревью необходимо при подобном образе работы. Тонны говнокода которые я видел на любых языках не имеют абсолютно никакой корреляции с его, языка, особенностями.
                              • 0
                                Ревью необходимо при подобном образе работы

                                Абсолютно согласен.

                                Тонны говнокода которые я видел на любых языках не имеют абсолютно никакой корреляции с его, языка, особенностями.

                                Ха
                        • 0
                          Впрочем, как уже отметили выше, в Питоне совсем наговнокодить не выйдет, ибо TypeError: can't set attributes of built-in/extension type INSERT_TYPE_NAME, а в Ruby (насколько я знаю, поправьте если я неправ) переопределение аттрибутов/методов встроенных типов/классов возможно… И вот здесь и начинается АДЪ.
                          • 0
                            В ruby все возможно. Только ада нет.
                            • 0
                              Скажите, вы когда-нибудь устанавливали Redmine? :)
                              Возможность переопределения аттрибутов/методов у стандартных типов/классов — потенциальная дыра в безопасности приложения.
                              • 0
                                Не используйте интерпретируемые языки — в них часто можно переопределить множество вещей. Не используйте интернет — там можно ходить на другой компьютер. Не используйте компьютер — там можно писать в память.

                                Чем возможность дописывать в стандартные классы так опасна в сравнении с возможностью писать не в стандартные? Откуда вообще опасность? Покажете мне пример эксплуатации? ;) В rails находили много дырявостей, но открытые классы еще ни в одном случае не были причиной ужасной опасности.
                                • 0
                                  1) Дописывание — хорошо, переписывание — зло;
                                  2) Опасность в переопределение методов стандартных классов заключается в возможном исполнении этих методов (привет blackhat-сообщество);
                                  3) Не покажу, абсолютно не знаю Руби. По маштабам — представьте переопределение escape-методов для любой ORM;

                                  Призываю chikey в тред ;)
                                  • 0
                                    Ну какие еще хакеры? Зачем вы даете им писать в свой проект? Ну если NSA приглашали, то им же удобнее, правда?
                                    И потому, ну что вы в самом деле, escape никогда не определялся на встроенных типах.
                                    • –2
                                      Есть понятие «потенциальная дырка» и возможность существования таковой отлична от нуля. Человеческий фактор и социнженерия…
                      • +3
                        Объясните пожалуйста, я правильно понял (не знаю ruby), что вы взяли и какому-то стандартному классу поменяли поведение? Я б за такое по рукам бил. Берешь стандартный класс, используешь его, а у него другое поведение…
                      • +1
                        Как же, всё-таки, прекрасно, что в питоне так не делают!
                        • 0
                          Это абсолютно логично, что в питоне так не делают, потому что в нем делают по другому, что никоим образом не делает способ, которым это делают в ruby хоть сколько-нибудь неверным.
                          • +1
                            В питоне вообще ни коим образом не меняют поведение базовых объектов, а monkey patching стандартных и не очень библиотек считается не просто дурным тоном, а чем-то, за что полагается смерть через колесование. Исключения есть, но они настолько явные, что никаких вопросов не остаётся.
                            Я про то, что ни одному зрелому питонщику не придёт в голову писать статью про то, как круто править чужие объекты. Потому что это не круто.
                            • 0
                              Я же написал — не менять, а расширять среду. Менять нельзя, все же поломается.
                              • 0
                                Расширение, это изменение. А теперь представь, что изменения среды разных библиотек сконфликтовали друг с другом… Это может превратиться в такой адов пипец, что своим матом ты вызовешь Watman'a.

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