Як я можу відійти від школи думки "для циклу"?


79

Це досить концептуальне питання, але я сподівався, що зможу отримати якісь гарні поради з цього приводу. Багато програм, які я роблю, - це з ( NumPy ) масивами; Мені часто доводиться зіставляти елементи в двох або більше масивах, що мають різний розмір, і перше, що я переходжу, - це цикл for або, що ще гірше, вкладений цикл for. Я хочу максимально уникати фор-циклів, тому що вони повільні (принаймні в Python).

Я знаю, що для багатьох речей з NumPy є заздалегідь визначені команди, які мені просто потрібно досліджувати, але у вас (як більш досвідчених програмістів) є загальний розумовий процес, який спадає на думку, коли вам доведеться щось ітератувати?

Тому у мене часто є щось подібне, що жахливо, і я хочу цього уникнути:

small_array = np.array(["one", "two"])
big_array = np.array(["one", "two", "three", "one"])

for i in range(len(small_array)):
    for p in range(len(big_array)):
        if small_array[i] == big_array[p]:
            print "This item is matched: ", small_array[i]

Я знаю, що існує багато різних способів досягти цього зокрема, але мене цікавить загальний метод мислення, якщо він існує.


10
Ви шукаєте функціональне програмування : лямбда-вирази, функції вищого порядку, генерування виразів тощо. Google ці.
Кіліан Фот

42
I want to avoid for-loops as much as possible because they are slow (at least in Python).Здається, що ви тут вирішуєте неправильну проблему. Якщо вам потрібно щось перебрати через щось, вам потрібно повторити щось; ви будете приймати аналогічний хіт продуктивності незалежно від того, яку конструкцію Python ви використовуєте. Якщо ваш код повільний, це не тому, що у вас є forпетлі; це тому, що ти робиш непотрібну роботу або робиш роботу на стороні Python, яку можна було б зробити на стороні С. У вашому прикладі ви робите додаткову роботу; ви могли зробити це одним циклом замість двох.
Довал

24
@Doval На жаль, ні - в NumPy . Python для циклу, що виконує елементне додавання, може бути в декілька разів повільнішим!

46
Деякі з коментарів вище, здається, неправильно розуміють питання. Програмуючи в NumPy, ви отримуєте найкращі результати, якщо зможете векторизувати свої обчислення - тобто замінити явні петлі в Python на операції з цілим масивом у NumPy. Це концептуально дуже відрізняється від звичайного програмування в Python і потребує часу для вивчення. Тому я вважаю, що ОП доцільно звертатися за порадою, як слід навчитися це робити.
Гарет Різ

3
@PieterB: Так, саме так. "Векторизація" - це не те саме, що "вибір найкращого алгоритму". Вони є двома окремими джерелами труднощів у пошуку ефективних реалізацій, тому краще подумати про них по черзі.
Гарет Різ

Відповіді:


89

Це загальна концептуальна складність при навчанні ефективно використовувати NumPy . Зазвичай обробку даних в Python найкраще виражати в ітераторах , щоб знизити використання пам'яті, максимізувати можливості паралелізму з системою вводу-виводу, а також забезпечити повторне використання та комбінування частин алгоритмів.

Але NumPy перетворює все це назовні: найкращий підхід - це виразити алгоритм як послідовність операцій із цілим масивом , мінімізувати кількість часу, проведеного у повільному інтерпретаторі Python, та максимізувати кількість часу, проведеного у швидко складених NumPy процедурах.

Ось загальний підхід:

  1. Зберігайте оригінальну версію функції (на яку ви впевнені, що вона правильна), щоб ви могли перевірити її щодо покращених версій як щодо правильності, так і швидкості.

  2. Робота зсередини назовні: тобто починайте з внутрішньої петлі і подивіться, чи можна векторизувати; тоді, коли ви це зробите, виведіть один рівень і продовжуйте.

  3. Проведіть багато часу, читаючи документацію NumPy . Там багато функцій та операцій, і їх не завжди геніально називають, тому варто їх познайомити. Зокрема, якщо вам здається, що ви думаєте, "якби тільки була така функція, яка виконувала таке-то-таке", тоді варто витратити десять хвилин на це. Зазвичай там десь.

Практики немає заміни, тому я надам вам кілька прикладів проблем. Мета кожної проблеми - переписати функцію так, щоб вона була повністю векторизована : тобто таким чином, щоб вона складалася з послідовності операцій NumPy на цілих масивах, без вроджених циклів Python (ні forабо whileзаяви, ні ітератори, ні розуміння).

Завдання 1

def sumproducts(x, y):
    """Return the sum of x[i] * y[j] for all pairs of indices i, j.

    >>> sumproducts(np.arange(3000), np.arange(3000))
    20236502250000

    """
    result = 0
    for i in range(len(x)):
        for j in range(len(y)):
            result += x[i] * y[j]
    return result

Завдання 2

def countlower(x, y):
    """Return the number of pairs i, j such that x[i] < y[j].

    >>> countlower(np.arange(0, 200, 2), np.arange(40, 140))
    4500

    """
    result = 0
    for i in range(len(x)):
        for j in range(len(y)):
            if x[i] < y[j]:
                result += 1
    return result

Завдання 3

def cleanup(x, missing=-1, value=0):
    """Return an array that's the same as x, except that where x ==
    missing, it has value instead.

    >>> cleanup(np.arange(-3, 3), value=10)
    ... # doctest: +NORMALIZE_WHITESPACE
    array([-3, -2, 10, 0, 1, 2])

    """
    result = []
    for i in range(len(x)):
        if x[i] == missing:
            result.append(value)
        else:
            result.append(x[i])
    return np.array(result)

Спойлери нижче. Ви отримаєте набагато найкращі результати, якщо будете самі ходити, перш ніж дивитись на мої рішення!

Відповідь 1

np.sum (x) * np.sum (y)

Відповідь 2

np.sum (np.searchsorted (np.sort (x), y))

Відповідь 3

np.where (x == відсутні, значення, x)


Зачекайте, чи є помилка друку в останній відповіді чи NumPy модифікує, як Python інтерпретує код?
Ізката

1
@Izkata Вона сама по собі нічого не змінює, але логічні операції, застосовані до масивів, визначаються для повернення булевих масивів.
сапі

@sapi Ах, я пропустив те, що відбувається в доктестах, подумав, що це простоlist
Izkata

Можливо, має бути спосіб вбудувати APL?

Мені подобається, як ти даєш домашні завдання.
Корай Тугай

8

Для того, щоб зробити це швидше, ви повинні прочитати свої структури даних і використовувати відповідні.

Для нетривіальних розмірів малого масиву та великого масиву (скажімо, малий = 100 елементів та великий = 10 000 елементів) один із способів зробити це - сортувати малий масив, потім перебирати на великий масив та використовувати двійковий пошук для пошуку відповідних елементів у малому масиві.

Це зробило б максимальну складність у часі, O (N log N) (а для малих малих масивів і дуже великих великих масивів це ближче до O (N)), де ваше вкладене рішення циклу - O (N ^ 2)

Однак. які структури даних є найбільш ефективними, сильно залежать від фактичної проблеми.


-3

Ви можете використовувати словник, щоб значно оптимізувати продуктивність

Це ще один приклад:

locations = {}
for i in range(len(airports)):
    locations[airports["abb"][i][1:-1]] = (airports["height"][i], airports["width"][i])

for i in range(len(uniqueData)):
    h, w = locations[uniqueData["dept_apt"][i]]
    uniqueData["dept_apt_height"][i] = h
    uniqueData["dept_apt_width"][i] = w

2
Це досить очевидно досі з використанням школи думки "за циклом".
8bittree
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.