В предыдущей статье мы рассмотрели несколько интересных моментов языка python, разумеется, одной статьёй они не исчерпываются, поэтому продолжим.

В одном из комментариев рассматривался следующий код:

SEX = 'Female', 'Male'
sex = SEX[True]  # -> Male
sex = SEX[False] # -> Female

Он работает так как логический тип данных эквивалентен 0 и 1, более того по началу никакого особого типа не было и использовались именно значения 0 и 1. А когда его всё же ввели, то решили унаследовать от целого.

print(int.__subclasses__()) # -> [<class 'bool'>]

Но вернёмся к получению значений списка, допустим у нас есть следующие данные:

books = [["Sherlock Holmes", "Arthur Conan Doyle", 1986, True, 12.51],
        ["The Lost World", "Arthur Conan Doyle", 2015, False, 5.95],
        ["The Art of Computer Programming", "Donald E. Knuth ", 2017, True, 190.54] ]

Разумеется, обращаться к значениям по захардкоженным индексам — не наш путь. Можно создать константу, например AUTHOR и указывать её, но можно пойти дальше:

TITLE_AUTHOR = slice(0, 2)
PRICE_IN_STOCK = slice(3, 5)
print(books[0][TITLE_AUTHOR])
print([book for book in books if book[PRICE_IN_STOCK] > [True, 10.0]])

Мы можем создать объект среза и указать ему, по каким индексам находятся, интересующие нас поля. Мы можем даже достаточно расхрабриться, чтобы написать что-то подобное последней строке в данном блоке, который выводит книги, имеющиеся на складе и дороже 10 чего бы то ни было. Данный код будет работать, так как сравнение списков и кортежей происходит лексикографически: сначала сравниваются первые элементы, потом вторые и так далее, то есть любой объект, у которого «поле» in_stock равно false, будет меньше списка [True, 10.0], так как True > False.

Примечание: разумеется данный код не стоит использовать в проектах, а лучше ограничиться лишь получением каких-то значений по срезу, выборки лучше делать в базе данных, либо, в Pandas, либо в виде метода класса Book. Можно и использовать namedtuple и написать небольшой модуль, куда положить методы с подобными выборками.

Мы уже рассмотрели аргументы, которые можно передать в функцию только по ключу. Недавно, изучая документацию функции bool, я обнаружил приписку:
Changed in version 3.7: x is now a positional-only parameter.
Сейчас в разработке находится PEP 570, который откроет возможность задавать подобные параметры, это может быть полезно, когда имя параметра не имеет значения, вроде, bool(), pow() и т.д.

Черновик синтаксиса выглядит так:

def name(positional_only_parameters, /, positional_or_keyword_parameters,
         *, keyword_only_parameters):

Возвращаясь к функции bool, которая работает точно так же, как проверка в условиях и циклах, когда вы исполняете код

if obj:
# или
while obj:

python выполняет довольно интересную процедуру вывода булевого значения:

  1. Если объект реализует метод __bool__(), то возвращается результат вызова этого метода
  2. В противном случае проверяется, реализован ли метод __len__(), если да, то проверяется, что он возвращает, если 0 — то результат будет False.
  3. Если ни тот, ни другой метод не реализован, то возвращается True.

class A:
    pass

a = A()

print(bool(a))   # -> True

Целые числа реализуют метод __bool__, стандартные коллекции его не реализуют, но реализуют метод __len__:

print(dir(int))   # -> ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', ...
print(dir(list))  # -> ['__add__', '__class__', '__contains__', ...

В другом комментарии было приведено «нестандартное» поведение атрибутов класса:

class Example:
    arr=[]

    def __init__(self):
        pass


a = Example()
b = Example()
a.arr.append(1)
print(a.arr)          # -> [1]
print(b.arr)          # -> [1]
print(Example.arr)    # -> [1]

Очень важно различать атрибуты класса и объекта, атрибуты объекта создаются, ВНЕЗАПНО, у объекта.
Важно понимать, что любые объявленные в теле класса поля и методы принадлежат классу. Чтобы создать атрибут у объекта его нужно присвоить объекту, в методе класса через self или в тексте программы, через переменную объекта.

Если запутались, давайте разбирать предыдущий код, в нём создаётся два экземпляра класса, после чего к полю arr добавляется 1 через один из объектов. Но в объекте такого атрибута нет, конструктор у нас пуст, поэтому python ищет данный атрибут в классе и находит его, однако он разделяется всеми экземплярами класса. Что это именно атрибут класса можно убедиться по последней строке, так как обращение по имени данного атрибута к классу не сгенерировало никакой ошибки.

При изучении нового языка важно понимать, что у каждого языка своя философия, и не во всех языках должно присутствовать ключевое слово static. Иногда похожий механизм обеспечивается другими механизмами.

Ярче всего идею, что python это не java иллюстрирует то, что добавление полей в объектах и классах осуществляется через обычное присваивание:

def make_title(self):
    if self.title == 'Mister':
        return 'Mr. ' + self.surname


class Person:
    pass


john = Person()
john.title = 'Mister'
john.name = 'John'
john.surname = 'Peterson'
john.age = 33
Person.make_title = make_title
print(john.make_title())

Общая рекомендация: не храните изменяемые типы как экземпляры класса, это чревато, однако, если вы знаете, что делаете, то используйте данный функционал во всю катушку, главное хорошо всё продумайте вначале и задокументируйте после.

Например, очень просто можно создать класс, который хранит ссылки на все свои экземпляры:

class RememberAll():
    instances = []

    def __init__(self):
        self.instances.append(self)


a = RememberAll()
b = RememberAll()
print(RememberAll.instances)  #-> [<__main__.RememberAll object at 0x7faa4a5ab7b8>, <__main__.RememberAll object at 0x7faa4a523c88>]

Можно реализовать класс какого-то физического объекта и при изменении его характеристик вызывать методы перерасчёта характеристик других элементов системы. Либо запомнить все ползунки на форме и при сдвиге одного двигать остальные автоматически. Не берусь утверждать, что подобная реализация прям всегда хорошо, но иногда может быть полезна.

Ну и раз мы коснулись физики, хочется упомянуть про оператор @. Да, такой есть. Вводился он для умножения матриц, что самое забавное, но, вроде бы, ни для одного из стандартных типов данных он не реализован, то есть он изначально вводился только для сторонних библиотек. Но ничто не мешает вам добавить его поддержку для своих типов данных реализовав методы __matmul__, __rmatmul__, __imatmul__

class Person:
    def __init__(self, name):
        self.name = name

    def __matmul__(self, msg):
        return "@" + self.name + " " + msg


john = Person("John")
print(john @ "hello")          # -> @John hello

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


  1. Kwent
    04.09.2018 23:49
    +1

    Спасибо за статью, не знал про оператор @.
    И вместо «расхрабриться» прочитал «расхабриться», что в этом случае даже уместней :)


    1. LinearLeopard Автор
      05.09.2018 09:29

      Я вам даже больше скажу, изначально там именно так и было, только при предфинальной вычитке заметил, была даже идея, поставить там строчную «Р», но вы правы, оба слова подходят.


  1. DollaR84
    05.09.2018 00:05

    Да, спасибо, сколько пишу на python, а все время узнаю что-то новое. Даже не догадывался об операторе @.


  1. uchitel
    05.09.2018 10:32

    >>> карма_писателя = 10
    >>> #  Карма писателя после этой статьи:
    ... карма_писателя += 10
    >>> карма_писателя
    20

    Хотел еще после первой статьи ссылку кинуть, но не смог зайти. Вот еще немного интересненького про python


  1. suharik
    05.09.2018 10:34

    Вот не знаю, полезно ли это, или просто возможность для хулиганства, но:

    cats = ['Puffy', 'Smoothy', 'Whisky', 'Fluffy']
    print(cats[False])
    print(cats[True-True])
    print(cats[True*2])
    print(cats[True or False])

    это язык очень не строгой типизации )


    1. LinearLeopard Автор
      05.09.2018 10:45
      +1

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


    1. iroln
      06.09.2018 10:49

      В Python сильная типизация. '10' + 20 сделать не выйдет.


      1. suharik
        06.09.2018 11:00

        Конечно, он не умеет складывать Str и Int. Зато вполне выйдет '10' * 2. Попробуйте то же самое провернуть в С++.


        1. LinearLeopard Автор
          06.09.2018 11:15

          Ну с точки зрения математики умножение на скаляр N — это сложение объекта с самим собой N раз. Вот python и складывает строку саму с собой 2 раза, со списками и кортежами так тоже можно.


  1. Zanak
    05.09.2018 12:41

    Маленькое дополнение/пояснение по поводу атрибутов класса.
    В питон следует различать класс как таковой и его экземпляры. Если в объявлении класса вы укажите не только методы, но и атрибуты, то эти атрибуты разделят между собой все экземпляры класса. Поведение таких атрибутов аналогично static значениям во многих других языках. Для доступа к таким значениям не обязательно создавать инстанс класса, можно через точку, как к атрибуту типа.
    Атрибуты класса можно создавать и инициализировать в реализации метода __init__, который играет роль конструктора экземпляра, или динамически. В случае, когда динамическое создание атрибутов нежелательно, можно в декларации класса объявить массив __slots__, с перечислением имен всех доступных атрибутов, и питон не позволит создавать ни какие другие атрибуты, кроме этих.


    1. LinearLeopard Автор
      05.09.2018 13:05

      Спасибо, я именно это и хотел сказать, но у вас получилось понятнее.
      Про __slots__ думал в следующих статьях написать, но пока не придумал, как это получше сделать, наверно, стоило прямо здесь и сказать сразу после примера с добавлением полей, но по хорошему, надо ещё и механизм, как это обеспечивается описать, а я пока не разбирался.


    1. trapwalker
      06.09.2018 18:15

      Мне кажется об этом правильнее рассказывать по-другому.
      1. У объекта могут быть атрибуты.
      2. Класс — это тоже объект.
      3. При обращении к атрибуту объекта (через точку или getattr) поиск происходит сначала в контексте самого объекта, потом в контексте его классов.
      4. Запись атрибута происходит в контекст самого объекта.


      1. Zanak
        06.09.2018 19:07

        3. При обращении к атрибуту объекта (через точку или getattr) поиск происходит сначала в контексте самого объекта, потом в контексте его классов.
        Главное не переборщить, чтобы не получилось каши. Обычно, имена хранятся в массиве __dir__, но есть еще слоты, а еще __getattr__ и __getattribute__, которые могут быть перекрыты, и реализовывать вычислимые атрибуты, и не только их. И это только про текущий контекст. А еще, если я не ошибаюсь, то логика поиска имен в родительских классах для второй и третей ветки различается. И чтобы было совсем весело, можно расказать о загрузке пакетов и классов, здесь тоже, если я не путаю, есть различия, потому что третий питон старается бороться с циклическим импортом.


        1. trapwalker
          07.09.2018 10:47

          Да, вы правы, всегда можно погрузиться еще глубже. Мне кажется, что осваивать такие вещи новичкам нужно поэтапно. Выделить начальные этапы так, чтобы их описание не вводило в заблуждение при погружении в более глубокие слои — это искусство преподавания.


  1. unabl4
    05.09.2018 13:18

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


  1. Komesk
    05.09.2018 14:47

    Ярче всего идею, что python это не java иллюстрирует то, что добавление полей в объектах и классах осуществляется через обычное присваивание


    Спасибо за наводку. Я даже предположить не мог что такое возможно. Это же прямое нарушение принципов ООП? или Питон не является объектно ориентированным?


    1. homocomputeris
      05.09.2018 21:11

      Если это про нарушение инкапсуляции, то сеттеры её тоже нарушают (та же идея тут).


      1. Komesk
        06.09.2018 17:04

        Каким образом? Они как раз запрещают прямое изменения членов класса, для того чтобы сохранить все внутренние зависимости.


    1. LinearLeopard Автор
      06.09.2018 13:45

      Какого принципа? Инкапсуляции? Как уже было сказано, этот принцип нарушается и в хвост, и в гриву.
      И как можно быть не объектно-ориентируемым, но поддерживать создание классов и объектов?)
      Вообще существует множество видений ООП, и видение Java, насколько я могу судить, ни чуть не ближе к Smalltalk, чем python и Java между собой.


      1. Komesk
        06.09.2018 16:58

        Принципа абстракции.
        Если определять язык программирования объектно ориентированным только по наличию конструкций для создания классов, объектов — то да. Только вот чем класс в Питоне отличается от просто контейнера, в который можно напихать что угодно?


        1. LinearLeopard Автор
          06.09.2018 17:07

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


    1. trapwalker
      06.09.2018 18:20

      Является. Если кто-то и нарушит эти принципы, то это будет программист. Он это сделает либо нарочно, а значит это его дело, либо нечаянно — такие ошибки надо искать линтерами. Просто питон не налагает лишних ограничений и не разводит бюрократию. Это позволяет ему быть лаконичным и простым.


    1. iroln
      06.09.2018 18:27

      Нарушение каких принципов? Если вы имеете в виду инкапсуляцию, то не надо путать это понятие с сокрытием данных. Инкапсуляция — это по сути размещение в одном объекте данных и функций, которые используют и/или изменяют эти данные. В Python нет сокрытия данных и это не нарушает никакие принципы ООП.