configparser — 설정 파일 파서
configparser — 설정 파일 파서
configparser 모듈은 Microsoft Windows INI 파일과 비슷한 구조를 제공하는 기본 설정 언어를 구현하는 ConfigParser 클래스를 제공해요. 이를 이용하면 최종 사용자가 쉽게 커스터마이즈할 수 있는 Python 프로그램을 작성할 수 있습니다.
출처: Python 표준 라이브러리
본문
참고: 이 라이브러리는 Windows 레지스트리 확장 버전의 INI 문법에서 쓰는 값 타입 접두사를 해석하거나 쓰지 않아요.
참고: 애플리케이션 설정 파일에 더 잘 정의된 형식을 원한다면
tomllib모듈(TOML)을 참고하세요. TOML은 INI의 개선판으로 특별히 설계된 형식이에요. Unix 셸류의 미니 언어를 만들려면shlex, 구성용 JSON은json모듈(주석 미지원)도 참고할 수 있습니다.
빠른 시작
다음과 같은 매우 기본적인 설정 파일을 생각해 봅시다.
[DEFAULT]
ServerAliveInterval = 45
Compression = yes
CompressionLevel = 9
ForwardX11 = yes
[forge.example]
User = hg
[topsecret.server.example]
Port = 50022
ForwardX11 = no
INI 파일 구조는 본질적으로 섹션(section)들로 이뤄지고, 각 섹션은 값이 있는 키들을 담아요. configparser 클래스는 이런 파일을 읽고 쓸 수 있습니다. 먼저 위 설정 파일을 프로그램으로 만들어 볼게요.
>>> import configparser
>>> config = configparser.ConfigParser()
>>> config['DEFAULT'] = {'ServerAliveInterval': '45',
... 'Compression': 'yes',
... 'CompressionLevel': '9'}
>>> config['forge.example'] = {}
>>> config['forge.example']['User'] = 'hg'
>>> config['topsecret.server.example'] = {}
>>> topsecret = config['topsecret.server.example']
>>> topsecret['Port'] = '50022' # mutates the parser
>>> topsecret['ForwardX11'] = 'no' # same here
>>> config['DEFAULT']['ForwardX11'] = 'yes'
>>> with open('example.ini', 'w') as configfile:
... config.write(configfile)
...
보시다시피 설정 파서를 딕셔너리처럼 다룰 수 있어요. 몇 가지 차이가 있지만 동작은 딕셔너리에서 기대하는 것과 아주 가깝습니다.
이제 저장된 설정 파일을 다시 읽어 데이터를 살펴볼게요.
>>> config = configparser.ConfigParser()
>>> config.sections()
[]
>>> config.read('example.ini')
['example.ini']
>>> config.sections()
['forge.example', 'topsecret.server.example']
>>> 'forge.example' in config
True
>>> 'python.org' in config
False
>>> config['forge.example']['User']
'hg'
>>> config['DEFAULT']['Compression']
'yes'
>>> topsecret = config['topsecret.server.example']
>>> topsecret['ForwardX11']
'no'
>>> topsecret['Port']
'50022'
>>> for key in config['forge.example']:
... print(key)
user
compressionlevel
serveraliveinterval
compression
forwardx11
>>> config['forge.example']['ForwardX11']
'yes'
위에서 보듯 API는 꽤 직관적이에요. 유일한 마법은 다른 모든 섹션에 기본값을 제공하는 DEFAULT 섹션뿐입니다. 섹션의 키는 대소문자를 구분하지 않고 소문자로 저장된다는 점도 주의하세요.
하나의 ConfigParser에 여러 설정을 읽을 수도 있는데, 가장 최근에 추가된 설정이 가장 높은 우선순위를 가져요. 충돌하는 키는 더 최근 설정에서 가져오고, 기존 키는 그대로 유지됩니다. 아래 예는 example.ini의 충돌 키를 덮어쓸 override.ini를 읽습니다.
[DEFAULT]
ServerAliveInterval = -1
>>> config_override = configparser.ConfigParser()
>>> config_override['DEFAULT'] = {'ServerAliveInterval': '-1'}
>>> with open('override.ini', 'w') as configfile:
... config_override.write(configfile)
...
>>> config_override = configparser.ConfigParser()
>>> config_override.read(['example.ini', 'override.ini'])
['example.ini', 'override.ini']
>>> print(config_override.get('DEFAULT', 'ServerAliveInterval'))
-1
이 동작은 filenames 매개변수에 여러 파일을 넘기는 ConfigParser.read() 호출과 같습니다.
지원하는 데이터 타입
설정 파서는 설정 파일 값의 데이터 타입을 추측하지 않고, 항상 내부적으로 문자열로 저장해요. 즉 다른 타입이 필요하면 직접 변환해야 합니다.
>>> int(topsecret['Port'])
50022
>>> float(topsecret['CompressionLevel'])
9.0
이 작업이 워낙 흔해서 설정 파서는 정수·부동소수점·불리언을 다루는 다양한 편리한 getter 메서드를 제공해요. 마지막 불리언이 가장 흥미로운데, 값을 bool()에 그냥 넘기면 bool('False')도 여전히 True라서 소용이 없기 때문이에요. 그래서 설정 파서는 getboolean()도 제공합니다. 이 메서드는 대소문자를 구분하지 않고 'yes'/'no', 'on'/'off', 'true'/'false', '1'/'0'에서 불리언 값을 인식해요.
>>> topsecret.getboolean('ForwardX11')
False
>>> config['forge.example'].getboolean('ForwardX11')
True
>>> config.getboolean('forge.example', 'Compression')
True
getboolean() 외에도 설정 파서는 동등한 getint(), getfloat() 메서드를 제공하고, 자신만의 컨버터를 등록하거나 제공된 것을 커스터마이즈할 수 있어요.
폴백 값
딕셔너리처럼 섹션의 get() 메서드로 폴백 값을 제공할 수 있어요.
>>> topsecret.get('Port')
'50022'
>>> topsecret.get('CompressionLevel')
'9'
>>> topsecret.get('Cipher')
>>> topsecret.get('Cipher', '3des-cbc')
'3des-cbc'
기본값(default)이 폴백 값보다 우선한다는 점에 주의하세요. 예를 들어 우리 예제에서 'CompressionLevel' 키는 'DEFAULT' 섹션에만 있었어요. 'topsecret.server.example' 섹션에서 가져오려 하면 폴백을 지정해도 항상 기본값을 얻게 됩니다.
>>> topsecret.get('CompressionLevel', '3')
'9'
한 가지 더 주의할 점은, 파서 수준의 get() 메서드는 하위 호환을 위해 유지되는 더 복잡한 커스텀 인터페이스를 제공한다는 거예요. 이 메서드를 쓸 때는 fallback 키워드 전용 인자로 폴백 값을 제공할 수 있습니다.
>>> config.get('forge.example', 'monster',
... fallback='No such things as monsters')
'No such things as monsters'
같은 fallback 인자는 getint(), getfloat(), getboolean() 메서드에서도 쓸 수 있어요.
>>> 'BatchMode' in topsecret
False
>>> topsecret.getboolean('BatchMode', fallback=True)
True
>>> config['DEFAULT']['BatchMode'] = 'no'
>>> topsecret.getboolean('BatchMode', fallback=True)
False
지원하는 INI 파일 구조
설정 파일은 섹션들로 이뤄지고, 각 섹션은 [section] 헤더로 시작하며 그 뒤에 특정 문자열(= 또는 : 기본값)로 구분된 키/값 항목이 이어져요. 기본적으로 섹션 이름은 대소문자를 구분하지만 키는 그렇지 않아요. 키와 값의 앞·뒤 공백은 제거됩니다. 파서가 허용하도록 설정되면 값은 생략될 수 있고, 이 경우 키/값 구분자도 빠질 수 있어요. 값은 값의 첫 줄보다 깊게 들여쓰기돼 있으면 여러 줄에 걸칠 수도 있고, 파서 모드에 따라 빈 줄은 여러 줄 값의 일부로 취급되거나 무시됩니다.
기본적으로 유효한 섹션 이름은 '\n'을 포함하지 않는 모든 문자열이에요. 이를 바꾸려면 ConfigParser.SECTCRE를 보세요.
allow_unnamed_section=True로 무명 최상위 섹션을 허용하도록 설정하면 첫 번째 섹션 이름은 생략될 수 있어요. 이 경우 config[UNNAMED_SECTION]처럼 UNNAMED_SECTION으로 키/값을 가져올 수 있습니다.
설정 파일은 특정 문자(#과 ; 기본값)로 시작하는 주석을 포함할 수 있어요. 주석은 빈 줄에 홀로(들여쓰기 가능) 나타날 수 있습니다.
예:
[Simple Values]
key=value
spaces in keys=allowed
spaces in values=allowed as well
spaces around the delimiter = obviously
you can also use : to delimit keys from values
[All Values Are Strings]
values like this: 1000000
or this: 3.14159265359
are they treated as numbers? : no
integers, floats and booleans are held as: strings
can use the API to get converted values directly: true
[Multiline Values]
chorus: I'm a lumberjack, and I'm okay
I sleep all night and I work all day
[No Values]
key_without_value
empty string value here =
[You can use comments]
# like this
; or this
# By default only in an empty line.
# Inline comments can be harmful because they prevent users
# from using the delimiting characters as parts of values.
# That being said, this can be customized.
[Sections Can Be Indented]
can_values_be_as_well = True
does_that_mean_anything_special = False
purpose = formatting for readability
multiline_values = are
handled just fine as
long as they are indented
deeper than the first line
of a value
# Did I mention we can indent comments, too?
무명 섹션
첫 번째(또는 유일한) 섹션의 이름은 생략될 수 있고, 값은 UNNAMED_SECTION 속성으로 가져옵니다.
>>> config = """
... option = value
...
... [ Section 2 ]
... another = val
... """
>>> unnamed = configparser.ConfigParser(allow_unnamed_section=True)
>>> unnamed.read_string(config)
>>> unnamed.get(configparser.UNNAMED_SECTION, 'option')
'value'
값 보간(Interpolation)
핵심 기능 위에 ConfigParser는 보간을 지원해요. 즉 값은 get() 호출에서 반환되기 전에 전처리될 수 있습니다.
class configparser.BasicInterpolation — ConfigParser가 사용하는 기본 구현이에요. 값이 같은 섹션의 다른 값이나 특별한 기본 섹션의 값을 참조하는 형식 문자열을 포함할 수 있게 해 줍니다. 초기화 시 추가 기본값을 제공할 수도 있어요.
[Paths]
home_dir: /Users
my_dir: %(home_dir)s/lumberjack
my_pictures: %(my_dir)s/Pictures
[Escape]
# use a %% to escape the % sign (% is the only character that needs to be escaped):
gain: 80%%
위 예에서 보간을 BasicInterpolation()으로 설정한 ConfigParser는 %(home_dir)s를 home_dir의 값(여기서는 /Users)으로 해석하고, %(my_dir)s는 사실상 /Users/lumberjack으로 해석해요. 모든 보간은 요청 시 이루어지므로 참조 체인에 쓰인 키가 설정 파일에 특정 순서로 지정될 필요가 없어요.
보간을 None으로 설정하면 파서는 그냥 my_pictures의 값으로 %(my_dir)s/Pictures, my_dir의 값으로 %(home_dir)s/lumberjack을 반환합니다.
class configparser.ExtendedInterpolation — 예를 들어 zc.buildout에서 쓰는 더 고급 문법을 구현하는 대체 핸들러예요. 확장 보간은 ${section:option}을 사용해 다른 섹션의 값을 나타냅니다. 보간은 여러 수준을 걸칠 수 있어요. 편의상 section: 부분을 생략하면 보간은 현재 섹션(그리고 가능하면 특별 섹션의 기본값)으로 기본 설정됩니다.
기본 보간으로 위에 지정한 설정은 확장 보간에서는 이렇게 보입니다.
[Paths]
home_dir: /Users
my_dir: ${home_dir}/lumberjack
my_pictures: ${my_dir}/Pictures
[Escape]
# use a $$ to escape the $ sign ($ is the only character that needs to be escaped):
cost: $$80
다른 섹션의 값도 가져올 수 있습니다.
[Common]
home_dir: /Users
library_dir: /Library
system_dir: /System
macports_dir: /opt/local
[Frameworks]
Python: 3.2
path: ${Common:system_dir}/Library/Frameworks/
[Arthur]
nickname: Two Sheds
last_name: Jackson
my_dir: ${Common:home_dir}/twosheds
my_pictures: ${my_dir}/Pictures
python_dir: ${Frameworks:path}/Python/Versions/${Frameworks:Python}
매핑 프로토콜 접근
매핑 프로토콜 접근은 커스텀 객체를 딕셔너리처럼 사용할 수 있게 하는 기능의 일반 이름이에요. configparser의 경우 매핑 인터페이스 구현은 parser['section']['option'] 표기를 사용합니다.
parser['section']은 특히 파서에서 섹션 데이터의 프록시를 반환해요. 즉 값이 복사되는 게 아니라 원래 파서에서 요청 시 가져옵니다. 더 중요한 건 섹션 프록시에서 값을 바꾸면 실제로 원래 파서에서 변경된다는 거예요.
configparser 객체는 실제 딕셔너리에 최대한 가깝게 동작해요. 매핑 인터페이스는 완전하며 MutableMapping ABC를 따릅니다. 몇 가지 차이는 다음과 같습니다.
- 기본적으로 섹션의 모든 키는 대소문자를 구분하지 않고 접근돼요. 예를 들어
for option in parser["section"]은optionxform된 옵션 키 이름만 내놓는데, 이는 기본적으로 소문자 키를 뜻해요. 동시에 키'a'를 가진 섹션에서"a" in parser["section"]과"A" in parser["section"]둘 다True를 반환해요. - 모든 섹션은
DEFAULTSECT값도 포함하므로, 섹션의.clear()가 섹션을 눈에 보이게 비게 만들지 않을 수 있어요. 기본값은 섹션에서 삭제할 수 없기 때문이에요(기술적으로 거기에 없으니까요). 섹션에서 덮어쓰면, 삭제하면 기본값이 다시 보입니다. 기본값을 삭제하려 하면KeyError가 발생해요. DEFAULTSECT는 파서에서 제거할 수 없어요. 삭제하려 하면ValueError,parser.clear()는 그대로 두고,parser.popitem()은 절대 반환하지 않아요.parser.get(section, option, **kwargs)— 두 번째 인자는 폴백 값이 아니에요. 단 섹션 수준의get()메서드는 매핑 프로토콜과 고전configparserAPI 모두와 호환됩니다.parser.items()는 매핑 프로토콜과 호환돼요(DEFAULTSECT를 포함한section_name, section_proxy쌍의 리스트 반환). 그러나 이 메서드는 인자와 함께 호출될 수도 있어요:parser.items(section, raw, vars). 후자 호출은 지정된section에 대한option, value쌍의 리스트를 모든 보간 적용 후에 반환합니다.
파서 동작 커스터마이즈
ConfigParser에 커스텀 동작을 추가하려면 서브클래싱을 사용할 수 있어요. 예를 들어 optionxform()을 오버라이드하면 옵션 이름이 소문자로 변환되는 기본 동작을 바꿀 수 있고, 컨버터를 converters에 추가하면 get*() 메서드를 확장할 수 있어요. 또 interpolation 매개변수를 통해 보간 방식을 바꾸거나 끌 수 있습니다.
레거시 API 예제
RawConfigParser를 써서 파일을 쓰는 예:
import configparser
config = configparser.RawConfigParser()
# Please note that using RawConfigParser's set functions, you can assign
# non-string values to keys internally, but will receive an error when
# attempting to write to a file or when you get it in non-raw mode. Setting
# values using the mapping protocol or ConfigParser's set() does not allow
# such assignments to take place.
config.add_section('Section1')
config.set('Section1', 'an_int', '15')
config.set('Section1', 'a_bool', 'true')
config.set('Section1', 'a_float', '3.1415')
config.set('Section1', 'baz', 'fun')
config.set('Section1', 'bar', 'Python')
config.set('Section1', 'foo', '%(bar)s is %(baz)s!')
# Writing our configuration file to 'example.cfg'
with open('example.cfg', 'w') as configfile:
config.write(configfile)
설정 파일을 다시 읽는 예:
import configparser
config = configparser.RawConfigParser()
config.read('example.cfg')
# getfloat() raises an exception if the value is not a float
# getint() and getboolean() also do this for their respective types
a_float = config.getfloat('Section1', 'a_float')
an_int = config.getint('Section1', 'an_int')
print(a_float + an_int)
# Notice that the next output does not interpolate '%(bar)s' or '%(baz)s'.
# This is because we are using a RawConfigParser().
if config.getboolean('Section1', 'a_bool'):
print(config.get('Section1', 'foo'))
보간을 얻으려면 ConfigParser를 쓰세요.
import configparser
cfg = configparser.ConfigParser()
cfg.read('example.cfg')
# Set the optional *raw* argument of get() to True if you wish to disable
# interpolation in a single get operation.
print(cfg.get('Section1', 'foo', raw=False)) # -> "Python is fun!"
print(cfg.get('Section1', 'foo', raw=True)) # -> "%(bar)s is %(baz)s!"
# The optional *vars* argument is a dict with members that will take
# precedence in interpolation.
print(cfg.get('Section1', 'foo', vars={'bar': 'Documentation',
'baz': 'evil'}))
# The optional *fallback* argument can be used to provide a fallback value
print(cfg.get('Section1', 'foo'))
# -> "Python is fun!"
print(cfg.get('Section1', 'foo', fallback='Monty is not.'))
# -> "Python is fun!"
print(cfg.get('Section1', 'monster', fallback='No such things as monsters.'))
# -> "No such things as monsters."
# A bare print(cfg.get('Section1', 'monster')) would raise NoOptionError
# but we can also use:
print(cfg.get('Section1', 'monster', fallback=None))
# -> None
기본값은 두 종류의 ConfigParser 모두에서 사용할 수 있어요. 다른 데서 정의되지 않은 옵션을 보간할 때 사용됩니다.
import configparser
# New instance with 'bar' and 'baz' defaulting to 'Life' and 'hard' each
config = configparser.ConfigParser({'bar': 'Life', 'baz': 'hard'})
config.read('example.cfg')
print(config.get('Section1', 'foo')) # -> "Python is fun!"
config.remove_option('Section1', 'bar')
config.remove_option('Section1', 'baz')
print(config.get('Section1', 'foo')) # -> "Life is hard!"
ConfigParser 객체
class configparser.ConfigParser(defaults=None, dict_type=dict, allow_no_value=False, *, delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BasicInterpolation(), converters={}, allow_unnamed_section=False) — 주요 설정 파서예요.
defaults가 주어지면 내재적 기본값의 딕셔너리로 초기화돼요.dict_type은 섹션 목록, 섹션 내 옵션, 기본값에 쓰일 딕셔너리 객체를 만드는 데 사용됩니다.delimiters는 키와 값을 나누는 부분 문자열의 집합이에요.comment_prefixes는 비어 있는 줄에서 주석을 시작하는 부분 문자열의 집합이에요(주석은 들여쓰기 가능).inline_comment_prefixes는 비어 있지 않은 줄에서 주석을 시작하는 부분 문자열 집합이에요.strict가True(기본)면 단일 소스(파일·문자열·딕셔너리)에서 읽을 때 섹션이나 옵션 중복을 허용하지 않고DuplicateSectionError나DuplicateOptionError를 일으켜요.empty_lines_in_values가False(기본True)면 각 빈 줄이 옵션의 끝을 표시해요. 그 외에는 여러 줄 옵션의 내부 빈 줄이 값의 일부로 유지됩니다.allow_no_value가True(기본False)면 값 없는 옵션을 받아들여요. 이 옵션들이 보유한 값은None이고 끝 구분자 없이 직렬화됩니다.default_section은 다른 섹션의 기본값과 보간 목적의 특별 섹션 이름(보통"DEFAULT")을 지정해요. 이 값은default_section인스턴스 속성으로 런타임에 가져오고 바꿀 수 있어요. 이미 파싱된 설정 파일을 다시 평가하지는 않지만, 파싱된 설정을 새 파일에 쓸 때 사용됩니다.interpolation을 통해 보간 동작을 커스터마이즈할 수 있어요.None으로 완전히 끄거나,ExtendedInterpolation()으로 zc.buildout에서 영감을 받은 더 고급 변형을 쓸 수 있어요.- 보간에 쓰이는 모든 옵션 이름은 다른 옵션 이름 참조처럼
optionxform()메서드를 통과해요. 기본optionxform()(옵션 이름을 소문자로 변환)을 쓰면foo %(bar)s와foo %(BAR)s는 동등합니다. converters는 각 키가 타입 컨버터 이름, 각 값이 문자열에서 원하는 데이터 타입으로의 변환을 구현하는 호출 가능한 객체인 딕셔너리예요. 각 컨버터는 파서 객체와 섹션 프록시에 해당하는get*()메서드를 갖게 됩니다.allow_unnamed_section이True(기본False)면 첫 섹션 이름을 생략할 수 있어요.
참고: 파서에 여러 설정을 읽으면 가장 최근에 추가된 설정이 최우선입니다(앞선 Quick Start 참고). 원래 설정 파일의 주석은 다시 쓸 때 보존되지 않아요.
configparser.UNNAMED_SECTION — 무명 섹션을 참조하는 데 쓰는 섹션 이름을 나타내는 특수 객체.
configparser.MAX_INTERPOLATION_DEPTH — raw 매개변수가 거짓일 때 get()의 재귀 보간 최대 깊이. 기본 보간을 쓸 때만 관련 있어요.
RawConfigParser 객체
class configparser.RawConfigParser(defaults=None, dict_type=dict, allow_no_value=False, *, delimiters=('=', ':'), comment_prefixes=('#', ';'), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BasicInterpolation(), converters={}, allow_unnamed_section=False) — ConfigParser의 레거시 변형이에요. 기본적으로 보간이 꺼져 있고, 안전하지 않은 add_section/set 메서드와 레거시 defaults= 키워드 인자 처리를 통해 문자열이 아닌 섹션 이름·옵션 이름·값을 허용합니다.
참고: 내부적으로 저장할 값의 타입을 검사하는
ConfigParser를 사용하는 걸 고려하세요. 보간이 필요 없다면ConfigParser(interpolation=None)을 쓸 수 있어요.
예외
configparser.Error— 다른 모든 configparser 예외의 기반 클래스.configparser.NoSectionError— 지정한 섹션을 찾을 수 없을 때 발생.configparser.DuplicateSectionError— 이미 있는 섹션 이름으로add_section()을 호출하거나, strict 파서에서 단일 입력 파일·문자열·딕셔너리에 섹션이 두 번 이상 나타날 때 발생.configparser.DuplicateOptionError— strict 파서가 단일 파일·문자열·딕셔너리에서 읽는 동안 단일 옵션을 두 번 발견할 때 발생. 오타와 대소문자 관련 오류(예: 같은 대소문자 무감각 설정 키를 나타내는 두 키)를 잡아줍니다.configparser.NoOptionError— 지정한 옵션이 지정한 섹션에 없을 때 발생.configparser.InterpolationError— 문자열 보간 중 문제가 발생할 때 일어나는 예외들의 기반 클래스.configparser.InterpolationDepthError— 반복 횟수가MAX_INTERPOLATION_DEPTH를 초과해 문자열 보간을 완료할 수 없을 때 발생.InterpolationError의 서브클래스.configparser.InterpolationMissingOptionError— 값에서 참조하는 옵션이 없을 때 발생.InterpolationError의 서브클래스.configparser.InterpolationSyntaxError— 치환이 들어갈 소스 텍스트가 요구 문법을 따르지 않을 때 발생.InterpolationError의 서브클래스.configparser.MissingSectionHeaderError— 섹션 헤더가 없는 파일을 파싱하려 할 때 발생.configparser.ParsingError— 파일을 파싱하는 동안 오류가 발생할 때.configparser.MultilineContinuationError— 값이 없는 키가 들여쓰기된 줄로 계속될 때 발생. (3.13 추가)configparser.UnnamedSectionDisabledError— 활성화하지 않고UNNAMED_SECTION을 사용하려 할 때 발생. (3.14 추가)configparser.InvalidWriteError— 시도한ConfigParser.write()가 이후ConfigParser.read()로 정확히 파싱되지 않을 때 발생. 예:ConfigParser.SECTCRE패턴으로 시작하는 키를 쓰면 읽을 때 섹션 헤더로 파싱되므로, 이런 쓰기는 이 예외를 일으킵니다. (3.14 추가)
더 알아보기
- 원문: python: configparser — Configuration file parser
- tomllib — TOML 파서 — 잘 정의된 설정 형식