هل يجب أن تكون عبارات الاستيراد دائمًا في أعلى الوحدة؟

StackOverflow https://stackoverflow.com/questions/128478

سؤال

بيب 08 تنص على:

يتم دائمًا وضع عمليات الاستيراد في أعلى الملف، مباشرةً بعد أي تعليقات على الوحدة النمطية وسلاسل المستندات، وقبل القيم العامة والثوابت للوحدة النمطية.

ومع ذلك، إذا تم استخدام الفئة/الطريقة/الوظيفة التي أقوم باستيرادها فقط في حالات نادرة، فمن المؤكد أنه من الأكثر كفاءة القيام بالاستيراد عند الحاجة إليها؟

أليس هذا:

class SomeClass(object):

    def not_often_called(self)
        from datetime import datetime
        self.datetime = datetime.now()

أكثر كفاءة من هذا؟

from datetime import datetime

class SomeClass(object):

    def not_often_called(self)
        self.datetime = datetime.now()
هل كانت مفيدة؟

المحلول

يعد استيراد الوحدات سريعًا جدًا، ولكنه ليس فوريًا.هذا يعني ذاك:

  • إن وضع الواردات في الجزء العلوي من الوحدة أمر جيد، لأنها تكلفة تافهة يتم دفعها مرة واحدة فقط.
  • سيؤدي وضع الواردات داخل إحدى الوظائف إلى استغراق وقت أطول في استدعاء هذه الوظيفة.

لذا، إذا كنت تهتم بالكفاءة، ضع الواردات في الأعلى.انقلهم إلى وظيفة فقط إذا أظهر ملف التعريف الخاص بك أن ذلك من شأنه أن يساعدك (أنت فعل الملف الشخصي لمعرفة أفضل مكان لتحسين الأداء، أليس كذلك؟؟)


أفضل الأسباب التي رأيتها لإجراء عمليات الاستيراد البطيئة هي:

  • دعم المكتبة الاختياري.إذا كانت التعليمات البرمجية الخاصة بك تحتوي على مسارات متعددة تستخدم مكتبات مختلفة، فلا تنقطع إذا لم يتم تثبيت مكتبة اختيارية.
  • في ال __init__.py مكون إضافي، والذي قد يتم استيراده ولكن لا يتم استخدامه فعليًا.ومن الأمثلة على ذلك مكونات Bazaar الإضافية، والتي تستخدم bzrlibإطار عمل التحميل البطيء.

نصائح أخرى

يمكن أن يؤدي وضع عبارة الاستيراد داخل إحدى الوظائف إلى منع التبعيات الدائرية.على سبيل المثال، إذا كان لديك وحدتين، X.py وY.py، وكلاهما بحاجة إلى استيراد بعضهما البعض، فسيؤدي ذلك إلى تبعية دائرية عند استيراد إحدى الوحدات مما يسبب حلقة لا نهائية.إذا قمت بنقل بيان الاستيراد في إحدى الوحدات، فلن يحاول استيراد الوحدة الأخرى حتى يتم استدعاء الوظيفة، وسيتم استيراد هذه الوحدة بالفعل، لذلك لا توجد حلقة لا نهائية.إقرأ هنا للمزيد - effbot.org/zone/import-confusion.htm

لقد اعتمدت ممارسة وضع كافة الواردات في الوظائف التي تستخدمها، وليس في الجزء العلوي من الوحدة.

الفائدة التي أحصل عليها هي القدرة على إعادة البناء بشكل أكثر موثوقية.عندما أنقل وظيفة من وحدة إلى أخرى، أعلم أن الوظيفة ستستمر في العمل مع الحفاظ على تراثها من الاختبار سليمًا.إذا كانت وارداتي في الجزء العلوي من الوحدة، فعندما أنقل وظيفة، أجد أن الأمر ينتهي بي إلى قضاء الكثير من الوقت في إكمال واردات الوحدة الجديدة والحد الأدنى منها.قد يجعل IDE لإعادة البناء هذا غير ذي صلة.

هناك عقوبة السرعة كما ذكر في مكان آخر.لقد قمت بقياس هذا في طلبي ووجدت أنه غير مهم لأغراضي.

من الجيد أيضًا أن تكون قادرًا على رؤية جميع تبعيات الوحدة مقدمًا دون اللجوء إلى البحث (على سبيل المثال.جريب).ومع ذلك، فإن سبب اهتمامي بتبعيات الوحدة هو عمومًا أنني أقوم بتثبيت أو إعادة بناء أو نقل نظام كامل يشتمل على ملفات متعددة، وليس مجرد وحدة واحدة.في هذه الحالة، سأقوم بإجراء بحث عالمي على أي حال للتأكد من أن لدي التبعيات على مستوى النظام.لذلك لم أجد واردات عالمية تساعدني في فهم النظام في الممارسة العملية.

عادة ما أضع استيراد sys داخل if __name__=='__main__' تحقق ثم قم بتمرير الوسائط (مثل sys.argv[1:]) إلى أ main() وظيفة.هذا يسمح لي باستخدام main في سياق حيث sys لم يتم استيرادها.

في معظم الأحيان، قد يكون هذا مفيدًا للوضوح ومعقولًا، لكن الأمر ليس كذلك دائمًا.فيما يلي بعض الأمثلة على الظروف التي قد تكون فيها عمليات استيراد الوحدات موجودة في مكان آخر.

أولاً، يمكن أن يكون لديك وحدة تحتوي على اختبار وحدة للنموذج:

if __name__ == '__main__':
    import foo
    aa = foo.xyz()         # initiate something for the test

ثانيًا، قد تحتاج إلى استيراد بعض الوحدات المختلفة بشكل مشروط في وقت التشغيل.

if [condition]:
    import foo as plugin_api
else:
    import bar as plugin_api
xx = plugin_api.Plugin()
[...]

من المحتمل أن تكون هناك مواقف أخرى حيث يمكنك إجراء عمليات استيراد في أجزاء أخرى من التعليمات البرمجية.

المتغير الأول هو في الواقع أكثر كفاءة من الثاني عندما يتم استدعاء الدالة إما صفر أو مرة واحدة.ومع ذلك، مع الاستدعاءات الثانية واللاحقة، يكون أسلوب "استيراد كل مكالمة" أقل كفاءة في الواقع.يرى هذا الرابط لتقنية التحميل البطيء التي تجمع بين أفضل ما في كلا الطريقتين من خلال إجراء "استيراد بطيء".

ولكن هناك أسباب أخرى غير الكفاءة قد تجعلك تفضل واحدًا على الآخر.أحد الأساليب هو توضيح الأمر أكثر لشخص يقرأ الكود فيما يتعلق بالتبعيات التي تمتلكها هذه الوحدة.لديهم أيضًا خصائص فشل مختلفة تمامًا - الأول سيفشل في وقت التحميل إذا لم يكن هناك وحدة "تاريخ/وقت" بينما لن يفشل الثاني حتى يتم استدعاء الطريقة.

ملاحظة مضافة: في IronPython، يمكن أن تكون عمليات الاستيراد أكثر تكلفة قليلاً مما هي عليه في CPython لأنه يتم تجميع التعليمات البرمجية بشكل أساسي أثناء استيرادها.

يشير كيرت إلى نقطة جيدة:الإصدار الثاني أكثر وضوحًا وسيفشل أثناء التحميل وليس لاحقًا وبشكل غير متوقع.

عادةً لا أقلق بشأن كفاءة تحميل الوحدات، نظرًا لأنه (أ) سريع جدًا، و(ب) يحدث غالبًا عند بدء التشغيل فقط.

إذا كان عليك تحميل وحدات ثقيلة الوزن في أوقات غير متوقعة، فمن المحتمل أن يكون من المنطقي تحميلها ديناميكيًا باستخدام ملف __import__ وظيفة، ويكون بالتأكيد للإمساك ImportError الاستثناءات، والتعامل معها بطريقة معقولة.

لن أقلق بشأن كفاءة تحميل الوحدة مقدمًا أكثر من اللازم.لن تكون الذاكرة التي تشغلها الوحدة كبيرة جدًا (بافتراض أنها معيارية بدرجة كافية) وستكون تكلفة بدء التشغيل ضئيلة.

في معظم الحالات، تريد تحميل الوحدات الموجودة أعلى الملف المصدر.بالنسبة لشخص يقرأ الكود الخاص بك، فإنه يجعل من السهل جدًا معرفة الوظيفة أو الكائن الذي جاء من أي وحدة.

أحد الأسباب الجيدة لاستيراد وحدة نمطية في مكان آخر من التعليمات البرمجية هو إذا تم استخدامها في عبارة تصحيح الأخطاء.

على سبيل المثال:

do_something_with_x(x)

يمكنني تصحيح هذا باستخدام:

from pprint import pprint
pprint(x)
do_something_with_x(x)

بالطبع، السبب الآخر لاستيراد الوحدات في مكان آخر في الكود هو إذا كنت بحاجة إلى استيرادها ديناميكيًا.هذا لأنه ليس لديك أي خيار إلى حد كبير.

لن أقلق بشأن كفاءة تحميل الوحدة مقدمًا أكثر من اللازم.لن تكون الذاكرة التي تشغلها الوحدة كبيرة جدًا (بافتراض أنها معيارية بدرجة كافية) وستكون تكلفة بدء التشغيل ضئيلة.

إنها مقايضة لا يمكن إلا للمبرمج أن يقرر القيام بها.

توفر الحالة 1 بعض الذاكرة ووقت بدء التشغيل من خلال عدم استيراد وحدة التاريخ والوقت (والقيام بأي عملية تهيئة قد تتطلبها) حتى الحاجة إليها.لاحظ أن إجراء الاستيراد "فقط عند الاتصال" يعني أيضًا القيام بذلك "في كل مرة عند الاتصال"، لذا فإن كل مكالمة بعد المكالمة الأولى لا تزال تتكبد الحمل الإضافي لإجراء الاستيراد.

توفر الحالة 2 بعض وقت التنفيذ ووقت الاستجابة عن طريق استيراد وقت التاريخ مسبقًا بحيث يعود not_often_call() بسرعة أكبر عندما يتم ذلك يكون يتم الاتصال بها، وأيضًا من خلال عدم تحمل النفقات العامة للاستيراد في كل مكالمة.

إلى جانب الكفاءة، من الأسهل رؤية تبعيات الوحدة مقدمًا إذا كانت بيانات الاستيراد ...صريح.قد يؤدي إخفاءها في الكود إلى زيادة صعوبة العثور بسهولة على الوحدات التي يعتمد عليها شيء ما.

أنا شخصياً أتبع PEP بشكل عام باستثناء أشياء مثل اختبارات الوحدة بحيث لا أريد تحميلها دائمًا لأنني يعرف لن يتم استخدامها باستثناء رمز الاختبار.

فيما يلي مثال حيث تكون جميع الواردات في الأعلى (هذه هي المرة الوحيدة التي أحتاج فيها للقيام بذلك).أريد أن أكون قادرًا على إنهاء عملية فرعية على كل من Un*x وWindows.

import os
# ...
try:
    kill = os.kill  # will raise AttributeError on Windows
    from signal import SIGTERM
    def terminate(process):
        kill(process.pid, SIGTERM)
except (AttributeError, ImportError):
    try:
        from win32api import TerminateProcess  # use win32api if available
        def terminate(process):
            TerminateProcess(int(process._handle), -1)
    except ImportError:
        def terminate(process):
            raise NotImplementedError  # define a dummy function

(في المراجعة:ماذا جون ميليكين قال.)

وهذا مثل العديد من التحسينات الأخرى - فأنت تضحي ببعض سهولة القراءة من أجل السرعة.كما ذكر جون، إذا قمت بواجبك المنزلي ووجدت أن هذا تغيير مفيد بدرجة كافية و أنت بحاجة إلى السرعة الإضافية، فابحث عنها.ربما يكون من الجيد كتابة ملاحظة مع جميع الواردات الأخرى:

from foo import bar
from baz import qux
# Note: datetime is imported in SomeClass below

تحدث تهيئة الوحدة مرة واحدة فقط - عند الاستيراد الأول.إذا كانت الوحدة المعنية من المكتبة القياسية، فمن المحتمل أن تقوم باستيرادها من وحدات أخرى في برنامجك أيضًا.بالنسبة لوحدة سائدة مثل التاريخ والوقت، فمن المحتمل أيضًا أن تكون تبعية لعدد كبير من المكتبات القياسية الأخرى.لن يكلف بيان الاستيراد سوى القليل جدًا نظرًا لأن تهيئة الوحدة قد حدثت بالفعل.كل ما تفعله في هذه المرحلة هو ربط كائن الوحدة النمطية الموجود بالنطاق المحلي.

قم بإقران هذه المعلومات بحجة سهولة القراءة وأود أن أقول أنه من الأفضل أن يكون بيان الاستيراد في نطاق الوحدة النمطية.

فقط لإكمال إجابة مو والسؤال الأصلي:

عندما يتعين علينا التعامل مع التبعيات الدائرية يمكننا القيام ببعض "الحيل". على افتراض أننا نعمل مع الوحدات النمطية a.py و b.py التي تحتوي على x() وب y(), ، على التوالى.ثم:

  1. يمكننا تحريك واحدة من from imports في الجزء السفلي من الوحدة.
  2. يمكننا تحريك واحدة من from imports داخل الوظيفة أو الطريقة التي تتطلب الاستيراد بالفعل (هذا ليس ممكنًا دائمًا، حيث يمكنك استخدامه من عدة أماكن).
  3. يمكننا تغيير واحد من الاثنين from imports ليكون استيرادًا يبدو كالتالي: import a

لذلك، في الختام.إذا كنت لا تتعامل مع التبعيات الدائرية وتقوم ببعض الحيل لتجنبها، فمن الأفضل وضع جميع وارداتك في الأعلى للأسباب الموضحة بالفعل في إجابات أخرى على هذا السؤال.ومن فضلك، عند القيام بهذه "الحيل" قم بتضمين تعليق، فهو موضع ترحيب دائمًا!:)

بالإضافة إلى الإجابات الممتازة التي تم تقديمها بالفعل، تجدر الإشارة إلى أن وضع الواردات ليس مجرد مسألة أسلوب.في بعض الأحيان تحتوي الوحدة على تبعيات ضمنية تحتاج إلى استيرادها أو تهيئتها أولاً، وقد يؤدي الاستيراد على المستوى الأعلى إلى انتهاك ترتيب التنفيذ المطلوب.

غالبًا ما تظهر هذه المشكلة في Python API الخاص بـ Apache Spark، حيث تحتاج إلى تهيئة SparkContext قبل استيراد أي حزم أو وحدات pyspark.من الأفضل وضع عمليات استيراد pyspark في نطاق يضمن توفر SparkContext فيه.

لا أطمح إلى تقديم إجابة كاملة، لأن الآخرين فعلوا ذلك بشكل جيد للغاية.أريد فقط أن أذكر حالة استخدام واحدة عندما أجد أنه من المفيد بشكل خاص استيراد الوحدات داخل الوظائف.يستخدم تطبيقي حزم ووحدات بايثون المخزنة في موقع معين كمكونات إضافية.أثناء بدء تشغيل التطبيق، يمر التطبيق عبر جميع الوحدات الموجودة في الموقع ويستوردها، ثم يبحث داخل الوحدات وإذا وجد بعض نقاط التثبيت للمكونات الإضافية (في حالتي، فهي فئة فرعية من فئة أساسية معينة لها خاصية فريدة ID) يقوم بتسجيلهم.عدد المكونات الإضافية كبير (الآن العشرات، ولكن ربما المئات في المستقبل) ونادرًا ما يتم استخدام كل منها.كان وجود عمليات استيراد لمكتبات الطرف الثالث في الجزء العلوي من وحدات المكونات الإضافية الخاصة بي بمثابة عقوبة بعض الشيء أثناء بدء تشغيل التطبيق.لا سيما أن بعض مكتبات الطرف الثالث ثقيلة الاستيراد (على سبيل المثال.يحاول import ofploly الاتصال بالإنترنت وتنزيل شيء كان يضيف حوالي ثانية واحدة لبدء التشغيل).من خلال تحسين عمليات الاستيراد (استدعائها فقط في الوظائف التي يتم استخدامها فيها) في المكونات الإضافية، تمكنت من تقليص مدة بدء التشغيل من 10 ثوانٍ إلى حوالي ثانيتين.وهذا فرق كبير بالنسبة للمستخدمين.

لذا فإن إجابتي هي لا، لا تضع دائمًا الواردات في أعلى وحداتك.

لقد فوجئت بعدم رؤية أرقام التكلفة الفعلية لعمليات فحص التحميل المتكررة التي تم نشرها بالفعل، على الرغم من وجود العديد من التفسيرات الجيدة لما يمكن توقعه.

إذا قمت بالاستيراد في الجزء العلوي، فستتلقى ضربة التحميل بغض النظر عن الأمر.وهذا صغير جدًا، ولكنه عادةً ما يكون بالمللي ثانية، وليس بالنانو ثانية.

إذا قمت بالاستيراد داخل وظيفة (وظائف)، فلن تحصل إلا على النتيجة للتحميل لو و متى يتم استدعاء إحدى هذه الوظائف أولاً.وكما أشار الكثيرون، إذا لم يحدث ذلك على الإطلاق، فإنك توفر وقت التحميل.ولكن إذا تم استدعاء الوظيفة (الوظائف) كثيرًا، فستتلقى ضربة متكررة وإن كانت أصغر بكثير (للتحقق من أنها لديه تم تحميلها؛ليس لإعادة التحميل فعليًا).من ناحية أخرى، كما أشار @aaronasterling، يمكنك أيضًا توفير القليل لأن الاستيراد داخل إحدى الوظائف يتيح للوظيفة استخدام أسرع قليلاً متغير محلي عمليات بحث للتعرف على الاسم لاحقًا (http://stackoverflow.com/questions/477096/python-import-coding-style/4789963#4789963).

فيما يلي نتائج اختبار بسيط يستورد بعض الأشياء من داخل الوظيفة.الأوقات المذكورة (في Python 2.7.14 على معالج Intel Core i7 بتردد 2.3 جيجاهرتز) موضحة أدناه (تبدو المكالمة الثانية التي تستغرق أكثر من المكالمات اللاحقة متسقة، على الرغم من أنني لا أعرف السبب).

 0 foo:   14429.0924 µs
 1 foo:      63.8962 µs
 2 foo:      10.0136 µs
 3 foo:       7.1526 µs
 4 foo:       7.8678 µs
 0 bar:       9.0599 µs
 1 bar:       6.9141 µs
 2 bar:       7.1526 µs
 3 bar:       7.8678 µs
 4 bar:       7.1526 µs

الرمز:

from __future__ import print_function
from time import time

def foo():
    import collections
    import re
    import string
    import math
    import subprocess
    return

def bar():
    import collections
    import re
    import string
    import math
    import subprocess
    return

t0 = time()
for i in xrange(5):
    foo()
    t1 = time()
    print("    %2d foo: %12.4f \xC2\xB5s" % (i, (t1-t0)*1E6))
    t0 = t1
for i in xrange(5):
    bar()
    t1 = time()
    print("    %2d bar: %12.4f \xC2\xB5s" % (i, (t1-t0)*1E6))
    t0 = t1

من المثير للاهتمام أنه لم تذكر إجابة واحدة المعالجة المتوازية حتى الآن، حيث قد يكون مطلوبًا أن تكون الواردات في الوظيفة، عندما يكون رمز الوظيفة المتسلسل هو ما يتم دفعه إلى النوى الأخرى، على سبيل المثال.كما هو الحال في حالة ipyparallel.

يمكن أن يكون هناك مكاسب في الأداء عن طريق استيراد المتغيرات/النطاق المحلي داخل الوظيفة.هذا يعتمد على استخدام الشيء المستورد داخل الوظيفة.إذا كنت تقوم بالتكرار عدة مرات وتصل إلى كائن وحدة نمطية عمومي، فإن استيراده ككائن محلي يمكن أن يساعدك.

test.py

X=10
Y=11
Z=12
def add(i):
  i = i + 10

runlocal.py

from test import add, X, Y, Z

    def callme():
      x=X
      y=Y
      z=Z
      ladd=add 
      for i  in range(100000000):
        ladd(i)
        x+y+z

    callme()

run.py

from test import add, X, Y, Z

def callme():
  for i in range(100000000):
    add(i)
    X+Y+Z

callme()

يظهر الوقت على نظام التشغيل Linux مكسبًا صغيرًا

/usr/bin/time -f "\t%E real,\t%U user,\t%S sys" python run.py 
    0:17.80 real,   17.77 user, 0.01 sys
/tmp/test$ /usr/bin/time -f "\t%E real,\t%U user,\t%S sys" python runlocal.py 
    0:14.23 real,   14.22 user, 0.01 sys

الحقيقية هي ساعة الحائط.المستخدم هو الوقت في البرنامج.sys هو الوقت المناسب لاستدعاءات النظام.

https://docs.python.org/3.5/reference/executionmodel.html#resolution-of-names

أود أن أذكر حالة استخدام خاصة بي، تشبه إلى حد كبير تلك التي ذكرها @John Millikin و@V.K.:

الواردات الاختيارية

أقوم بتحليل البيانات باستخدام Jupyter Notebook، وأستخدم نفس دفتر IPython كقالب لجميع التحليلات.في بعض المناسبات، أحتاج إلى استيراد Tensorflow لإجراء بعض عمليات تشغيل النماذج السريعة، لكن أحيانًا أعمل في أماكن لم يتم إعداد Tensorflow فيها/يكون استيرادها بطيئًا.في هذه الحالات، أقوم بتغليف عملياتي المعتمدة على Tensorflow في وظيفة مساعدة، واستيراد Tensorflow داخل تلك الوظيفة، وربطها بزر.

بهذه الطريقة، يمكنني إجراء "إعادة التشغيل والتشغيل للجميع" دون الحاجة إلى انتظار الاستيراد، أو الاضطرار إلى استئناف بقية الخلايا عند فشلها.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top