Метка: attribute

Munch – вседозволенный объект

КДПВ

Привет. Хочу познакомить вас библиотекой Munch, которая является форком более старой библиотеки Bunch. Рассмотрим суть проблемы, которую она решает. Задать атрибуты объекта, не описывая их по одному в конструкторе. Легче понять на примере:

>>> f = object()
>>> f.x = 10
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'object' object has no attribute 'x'

Видно, что object нам не поможет. Но в Python 3 можно сделать пустой класс, это не вызовет ошибки:

class Bunch: ...

foo = Bunch()
foo.x = 10
foo.y = 20

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

Установка:

pip install munch

Объект Munch – это наследник словаря dict, с ним можно работать как со словарем, но можно также произвольно работать с его атрибутами:

from munch import *

# пустой
b = Munch()

# задаем атрибуты
b.hello = 'world'
print(b.hello)  # world

b['hello'] += "!"
print(b.hello)  # world!

print(b.hello is b['hello'])  # True

# атрибут может быть тоже Munch
b.bar = Munch()
b.bar.baz = 100
print(b.bar.baz)  # 100

Т.е. мы может обращаться к данным как через точку (как атрибут), так и через квадратные скобки (как с обычным словарем) – это будут одни и те же данные, при условии равных имен.

Очень удобная фишка – создание Munch через конструктор, просто перечисляем ключевые слова, и они станут атрибутами:

# задать через конструктор
c = Munch(x=10, y=20, z=30)
print(c.x, c.y, c.z)  # 10 20 30

С Munch можно работать, как с обычным dict, например:

c = Munch(x=10, y=20, z=30)
print(list(c.keys()))  # список атрибутов

c.update({
    'w': 10,
    'name': 'ganesh'
})
print(c)  # Munch({'x': 10, 'y': 20, 'z': 30, 'w': 10, 'name': 'ganesh'})

print([(k, v) for k, v in c.items()])
# [('x', 10), ('y', 20), ('z', 30), ('w', 10), ('name', 'ganesh')]

Удобно сеарилизовтать такие объекты:

# JSON

b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!')
import json
print(json.dumps(b))
#  {"foo": {"lol": true}, "hello": 42, "ponies": "are pretty!"}

# YAML - если есть.
import yaml
print(yaml.dump(b))  # или
print(yaml.safe_dump(b))

Замечание

В библиотеку collections Python 3 уже включен объект UserDict со схожей функциональностью:

from collections import UserDict

a = UserDict()
a.p = 10

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Доступ к атрибутам

Скрин из Готики 1: страж не пускает в замок героя

Атрибуты объекта в Python – это именованные поля (данные, функции), присущие данному объекту (экземпляру, классу). Самый простой доступ к атрибутам – через точку:

class Foo:
     def init(self):
         self.x = 88  # установка значения атрибута      
 f = Foo()
 print(f.x)  # доступ к атрибуту через точку

Если мы обратимся к атрибуту, которого нет, то получим ошибку AttributeError. Мы можем переопределить это поведение путем реализации магических методов __getattr__ или __getattribute__.

__getattr__ вызывается, если атрибут не найден обычным способом (не был задан ранее через точку, функцию setattr, или через __dict__). Если атрибут найден, то __getattr__ НЕ вызывается.

📎 Пример. Возвращаем -1 для любого несуществующего атрибута.

class Test:
    def __getattr__(self, item):
        print(f'__getattr__({item})')
        return -1  

t = Test()
# зададим x и y
t.x = 10
setattr(t, 'y', 33)

print(t.x)  # 10
print(t.y)  # 33  
print(t.z)  # __getattr__(z) -1

Метод __getattribute__ вызывается, когда мы пытаемся получить любой атрибут, не зависимо от того, есть он или нет. Этот метод, вызывается прежде __getattr__. Он немного хитрее. Если __getattribute__ кидает AttributeError, то будет вызвана __getattr__.

📎 Пример. Мы можем запретить чтение каких-то атрибутов:

class Test:
    def __getattr__(self, item):
        print(f'__getattr__({item})')
        return -1

    def __getattribute__(self, item):
        print(f'__getattribute__({item})')
        if item == 'y':  # запретим получать y
            raise AttributeError
        return super().__getattribute__(item)


# зададим x и y
t = Test()
t.x = 10
t.y = 20

print(t.x)  # __getattribute__(x) 10
print(t.y)  # __getattribute__(y) __getattr__(y) -1
print(t.z)  # __getattribute__(z) __getattr__(z) -1

⚠️ Внимание! В __getattribute__ мы можем вызвать super().__getattribute__(item) или object.__getattribute__(self, item), что посути тоже самое, но не следует делать return self.__dict__[item] или return self.__getattribute__(item) или return getattr(self, item), так как это приведет к бесконечной рекурсии.

💡 Также есть магический метод __setattr__(self, key, value), вызываемый при obj.key = value или setattr(obj, 'key', value). У него нет более длинно-названного брата-близнеца.

Для полноты картины еще есть встроенная функция getattr(object, name[, default]). Вызов getattr(x, 'y') аналогичен обращению через точку: x.y В первом случае ‘y’ – это строка, что позволяет нам динамически получать атрибуты объектов, в отличие от точки, которая требует фиксированного имени на этапе написания кода. В случае, если атрибут недоступен мы получим AttributeError при незаданном default или получим default (без возникновения ошибки), если default был задан третьим аргументом.

Специально для канала @pyway.