Есть ли способ передать фабричные функции подклассам `typing.NamedTuple`? - PullRequest
0 голосов
/ 24 апреля 2019

Примечание: я сделал это с Python 3.6.7, но документация там не изменилась для NamedTuple, поэтому я сомневаюсь, что он там изменится.

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

.

(Напомним: если вы установите [] в значение по умолчанию, вы получите все объекты, имеющие один и тот же список. Это верно как для старого collections.namedtuple, так и для нового typing.NamedTuple. Вот почему collections.defaultdict класс имеет аргумент default_factory в конструкторе.)

>>> from typing import NamedTuple, List
>>> class Person(NamedTuple):
...   name: str
...   children: List['Person']
...   def __new__(self, name: str, children: List['Person'] = None):
...     return Person._make(name, children if children is not None else [])
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.6/typing.py", line 2163, in __new__
    raise AttributeError("Cannot overwrite NamedTuple attribute " + key)
AttributeError: Cannot overwrite NamedTuple attribute __new__
>>>

Я продолжал ломать голову над тем, как проникнуть в упрямый список __new__, но тогда это вообще не имело смысла.

>>> class Person(NamedTuple):
...   name: str
...   children: List['Person'] = None
... 
>>> 
>>> def new_new(name: str, children: List[Person] = None) -> Person:
...   return Person(name, [] if children is None else children)
... 
>>> old_new = Person.__new__
>>> def new_new(name: str, children: List[Person] = None) -> Person:
...   return old_new(name, [] if children is None else children)
... 
>>> Person.__new__ = new_new
>>> 
>>> Person('John')
Person(name='John', children=None)
>>> Person.__new__
<function new_new at 0x7f776b2b2e18>
>>> new_new('John')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in new_new
  File "<string>", line 14, in __new__
TypeError: tuple.__new__(X): X is not a type object (str)
>>> new_new(Person, 'John')
Person(name='John', children=None)
>>> 

Итак, как это должно быть сделано? Я привык делать то же самое для collections.namedtuple классов, и это в основном любопытство.

Во всей практичности я, вероятно, должен сделать это:

class Person(NamedTuple):
  name: str
  children: List['Person'] = None

  @classmethod
  def create(cls, name, children=None):
    return cls(name, [] if children is None else children)

1 Ответ

1 голос
/ 24 апреля 2019

NamedTuple не предназначен для предоставления более чем именованного кортежа с проверкой типа. NamedTupleMeta явно запрещает перезапись __new__, __init__ и некоторые другие. Ваш единственный шанс - изменить работу NamedTuple.


Защита от перезаписи __new__ сама по себе не защищена. Это позволяет обезьяне исправлять это различными способами.


Вы можете удалить __new__ из защищенных имен.

import typing
typing._prohibited = typing._prohibited[1:]

Это позволяет вам напрямую перезаписать __new__ в вашем классе. Обратите внимание, что это повлияет на все NamedTuple подтипов.


Вы можете получить новый метакласс, который не защищает __new__, и использовать его для своих NamedTuple экземпляров:

class NamedTupleUnprotectedMeta(typing.NamedTupleMeta):
    def __new__(cls, typename, bases, ns):
        ...
        # copy verbatim from NamedTupleMeta
        ...
        # update from user namespace without protection
        for key in ns:
            if key not in typing._special and key not in nm_tpl._fields:
                setattr(nm_tpl, key, ns[key])
        return nm_tpl


class Person(NamedTuple, metaclass=NamedTupleUnprotectedMeta):
    ...

Это позволяет вам напрямую перезаписывать __new__ в вашем классе, не затрагивая другие подтипы NamedTuple. Обратите внимание, что ваш метакласс также может добавить пользовательский new, который вызывает значения по умолчанию, установленные в теле класса.


Обратите внимание, что NamedTupleMeta также игнорирует bases, поэтому вы не можете использовать миксины и тому подобное. Любые значительные изменения требуют сначала переписать NamedTupleMeta.


Если вам нужен только класс хранилища с настройками по умолчанию, стандартный библиотечный пакет dataclasses и сторонний пакет attrs предоставляют объявления, соответствующие описанному вами:

@attr.s(auto_attribs=True)
class Person:
    name: str
    children: List['Person'] = attr.Factory(list)

Оба пакета поддерживают замораживание, что аналогично неизменяемости кортежей.

...