Dlaczego nie może być żadnych niejawnych konwersji?

14

Jak rozumiem, niejawne konwersje mogą powodować błędy.

Ale to nie ma sensu - czy zatem normalne konwersje nie powinny również powodować błędów?

Dlaczego nie mieć?

len(100)

praca przez język interpretujący (lub kompilujący) jako

len(str(100))

zwłaszcza, że ​​jest to jedyny (wiem) sposób na to, aby działał. Język wie, jaki jest błąd, dlaczego go nie naprawić?

W tym przykładzie użyłem Pythona, choć wydaje mi się, że dla czegoś tak małego jest on w zasadzie uniwersalny.

Quelklef
źródło
2
perl -e 'print length(100);'wydruki 3.
2
I w ten sposób natura języka i jego system typów. Jest to część projektu Pythona.
2
Nie naprawia tego, ponieważ nie zna twojego stanu. może chciałeś zrobić coś zupełnie innego. jak pozywanie pętli, ale nigdy wcześniej nie robił czegoś takiego jak programowanie. więc jeśli to naprawi samodzielnie, użytkownik nie wie, że się mylił ani że realizacja nie zrobi tego, czego się spodziewa.
Zaibis
5
@PieCrust W jaki sposób Python powinien konwertować? To nie prawda, że wszystko jest możliwe przekształcenie wróci ten sam rezultat.
Bakuriu
7
@PieCrust: ciągi i tablice są iterowalne. dlaczego miałby strbyć niejawnym sposobem na przekształcenie int w iterowalną? jak o range? lub bin( hex, oct) lub chr(lub unichr)? Wszystkie te zwroty iteracyjne, nawet jeśli strwydaje Ci się to najbardziej oczywiste w tej sytuacji.
njzk2

Odpowiedzi:

37

Za to, co jest warte len(str(100)), len(chr(100))i len(hex(100))wszystkie są różne. niestr jest jedynym sposobem na to, aby działał, ponieważ w Pythonie istnieje więcej niż jedna konwersja z liczby całkowitej na ciąg. Jeden z nich jest oczywiście najczęstszy, ale niekoniecznie trzeba powiedzieć, że o to ci chodziło. Ukryta konwersja dosłownie oznacza „to oczywiste”.

Jednym z praktycznych problemów z niejawnymi konwersjami jest to, że nie zawsze jest oczywiste, która konwersja zostanie zastosowana, gdy istnieje kilka możliwości, a to powoduje, że czytelnicy popełniają błędy podczas interpretacji kodu, ponieważ nie potrafią znaleźć prawidłowej niejawnej konwersji. Wszyscy zawsze mówią, że ich zamierzeniem jest „oczywista interpretacja”. Jest to dla nich oczywiste, ponieważ o to im chodziło. Dla kogoś innego może to nie być oczywiste.

Dlatego (przez większość czasu) Python woli jawnie niż dorozumiany, woli nie ryzykować. Głównym przypadkiem, w którym Python robi przymus typu, jest arytmetyka. Pozwala 1 + 1.0ponieważ alternatywą byłoby zbyt uciążliwe, aby żyć, ale to nie pozwala 1 + "1", ponieważ uważa, że nie powinno być określenie, czy to znaczy int("1"), float("1"), ord("1"), str(1) + "1", lub coś innego. To również nie pozwala (1,2,3) + [4,5,6], nawet jeśli może zdefiniować reguły wyboru typu wyniku, podobnie jak definiuje reguły wyboru typu wyniku 1 + 1.0.

Inne języki nie zgadzają się i mają wiele niejawnych konwersji. Im więcej zawierają, tym mniej oczywiste stają się. Spróbuj zapamiętać reguły ze standardu C dotyczące „promocji całkowitych” i „zwykłych konwersji arytmetycznych” przed śniadaniem!

Steve Jessop
źródło
+1 Za wykazanie, że założenie początkowe, które lenmoże działać tylko z jednym typem, jest zasadniczo wadliwe.
KChaloux
2
@KChaloux: faktycznie, w moim przykładzie str, chri hexwszystkie zwracają ten sam typ! Próbowałem wymyślić inny typ, którego konstruktor może przyjąć tylko int, ale jeszcze niczego nie wymyśliłem. Idealny byłby jakiś pojemnik, w Xktórym len(X(100)) == 100;-) numpy.zeroswydaje się nieco niejasny.
Steve Jessop
2
Przykłady możliwych problemów można znaleźć tutaj: docs.google.com/document/d/… i destroyallsoftware.com/talks/wat
Jens Schauder
1
Niejawna konwersja (myślę, że to się nazywa przymus) może powodować nieprzyjemnych rzeczy jak posiadanie a == b, b == cale a == cnie jest prawdą.
bgusach
Doskonała odpowiedź. Wszystko, co chciałem powiedzieć, brzmiało lepiej! Moim jedynym drobnym sporem jest to, że promocje liczb całkowitych C są dość proste w porównaniu do zapamiętywania reguł przymusu JavaScript lub owijania głowy wokół bazy kodu C ++ z nieostrożnym użyciem niejawnych konstruktorów.
GrandOpener
28

Jak rozumiem, niejawne konwersje mogą powodować błędy.

Brakuje Ci słowa: niejawne konwersje mogą powodować błędy w czasie wykonywania .

W przypadku tak prostego przypadku, jak pokazujesz, jasne jest, co miałeś na myśli. Ale języki nie mogą działać na przypadkach. Muszą pracować z regułami. W wielu innych sytuacjach nie jest jasne, czy programista popełnił błąd (używając niewłaściwego typu), czy też programista chciał zrobić to, co zakłada kod.

Jeśli kod zakłada błędnie, pojawia się błąd czasu wykonania. Ponieważ śledzenie ich jest żmudne, wiele języków nie zgadza się z informacją, że popełniłeś błąd i pozwalasz komputerowi powiedzieć, co naprawdę miałeś na myśli (napraw błąd lub jawnie dokonaj konwersji). Zgadują inne języki, ponieważ ich styl nadaje się do szybkiego i łatwego kodu, który jest łatwiejszy do debugowania.

Należy zauważyć, że niejawne konwersje sprawiają, że kompilator jest nieco bardziej złożony. Musisz bardziej uważać na cykle (spróbujmy tej konwersji z A na B; ups, które nie działały, ale istnieje konwersja z B na A !, a następnie musisz się martwić o cykle wielkości C i D ), które to niewielka motywacja do unikania niejawnych konwersji.

Telastyn
źródło
Głównie mówiono o funkcjach, które mogą działać tylko z jednym typem, dzięki czemu konwersja jest oczywista. Jaki byłby taki, który działałby z wieloma typami, a wprowadzony typ robi różnicę? print („295”) = print (295), więc nie zrobiłoby to różnicy, z wyjątkiem zmiennych. To, co powiedziałeś, ma sens, z wyjątkiem akapitu 3d ... Czy możesz powtórzyć?
Quelklef
30
@PieCrust - 123 + „456”, czy chciałeś „123456” czy 579? Języki programowania nie robią kontekstu, więc trudno im to „rozgryźć”, ponieważ musieliby znać kontekst, w którym dokonuje się dodawania. Który akapit jest niejasny?
Telastyn
1
Być może interpretacja jako base-10 nie jest tym, czego chciałeś. Tak, niejawne konwersje są dobrą rzeczą , ale tylko wtedy, gdy nie mogą ukryć błędu.
Deduplicator
13
@PieCrust: Szczerze mówiąc, nigdy nie spodziewałbym len(100)się powrotu 3. O wiele bardziej intuicyjne byłoby obliczanie liczby bitów (lub bajtów) w reprezentacji 100.
user541686
3
Jestem z @Mehrdad, z twojego przykładu jasno wynika, że uważasz, że jest to w 100% oczywiste, że len(100)powinno dać ci liczbę znaków dziesiętnej reprezentacji liczby 100. Ale to jest naprawdę mnóstwo założeń, których nie przyjmujesz z nich. Można argumentować, dlaczego liczba bitów, bajtów lub znaków reprezentacji szesnastkowej ( 64-> 2 znaki) powinna być zwracana len().
funkwurm
14

Możliwe są niejawne konwersje. Sytuacja, w której masz kłopoty, polega na tym, że nie wiesz, w jaki sposób coś powinno działać.

Przykładem tego jest Javascript, w którym +operator działa w różny sposób w różnym czasie.

>>> 4 + 3
7
>>> "4" + 3
43
>>> 4 + "3"
43

Jeśli jeden z argumentów jest łańcuchem, to +operator jest łańcuchem łączenia, w przeciwnym razie jest to dodawanie.

Jeśli otrzymujesz argument i nie wiesz, czy jest to ciąg znaków, czy liczba całkowita i chcesz dodać do niego argument, może to być trochę bałaganu.

Innym sposobem radzenia sobie z tym jest z Podstawowego dziedzictwa (z którego pochodzi perl - patrz Programowanie jest trudne, chodźmy skrypty ... )

W Basicu lenfunkcja ma sens tylko przywoływana na String (docs dla visual basic : „Dowolne prawidłowe wyrażenie String lub nazwa zmiennej. Jeśli Expression jest typu Object, funkcja Len zwraca rozmiar, który zostanie zapisany w pliku przez funkcja FilePut. ”).

Perl podąża za tą koncepcją kontekstu. Zamieszanie występujące w JavaScript z niejawną konwersją typów dla +operatora będącego czasem dodawaniem, a czasem konkatenacją nie występuje w Perlu, ponieważ zawsze+ jest dodawaniem i zawsze jest konkatenacją..

Jeśli coś jest używane w kontekście skalarnym, jest to skalar (np. Używając listy jako skalara, lista zachowuje się tak, jakby była liczbą odpowiadającą jej długości). Jeśli używasz operatora łańcucha ( eqdo testu równości, cmpdo porównania łańcucha), skalar jest używany tak, jakby był łańcuchem. Podobnie, jeśli coś zostało użyte w kontekście matematycznym ( ==do testu równości i <=>porównania numerycznego), skalar jest używany tak, jakby był liczbą.

Podstawową zasadą wszystkich programów jest „robienie rzeczy, które najmniej zaskakują człowieka”. Nie oznacza to, że nie ma w tym niespodzianek, ale staramy się jak najmniej zaskoczyć osobę.

Idąc do bliskiego kuzyna perl - php, zdarzają się sytuacje, w których operator może działać na coś w kontekście łańcuchowym lub liczbowym, a zachowanie może być zaskakujące dla ludzi. ++Operator jest jednym z przykładów. W przypadku liczb zachowuje się dokładnie tak, jak oczekiwano. Podczas działania na łańcuch, na przykład "aa", zwiększa łańcuch ( $foo = "aa"; $foo++; echo $foo;drukuje ab). Będzie również przewracał się, aby azpo zwiększeniu stał się ba. Nie jest to jeszcze szczególnie zaskakujące.

$foo = "3d8";
echo "$foo\n";
$foo++;
echo "$foo\n";
$foo++;
echo "$foo\n";
$foo++;
echo "$foo\n";

( ideone )

To drukuje:

3d8
3d9
3e0
4

Witaj w niebezpieczeństwach niejawnych konwersji i działania różnych operatorów na tym samym łańcuchu. (Uchwyty Perl że kod blokują trochę inaczej - uzna, że "3d8"gdy ++operator jest stosowana jest wartość liczbowa od samego początku i idzie 4od razu ( ideone ) - takie zachowanie jest dobrze opisana w perlop: Auto-inkrementacja i Auto-ubytek )

Teraz, dlaczego jeden język robi coś w jedną stronę, a inny w inny sposób, przechodzi do myśli projektowych projektantów. Filozofią Perla jest to, że można to zrobić na więcej niż jeden sposób - i mogę wymyślić kilka sposobów wykonania niektórych z tych operacji. Z drugiej strony Python ma filozofię opisaną w PEP 20 - Zen of Python, która stwierdza (między innymi): „Powinien istnieć jeden - a najlepiej tylko jeden - oczywisty sposób”.

Te różnice projektowe doprowadziły do ​​różnych języków. Jest jeden sposób na uzyskanie długości liczby w Pythonie. Niejawna konwersja jest sprzeczna z tą filozofią.

Literatura pokrewna: Dlaczego Ruby nie ma niejawnej konwersji Fixnum na String?

Społeczność
źródło
IMHO, dobry język / framework powinien często mieć wiele sposobów robienia rzeczy, które inaczej zajmują się wspólnymi przypadkami narożnymi (lepiej obsłużyć wspólny przypadek narożny raz w języku lub frameworku niż 1000 razy w 1000 programów); fakt, że dwaj operatorzy robią to samo przez większość czasu, nie powinien być uważany za złą rzecz, jeśli jeśli ich zachowania się różnią, korzyści z każdej odmiany są korzystne. To nie powinno być trudne do porównania dwóch zmiennych numerycznych xoraz yw taki sposób, aby wydajność relacją równoważności lub ranking, ale porównania operatorzy IIRC Pythona ...
Supercat
... nie wdrażają ani relacji równoważności, ani rankingu, a Python nie udostępnia żadnych wygodnych operatorów, którzy to robią.
supercat
12

ColdFusion zrobił większość tego. Definiuje zestaw reguł do obsługi niejawnych konwersji, ma jeden typ zmiennej i gotowe.

Rezultatem jest całkowita anarchia, w której dodanie „4a” do 6 to 6.16667.

Dlaczego? Ponieważ pierwsza z dwóch zmiennych jest liczbą, więc wynik będzie liczbowy. „4a” jest analizowany jako data i jest postrzegany jako „4 rano”. 4 rano jest 4: 00/24: 00 lub 1/6 dnia (0,16667). Dodaj do 6, a otrzymasz 6.16667.

Listy mają domyślne znaki oddzielające przecinek, więc jeśli kiedykolwiek dodasz element do listy zawierającej przecinek, po prostu dodasz dwa elementy. Ponadto listy są potajemnie łańcuchami, dzięki czemu mogą być analizowane jako daty, jeśli zawierają tylko 1 element.

Porównanie ciągów sprawdza, czy oba ciągi można najpierw przeanalizować z datami. Ponieważ nie ma typu daty, robi to. To samo dotyczy liczb i ciągów zawierających liczby, notację ósemkową i logiczne („prawda” i „tak” itp.)

Zamiast szybko zawieść i zgłosić błąd, zamiast tego ColdFusion zajmie się konwersją typu danych. I nie w dobry sposób.

Oczywiście można naprawić niejawne konwersje, wywołując funkcje jawne, takie jak DateCompare ... ale wtedy utracisz „korzyści” z niejawnej konwersji.


W przypadku ColdFusion jest to sposób na wsparcie programistów. Zwłaszcza, gdy wszyscy ci programiści są przyzwyczajeni do HTML (ColdFusion działa z tagami, nazywa się to CFML, później dodali także skrypty za pośrednictwem <cfscript>, gdzie nie trzeba dodawać tagów do wszystkiego). I działa dobrze, gdy chcesz po prostu coś uruchomić. Ale gdy potrzebujesz rzeczy, które mają się wydarzyć w bardziej precyzyjny sposób, potrzebujesz języka, który odmawia dokonywania niejawnych konwersji dla wszystkiego, co wygląda na to, że mogło być błędem.

Pimgd
źródło
1
wow, „totalna anarchia”!
hoosierEE
7

Mówisz, że niejawne konwersje mogą być dobrym pomysłem dla operacji, które są jednoznaczne, na przykład int a = 100; len(a), gdy oczywiście zamierzasz przekonwertować int na ciąg przed wywołaniem.

Ale zapominasz, że te rozmowy mogą być składniowo jednoznaczne, ale mogą one reprezentować literówkę wykonany przez programistę, który ma na celu przekazać a1, który jest ciągiem. Jest to wymyślony przykład, ale z IDE zapewniającymi autouzupełnianie nazw zmiennych, błędy te się zdarzają.

System typów ma na celu pomóc nam uniknąć błędów, a w przypadku języków, które wybierają bardziej rygorystyczne sprawdzanie typu, ukryte konwersje podważają to.

Sprawdź nieszczęścia Javascript ze wszystkimi ukrytymi konwersjami wykonanymi przez ==, tak bardzo, że wielu teraz zaleca trzymanie się operatora no-implicit -version ===.

Avner Shahar-Kashtan
źródło
6

Zastanów się przez chwilę nad kontekstem swojego oświadczenia. Mówisz, że to jest „jedyny sposób”, w jaki może to działać, ale czy naprawdę jesteś pewien, że tak będzie? A co z tym:

def approx_log_10(s):
    return len(s)
print approx_log_10(3.5)  # "3" is probably not what I'm expecting here...

Jak wspomnieli inni, w bardzo konkretnym przykładzie kompilator wydaje się intuicyjny w zrozumieniu tego, co miałeś na myśli. Ale w szerszym kontekście bardziej skomplikowanych programów często nie jest wcale oczywiste, czy zamierzałeś wprowadzić ciąg, czy literować jedną nazwę zmiennej dla innej, czy wywołać niewłaściwą funkcję, czy którykolwiek z dziesiątek innych potencjalnych błędów logicznych .

W przypadku, gdy interpreter / kompilator zgaduje, co miałeś na myśli, czasami działa to magicznie, a innym razem spędzasz godziny na debugowaniu czegoś, co w tajemniczy sposób nie działa bez wyraźnego powodu. W przeciętnym przypadku o wiele bezpieczniej jest powiedzieć, że to stwierdzenie nie ma sensu, i poświęcić kilka sekund na poprawienie tego, co naprawdę miałeś na myśli.

GrandOpener
źródło
3

Niejawne konwersje mogą być prawdziwym bólem w pracy. W PowerShell:

$a = $(dir *.xml) # typeof a is a list, because there are two XML files in the folder.
$a = $(dir *.xml) # typeof a is a string, because there is one XML file in the folder.

Nagle potrzebne są dwa razy więcej testów i dwa razy więcej błędów niż w przypadku domniemanej konwersji.

Esben Skov Pedersen
źródło
2

Jawne obsady są ważne, ponieważ wyjaśniają twoją intencję. Przede wszystkim użycie jawnych rzutów opowiada historię komuś, kto czyta Twój kod. Ujawniają, że celowo zrobiłeś to, co zrobiłeś. Ponadto to samo dotyczy kompilatora. Następujące działania są nielegalne na przykład w języku C #

double d = 3.1415926;
// code elided
int i = d;

Obsada sprawi, że stracisz precyzję, co z łatwością może być błędem. Dlatego kompilator odmawia kompilacji. Używając jawnej obsady mówisz kompilatorowi: „Hej, wiem co robię”. I powie: „Okej!” I skompiluj.

Paul Kertscher
źródło
1
W rzeczywistości nie lubię najbardziej jawnych rzutów niż ukrytych; IMHO, jedyne, co (X)ypowinno znaczyć, to: „Myślę, że Y można przekształcić na X; wykonaj konwersję, jeśli to możliwe, lub wyrzuć wyjątek, jeśli nie”; jeśli konwersja się powiedzie, należy przewidzieć, jaka będzie wynikowa wartość * bez konieczności znajomości specjalnych zasad dotyczących konwersji z typu yna typ X. Jeśli zostaniesz poproszony o konwersję -1,5 i 1,5 na liczby całkowite, niektóre języki przyniosą (-2,1); niektóre (-2,2), niektóre (-1,1) i niektóre (-1,2). Podczas gdy reguły C nie są wcale niejasne ...
supercat
1
... wydaje się, że tego rodzaju konwersja byłaby bardziej odpowiednio wykonana metodą niż za pomocą castingu (szkoda, że ​​.NET nie zawiera wydajnych funkcji konwersji i konwersji, a Java jest przeciążona w nieco głupi sposób).
supercat
Mówisz kompilatorowi: „ Myślę, że wiem, co robię”.
gnasher729
@ gnasher729 Jest w tym trochę prawdy :)
Paul Kertscher,
1

Języki, które obsługują niejawne konwersje / rzutowania lub słabe pisanie, jak to się czasem nazywa, przyjmą za Ciebie założenia, które nie zawsze pasują do zamierzonego lub oczekiwanego zachowania. Projektanci języka mogli mieć ten sam proces myślowy, co w przypadku pewnego rodzaju konwersji lub serii konwersji, a w takim przypadku nigdy nie zobaczysz problemu; jednak jeśli nie, Twój program zawiedzie.

Niepowodzenie może jednak być oczywiste. Ponieważ te niejawne rzutowania zachodzą w czasie wykonywania, twój program będzie działał szczęśliwie i zobaczysz problem tylko wtedy, gdy spojrzysz na wynik lub wynik swojego programu. Język, który wymaga jawnych rzutowań (niektórzy nazywają to silnym typowaniem), dawałby ci błąd, zanim program zacznie nawet wykonywać wykonywanie, co jest znacznie bardziej oczywistym i łatwiejszym problemem do naprawienia, że ​​ukryte rzutowania poszły źle.

Któregoś dnia mój przyjaciel zapytał 2-latka, czym jest 20 + 20, i odpowiedział 2020. Powiedziałem mojemu przyjacielowi: „Zostanie programistą Javascript”.

W JavaScript:

20+20
40

20+"20"
"2020"

W ten sposób można zobaczyć problemy, które mogą tworzyć niejawne rzutowania i dlaczego nie jest to coś, co można po prostu naprawić. Naprawdę, jak twierdzę, jest użycie jawnych rzutowań.

Fred Thomsen
źródło