UFO ET IT

Python 가져 오기 코딩 스타일

ufoet 2020. 11. 25. 21:43
반응형

Python 가져 오기 코딩 스타일


새로운 패턴을 발견했습니다. 이 패턴은 잘 알려져 있습니까? 아니면 그것에 대한 의견은 무엇입니까?

기본적으로 소스 파일을 위아래로 스크러빙하여 사용 가능한 모듈 가져 오기 등을 파악하는 데 어려움을 겪습니다.

import foo
from bar.baz import quux

def myFunction():
    foo.this.that(quux)

모든 가져 오기를 실제로 사용되는 함수로 이동합니다. 다음과 같이 :

def myFunction():
    import foo
    from bar.baz import quux

    foo.this.that(quux)

이것은 몇 가지 일을합니다. 첫째, 실수로 다른 모듈의 내용으로 모듈을 오염시키는 일이 거의 없습니다. __all__모듈에 대한 변수를 설정할 수는 있지만 모듈이 발전함에 따라이를 업데이트해야합니다. 그러면 실제로 모듈에있는 코드의 네임 스페이스 오염에 도움이되지 않습니다.

둘째, 모듈 상단에 가져 오기가 많지 않은 경우가 거의 없습니다.이 중 절반 이상은 리팩토링했기 때문에 더 이상 필요하지 않습니다. 마지막으로, 참조 된 모든 이름이 함수 본문에 바로 있기 때문에이 패턴을 훨씬 쉽게 읽을 수 있습니다.


이 질문에 대한 (이전) 최고 투표 답변 은 형식이 훌륭하지만 성능에 대해서는 완전히 잘못되었습니다. 내가 보여 줄게

공연

최고 수입

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())


for i in xrange(1000):
    f()

$ time python import.py

real        0m0.721s
user        0m0.412s
sys         0m0.020s

함수 본문에서 가져 오기

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(1000):
    f()

$ time python import2.py

real        0m0.661s
user        0m0.404s
sys         0m0.008s

보시다시피 함수에서 모듈을 가져 오는 것이 효율적일 수 있습니다 . 그 이유는 간단합니다. 참조를 전역 참조에서 로컬 참조로 이동합니다. 이는 적어도 CPython의 경우 컴파일러가 LOAD_FAST명령어 대신 명령어 를 내보냄을 의미합니다 LOAD_GLOBAL. 이름에서 알 수 있듯이 이것들은 더 빠릅니다. 다른 응답자 는 루프의 모든 반복을 가져옴sys.modules 으로써 인위적으로 성능 저하를 부풀 렸습니다 .

일반적으로 맨 위에 가져 오는 것이 가장 좋지만 모듈에 여러 번 액세스하는 경우 성능이 이유 아닙니다 . 그 이유는 모듈이 무엇에 의존하는지 더 쉽게 추적 할 수 있고 그렇게하는 것이 나머지 Python 유니버스의 대부분과 일치하기 때문입니다.


여기에는 몇 가지 단점이 있습니다.

테스팅

런타임 수정을 통해 모듈을 테스트하고 싶을 때 더 어려워 질 수 있습니다. 하는 대신

import mymodule
mymodule.othermodule = module_stub

당신은해야 할 것입니다

import othermodule
othermodule.foo = foo_stub

이것은 mymodule의 참조가 가리키는 것을 변경하는 것과는 반대로 othermodule을 전역 적으로 패치해야 함을 의미합니다.

종속성 추적

이것은 모듈이 어떤 모듈에 의존하는지 명확하지 않게 만듭니다. 타사 라이브러리를 많이 사용하거나 코드를 재구성하는 경우 특히 짜증이납니다.

여기 저기 인라인으로 가져 오기를 사용하는 레거시 코드를 유지해야했기 때문에 코드를 리팩터링하거나 리 패키징하기가 매우 어려웠습니다.

성능에 대한 참고 사항

파이썬이 모듈을 캐시하는 방식 때문에 성능 저하가 없습니다. 사실, 모듈이 로컬 네임 스페이스에 있기 때문에 함수에서 모듈을 가져 오면 약간의 성능 이점이 있습니다.

최고 수입

import random

def f():
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()


$ time python test.py 

real   0m1.569s
user   0m1.560s
sys    0m0.010s

함수 본문에서 가져 오기

def f():
    import random
    L = []
    for i in xrange(1000):
        L.append(random.random())

for i in xrange(10000):
    f()

$ time python test2.py

real    0m1.385s
user    0m1.380s
sys     0m0.000s

이 접근 방식의 몇 가지 문제점 :

  • 어떤 모듈에 의존하는지 파일을 열 때 즉시 명확하지 않습니다.
  • 이 같은 종속성을 분석해야 프로그램, 혼동 것 py2exe, py2app
  • 많은 기능에서 사용하는 모듈은 어떻습니까? 많은 중복 가져 오기로 끝나거나 파일 상단에 일부와 내부 기능이 있어야합니다.

따라서 ... 선호되는 방법은 모든 가져 오기를 파일 맨 위에 두는 것입니다. 내 수입품을 추적하기가 어렵다면 일반적으로 코드가 너무 많아서 두 개 이상의 파일로 분할하는 것이 더 낫다는 것을 의미합니다.

내가 어떤 상황에 있는 기능 내부 수입이 유용하다는 것을 발견 :

  • 순환 종속성을 처리하려면 (정말 피할 수없는 경우)
  • 플랫폼 별 코드

또한 각 함수에 가져 오기를 넣는 것은 실제로 파일 상단에서보다 느리지 않습니다 . 각 모듈이 처음로드 될 때에 배치되고 sys.modules이후의 각 가져 오기에는 모듈을 찾는 데 필요한 시간 만 소요되며, 이는 상당히 빠릅니다 (다시로드되지 않음).


주목해야 할 또 다른 유용한 점 from module import *은 함수 내부의 구문이 Python 3.0에서 제거되었다는 것입니다.

여기 "Removed Syntax"아래에 간단한 언급이 있습니다.

http://docs.python.org/3.0/whatsnew/3.0.html


from foo import bar수입품 을 피하는 것이 좋습니다 . 나는 그것들을 패키지 안에서만 사용하는데, 모듈로 나누는 것은 구현 세부 사항이고 어쨌든 그것들이 많지 않을 것입니다.

패키지를 가져 오는 다른 모든 위치에서는 사용 import foo하고 전체 이름으로 참조하십시오 foo.bar. 이렇게하면 특정 요소의 출처를 항상 알 수 있으며 가져온 요소 목록을 유지할 필요가 없습니다 (실제로는 항상 구식이며 더 이상 사용되지 않는 요소를 가져옵니다).

foo이름이 정말 긴 경우 으로 단순화 import foo as f한 다음 f.bar. 이것은 모든 from수입품을 유지하는 것보다 훨씬 더 편리하고 명시 적 입니다.


사람들은 인라인 가져 오기를 피해야하는 이유를 아주 잘 설명했지만 처음에 원하는 이유를 해결하기위한 대체 워크 플로는 아닙니다.

어떤 모듈 가져 오기를 사용할 수 있는지 파악하기 위해 소스 파일을 위아래로 스크러빙하는 데 어려움이 있습니다.

사용하지 않는 수입품을 확인하기 위해 나는 pylint를 사용 합니다 . 파이썬 코드의 정적 (ish) 분석을 수행하며, 검사하는 (많은) 것 중 하나는 사용되지 않은 가져 오기입니다. 예를 들어, 다음 스크립트 ..

import urllib
import urllib2

urllib.urlopen("http://stackoverflow.com")

.. 다음 메시지를 생성합니다.

example.py:2 [W0611] Unused import urllib2

사용 가능한 가져 오기를 확인하려면 일반적으로 TextMate의 (상당히 단순한) 완성에 의존합니다. Esc 키를 누르면 문서의 다른 단어와 함께 현재 단어가 완성됩니다. 을 완료 한 import urllib경우 urll[Esc]로 확장되고 urllib그렇지 않은 경우 파일의 시작 부분으로 이동하여 가져 오기를 추가합니다.


성능 관점에서 볼 수 있습니다. Python import 문이 항상 모듈의 맨 위에 있어야합니까?

일반적으로 종속성주기를 끊기 위해 로컬 가져 오기만 사용합니다.


파이썬 위키 에서 Import 문 오버 헤드살펴볼 수 있습니다 . 요컨대 : 모듈이 이미로드 된 경우 (참조 sys.modules) 코드가 느리게 실행됩니다. 모듈이 아직로드되지 않았고 foo필요할 때만로드되는 경우 (0 회일 수 있음) 전체 성능이 향상됩니다.


나는 이것이 일부 경우 / 시나리오에서 권장되는 접근 방식이라고 생각합니다. 예를 들어 Google App Engine에서는 새로운 Python VM / 인터프리터를 인스턴스화하는 데 드는 준비 비용을 최소화하므로 큰 모듈을 지연로드하는 것이 좋습니다. 이를 설명 하는 Google 엔지니어의 프레젠테이션을 살펴보십시오 . 그러나 이것이 모든 모듈을 지연로드해야한다는 의미 는 아닙니다 .


두 변종 모두 용도가 있습니다. 그러나 대부분의 경우 함수 내부가 아닌 외부에서 가져 오는 것이 좋습니다.

공연

여러 답변에서 언급되었지만 제 생각에는 모두 완전한 토론이 부족합니다.

파이썬 인터프리터에서 모듈을 처음으로 가져 오면 최상위 레벨이든 함수 내부이든 상관없이 느려질 것입니다. Python (CPython에 초점을 맞추고 있지만 다른 Python 구현에서는 다를 수 있음)이 여러 단계를 수행하기 때문에 느립니다.

  • 패키지를 찾습니다.
  • 패키지가 이미 바이트 코드 (유명한 __pycache__디렉토리 또는 .pyx파일)로 변환되었는지 확인하고 그렇지 않은 경우이를 바이트 코드로 변환합니다.
  • 파이썬은 바이트 코드를로드합니다.
  • 로드 된 모듈은 sys.modules.

파이썬은 단순히 .NET Framework에서 모듈을 반환 할 수 있기 때문에 후속 가져 오기는 이러한 모든 작업을 수행 할 필요가 없습니다 sys.modules. 따라서 후속 가져 오기가 훨씬 빠릅니다.

모듈의 함수가 실제로 자주 사용되지는 않지만 import시간이 오래 걸리는 에 따라 달라질 수 있습니다. 그런 다음 실제로 import함수 내부를 이동할 수 있습니다. 그러면 모듈 가져 오기가 더 빨라지지만 (긴로드 패키지를 즉시 가져올 필요가 없기 때문에) 함수가 마지막으로 사용되면 첫 번째 호출에서 느려질 것입니다 (그러면 모듈을 가져와야하기 때문). 모든 사용자를 느리게하는 대신 느리게로드하는 종속성에 의존하는 기능을 사용하는 사용자 만 느리게하기 때문에인지 된 성능에 영향을 미칠 수 있습니다.

그러나 조회 sys.modules는 무료가 아닙니다. 매우 빠르지 만 무료는 아닙니다. 따라서 실제로 import패키지를 자주 호출하는 함수를 호출하면 성능이 약간 저하되는 것을 알 수 있습니다.

import random
import itertools

def func_1():
    return random.random()

def func_2():
    import random
    return random.random()

def loopy(func, repeats):
    for _ in itertools.repeat(None, repeats):
        func()

%timeit loopy(func_1, 10000)
# 1.14 ms ± 20.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit loopy(func_2, 10000)
# 2.21 ms ± 138 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

거의 두 배 느립니다.

aaronasterling이 대답에서 약간 "속임수"를 썼다는 것을 깨닫는 것은 매우 중요합니다 . 그는 함수에서 가져 오기를 수행하면 실제로 함수가 더 빨라진다 고 말했습니다. 그리고 어느 정도까지 이것은 사실입니다. 파이썬이 이름을 찾는 방식 때문입니다.

  • 먼저 로컬 범위를 확인합니다.
  • 다음에 주변 스코프를 확인합니다.
  • 그런 다음 다음 주변 범위가 확인됩니다.
  • ...
  • 전역 범위가 확인됩니다.

따라서 로컬 범위를 확인한 다음 전역 범위를 확인하는 대신 모듈 이름을 로컬 범위에서 사용할 수 있으므로 로컬 범위를 확인하는 것으로 충분합니다. 그것은 실제로 그것을 더 빠르게 만듭니다! 그러나 그것은 "루프 불변 코드 모션" 이라는 기술 입니다. 기본적으로 루프 (또는 반복 호출) 전에 변수에 저장하여 루프에서 (또는 반복적으로) 수행되는 작업의 오버 헤드를 줄입니다. 따라서 import함수에서 사용하는 대신 변수를 사용하여 전역 이름에 할당 할 수도 있습니다.

import random
import itertools

def f1(repeats):
    "Repeated global lookup"
    for _ in itertools.repeat(None, repeats):
        random.random()

def f2(repeats):
    "Import once then repeated local lookup"
    import random
    for _ in itertools.repeat(None, repeats):
        random.random()

def f3(repeats):
    "Assign once then repeated local lookup"
    local_random = random
    for _ in itertools.repeat(None, repeats):
        local_random.random()

%timeit f1(10000)
# 588 µs ± 3.92 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f2(10000)
# 522 µs ± 1.95 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f3(10000)
# 527 µs ± 4.51 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

전역에 대한 반복적 인 조회를 수행하는 random것이 느리다는 것을 분명히 알 수 있지만 함수 내부에 모듈을 가져 오는 것과 함수 내부의 변수에 전역 모듈을 할당하는 것 사이에는 거의 차이가 없습니다.

This could be taken to extremes by also avoiding the function lookup inside the loop:

def f4(repeats):
    from random import random
    for _ in itertools.repeat(None, repeats):
        random()

def f5(repeats):
    r = random.random
    for _ in itertools.repeat(None, repeats):
        r()

%timeit f4(10000)
# 364 µs ± 9.34 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f5(10000)
# 357 µs ± 2.73 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

Again much faster but there's almost no difference between the import and the variable.

Optional dependencies

Sometimes having a module-level import can actually be a problem. For example if you don't want to add another install-time dependency but the module would be really helpful for some additional functionality. Deciding if a dependency should be optional shouldn't be done lightly because it will affect the users (either if they get an unexpected ImportError or otherwise miss out the "cool features") and it makes installing the package with all features more complicated, for normal dependencies pip or conda (just to mention two package managers) work out of the box, but for optional dependencies the users have to manually install packages later-on (there are some options that make it possible to customize the requirements but then again the burden of installing it "correctly" is put on the user).

But again this could be done in both ways:

try:
    import matplotlib.pyplot as plt
except ImportError:
    pass

def function_that_requires_matplotlib():
    plt.plot()

or:

def function_that_requires_matplotlib():
    import matplotlib.pyplot as plt
    plt.plot()

This could be more customized by providing alternative implementations or customizing the exception (or message) the user sees but this is the main gist.

The top-level approach could be a bit better if one wants to provide an alternative "solution" to the optional dependency, however generally people use the in-function import. Mostly because it leads to a cleaner stacktrace and is shorter.

Circular Imports

In-Function imports can be very helpful to avoid ImportErrors due to circular imports. In lots of cases circular imports are a sign of "bad" package-structure but if there is absolutely no way to avoid a circular import the "circle" (and thus the problems) are solved by putting the imports that lead to the circle inside the functions that actually use it.

Don't repeat yourself

If you actually put all imports in the function instead of the module scope you will introduce redundancy, because it's likely that functions require the same imports. That has a few disadvantages:

  • You have now multiple places to check if any import has become obsolete.
  • In case you mispelled some import you'll only find out when you run the specific function and not on load-time. Because you have more import statements the likelihood of a mistake increases (not much) and it just becomes a tiny bit more essential to test all functions.

Additional thoughts:

I rarely end up with a litany of imports at the top of my modules, half or more of which I no longer need because I've refactored it.

Most IDEs already have a checker for unused imports, so that's probably just a few clicks to remove them. Even if you don't use an IDE you can use a static code checker script once in a while and fix it manually. Another answer mentioned pylint, but there are others (for example pyflakes).

I rarely accidentally pollute my modules with the contents of other modules

That's why you typically use __all__ and/or define your functions submodules and only import the relevant classes/functions/... in the main module, for example the __init__.py.

Also if you think you polluted the module namespace too much then you probably should consider splitting the module into submodules, however that only makes sense for dozens of imports.

One additional (very important) point to mention if you want to reduce namespace pollution is by avoiding an from module import * imports. But you may also want to avoid from module import a, b, c, d, e, ... imports that import too many names and just import the module and access the functions with module.c.

As a last resort you can always use aliases to avoid polluting the namespace with "public" imports by using: import random as _random. That will make the code harder to understand but it makes it very clear what should be publicly visible and what shouldn't. It's not something I would recommend , you should just keep the __all__ list up-to-date (which is the recommended and sensible approach).

Summary

  • The performance impact is visible but almost always it will be micro-optimizing, so don't let the decision where you put the imports be guided by micro-benchmarks. Except if the dependency is really slow on first import and it's only used for a small subset of the functionality. Then it can actually have a visible impact on the perceived performance of your module for most users.

  • Use the commonly understood tools for defining the public API, I mean the __all__ variable. It might be a bit annoying to keep it up-to-date but so is checking all functions for obsolete imports or when you add a new function to add all the relevant imports in that function. In the long run you'll probably have to do less work by updating __all__.

  • It really doesn't matter which one you prefer, both do work. If you're working alone you can reason about the pros and cons and do which one you think is best. However if you work in a team you probably should stick to known-patterns (which would be top-level imports with __all__) because it allows them to do what they (probably) always have done.


Security Implementations

Consider an environment where all of your Python code is located within a folder only a privileged user has access to. In order to avoid running your whole program as privileged user, you decide to drop privileges to an unprivileged user during execution. As soon as you make use of a function that imports another module, your program will throw an ImportError since the unprivileged user is unable to import the module due to file permissions.

참고URL : https://stackoverflow.com/questions/477096/python-import-coding-style

반응형