Динамически определять атрибуты, которые зависят от ранее определенных атрибутов
Я хочу объект, который представляет путь root
и произвольное количество подкаталогов, которые построены с os.path.join(root)
, Я хочу получить доступ к этим путям с помощью формы self.root
, self.path_a
, self.path_b
и т.д... В дополнение к доступу к ним напрямую через self.path_a
Я хочу иметь возможность перебирать их. К сожалению, приведенный ниже подход не позволяет перебирать их через attr.astuple(paths)
Первый фрагмент кода ниже - это то, что я придумал. Это работает, но кажется мне немного хакерским. Так как это мое первое использование attrs, мне интересно, есть ли более интуитивный / идиоматический способ приблизиться к этому. Мне потребовалось много времени, чтобы понять, как написать довольно простой класс ниже, поэтому я подумал, что могу упустить что-то очевидное.
Мой подход
@attr.s
class Paths(object):
subdirs = attr.ib()
root = attr.ib(default=os.getcwd())
def __attrs_post_init__(self):
for name in self.subdirs:
subdir = os.path.join(self.root, name)
object.__setattr__(self, name, subdir)
def mkdirs(self):
"""Create `root` and `subdirs` if they don't already exist."""
if not os.path.isdir(self.root):
os.mkdir(self.root)
for subdir in self.subdirs:
path = self.__getattribute__(subdir)
if not os.path.isdir(path):
os.mkdir(path)
Выход
>>> p = Paths(subdirs=['a', 'b', 'c'], root='/tmp')
>>> p
Paths(subdirs=['a', 'b', 'c'], root='/tmp')
>>> p.a
'/tmp/a'
>>> p.b
'/tmp/b'
>>> p.c
'/tmp/c'
Следующее было моей первой попыткой, которая не работает.
Неудачная попытка
@attr.s
class Paths(object):
root = attr.ib(default=os.getcwd())
subdir_1= attr.ib(os.path.join(root, 'a'))
subdir_2= attr.ib(os.path.join(root, 'b'))
Выход
------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-31-71f19d55e4c3> in <module>()
1 @attr.s
----> 2 class Paths(object):
3 root = attr.ib(default=os.getcwd())
4 subdir_1= attr.ib(os.path.join(root, 'a'))
5 subdir_2= attr.ib(os.path.join(root, 'b'))
<ipython-input-31-71f19d55e4c3> in Paths()
2 class Paths(object):
3 root = attr.ib(default=os.getcwd())
--> 4 subdir_1= attr.ib(os.path.join(root, 'a'))
5 subdir_2= attr.ib(os.path.join(root, 'b'))
6
~/miniconda3/lib/python3.6/posixpath.py in join(a, *p)
76 will be discarded. An empty last part will result in a path that
77 ends with a separator."""
--> 78 a = os.fspath(a)
79 sep = _get_sep(a)
80 path = a
TypeError: expected str, bytes or os.PathLike object, not _CountingAttr
2 ответа
Первая попытка: вы не можете просто прикрепить случайные данные к классу и надеяться, что attrs (в данном случае astuple) их заберет. attrs специально пытается избежать магии и догадок, а это значит, что вы действительно должны определить свои атрибуты в классе.
Вторая попытка: вы не можете использовать имя атрибута в области видимости класса (т.е. внутри class Paths:
но вне метода, потому что - как говорит вам Python - на данный момент они все еще являются внутренними данными, которые используются @attr.s
,
Самый элегантный подход, который я могу придумать, - это универсальная фабрика, которая принимает путь в качестве аргумента и строит полный путь:
In [1]: import attr
In [2]: def make_path_factory(path):
...: def path_factory(self):
...: return os.path.join(self.root, path)
...: return attr.Factory(path_factory, takes_self=True)
Который вы можете использовать так:
In [7]: @attr.s
...: class C(object):
...: root = attr.ib()
...: a = attr.ib(make_path_factory("a"))
...: b = attr.ib(make_path_factory("b"))
In [10]: C("/tmp")
Out[10]: C(root='/tmp', a='/tmp/a', b='/tmp/b')
In [11]: attr.astuple(C("/tmp"))
Out[11]: ('/tmp', '/tmp/a', '/tmp/b')
attrs - это attrs, вы, конечно, можете пойти дальше и определить свою собственную оболочку attr.ib:
In [12]: def path(p):
...: return attr.ib(make_path_factory(p))
In [13]: @attr.s
...: class D(object):
...: root = attr.ib()
...: a = path("a")
...: b = path("b")
...:
In [14]: D("/tmp")
Out[14]: D(root='/tmp', a='/tmp/a', b='/tmp/b')
Не могу догадаться, почему вы хотели бы получить доступ как self.paths.path
, Но вот что я бы сделал:
class D(object):
root = os.getcwd()
paths = dict()
def __init__(self, paths=[]):
self.paths.update({'root': self.root})
for path in paths:
self.paths.update({path: os.path.join(self.root, path)})
def __str__(self):
return str(self.paths)
d = D(paths=['static', 'bin', 'source'])
print(d)
print(d.paths['bin'])
выход
{'root': '/home/runner', 'static': '/home/runner/static', 'bin': '/home/runner/bin', 'source': '/home/runner/source'}
/home/runner/bin
Вы можете сделать это более сложным. Просто пример. Надеюсь, поможет.