Home [Python] Advanced Technique Python (2)
Post
Cancel

[Python] Advanced Technique Python (2)

Intro

  • import를 통해 코드를 포함시킬때 효과적으로 구조화하는게 필요
  • import를 적절하게 사용하면 생산성이 향상되어 프로젝트 유지관리 하면서 코드 재사용이 가능
  • import가 어떻게 동작하는지 코드로 이해
  • 배우는 내용
    • module, packages, namespace packages
    • handle resources and data file inside your package
    • cutomize python’s import system
  • python은 modulepackages로 이루어져 있음

Modules

  • Python 코드의 Unit 역할을 하는 object
  • module에는 arbitrary Python Object를 포함하는 namespace가 있음
  • import를 통해 추가
1
2
3
4
5
6
7
8
9
10
11
12
import math # math module
math.pi # variable

# contents of a namespace
dir() 
dir(math)

from math import pi
pi # global namespace 에 배치

import math as m # rename moudule
m.pi

Packages

  • package를 사용하여 moudle을 추가로 구성할수 있다.
  • submodule 또는 recursively sub packages를 포함할수 있는 Python 모듈
  • 기술적으로 __path__ 속성이 있는 파이썬 모듈
  • 일반적으로 python file 및 기타 directory를 포함하는 file directory에 해당
  • 파이썬 패키지를 직접 만들려면 directory와 그 안에 __init__.py를 만들어야 한다.
  • __init__.py 파일은 모듈로 취급될 때 패키지의 내용을 포함한다.
    • __init__.py가 없으면 일반 패키지가 아니라 namespace packages
  • 일반적으로 패키지를 가져올 때 하위 모듈 및 하위 패키지를 가져오지 않는다. 그러나 원하는 경우 __init__.py를 사용하여 일부 또는 모든 하위 모듈 및 하위 패키지를 포함 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
world/
│
├── africa/
│   ├── __init__.py
│   └── zimbabwe.py
│
├── europe/
│   ├── __init__.py
│   ├── greece.py
│   ├── norway.py
│   └── spain.py
│
└── __init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# world/africa/__init__.py  (Empty file)
# zimbabwe를 import하지 않음

# world/africa/zimbabwe.py
print("Shona: Mhoroyi vhanu vese")
print("Ndebele: Sabona mhlaba")

# world/europe/__init__.py
# greece, norway는 import를 했지만 spain은 import하지 않음
from . import greece
from . import norway

# world/europe/greece.py
print("Greek: Γειά σας Κόσμε")

# world/europe/norway.py
print("Norwegian: Hei verden")

# world/europe/spain.py
print("Castellano: Hola mundo")

# world/__init__.py
from . import africa # europe은 import하지 않음
  • 위 코드에서 각 country의 모듈은 import 될때 print()를 수행
1
2
3
4
5
6
7
8
9
10
import world
world
world.africa
world.europe # world/__init__.py에 europe은 import하지 않았기 때문에 AttributeError가 발생

from world import europe # 하는 순간 eurpoe/__init__.py 에 포함된 greece, norway print문이 출력됨
europe.spain # AttributeError # europe/__init_-.py에 spain은 import하지 않음

import world.europe.spain # spain을 사용하기 위해서는 import하는 순간 print()가 실행
from world.europe import normway # 출력하지 않고 global namespace에 포함
  • world/africa/__init__.py의 파일내부가 비어있기 때문에 world.africa를 import하면 다른 효과는 없이 namespace가 생성
1
2
3
world.africa.zimbabwe # Attribute Error
from world.africa import zimbabwe # zimbabwe가 global namespace에 추가되면서 `print`
world.africa.zimbabwe # 로도 접근이 가능
  • 모듈을 가져오면 내용이 로드되고 내용이 포함된 namespace가 생성된다는 점을 기억해야 한다.
  • 동일한 모듈이 다른 namespace의 일부가 될 수 있음을 보여주는 예
1
2
3
import math
math.__dict__['pi'] # module namespace는 python dict으로 구현되며 `.__dict__` attribute에서 사용 가능
# global namespace도 dictionary, `global()`로 접근 가능
  • 사용자들이 더 쉽게 사용할 수 있도록 __init__.py 파일에 하위 패키지와 하위 모듈을 가져오는게 일반적

Absolute and Relative Import

  • world/__init__.py에서 from . import africa
    • from .의 의미는? .(dot)은 현재 패키지를 가리키며 relative import의 예
    • “현재 패키지에서 subpackage africa를 import 해라” 를 의미
  • from world import africa와 같이 absolute import도 가능하다.
  • PEP 8 style guide 에서는 일반적으로 absolute import를 권장
  • relative import는 패키지 hierarchies 구조를 구성하는 대안a

Python’s Import Path

  • python에서는 modules과 packges를 어떻게 찾아서 import를 할까?
  • import path에서 모듈과 패키지를 찾는다.
    • import하기 위한 modules을 검색하는 location의 리스트를 나타냄
  • sys.path를 printing하여 Python의 import list를 inspect 할 수 있음
  • import list에는 아래와 같이 세가지 종류의 위치가 포함
    • 현재 스크립트의 directory
    • PYTHONPATH envrionment variable의 contents
    • Other, installation-dependent directories
  • 위 순서에서 첫번째로 매칭되는 module을 사용
  • shadow를 조심해야 하는데, 현재 프로젝트에서 math의 모듈을 만들면 standard lib math를 사용할 수 없음 (덮어씌워짐)
    • 모듈, pakcage이름을 잘지어야해 (unique하게)

Example: Structure Your Imports

  • PYTHONPATHsys.path를 조작해서 import를 구성하는 것이 가능하지만 오류가 발생하기 쉽다
1
2
3
4
structure/
│
├── files.py # libraray module의 역할
└── structure.py # main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# structure/files.py

def unique_path(directory, name_pattern):
    """Find a path name that does not already exist"""
    counter = 0
    while True:
        counter += 1
        path = directory / name_pattern.format(counter)
        if not path.exists():
            return path

def add_empty_file(path):
    """Create an empty file at the given path"""
    print(f"Create file: {path}")
    path.parent.mkdir(parents=True, exist_ok=True)
    path.touch()

# structure/structure.py

# Standard library imports
import pathlib
import sys

# Local imports
import files

def main():
    # Read path from command line (=root path)
    try:
        root = pathlib.Path(sys.argv[1]).resolve()
    except IndexError:
        print("Need one argument: the root of the original file tree")
        raise SystemExit()

    # Re-create the file structure
    new_root = files.unique_path(pathlib.Path.cwd(), "{:03d}")
    for path in root.rglob("*"):
        if path.is_file() and new_root not in path.parents:
            rel_path = path.relative_to(root)
            files.add_empty_file(new_root / rel_path)

if __name__ == "__main__":
    main()
1
2
3
4
5
6
7
structure/
│
├── structure/
│   ├── files.py
│   └── structure.py
│
└── cli.py
1
2
3
4
5
6
7
8
# cli.py

from structure.structure import main

if __name__ == "__main__":
    main()

# $ python cli.py structure # ModuleNotFoundError: no module named 'files'
  • 현재 스크립트를 실행하는 위치가 변경되었기 때문에 문제가 발생 (=import path가 변경)
1
2
3
4
5
6
7
8
9
# Local imports -.-? 이렇게?` no
sys.path.insert(0, str(pathlib.Path(__file__).parent))
import files

# relative import를 사용해서 해결
# Local imports
from . import files
# $ python cli.py structure
# $ python structure.py . <--불가능해짐
  • relative imports가 가져온 모듈과 스크립트에서 다르게 해석되기 때문에 발생한 문제
  • absolute import를 통해 해결이 가능하지만 복잡
  • 실제로 더 좋고 안정적적으로 하기 위해서는 import, packaing system과 함께 사용하고 pip를 사용하여 프로젝트를 로컬 패키지로 설치하는 것이다.

Create and Install a Local Package

  • https:/pypi.org/
  • setup.cfg, setup.py의 파일을 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# setup.cfg

[metadata]
name = local_structure
version = 0.1.0

[options]
packages = structure # 소스 코드가 포함된 디렉토리를 나열

# setup.py

import setuptools

setuptools.setup()
1
python -m pip install -e .
  • 위와 같은 방법으로 로컬 패키지를 설치 할수 있음
  • python의 import path 에서 structure를 찾을 수 있다.
  • 즉, 스크립트 디렉토리, relative imports, 또한 기타 복잡한 문제에 대해 걱정할 필요 없이 어디서나 사용이 가능
  • -e 옵션은 편집 가능함을 나타내고, 패키지를 다시 설치하지 않고도 패키지의 소스 코드를 변경할 수 있도록 해주기 때문에 중요
  • publishing 할때는 https://realpython.com/pypi-publish-python-package/ 를 참고
  • sturcture가 설치된 이후에는 아래와 같이 import
1
2
# Local imports
from structure import files

Tip: Script와 Library를 구분해라 (Script는 실행, Library는 imported)

Namespace Packages

  • python 3.3부터 사용할수 있음
  • 기본 hierarchy에 덜 의존적이다.
  • 네임스페이스 패키지는 여러 디렉토리에 분할될 수 있다.
  • .py의 파일은 있고 __init.py__는 포함하지 않은 디렉토리가 있다면 namespace package가 자동으로 생성
  • namespace package가 왜 필요한지? 유용한지?
1
2
3
>>> song = Song(song_id="1", title="The Same River", artist="Riverside")
>>> song.serialize()
'{"id": "1", "title": "The Same River", "artist": "Riverside"}'
1
2
3
4
5
third_party/
│
└── serializers/
    ├── json.py
    └── xml.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# third_party/serializers/json.py

import json

class JsonSerializer:
    def __init__(self):
        self._current_object = None

    def start_object(self, object_name, object_id):
        self._current_object = dict(id=object_id)

    def add_property(self, name, value):
        self._current_object[name] = value

    def __str__(self):
        return json.dumps(self._current_object)

# third_party/serializers/xml.py

import xml.etree.ElementTree as et

class XmlSerializer:
    def __init__(self):
        self._element = None

    def start_object(self, object_name, object_id):
        self._element = et.Element(object_name, attrib={"id": object_id})

    def add_property(self, name, value):
        prop = et.SubElement(self._element, name)
        prop.text = value

    def __str__(self):
        return et.tostring(self._element, encoding="unicode")

# song.py

class Song:
    def __init__(self, song_id, title, artist):
        self.song_id = song_id
        self.title = title
        self.artist = artist

    def serialize(self, serializer):
        serializer.start_object("song", self.song_id)
        serializer.add_property("title", self.title)
        serializer.add_property("artist", self.artist)

        return str(serializer)
  • third-party serializers packages가 설치되어 있다면 아래와 같이 사용
1
2
3
4
5
6
7
8
9
10
>>> from serializers.json import JsonSerializer
>>> from serializers.xml import XmlSerializer
>>> from song import Song
>>> song = Song(song_id="1", title="The Same River", artist="Riverside")

>>> song.serialize(JsonSerializer())
'{"id": "1", "title": "The Same River", "artist": "Riverside"}'

>>> song.serialize(XmlSerializer())
'<song id="1"><title>The Same River</title><artist>Riverside</artist></song>'
  • third-part lib에 Yaml의 Serializer가 없는경우 namepsace package의 magic~
  • 나만의 YamlSerializer를 third-party lib을 건들지 않고 추가가 가능하다.
  • serializers의 디렉토리를 생성
1
2
3
4
5
local/
│
└── serializers/
    └── yaml.py
$ python -m pip install PyYAML
1
2
3
4
5
6
7
8
9
10
11
12
# local/serializers/yaml.py

import yaml
from serializers.json import JsonSerializer

class YamlSerializer(JsonSerializer):
    def __str__(self):
        return yaml.dump(self._current_object)

>>> from serializers.yaml import YamlSerializer
>>> song.serialize(YamlSerializer())
"artist: Riverside\nid: '1'\ntitle: The Same River\n"
  • third-party 패키지와 local-package를 어떻게 합치는지 테스트를 해봤다.
  • 아래와 같이 path를 확인해보면 다르게 나온다.
1
2
3
4
5
6
7
8
9
import sys
sys.path.extend(["third_party", "local"])

from serializers import json, xml, yaml
json
<module 'serializers.json' from 'third_party/serializers/json.py'>

yaml
<module 'serializers.yaml' from 'local/serializers/yaml.py'>

Imports Style Guide

마무리

참고

This post is licensed under CC BY 4.0 by the author.

Udacity A/B Testing

[Git] Remote branch로 덮어 씌우는 방법