developer tip

부분 문자열 형식

optionbox 2020. 8. 17. 08:46
반응형

부분 문자열 형식


문자열 템플릿 safe_substitute()함수 와 유사한 고급 문자열 형식화 방법으로 부분 문자열 형식화를 수행 할 수 있습니까?

예를 들면 :

s = '{foo} {bar}'
s.format(foo='FOO') #Problem: raises KeyError 'bar'

매핑을 덮어 써서 부분 서식으로 속일 수 있습니다.

import string

class FormatDict(dict):
    def __missing__(self, key):
        return "{" + key + "}"

s = '{foo} {bar}'
formatter = string.Formatter()
mapping = FormatDict(foo='FOO')
print(formatter.vformat(s, (), mapping))

인쇄

FOO {bar}

물론이 기본 구현은 기본적인 경우에만 올바르게 작동합니다.


서식을 지정하는 순서를 알고있는 경우 :

s = '{foo} {{bar}}'

다음과 같이 사용하십시오.

ss = s.format(foo='FOO') 
print ss 
>>> 'FOO {bar}'

print ss.format(bar='BAR')
>>> 'FOO BAR'

당신은 지정할 수 없습니다 foobar같은 시간에 - 당신은 순차적으로 그것을 할 수 있습니다.


짧고 가독성이 뛰어나며 코더의 의도를 가장 잘 설명 하는 partial함수를 사용할 수 있습니다 functools.

from functools import partial

s = partial("{foo} {bar}".format, foo="FOO")
print s(bar="BAR")
# FOO BAR

.format()부분 대체를 할 수 없다는 이 한계는 나를 괴롭 혔습니다.

Formatter여기에 많은 답변에 설명 된대로 사용자 정의 클래스 작성을 평가 하고 lazy_format 과 같은 타사 패키지 사용을 고려한 후에도 훨씬 간단한 내장 솔루션 인 템플릿 문자열을 발견했습니다.

유사한 기능을 제공하지만 부분 대체 safe_substitute()방법 도 제공합니다 . 템플릿 문자열에는 $접두사 가 있어야 합니다 (조금 이상하게 느껴지지만 전반적인 솔루션이 더 낫다고 생각합니다).

import string
template = string.Template('${x} ${y}')
try:
  template.substitute({'x':1}) # raises KeyError
except KeyError:
  pass

# but the following raises no error
partial_str = template.safe_substitute({'x':1}) # no error

# partial_str now contains a string with partial substitution
partial_template = string.Template(partial_str)
substituted_str = partial_template.safe_substitute({'y':2}) # no error
print substituted_str # prints '12'

이를 기반으로 편의 래퍼를 형성했습니다.

class StringTemplate(object):
    def __init__(self, template):
        self.template = string.Template(template)
        self.partial_substituted_str = None

    def __repr__(self):
        return self.template.safe_substitute()

    def format(self, *args, **kws):
        self.partial_substituted_str = self.template.safe_substitute(*args, **kws)
        self.template = string.Template(self.partial_substituted_str)
        return self.__repr__()


>>> s = StringTemplate('${x}${y}')
>>> s
'${x}${y}'
>>> s.format(x=1)
'1${y}'
>>> s.format({'y':2})
'12'
>>> print s
12

마찬가지로 기본 문자열 형식을 사용하는 Sven의 답변을 기반으로 한 래퍼 :

class StringTemplate(object):
    class FormatDict(dict):
        def __missing__(self, key):
            return "{" + key + "}"

    def __init__(self, template):
        self.substituted_str = template
        self.formatter = string.Formatter()

    def __repr__(self):
        return self.substituted_str

    def format(self, *args, **kwargs):
        mapping = StringTemplate.FormatDict(*args, **kwargs)
        self.substituted_str = self.formatter.vformat(self.substituted_str, (), mapping)

이것이 빠른 해결 방법으로 괜찮은지는 확실하지 않지만

s = '{foo} {bar}'
s.format(foo='FOO', bar='{bar}')

? :)


메서드 Formatter를 재정의하는 사용자 정의를 정의하면 정의 get_value되지 않은 필드 이름을 원하는대로 매핑하는 데 사용할 수 있습니다.

http://docs.python.org/library/string.html#string.Formatter.get_value

예를 들어, 당신은 매핑 할 수 있습니다 bar"{bar}"경우 barkwargs로 아닙니다.

그러나이를 위해서는 format()문자열의 format()메서드가 아닌 Formatter 개체 메서드 를 사용해야합니다 .


>>> 'fd:{uid}:{{topic_id}}'.format(uid=123)
'fd:123:{topic_id}'

이것을 시도하십시오.


Amber 의 의견 덕분에 나는 이것을 생각해 냈습니다.

import string

try:
    # Python 3
    from _string import formatter_field_name_split
except ImportError:
    formatter_field_name_split = str._formatter_field_name_split


class PartialFormatter(string.Formatter):
    def get_field(self, field_name, args, kwargs):
        try:
            val = super(PartialFormatter, self).get_field(field_name, args, kwargs)
        except (IndexError, KeyError, AttributeError):
            first, _ = formatter_field_name_split(field_name)
            val = '{' + field_name + '}', first
        return val

나를 위해 이것은 충분했습니다.

>>> ss = 'dfassf {} dfasfae efaef {} fds'
>>> nn = ss.format('f1', '{}')
>>> nn
'dfassf f1 dfasfae efaef {} fds'
>>> n2 = nn.format('whoa')
>>> n2
'dfassf f1 dfasfae efaef whoa fds'

내 제안은 다음과 같습니다 (Python3.6으로 테스트 됨).

class Lazymap(object):
       def __init__(self, **kwargs):
           self.dict = kwargs

       def __getitem__(self, key):
           return self.dict.get(key, "".join(["{", key, "}"]))


s = '{foo} {bar}'

s.format_map(Lazymap(bar="FOO"))
# >>> '{foo} FOO'

s.format_map(Lazymap(bar="BAR"))
# >>> '{foo} BAR'

s.format_map(Lazymap(bar="BAR", foo="FOO", baz="BAZ"))
# >>> 'FOO BAR'

업데이트 : 더 우아한 방법 (서브 클래 싱 dict및 오버로딩 __missing__(self, key))이 여기에 표시됩니다 : https://stackoverflow.com/a/17215533/333403


완전히 채워질 때까지 문자열을 사용하지 않는다고 가정하면 다음 클래스와 같이 할 수 있습니다.

class IncrementalFormatting:
    def __init__(self, string):
        self._args = []
        self._kwargs = {}
        self._string = string

    def add(self, *args, **kwargs):
        self._args.extend(args)
        self._kwargs.update(kwargs)

    def get(self):
        return self._string.format(*self._args, **self._kwargs)

예:

template = '#{a}:{}/{}?{c}'
message = IncrementalFormatting(template)
message.add('abc')
message.add('xyz', a=24)
message.add(c='lmno')
assert message.get() == '#24:abc/xyz?lmno'

이를 달성하는 또 다른 방법이 있습니다. 즉, 변수를 사용 format하고 %대체하는 것입니다. 예를 들면 :

>>> s = '{foo} %(bar)s'
>>> s = s.format(foo='my_foo')
>>> s
'my_foo %(bar)s'
>>> s % {'bar': 'my_bar'}
'my_foo my_bar'

매우 추악하지만 가장 간단한 해결책은 다음과 같습니다.

tmpl = '{foo}, {bar}'
tmpl.replace('{bar}', 'BAR')
Out[3]: '{foo}, BAR'

이렇게하면 tmpl일반 템플릿으로 계속 사용할 수 있으며 필요한 경우에만 부분 서식을 수행 할 수 있습니다. Mohan Raj와 같은 과도한 솔루션을 사용하기에는이 문제가 너무 사소하다고 생각합니다.


가장 유망한 솔루션을 테스트 한 후 이곳을 하고 있다 , 나는 그들 중 어느 것도 실현되지는 정말 다음과 같은 요구 사항을 충족 :

  1. str.format_map()템플릿 대해에서 인식하는 구문을 엄격히 준수합니다 .
  2. 복잡한 서식을 유지할 수 있습니다. 즉 Format Mini-Language를 완벽하게 지원합니다.

그래서 위의 요구 사항을 충족하는 자체 솔루션을 작성했습니다. ( 편집 : 이제 @SvenMarnach의 버전-이 답변에서보고 된대로-내가 필요한 코너 케이스를 처리하는 것 같습니다).

기본적으로 템플릿 문자열을 구문 분석하고 일치하는 중첩 {.*?}그룹을 찾고 ( find_all()도우미 함수를 사용하여 ) 형식화 된 문자열을 점진적으로 직접 사용 str.format_map()하면서 잠재적 인 KeyError.

def find_all(
        text,
        pattern,
        overlap=False):
    """
    Find all occurrencies of the pattern in the text.

    Args:
        text (str|bytes|bytearray): The input text.
        pattern (str|bytes|bytearray): The pattern to find.
        overlap (bool): Detect overlapping patterns.

    Yields:
        position (int): The position of the next finding.
    """
    len_text = len(text)
    offset = 1 if overlap else (len(pattern) or 1)
    i = 0
    while i < len_text:
        i = text.find(pattern, i)
        if i >= 0:
            yield i
            i += offset
        else:
            break
def matching_delimiters(
        text,
        l_delim,
        r_delim,
        including=True):
    """
    Find matching delimiters in a sequence.

    The delimiters are matched according to nesting level.

    Args:
        text (str|bytes|bytearray): The input text.
        l_delim (str|bytes|bytearray): The left delimiter.
        r_delim (str|bytes|bytearray): The right delimiter.
        including (bool): Include delimeters.

    yields:
        result (tuple[int]): The matching delimiters.
    """
    l_offset = len(l_delim) if including else 0
    r_offset = len(r_delim) if including else 0
    stack = []

    l_tokens = set(find_all(text, l_delim))
    r_tokens = set(find_all(text, r_delim))
    positions = l_tokens.union(r_tokens)
    for pos in sorted(positions):
        if pos in l_tokens:
            stack.append(pos + 1)
        elif pos in r_tokens:
            if len(stack) > 0:
                prev = stack.pop()
                yield (prev - l_offset, pos + r_offset, len(stack))
            else:
                raise ValueError(
                    'Found `{}` unmatched right token(s) `{}` (position: {}).'
                        .format(len(r_tokens) - len(l_tokens), r_delim, pos))
    if len(stack) > 0:
        raise ValueError(
            'Found `{}` unmatched left token(s) `{}` (position: {}).'
                .format(
                len(l_tokens) - len(r_tokens), l_delim, stack.pop() - 1))
def safe_format_map(
        text,
        source):
    """
    Perform safe string formatting from a mapping source.

    If a value is missing from source, this is simply ignored, and no
    `KeyError` is raised.

    Args:
        text (str): Text to format.
        source (Mapping|None): The mapping to use as source.
            If None, uses caller's `vars()`.

    Returns:
        result (str): The formatted text.
    """
    stack = []
    for i, j, depth in matching_delimiters(text, '{', '}'):
        if depth == 0:
            try:
                replacing = text[i:j].format_map(source)
            except KeyError:
                pass
            else:
                stack.append((i, j, replacing))
    result = ''
    i, j = len(text), 0
    while len(stack) > 0:
        last_i = i
        i, j, replacing = stack.pop()
        result = replacing + text[j:last_i] + result
    if i > 0:
        result = text[0:i] + result
    return result

(이 코드는 FlyingCircus 에서도 사용 가능 합니다. 면책 조항 : 본인이 주요 작성자입니다.)


이 코드의 사용법은 다음과 같습니다.

print(safe_format_map('{a} {b} {c}', dict(a=-A-)))
# -A- {b} {c}

의는 (친절하게 자신의 코드를 공유 @SvenMarnach으로 내가 가장 좋아하는 솔루션이 비교하자 여기거기에 ) :

import string


class FormatPlaceholder:
    def __init__(self, key):
        self.key = key
    def __format__(self, spec):
        result = self.key
        if spec:
            result += ":" + spec
        return "{" + result + "}"
    def __getitem__(self, index):
        self.key = "{}[{}]".format(self.key, index)
        return self
    def __getattr__(self, attr):
        self.key = "{}.{}".format(self.key, attr)
        return self


class FormatDict(dict):
    def __missing__(self, key):
        return FormatPlaceholder(key)


def safe_format_alt(text, source):
    formatter = string.Formatter()
    return formatter.vformat(text, (), FormatDict(source))

다음은 몇 가지 테스트입니다.

test_texts = (
    '{b} {f}',  # simple nothing useful in source
    '{a} {b}',  # simple
    '{a} {b} {c:5d}',  # formatting
    '{a} {b} {c!s}',  # coercion
    '{a} {b} {c!s:>{a}s}',  # formatting and coercion
    '{a} {b} {c:0{a}d}',  # nesting
    '{a} {b} {d[x]}',  # dicts (existing in source)
    '{a} {b} {e.index}',  # class (existing in source)
    '{a} {b} {f[g]}',  # dict (not existing in source)
    '{a} {b} {f.values}',  # class (not existing in source)

)
source = dict(a=4, c=101, d=dict(x='FOO'), e=[])

and the code to make it running:

funcs = safe_format_map, safe_format_alt

n = 18
for text in test_texts:
    full_source = {**dict(b='---', f=dict(g='Oh yes!')), **source}
    print('{:>{n}s} :   OK   : '.format('str.format_map', n=n) + text.format_map(full_source))
    for func in funcs:
        try:
            print(f'{func.__name__:>{n}s} :   OK   : ' + func(text, source))
        except:
            print(f'{func.__name__:>{n}s} : FAILED : {text}')

resulting in:

    str.format_map :   OK   : --- {'g': 'Oh yes!'}
   safe_format_map :   OK   : {b} {f}
   safe_format_alt :   OK   : {b} {f}
    str.format_map :   OK   : 4 ---
   safe_format_map :   OK   : 4 {b}
   safe_format_alt :   OK   : 4 {b}
    str.format_map :   OK   : 4 ---   101
   safe_format_map :   OK   : 4 {b}   101
   safe_format_alt :   OK   : 4 {b}   101
    str.format_map :   OK   : 4 --- 101
   safe_format_map :   OK   : 4 {b} 101
   safe_format_alt :   OK   : 4 {b} 101
    str.format_map :   OK   : 4 ---  101
   safe_format_map :   OK   : 4 {b}  101
   safe_format_alt :   OK   : 4 {b}  101
    str.format_map :   OK   : 4 --- 0101
   safe_format_map :   OK   : 4 {b} 0101
   safe_format_alt :   OK   : 4 {b} 0101
    str.format_map :   OK   : 4 --- FOO
   safe_format_map :   OK   : 4 {b} FOO
   safe_format_alt :   OK   : 4 {b} FOO
    str.format_map :   OK   : 4 --- <built-in method index of list object at 0x7f7a485666c8>
   safe_format_map :   OK   : 4 {b} <built-in method index of list object at 0x7f7a485666c8>
   safe_format_alt :   OK   : 4 {b} <built-in method index of list object at 0x7f7a485666c8>
    str.format_map :   OK   : 4 --- Oh yes!
   safe_format_map :   OK   : 4 {b} {f[g]}
   safe_format_alt :   OK   : 4 {b} {f[g]}
    str.format_map :   OK   : 4 --- <built-in method values of dict object at 0x7f7a485da090>
   safe_format_map :   OK   : 4 {b} {f.values}
   safe_format_alt :   OK   : 4 {b} {f.values}

as you can see, the updated version now seems to handle well the corner cases where the earlier version used to fail.


Timewise, they are within approx. 50% of each other, depending on the actual text to format (and likely the actual source), but safe_format_map() seems to have an edge in most of the tests I performed (whatever they mean, of course):

for text in test_texts:
    print(f'  {text}')
    %timeit safe_format(text * 1000, source)
    %timeit safe_format_alt(text * 1000, source)
  {b} {f}
3.93 ms ± 153 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
6.35 ms ± 51.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  {a} {b}
4.37 ms ± 57.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
5.2 ms ± 159 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  {a} {b} {c:5d}
7.15 ms ± 91.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
7.76 ms ± 69.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  {a} {b} {c!s}
7.04 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
7.56 ms ± 161 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  {a} {b} {c!s:>{a}s}
8.91 ms ± 113 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
10.5 ms ± 181 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  {a} {b} {c:0{a}d}
8.84 ms ± 147 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
10.2 ms ± 202 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  {a} {b} {d[x]}
7.01 ms ± 197 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
7.35 ms ± 106 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  {a} {b} {e.index}
11 ms ± 68.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
8.78 ms ± 405 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  {a} {b} {f[g]}
6.55 ms ± 88.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
9.12 ms ± 159 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
  {a} {b} {f.values}
6.61 ms ± 55.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
9.92 ms ± 98.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

You could wrap it in a function that takes default arguments:

def print_foo_bar(foo='', bar=''):
    s = '{foo} {bar}'
    return s.format(foo=foo, bar=bar)

print_foo_bar(bar='BAR') # ' BAR'

참고URL : https://stackoverflow.com/questions/11283961/partial-string-formatting

반응형