Именованные пространства имён: модули и классы

Модули

Статья «Классы в Python3 — это очень просто»

Примечание: достаточно сделать все примеры, и наступит Просветление! Если оно не наступило, прочитать комментарии к примерам. Если всё равно непонятно, пишите письма автору — значит, не везде удалось объяснить достаточно прозрачно.

Поля классов и объектов

Стандартные объекты — например, кортежи — рождаются с полями:

   1 >>> l=()
   2 >>> dir(l)
   3 ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']
   4 >>> l.new="QQ"
   5 Traceback (most recent call last):
   6   File "<stdin>", line 1, in <module>
   7 AttributeError: 'tuple' object has no attribute 'new'

Этих полей довольно много, но все они какие-то служебные и нам сейчас не интересны. А впихивать новые поля в стандартный объект нельзя.

Но можно самому создать объект. Для этого надо с помощью оператора class описать конструктор этого объекта, а потом вызвать этот конструктор. Он вернёт объект — ровно такой, как заказывали. Например, пустой:

   1 >>> class C:
   2 ...   pass
   3 ...
   4 >>> dir(C)
   5 ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
   6 >>> c=C()
   7 >>> dir(c)
   8 ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
   9 >>> c
  10 <__main__.C object at 0x7f39429f92b0>
  11 >>> C
  12 <class '__main__.C'>

Постой-постой, он не совсем пустой! В нём тоже есть несколько каких-то служебных имён, ну да это мелочи, главное — неслужебных нет. Итак, C — конструктор объекта, а c — сконструированный им объект (или «экземпляр», instance).

И вот в такой объект можно самому напихать какие хочешь поля:

   1 >>> c.new="QQ"
   2 >>> dir(c)
   3 ['__class__', …, 'new']
   4 >>> c.new
   5 'QQ'

Поля, добавленные в объект, в самом классе не появляются. Но и в сам класс (конструктор) тоже можно напихать какие хочешь поля:

   1 >>> C.strange="NN"
   2 >>> dir(C)
   3 ['__class__', …, 'strange']
   4 [
   5 >>> dir(c)
   6 ['__class__', …, 'new', 'strange']
   7 >>> c.strange
   8 'NN'

Более того, эти поля тут же становятся видны видны во всех экземплярах. Если в самом экземпляре поля нет, Python смотрит, а нет ли его в классе.

А если такое поле в объекте создать, оно «заслоняет» собой поле класса (как локальная переменная заслоняет глобальную):

   1 >>> d=C()
   2 >>> dir(d)
   3 ['__class__', …, 'strange']
   4 >>> d.strange='YY'
   5 >>> c.strange
   6 'NN'
   7 >>> C.strange
   8 'NN'
   9 >>> d.strange
  10 'YY'

Поле объекта можно удалить! Тогда снова станет видимым поле класса:

   1 >>> d.strange
   2 'YY'
   3 >>> del d.strange
   4 >>> d.strange
   5 'NN'

Поле класса можно удалить, разумеется, только в самом классе:

   1 >>> del d.strange
   2 Traceback (most recent call last):
   3   File "<stdin>", line 1, in <module>
   4 AttributeError: strange
   5 >>> dir(d)
   6 ['__class__', …, '__weakref__', 'strange']
   7 >>> del C.strange
   8 >>> d.strange
   9 Traceback (most recent call last):
  10   File "<stdin>", line 1, in <module>
  11 AttributeError: 'C' object has no attribute 'strange'
  12 >>> dir(d)
  13 ['__class__', …]
  14 >>> dir(C)
  15 ['__class__', …]

Задание полей в конструкторе, часть первая

Если мы хотим, чтобы при создании объекта в нём уже были какие-то поля, эти поля надо добавить в класс. Только вот делать это в виде класс.поле = значение там и сям в коде программы как-то… неструктурно. Можно сделать так:

   1 >>> class D:
   2 ...   w=10
   3 ...   h=20
   4 ...
   5 >>> d=D()
   6 >>> d.w, d.h
   7 (10, 20)

Теперь при обращении к полям w и h любого объекта типа D («экземпляра класса D») мы получим какое-нибудь значение — либо поля из класса, если мы ничего не добавляли в объект, либо поля объекта, если добавляли:

   1 >>> e=D()
   2 >>> e.w, e.h = 18, 11
   3 >>> d.w*d.h-e.w*e.h
   4 2

Тут есть одна неприятность. Если поле класса — не константа (например, список), и его можно менять, то пока мы его не переопределили в объекте, мы изменяем поле класса:

   1 >>> class E:
   2 ...   l=[]
   3 ...
   4 >>> e=E()
   5 >>> e.l, E.l
   6 ([], [])
   7 >>> e.l.append("QQ")
   8 >>> e.l, E.l
   9 (['QQ'], ['QQ'])

А как только мы зададим поле объекта, мы начинаем изменять поле объекта:

   1 >>> f=E()
   2 >>> f.l=[]
   3 >>> e.l, f.l, E.l
   4 (['QQ'], [], ['QQ'])
   5 >>> f.l.append("Q-K-R-Q")
   6 >>> e.l, f.l, E.l
   7 (['QQ'], ['Q-K-R-Q'], ['QQ'])

Та же история, что с глобальными и локальными переменными. Не было присваивания — видно поле класса, присвоили — трах! — появилось поле объекта. И за этим надо следить. Запомним на будущее.

Методы

Разумеется, поля объекта могут быть абсолютно любого типа. Числами. Строками. Функциями. Экземплярами класса. Классами. Любого типа.

Функциями — это хорошо! Вот, например, функция, которая показывает значение поля класса:

   1 >>> class F:
   2 ...   data = "dada"
   3 ...
   4 >>> def fun():
   5 ...   print(F.data)
   6 ...
   7 >>> f=F()
   8 >>> f.pri=fun
   9 >>> f.pri()
  10 dada
  11 >>> f.data = "nono"
  12 >>> f.pri()
  13 dada

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

А нельзя ли прямо в конструктор функцию записать? В класс? Чтобы она всегда там уже была? А ссылку на объект — что же, передадим и ссылку, жалко, что ли? Вот так как-нибудь:

   1 >>> class G:
   2 ...   d = 1
   3 ...   def fun(obj):
   4 ...     print(obj.d)
   5 ...
   6 >>> g=G()
   7 >>> g.fun(g)
   8 Traceback (most recent call last):
   9   File "<stdin>", line 1, in <module>
  10 TypeError: fun() takes 1 positional argument but 2 were given

Час от часу не легче! Как это так «задано два параметра»?! Один же!

Посмотрим. Передадим один параметр, а в функции напишем, что их два, раз уж оно так думает:

   1 >>> class H:
   2 ...   def test(p1, p2):
   3 ...     print(p1, p2)
   4 ...
   5 >>> h=H()
   6 >>> h.test(100500)
   7 <__main__.H object at 0x7feffab3fdd8> 100500
   8 >>> h
   9 <__main__.H object at 0x7feffab3fdd8>
  10 >>> H.test(h,"I've got it!")
  11 <__main__.H object at 0x7feffab3fdd8> I've got it!

Ах во-о-о-т как. Если функцию, определённую внутри класса, вызвать из экземпляра, Python передаёт ей на один параметр больше! При вызове вида объект.функция(параметр1, …) на самом деле вызывается класс.функция(объект, параметр1, …). Такой способ вызова превращает функцию в метод.

Собственно, Python как бы намекает нам на это:

   1 >>> class I:
   2 ...   def meth():
   3 ...     pass
   4 ...
   5 >>> def fun():
   6 ...   pass
   7 ...
   8 >>> I.subfun = fun
   9 >>> i = I()
  10 >>> fun
  11 <function fun at 0x7feffab441e0>
  12 >>> I.subfun
  13 <function fun at 0x7feffab441e0>
  14 >>> I.meth
  15 <function I.meth at 0x7feffab61048>
  16 >>> i.subfun
  17 <bound method fun of <__main__.I object at 0x7feffab604a8>>
  18 >>> i.meth
  19 <bound method I.meth of <__main__.I object at 0x7feffab604a8>>

Да. То есть функция вообще ничем не отличается от метода класса. Кроме имени, в котором отложилось имя исходного класса. Но метод экземпляра, оказывается — совсем другой объект! Очевидно, каким бы путём функция ни попала в поля класса, она становится методом, если вызвать её из экземпляра.

Ну-ка, ну-ка:

   1 >>> class J:
   2 ...   t = 42
   3 ...   def answer(obj):
   4 ...     print(obj.t)
   5 ...
   6 >>> j=J()
   7 >>> j.answer()
   8 42
   9 >>> j.t='forty two'
  10 >>> j.answer()
  11 forty two

Вуаля! Осталось только заметить, чтоб для удобства чтения этот первый параметр принято называть self.

Инициализация

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

Ну так вот, если задать специальный метод __init__(), конструктор его вызовет в процессе создания объекта (точнее — сразу после создания, но перед тем, как отдавать в программу результат). Там-то можно все поля свежесозданного, но незаполненного ещё объекта и заполнить.

Более того, если передать какие-то параметры конструктору, он их передаст в __init__() (разумеется, вместе со ссылкой на объект в качестве первого параметра):

   1 >>> class K:
   2 ...   def __init__(self, w, h):
   3 ...     self.w, self.h = w, h
   4 ...   def square(self):
   5 ...     return self.w*self.h
   6 ...
   7 >>> k=K(10,20)
   8 >>> dir(k)
   9 ['__doc__', '__init__', '__module__', 'h', 'square', 'w']
  10 >>> k.square()
  11 200

Вот.

До встречи в прямом эфире! Не пропустите следующую серию, в которой класс G скажет:

— Всему самому лучшему, что у меня есть, я обязан родителю!
  Я всё унаследовал от него — и эти поля, и эти методы — а сам лишь кое-что улучшил и добавил немного от себя.

Классы

Д/З

  1. Прочитать
  2. В задачах типа «написать модуль» требуется написать модуль (с любым именем, в тест он приедет с именем mod).

    EJudge: FakeRnd 'Неслучайные числа'

    Написать модуль, в котором будет две фальшивые функции: randrange() и randint(), которые принимают тек же параметры, что и настоящие. При каждом чётном по порядку вызове функция randint(a, b) возвращает a, при нечётном — b. Функция randrange() может принимать от 1 до 4 параметров. Четвёртый параметр она игнорирует, а начало диапазона a, конец диапазона b и шаг d интерпретирует так. Если при очередном вызове эти величины совпадают с предыдущим вызовом, то randrange() сперва работает как если бы возвращала очередной элемент range(), то есть сначала a, затем a+d и т. д., пока не доберётся до конца диапазона, после чего продолжает с начала диапазона (шаг при этом не сбивается, см. пример). Пустых диапазонов (когда знак шага не соответствует концам диапазона) во входных данных нет. Если же при очередном вызове значения a, b или d оказываются иными, процесс запускается с начала.

    Input:

    print(*(mod.randrange(5) for i in range(6)))
    print(*(mod.randrange(10,15,2) for i in range(7)))
    print(*(mod.randrange(20,36,3,False) for i in range(9)))
    print(*(mod.randint(5,7) for i in range(3)), *(mod.randint(5,17) for i in range(5)))
    Output:

    0 1 2 3 4 0
    10 12 14 11 13 10 12
    20 23 26 29 32 35 22 25 28
    5 7 5 17 5 17 5 17
  3. EJudge: CountFields 'Счётчик полей'

    Написать функцию fcounter(), которая первым параметром получает некоторый класс, а остальные параметры применяет для создания экземпляра этого класса. Функция должна возвращать 4 отсортированных списка: имена методов класса, имена полей класса, имена методов, которые появились в экземпляре и имена полей, которые появились в экземпляре (под «полями» имеются в виду не-callable() объекты).

    Input:

    class C:
        x, y, z = 1, 3, 5
    
        def X(self): return self.x
        def Y(self): return self.y
    
        def __init__(self, dx, dy, dz):
            self.x = dx
            self.Y = dy
            self.Z = dz
    
    cm, cf, om, of = fcounter(C, 6, 7, 8)
    print("Class: methods", *cm)
    print("Class: fields", *cf)
    print("Object: methods", *om)
    print("Object: fields", *of)
    Output:

    Class: methods X Y
    Class: fields x y z
    Object: methods
    Object: fields Y Z

TODO

  1. EJudge: BitCoding 'Сжатие со словарём'

    Написать модуль, в котором будет 4 функции. Первые две: shex(n), которая переводит число n в 64-ричное представление, и xehs(s), которая переводит строку с 64-ричным числом в число. 64-ричная система счисления пользуется «цифрами» с ASCII-кодом от 32 до 32+63=95 (т. е. от пробела до подчёркивания) включительно. Функция encode(txt) упаковывает строку txt, состоящую из символов диапазона " "…"_" по следующим правилам. Символы, встретившиеся в тексте, упорядочиваются по убыванию частоты их появления в тексте (вторичный ключ — сам символ). Самому частому (и с наибольшим ASCII-кодом, если таковых несколько) ставится в соответствие бит "0", следующему — последовательность битов "10", следующему — "110", и т. д. Биты записываются единой строкой, строка дополняется нулями, если это необходимо, и превращается в 64-ричное число. Функция encode(txt) возвращает кортеж (длина txt, строка упорядоченных символов, закодированная строка). Четвёртая функция, decode(length, chars, code), раскодирует строку code, используя описанное выше сопоставление chars битам, и возвращает раскодированную строку длиной length.

    Input:

    print(xehs("BREAKFAST"))
    print(shex(10844745761445995))
    res = encode("ENGINEERING WITHOUT MANAGEMENT IS ART.")
    print(res)
    txt = decode(*res)
    print(txt)
    Output:

    9792630319357172
    FASTBREAK
    (38, 'NETI GARMWUSOH.', 'GW*_N?>_[M__?_O_;W^_/WU_IO=_][]_;__ ')
    ENGINEERING WITHOUT MANAGEMENT IS ART.

LecturesCMC/PythonIntro2019/09_ModulesClasses (последним исправлял пользователь ArsenyMaslennikov 2021-11-14 20:25:40)