Неправильное использование __new__ для генерации классов?

Я создаю несколько классов для работы с именами файлов в различных типах общих файлов (nfs, afp, s3, локальный диск) и т.д. Я получаю, как пользователь вводит строку, которая идентифицирует источник данных (т.е. "nfs://192.168.1.3" или "s3://mybucket/data") и т.д.

Я подклассифицирую определенные файловые системы из базового класса с общим кодом. Там, где я запутался, в создании объекта. У меня есть следующее:

import os

class FileSystem(object):
    class NoAccess(Exception):
        pass

    def __new__(cls,path):
        if cls is FileSystem:
            if path.upper().startswith('NFS://'): 
                return super(FileSystem,cls).__new__(Nfs)
            else: 
                return super(FileSystem,cls).__new__(LocalDrive)
        else:
            return super(FileSystem,cls).__new__(cls,path)

    def count_files(self):
        raise NotImplementedError

class Nfs(FileSystem):
    def __init__ (self,path):
        pass

    def count_files(self):
        pass

class LocalDrive(FileSystem):
    def __init__(self,path):
        if not os.access(path, os.R_OK):
            raise FileSystem.NoAccess('Cannot read directory')
        self.path = path

    def count_files(self):
        return len([x for x in os.listdir(self.path) if os.path.isfile(os.path.join(self.path, x))])

data1 = FileSystem('nfs://192.168.1.18')
data2 = FileSystem('/var/log')

print type(data1)
print type(data2)

print data2.count_files()

Я думал, что это будет полезно использовать __new__, но большинство сообщений, которые я прочитал об этом, используют, чтобы отговорить его. Есть ли более приемлемый способ решения этой проблемы?

Ответ 1

Я не думаю, что использование __new__() для выполнения того, что вы хотите, является неправильным. Другими словами, я не согласен с принятым ответом на этот вопрос о том, что функции Factory всегда являются "лучшим способом сделать это".

Если вы действительно хотите избежать его использования, то единственными опциями являются метаклассы или отдельная фабричная функция/метод. Учитывая доступные варианты, использование __new__() поскольку он статичен по умолчанию, является вполне разумным подходом.

Тем не менее, ниже я думаю, что это улучшенная версия вашего кода. Я добавил пару методов класса, чтобы помочь автоматически найти все подклассы. Они поддерживают самый важный способ, которым это лучше - который теперь добавляет подклассы, не требует изменения __new__(). Это означает, что теперь его легко расширять, поскольку он эффективно поддерживает то, что вы могли бы назвать виртуальными конструкторами.

Аналогичную реализацию можно также использовать для перемещения создания экземпляров из метода __new__ в отдельный (статический) фабричный метод - так что в некотором смысле показанная методика является просто относительно простым способом кодирования расширяемой универсальной фабричной функции независимо от того, какое имя это дано.

import os
import re

class FileSystem(object):
    class NoAccess(Exception): pass
    class Unknown(Exception): pass

    # Pattern for matching "xxx://" where x is any character except for ":".
    _PATH_PREFIX_PATTERN = re.compile(r'\s*([^:]+)://')

    @classmethod
    def _get_all_subclasses(cls):
        """ Recursive generator of all class' subclasses. """
        for subclass in cls.__subclasses__():
            yield subclass
            for subclass in subclass._get_all_subclasses():
                yield subclass

    @classmethod
    def _get_prefix(cls, s):
        """ Extract any file system prefix at beginning of string s and
            return a lowercase version of it or None when there isn't one.
        """
        match = cls._PATH_PREFIX_PATTERN.match(s)
        return match.group(1).lower() if match else None

    def __new__(cls, path):
        """ Create instance of appropriate subclass using path prefix. """
        path_prefix = cls._get_prefix(path)

        for subclass in cls._get_all_subclasses():
            if subclass.prefix == path_prefix:
                # Using "object" base class method avoids recursion here.
                return object.__new__(subclass)
        else:  # no subclass with matching prefix found (and no default defined)
            raise FileSystem.Unknown(
                'path "{}" has no known file system prefix'.format(path))

    def count_files(self):
        raise NotImplementedError


class Nfs(FileSystem):
    prefix = 'nfs'

    def __init__ (self, path):
        pass

    def count_files(self):
        pass


class LocalDrive(FileSystem):
    prefix = None  # Default when no file system prefix is found.

    def __init__(self, path):
        if not os.access(path, os.R_OK):
            raise FileSystem.NoAccess('Cannot read directory')
        self.path = path

    def count_files(self):
        return sum(os.path.isfile(os.path.join(self.path, filename))
                     for filename in os.listdir(self.path))


if __name__ == '__main__':

    data1 = FileSystem('nfs://192.168.1.18')
    data2 = FileSystem('c:/')  # Change as necessary for testing.

    print(type(data1))  # -> <class '__main__.Nfs'>
    print(type(data2))  # -> <class '__main__.LocalDrive'>

    print(data2.count_files())  # -> <some number>

Ответ 2

По-моему, использование __new__ таким образом действительно запутывает других людей, которые могут читать ваш код. Также для этого требуется несколько хакерский код, чтобы отличать систему угадывания от пользовательского ввода и создавать Nfs и LocalDrive с их соответствующими классами.

Почему бы не сделать отдельную функцию с таким поведением? Это может быть даже статический метод класса FileSystem:

class FileSystem(object):
    # other code ...

    @staticmethod
    def from_path(path):
        if path.upper().startswith('NFS://'): 
            return Nfs(path)
        else: 
            return LocalDrive(path)

И вы называете это следующим образом:

data1 = FileSystem.from_path('nfs://192.168.1.18')
data2 = FileSystem.from_path('/var/log')