В чём принципиальная разница, использовать магический метод __getitem__ или получать элемент по индексу?
Разница все же есть. Не в результате, а методе которым это результат получаем. (использование [] эффективней) 
Припустим есть у нас такой кодitems = [1, 2, 3, 4]

def get_item_as_method():
    return items.__getitem__(1)

def get_item_as_operator():
    return items[1]
Если мы дизассемблируем (модуль dis) эти функций увидим следующие:
Для get_item_as_method:LOAD_GLOBAL            0 (items)
LOAD_ATTR                1 (__getitem__)
LOAD_CONST             1 (1)
CALL_FUNCTION        1 (1 positional, 0 keyword pair)
RETURN_VALUE
Для get_item_as_operator:LOAD_GLOBAL              0 (items)
LOAD_CONST               1 (1)
BINARY_SUBSCR
RETURN_VALUE
Видим в первом случае для получения результата нужно выполнить две операций (LOAD_ATTR и CALL_FUNCTION), а в первом всего одну BINARY_SUBSCR.
Разницы, насколько я понимаю, никакой нет. __getitem__ служит для реализации собственных методов доступа и если он определен -  можно писать obj[i].