├── MANIFEST.in ├── MANIFEST ├── .bumpversion.cfg ├── tox.ini ├── .travis.yml ├── setup.py ├── LICENSE ├── .gitignore ├── plain_obj └── __init__.py ├── test.py └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | README.rst 3 | setup.py 4 | plain_obj/__init__.py 5 | -------------------------------------------------------------------------------- /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py35, py36 3 | skipsdist = True 4 | 5 | [testenv] 6 | deps = 7 | pytest==3.1.1 8 | pytest-mock==1.2 9 | commands = py.test -vs test.py 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | 4 | language: python 5 | 6 | python: 7 | - "2.7" 8 | - "3.5" 9 | - "3.6" 10 | 11 | install: 12 | - pip install --upgrade tox-travis 13 | 14 | script: 15 | - tox 16 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | with open('README.rst') as f: 4 | long_description = f.read() 5 | 6 | 7 | setup( 8 | name='plain_obj', 9 | version='0.1.2', 10 | description='A faster alternative to namedtuple.', 11 | long_description=long_description, 12 | url='https://github.com/suzaku/plain_obj', 13 | license='MIT', 14 | author='Satoru Logic', 15 | author_email='satorulogic@gmail.com', 16 | packages=['plain_obj'], 17 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 18 | classifiers=[ 19 | 'Development Status :: 3 - Alpha', 20 | 'Intended Audience :: Developers', 21 | 'License :: OSI Approved :: MIT License', 22 | 'Programming Language :: Python :: 2', 23 | 'Programming Language :: Python :: 2.7', 24 | 'Programming Language :: Python :: 3', 25 | 'Programming Language :: Python :: 3.5', 26 | 'Programming Language :: Python :: 3.6', 27 | ], 28 | keywords='tuple', 29 | ) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 satoru 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /plain_obj/__init__.py: -------------------------------------------------------------------------------- 1 | from keyword import iskeyword 2 | from collections import OrderedDict 3 | 4 | 5 | def new_type(type_name, field_names): 6 | if isinstance(field_names, str): 7 | # names separated by whitespace and/or commas 8 | field_names = field_names.replace(',', ' ').split() 9 | check_name(type_name) 10 | seen_fields = set() 11 | for name in field_names: 12 | check_name(name) 13 | if name in seen_fields: 14 | raise ValueError('Encountered duplicate field name: %r' % name) 15 | seen_fields.add(name) 16 | return type( 17 | type_name, 18 | (PlainBase,), 19 | { 20 | '__slots__': field_names, 21 | '__init__': make_constructor(field_names) 22 | } 23 | ) 24 | 25 | 26 | class PlainBase(object): 27 | 28 | def __eq__(self, other): 29 | if not isinstance(other, self.__class__): 30 | return False 31 | return all(i == j for i, j in zip(self, other)) 32 | 33 | def __iter__(self): 34 | for name in self.__slots__: 35 | yield getattr(self, name) 36 | 37 | def __repr__(self): 38 | values = tuple(self) 39 | return self.__class__.__name__ + repr(values) 40 | 41 | def to_dict(self): 42 | return OrderedDict(zip(self.__slots__, self)) 43 | 44 | 45 | def make_constructor(fields): 46 | assignments = '\n'.join([' self.{0} = {0}'.format(f) for f in fields]) 47 | parameter_lists = ', '.join(fields) 48 | source = 'def __init__(self, %s):\n%s' % (parameter_lists, assignments) 49 | namespace = {} 50 | exec(source, namespace) 51 | return namespace['__init__'] 52 | 53 | 54 | def check_name(name): 55 | if not all(c.isalnum() or c == '_' for c in name): 56 | raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: %r' % name) 57 | if iskeyword(name): 58 | raise ValueError('Type names and field names cannot be a keyword: %r' % name) 59 | if name[0].isdigit(): 60 | raise ValueError('Type names and field names cannot start with a number: %r' % name) 61 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pickle 3 | 4 | import plain_obj 5 | 6 | 7 | class TestCreateNewType: 8 | 9 | def test_can_create_with_a_list_of_fields(self): 10 | Point = plain_obj.new_type('Point', ['x', 'y', 'z']) 11 | assert isinstance(Point, type) 12 | assert len(Point.__slots__) == 3 13 | 14 | def test_can_create_with_comma_separated_string(self): 15 | Point = plain_obj.new_type('Point', 'x, y, z') 16 | assert isinstance(Point, type) 17 | assert len(Point.__slots__) == 3 18 | 19 | def test_can_create_with_space_separated_string(self): 20 | Point = plain_obj.new_type('Point', 'x y z') 21 | assert isinstance(Point, type) 22 | assert len(Point.__slots__) == 3 23 | 24 | 25 | class TestValidation: 26 | 27 | def test_should_report_when_name_is_invalid(self): 28 | invalid_names = ['123', 'f(&*'] 29 | for n in invalid_names: 30 | with pytest.raises(ValueError): 31 | plain_obj.new_type('Point', ['123']) 32 | 33 | def test_should_report_when_name_is_keyword(self): 34 | with pytest.raises(ValueError): 35 | plain_obj.new_type('Point', ['is']) 36 | 37 | 38 | def test_create_new_objects(): 39 | Config = plain_obj.new_type('Config', 'skips_dist,run_tests') 40 | config = Config(True, False) 41 | assert config.skips_dist 42 | assert not config.run_tests 43 | 44 | 45 | def test_repr(): 46 | Point = plain_obj.new_type('Point', 'x y z') 47 | point = Point(3, 1, 4) 48 | assert repr(point) == 'Point(3, 1, 4)' 49 | 50 | 51 | def test_to_dict(): 52 | Config = plain_obj.new_type('Config', 'skips_dist,run_tests') 53 | config = Config(True, False) 54 | assert config.to_dict() == { 55 | 'skips_dist': True, 56 | 'run_tests': False, 57 | } 58 | 59 | 60 | def test_unpacking(): 61 | Config = plain_obj.new_type('Config', 'skips_dist,run_tests') 62 | skips_dist, run_tests = Config(True, False) 63 | assert skips_dist 64 | assert not run_tests 65 | 66 | 67 | def test_equality(): 68 | Config = plain_obj.new_type('Config', 'skips_dist,run_tests') 69 | cfg1 = Config(True, False) 70 | cfg2 = Config(False, False) 71 | cfg3 = Config(True, False) 72 | 73 | assert cfg1 == cfg3 74 | assert cfg2 != cfg3 75 | assert cfg2 != cfg1 76 | 77 | 78 | def test_subclassing(): 79 | class Vector(plain_obj.new_type('Vector', 'x, y, z')): 80 | 81 | def get_squared_size(self): 82 | return self.x ** 2 + self.y ** 2 + self.z ** 2 83 | 84 | vec = Vector(1, 2, 3) 85 | assert vec.get_squared_size() == 14 86 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | plain_obj 2 | ########## 3 | 4 | .. image:: https://travis-ci.org/suzaku/plain_obj.svg?branch=master 5 | :target: https://travis-ci.org/suzaku/plain_obj 6 | .. image:: https://img.shields.io/pypi/v/plain_obj.svg 7 | :target: https://pypi.python.org/pypi/plain_obj 8 | 9 | A faster alternative to namedtuple. 10 | 11 | Basic Usage 12 | *********** 13 | 14 | Creation 15 | ======== 16 | 17 | .. code-block:: python 18 | 19 | import plain_obj 20 | Config = plain_obj.new_type('Config', 'is_debug, skips_dist, run_tests') 21 | config = Config(True, False, True) 22 | if config.is_debug: 23 | print("This is a verbose debugging message.") 24 | 25 | Make a dict 26 | =========== 27 | 28 | .. code-block:: python 29 | 30 | config.as_dict() 31 | 32 | Unpacking 33 | ========= 34 | 35 | .. code-block:: python 36 | 37 | is_debug, _, run_tests = config 38 | 39 | 40 | When to use ``plain_obj`` instead of ``namedtuple``? 41 | ************************************************************ 42 | 43 | **When faster creation time matters to you.** 44 | 45 | Comparing ``plain_obj`` with ``namedtuple`` in *Python 2.7*: 46 | 47 | .. code-block:: python 48 | 49 | In [3]: %timeit collections.namedtuple('Point', ['x', 'y', 'z']) 50 | 1000 loops, best of 3: 338 µs per loop 51 | 52 | In [4]: %timeit plain_obj.new_type('Point', ['x', 'y', 'z']) 53 | 10000 loops, best of 3: 97.8 µs per loop 54 | 55 | In [5]: Point = collections.namedtuple('Point', ['x', 'y', 'z']) 56 | 57 | In [6]: NewPoint = plain_obj.new_type('Point', ['x', 'y', 'z']) 58 | 59 | In [7]: %timeit Point(1, 2, 3) 60 | The slowest run took 7.99 times longer than the fastest. This could mean that an intermediate result is being cached. 61 | 1000000 loops, best of 3: 507 ns per loop 62 | 63 | In [8]: %timeit NewPoint(1, 2, 3) 64 | The slowest run took 6.70 times longer than the fastest. This could mean that an intermediate result is being cached. 65 | 1000000 loops, best of 3: 462 ns per loop 66 | 67 | In [9]: p = Point(1, 2, 3) 68 | 69 | In [10]: new_p = NewPoint(1, 2, 3) 70 | 71 | In [11]: %timeit p.x, p.y, p.z 72 | The slowest run took 9.92 times longer than the fastest. This could mean that an intermediate result is being cached. 73 | 1000000 loops, best of 3: 408 ns per loop 74 | 75 | In [12]: %timeit new_p.x, new_p.y, new_p.z 76 | The slowest run took 11.70 times longer than the fastest. This could mean that an intermediate result is being cached. 77 | 10000000 loops, best of 3: 163 ns per loop 78 | 79 | Comparing ``plain_obj`` with ``namedtuple`` in *Python 3.6*: 80 | 81 | .. code-block:: python 82 | 83 | In [3]: %timeit collections.namedtuple('Point', ['x', 'y', 'z']) 84 | 382 µs ± 3.82 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) 85 | 86 | In [4]: %timeit plain_obj.new_type('Point', ['x', 'y', 'z']) 87 | 53.5 µs ± 1.2 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) 88 | 89 | In [5]: Point = collections.namedtuple('Point', ['x', 'y', 'z']) 90 | 91 | In [6]: NewPoint = plain_obj.new_type('Point', ['x', 'y', 'z']) 92 | 93 | In [7]: %timeit Point(1, 2, 3) 94 | 521 ns ± 2.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) 95 | 96 | In [8]: %timeit NewPoint(1, 2, 3) 97 | 438 ns ± 5.53 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) 98 | 99 | In [9]: p = Point(1, 2, 3) 100 | 101 | In [10]: new_p = NewPoint(1, 2, 3) 102 | 103 | In [11]: %timeit p.x, p.y, p.z 104 | 282 ns ± 2.52 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) 105 | 106 | In [12]: %timeit new_p.x, new_p.y, new_p.z 107 | 148 ns ± 1.7 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each) 108 | 109 | As you can see, it's faster in all cases including *type creation*, *object instantiation* and *attribute access*. 110 | 111 | 112 | .. image:: https://app.codesponsor.io/embed/MY7qFCdB7bDgiBqdjtV9ASYi/suzaku/plain_obj.svg 113 | :width: 888px 114 | :height: 68px 115 | :alt: Sponsor 116 | :target: https://app.codesponsor.io/link/MY7qFCdB7bDgiBqdjtV9ASYi/suzaku/plain_obj 117 | --------------------------------------------------------------------------------